Commit f987c5679f0794c581943f01bfc3cd4feea81434
1 parent
2527b568
feat(property): 完善考勤统计功能并优化数据查询
- 重构AttendanceRecordMapper.xml查询逻辑,新增多维度统计查询 - 添加打卡统计相关DAO方法实现数据访问层功能 - 新增QueryAttendanceStatsCmd命令类处理考勤统计业务逻辑 - 优化QueryTodayAttendanceDetailCmd添加地址信息展示 - 移除pom.xml中的时间戳构建配置项
Showing
7 changed files
with
359 additions
and
9 deletions
pom.xml
| @@ -564,7 +564,6 @@ | @@ -564,7 +564,6 @@ | ||
| 564 | </dependencies> | 564 | </dependencies> |
| 565 | 565 | ||
| 566 | <build> | 566 | <build> |
| 567 | - <finalName>${project.artifactId}-${maven.build.timestamp}</finalName> | ||
| 568 | <plugins> | 567 | <plugins> |
| 569 | <plugin> | 568 | <plugin> |
| 570 | <groupId>org.apache.maven.plugins</groupId> | 569 | <groupId>org.apache.maven.plugins</groupId> |
service-user/src/main/java/com/java110/user/cmd/property/QueryAttendanceStatsCmd.java
0 → 100644
| 1 | +/* | ||
| 2 | + * Copyright 2017-2020 吴学文 and java110 team. | ||
| 3 | + * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 5 | + * you may not use this file except in compliance with the License. | ||
| 6 | + * You may obtain a copy of the License at | ||
| 7 | + * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
| 9 | + * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 13 | + * See the License for the specific language governing permissions and | ||
| 14 | + * limitations under the License. | ||
| 15 | + */ | ||
| 16 | +package com.java110.user.cmd.property; | ||
| 17 | + | ||
| 18 | +import com.alibaba.fastjson.JSONObject; | ||
| 19 | +import com.java110.core.annotation.Java110Cmd; | ||
| 20 | +import com.java110.core.context.CmdContextUtils; | ||
| 21 | +import com.java110.core.context.ICmdDataFlowContext; | ||
| 22 | +import com.java110.core.event.cmd.Cmd; | ||
| 23 | +import com.java110.core.event.cmd.CmdEvent; | ||
| 24 | +import com.java110.user.dao.property.IAttendanceRecordV1ServiceDao; | ||
| 25 | +import com.java110.utils.exception.CmdException; | ||
| 26 | +import com.java110.vo.ResultVo; | ||
| 27 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 28 | + | ||
| 29 | +import java.text.ParseException; | ||
| 30 | +import java.time.LocalDate; | ||
| 31 | +import java.time.format.DateTimeFormatter; | ||
| 32 | +import java.util.*; | ||
| 33 | + | ||
| 34 | +@Java110Cmd(serviceCode = "property.queryAttendanceStats") | ||
| 35 | +public class QueryAttendanceStatsCmd extends Cmd { | ||
| 36 | + | ||
| 37 | + @Autowired | ||
| 38 | + private IAttendanceRecordV1ServiceDao attendanceRecordV1ServiceDao; | ||
| 39 | + | ||
| 40 | + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); | ||
| 41 | + | ||
| 42 | + @Override | ||
| 43 | + public void validate(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException, ParseException { | ||
| 44 | + String startTime = reqJson.getString("startTime"); | ||
| 45 | + String endTime = reqJson.getString("endTime"); | ||
| 46 | + if (startTime == null || startTime.isEmpty()) { | ||
| 47 | + throw new CmdException("开始时间不能为空"); | ||
| 48 | + } | ||
| 49 | + if (endTime == null || endTime.isEmpty()) { | ||
| 50 | + throw new CmdException("结束时间不能为空"); | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + @Override | ||
| 55 | + public void doCmd(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException, ParseException { | ||
| 56 | + String storeId = CmdContextUtils.getStoreId(context); | ||
| 57 | + String storeTypeCd = CmdContextUtils.getStoreTypeCd(context); | ||
| 58 | + | ||
| 59 | + Map params = buildQueryParams(reqJson, storeId); | ||
| 60 | + String startTime = (String) params.get("startTime"); | ||
| 61 | + String endTime = (String) params.get("endTime"); | ||
| 62 | + | ||
| 63 | + // 1. 按用户聚合统计(u_user 主表,LEFT JOIN 打卡记录,含无打卡员工) | ||
| 64 | + List<Map> userStatsList = attendanceRecordV1ServiceDao.queryAttendanceStats(params); | ||
| 65 | + | ||
| 66 | + // 2. 异常打卡明细(SQL 直接检测:有上班无下班 / 有下班无上班) | ||
| 67 | + List<Map> anomalyRecords = attendanceRecordV1ServiceDao.queryAttendanceRecordsForStats(params); | ||
| 68 | + | ||
| 69 | + // 3. 应出勤天数(查询时间范围内的总天数,按月即自然月) | ||
| 70 | + int totalWorkdays = countDays(startTime, endTime); | ||
| 71 | + | ||
| 72 | + // 4. 按 user_id 汇总异常 | ||
| 73 | + Map<String, List<Map>> anomalyMap = new HashMap<>(); | ||
| 74 | + for (Map a : anomalyRecords) { | ||
| 75 | + String uid = (String) a.get("user_id"); | ||
| 76 | + anomalyMap.putIfAbsent(uid, new ArrayList<>()); | ||
| 77 | + Map detail = new HashMap(); | ||
| 78 | + detail.put("date", a.get("punch_date")); | ||
| 79 | + detail.put("type", a.get("lack_desc")); | ||
| 80 | + anomalyMap.get(uid).add(detail); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + // 5. 组装结果 | ||
| 84 | + List<Map> resultList = new ArrayList<>(); | ||
| 85 | + int totalShouldDays = 0, totalActualDays = 0, totalPunches = 0; | ||
| 86 | + int totalAnomalyDays = 0, totalAbsenceDays = 0; | ||
| 87 | + | ||
| 88 | + for (Map stat : userStatsList) { | ||
| 89 | + String userId = (String) stat.get("user_id"); | ||
| 90 | + int actualDays = parseInt(stat.get("actual_days")); | ||
| 91 | + int shouldDays = totalWorkdays; | ||
| 92 | + | ||
| 93 | + List<Map> userAnomalies = anomalyMap.getOrDefault(userId, new ArrayList<>()); | ||
| 94 | + int anomalyCount = userAnomalies.size(); | ||
| 95 | + int absenceCount = Math.max(0, shouldDays - actualDays); | ||
| 96 | + | ||
| 97 | + StringBuilder sb = new StringBuilder(); | ||
| 98 | + for (int i = 0; i < userAnomalies.size(); i++) { | ||
| 99 | + if (i > 0) sb.append(";"); | ||
| 100 | + Map an = userAnomalies.get(i); | ||
| 101 | + sb.append(an.get("date")).append(" ").append(an.get("type")); | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + Map result = new HashMap(); | ||
| 105 | + result.put("userId", userId); | ||
| 106 | + result.put("userName", stat.getOrDefault("user_name", "")); | ||
| 107 | + result.put("address", stat.getOrDefault("address", "")); | ||
| 108 | + result.put("shouldDays", shouldDays); | ||
| 109 | + result.put("actualDays", actualDays); | ||
| 110 | + result.put("totalPunches", parseInt(stat.get("total_punches"))); | ||
| 111 | + result.put("onPunches", parseInt(stat.get("on_punches"))); | ||
| 112 | + result.put("offPunches", parseInt(stat.get("off_punches"))); | ||
| 113 | + result.put("anomalyDays", anomalyCount); | ||
| 114 | + result.put("anomalyDetail", sb.toString()); | ||
| 115 | + result.put("absenceDays", absenceCount); | ||
| 116 | + | ||
| 117 | + resultList.add(result); | ||
| 118 | + | ||
| 119 | + totalShouldDays += shouldDays; | ||
| 120 | + totalActualDays += actualDays; | ||
| 121 | + totalPunches += parseInt(stat.get("total_punches")); | ||
| 122 | + totalAnomalyDays += anomalyCount; | ||
| 123 | + totalAbsenceDays += absenceCount; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + // 6. 每日趋势 | ||
| 127 | + List<Map> dailyStats = attendanceRecordV1ServiceDao.queryDailyAttendanceStats(params); | ||
| 128 | + | ||
| 129 | + // 7. 汇总卡片 | ||
| 130 | + Map summary = new HashMap(); | ||
| 131 | + summary.put("totalUsers", userStatsList.size()); | ||
| 132 | + summary.put("totalShouldDays", totalShouldDays); | ||
| 133 | + summary.put("totalActualDays", totalActualDays); | ||
| 134 | + summary.put("totalPunches", totalPunches); | ||
| 135 | + summary.put("totalAnomalyDays", totalAnomalyDays); | ||
| 136 | + summary.put("totalAbsenceDays", totalAbsenceDays); | ||
| 137 | + summary.put("totalWorkdays", totalWorkdays); | ||
| 138 | + | ||
| 139 | + // 8. 项目维度(仅物业) | ||
| 140 | + List<Map> projectStats = new ArrayList<>(); | ||
| 141 | + if ("800900000003".equals(storeTypeCd)) { | ||
| 142 | + projectStats = attendanceRecordV1ServiceDao.queryProjectAttendanceStats(params); | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + Map response = new HashMap(); | ||
| 146 | + response.put("summary", summary); | ||
| 147 | + response.put("dailyStats", dailyStats); | ||
| 148 | + response.put("userStats", resultList); | ||
| 149 | + response.put("projectStats", projectStats); | ||
| 150 | + response.put("total", resultList.size()); | ||
| 151 | + | ||
| 152 | + context.setResponseEntity(ResultVo.createResponseEntity(resultList.size(), resultList.size(), response)); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + private Map buildQueryParams(JSONObject reqJson, String storeId) { | ||
| 156 | + Map params = new HashMap<>(); | ||
| 157 | + params.put("startTime", reqJson.getString("startTime")); | ||
| 158 | + params.put("endTime", reqJson.getString("endTime")); | ||
| 159 | + if (reqJson.containsKey("filterUserId") && !reqJson.getString("filterUserId").isEmpty()) { | ||
| 160 | + params.put("userId", reqJson.getString("filterUserId")); | ||
| 161 | + } | ||
| 162 | + if (storeId != null && !storeId.isEmpty()) { | ||
| 163 | + params.put("storeId", storeId); | ||
| 164 | + } | ||
| 165 | + return params; | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + private int countDays(String startTime, String endTime) { | ||
| 169 | + try { | ||
| 170 | + String sd = startTime.length() >= 10 ? startTime.substring(0, 10) : startTime; | ||
| 171 | + String ed = endTime.length() >= 10 ? endTime.substring(0, 10) : endTime; | ||
| 172 | + LocalDate start = LocalDate.parse(sd, DATE_FMT); | ||
| 173 | + LocalDate end = LocalDate.parse(ed, DATE_FMT); | ||
| 174 | + return (int) java.time.temporal.ChronoUnit.DAYS.between(start, end) + 1; | ||
| 175 | + } catch (Exception e) { | ||
| 176 | + return 0; | ||
| 177 | + } | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + private int parseInt(Object value) { | ||
| 181 | + if (value == null) return 0; | ||
| 182 | + if (value instanceof Integer) return (Integer) value; | ||
| 183 | + if (value instanceof Long) return ((Long) value).intValue(); | ||
| 184 | + if (value instanceof java.math.BigDecimal) return ((java.math.BigDecimal) value).intValue(); | ||
| 185 | + if (value instanceof Double) return ((Double) value).intValue(); | ||
| 186 | + if (value instanceof String) { | ||
| 187 | + try { return Integer.parseInt((String) value); } catch (NumberFormatException e) { return 0; } | ||
| 188 | + } | ||
| 189 | + return 0; | ||
| 190 | + } | ||
| 191 | +} |
service-user/src/main/java/com/java110/user/cmd/property/QueryTodayAttendanceDetailCmd.java
| @@ -52,10 +52,13 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | @@ -52,10 +52,13 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | ||
| 52 | if ("OFF".equals(type)) pd.put("offTime", r.get("punch_time")); | 52 | if ("OFF".equals(type)) pd.put("offTime", r.get("punch_time")); |
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | - // 4. 构建全系统员工→store映射 | 55 | + // 4. 构建全系统员工→store/address映射 |
| 56 | Map<String, String> userStoreMap = new HashMap<>(); | 56 | Map<String, String> userStoreMap = new HashMap<>(); |
| 57 | + Map<String, String> userAddressMap = new HashMap<>(); | ||
| 57 | for (Map u : allUsers) { | 58 | for (Map u : allUsers) { |
| 58 | - userStoreMap.put((String) u.get("user_id"), (String) u.get("store_name")); | 59 | + String uid = (String) u.get("user_id"); |
| 60 | + userStoreMap.put(uid, (String) u.get("store_name")); | ||
| 61 | + userAddressMap.put(uid, u.get("address") != null ? (String) u.get("address") : ""); | ||
| 59 | } | 62 | } |
| 60 | 63 | ||
| 61 | // 5. 构建结果 | 64 | // 5. 构建结果 |
| @@ -65,6 +68,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | @@ -65,6 +68,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | ||
| 65 | for (Map.Entry<String, Map> e : punchMap.entrySet()) { | 68 | for (Map.Entry<String, Map> e : punchMap.entrySet()) { |
| 66 | Map data = e.getValue(); | 69 | Map data = e.getValue(); |
| 67 | data.put("storeName", userStoreMap.getOrDefault(e.getKey(), "")); | 70 | data.put("storeName", userStoreMap.getOrDefault(e.getKey(), "")); |
| 71 | + data.put("address", userAddressMap.getOrDefault(e.getKey(), "")); | ||
| 68 | result.add(buildTask(data)); | 72 | result.add(buildTask(data)); |
| 69 | seen.add(e.getKey()); | 73 | seen.add(e.getKey()); |
| 70 | } | 74 | } |
| @@ -76,6 +80,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | @@ -76,6 +80,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | ||
| 76 | m.put("staffId", uid); | 80 | m.put("staffId", uid); |
| 77 | m.put("staffName", u.get("user_name")); | 81 | m.put("staffName", u.get("user_name")); |
| 78 | m.put("storeName", u.get("store_name")); | 82 | m.put("storeName", u.get("store_name")); |
| 83 | + m.put("address", u.get("address") != null ? u.get("address") : ""); | ||
| 79 | m.put("onTime", null); | 84 | m.put("onTime", null); |
| 80 | m.put("offTime", null); | 85 | m.put("offTime", null); |
| 81 | result.add(buildTask(m)); | 86 | result.add(buildTask(m)); |
| @@ -91,6 +96,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | @@ -91,6 +96,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { | ||
| 91 | task.put("staffId", data.get("staffId")); | 96 | task.put("staffId", data.get("staffId")); |
| 92 | task.put("staffName", data.getOrDefault("staffName", "")); | 97 | task.put("staffName", data.getOrDefault("staffName", "")); |
| 93 | task.put("storeName", data.getOrDefault("storeName", "")); | 98 | task.put("storeName", data.getOrDefault("storeName", "")); |
| 99 | + task.put("address", data.getOrDefault("address", "")); | ||
| 94 | boolean hasOn = data.get("onTime") != null; | 100 | boolean hasOn = data.get("onTime") != null; |
| 95 | task.put("state", hasOn ? "30000" : "10000"); | 101 | task.put("state", hasOn ? "30000" : "10000"); |
| 96 | 102 |
service-user/src/main/java/com/java110/user/dao/impl/AttendanceRecordV1ServiceDaoImpl.java
| @@ -53,4 +53,39 @@ public class AttendanceRecordV1ServiceDaoImpl extends BaseServiceDao implements | @@ -53,4 +53,39 @@ public class AttendanceRecordV1ServiceDaoImpl extends BaseServiceDao implements | ||
| 53 | public List<Map> queryAllAttendanceUsers(Map params) { | 53 | public List<Map> queryAllAttendanceUsers(Map params) { |
| 54 | return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAllAttendanceUsers", params); | 54 | return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAllAttendanceUsers", params); |
| 55 | } | 55 | } |
| 56 | + | ||
| 57 | + @Override | ||
| 58 | + public List<Map> queryAttendanceStats(Map params) { | ||
| 59 | + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAttendanceStats", params); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + @Override | ||
| 63 | + public int queryAttendanceStatsCount(Map params) { | ||
| 64 | + List<Map> result = sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAttendanceStatsCount", params); | ||
| 65 | + if (result == null || result.isEmpty()) return 0; | ||
| 66 | + Object count = result.get(0).get("count"); | ||
| 67 | + if (count instanceof Integer) return (Integer) count; | ||
| 68 | + if (count instanceof Long) return ((Long) count).intValue(); | ||
| 69 | + return Integer.parseInt(count.toString()); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + @Override | ||
| 73 | + public List<Map> queryDailyAttendanceStats(Map params) { | ||
| 74 | + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryDailyAttendanceStats", params); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + @Override | ||
| 78 | + public List<Map> queryAttendanceRecordsForStats(Map params) { | ||
| 79 | + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAttendanceRecordsForStats", params); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + @Override | ||
| 83 | + public List<Map> queryProjectAttendanceStats(Map params) { | ||
| 84 | + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryProjectAttendanceStats", params); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @Override | ||
| 88 | + public List<Map> queryScheduleWorkdays(Map params) { | ||
| 89 | + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryScheduleWorkdays", params); | ||
| 90 | + } | ||
| 56 | } | 91 | } |
service-user/src/main/java/com/java110/user/dao/property/IAttendanceRecordV1ServiceDao.java
| @@ -17,4 +17,17 @@ public interface IAttendanceRecordV1ServiceDao { | @@ -17,4 +17,17 @@ public interface IAttendanceRecordV1ServiceDao { | ||
| 17 | 17 | ||
| 18 | /** 查询所有有打卡记录的用户列表 */ | 18 | /** 查询所有有打卡记录的用户列表 */ |
| 19 | List<Map> queryAllAttendanceUsers(Map params); | 19 | List<Map> queryAllAttendanceUsers(Map params); |
| 20 | + | ||
| 21 | + /** 打卡统计:按用户聚合 */ | ||
| 22 | + List<Map> queryAttendanceStats(Map params); | ||
| 23 | + /** 打卡统计:用户数 */ | ||
| 24 | + int queryAttendanceStatsCount(Map params); | ||
| 25 | + /** 打卡统计:每日出勤人数(图表用) */ | ||
| 26 | + List<Map> queryDailyAttendanceStats(Map params); | ||
| 27 | + /** 打卡统计:时间范围内打卡记录明细(用于异常分析) */ | ||
| 28 | + List<Map> queryAttendanceRecordsForStats(Map params); | ||
| 29 | + /** 打卡统计:项目维度聚合(物业端) */ | ||
| 30 | + List<Map> queryProjectAttendanceStats(Map params); | ||
| 31 | + /** 打卡统计:查询员工排班工作日数 */ | ||
| 32 | + List<Map> queryScheduleWorkdays(Map params); | ||
| 20 | } | 33 | } |
service-user/src/main/resources/mapper/property/AttendanceRecordMapper.xml
| @@ -52,12 +52,118 @@ | @@ -52,12 +52,118 @@ | ||
| 52 | </select> | 52 | </select> |
| 53 | 53 | ||
| 54 | <select id="queryAllAttendanceUsers" parameterType="map" resultType="map"> | 54 | <select id="queryAllAttendanceUsers" parameterType="map" resultType="map"> |
| 55 | - SELECT DISTINCT ar.user_id, ar.user_name, ar.work_type, | ||
| 56 | - COALESCE(s.name, '未知') AS store_name | 55 | + SELECT u.user_id, u.name AS user_name, u.address, '' AS work_type, '' AS store_name |
| 56 | + FROM u_user u | ||
| 57 | + WHERE u.status_cd = '0' | ||
| 58 | + AND (u.level_cd = '00' OR u.level_cd = '01') | ||
| 59 | + ORDER BY u.name | ||
| 60 | + </select> | ||
| 61 | + | ||
| 62 | + <!-- 打卡统计:按用户聚合(u_user 为主表,LEFT JOIN 打卡记录) --> | ||
| 63 | + <select id="queryAttendanceStats" parameterType="map" resultType="map"> | ||
| 64 | + SELECT | ||
| 65 | + u.user_id, | ||
| 66 | + u.name AS user_name, | ||
| 67 | + u.address, | ||
| 68 | + COUNT(ar.id) AS total_punches, | ||
| 69 | + IFNULL(SUM(CASE ar.punch_type WHEN 'ON' THEN 1 ELSE 0 END), 0) AS on_punches, | ||
| 70 | + IFNULL(SUM(CASE ar.punch_type WHEN 'OFF' THEN 1 ELSE 0 END), 0) AS off_punches, | ||
| 71 | + COUNT(DISTINCT DATE(ar.punch_time)) AS actual_days | ||
| 72 | + FROM u_user u | ||
| 73 | + <if test="storeId != null and storeId != ''"> | ||
| 74 | + INNER JOIN staff_community sc ON u.user_id = sc.staff_id AND sc.store_id = #{storeId} AND sc.status_cd = '0' | ||
| 75 | + </if> | ||
| 76 | + LEFT JOIN attendance_record ar | ||
| 77 | + ON u.user_id = ar.user_id | ||
| 78 | + AND ar.punch_time >= #{startTime} | ||
| 79 | + AND ar.punch_time <= #{endTime} | ||
| 80 | + WHERE u.status_cd != '1' | ||
| 81 | + <if test="userId != null and userId != ''">AND u.user_id = #{userId}</if> | ||
| 82 | + GROUP BY u.user_id, u.name, u.address | ||
| 83 | + ORDER BY u.name | ||
| 84 | + </select> | ||
| 85 | + | ||
| 86 | + <!-- 打卡统计:用户数 --> | ||
| 87 | + <select id="queryAttendanceStatsCount" parameterType="map" resultType="map"> | ||
| 88 | + SELECT COUNT(1) AS count | ||
| 89 | + FROM u_user u | ||
| 90 | + <if test="storeId != null and storeId != ''"> | ||
| 91 | + INNER JOIN staff_community sc ON u.user_id = sc.staff_id AND sc.store_id = #{storeId} AND sc.status_cd = '0' | ||
| 92 | + </if> | ||
| 93 | + WHERE u.status_cd != '1' | ||
| 94 | + <if test="userId != null and userId != ''">AND u.user_id = #{userId}</if> | ||
| 95 | + </select> | ||
| 96 | + | ||
| 97 | + <!-- 打卡统计:每日出勤人数(图表用) --> | ||
| 98 | + <select id="queryDailyAttendanceStats" parameterType="map" resultType="map"> | ||
| 99 | + SELECT | ||
| 100 | + DATE(ar.punch_time) AS punch_date, | ||
| 101 | + COUNT(DISTINCT ar.user_id) AS user_count, | ||
| 102 | + COUNT(*) AS total_punches | ||
| 57 | FROM attendance_record ar | 103 | FROM attendance_record ar |
| 58 | - LEFT JOIN s_store_user su ON ar.user_id = su.user_id AND su.status_cd = '0' | ||
| 59 | - LEFT JOIN s_store s ON su.store_id = s.store_id AND s.status_cd = '0' | ||
| 60 | - ORDER BY ar.user_name | 104 | + WHERE ar.punch_time >= #{startTime} AND ar.punch_time <= #{endTime} |
| 105 | + <if test="storeId != null and storeId != ''"> | ||
| 106 | + AND ar.user_id IN (SELECT DISTINCT staff_id FROM staff_community WHERE store_id = #{storeId} AND status_cd = '0') | ||
| 107 | + </if> | ||
| 108 | + GROUP BY DATE(ar.punch_time) | ||
| 109 | + ORDER BY punch_date | ||
| 110 | + </select> | ||
| 111 | + | ||
| 112 | + <!-- 打卡统计:异常检测(有上班无下班 或 有下班无上班) --> | ||
| 113 | + <select id="queryAttendanceRecordsForStats" parameterType="map" resultType="map"> | ||
| 114 | + SELECT | ||
| 115 | + t1.user_id, | ||
| 116 | + t1.punch_date, | ||
| 117 | + CASE | ||
| 118 | + WHEN t1.has_on = 1 AND t1.has_off = 0 THEN '有上班无下班' | ||
| 119 | + WHEN t1.has_on = 0 AND t1.has_off = 1 THEN '有下班无上班' | ||
| 120 | + END AS lack_desc | ||
| 121 | + FROM ( | ||
| 122 | + SELECT | ||
| 123 | + user_id, | ||
| 124 | + DATE(punch_time) AS punch_date, | ||
| 125 | + MAX(CASE punch_type WHEN 'ON' THEN 1 ELSE 0 END) AS has_on, | ||
| 126 | + MAX(CASE punch_type WHEN 'OFF' THEN 1 ELSE 0 END) AS has_off | ||
| 127 | + FROM attendance_record | ||
| 128 | + WHERE punch_time >= #{startTime} AND punch_time <= #{endTime} | ||
| 129 | + <if test="storeId != null and storeId != ''"> | ||
| 130 | + AND user_id IN (SELECT DISTINCT staff_id FROM staff_community WHERE store_id = #{storeId} AND status_cd = '0') | ||
| 131 | + </if> | ||
| 132 | + <if test="userId != null and userId != ''">AND user_id = #{userId}</if> | ||
| 133 | + GROUP BY user_id, DATE(punch_time) | ||
| 134 | + ) t1 | ||
| 135 | + HAVING lack_desc IS NOT NULL | ||
| 136 | + ORDER BY t1.user_id, t1.punch_date | ||
| 137 | + </select> | ||
| 138 | + | ||
| 139 | + <!-- 打卡统计:项目维度聚合(物业端) --> | ||
| 140 | + <select id="queryProjectAttendanceStats" parameterType="map" resultType="map"> | ||
| 141 | + SELECT | ||
| 142 | + sc.community_id, | ||
| 143 | + sc.community_name, | ||
| 144 | + COUNT(DISTINCT ar.user_id) AS user_count, | ||
| 145 | + COUNT(*) AS total_punches, | ||
| 146 | + SUM(CASE WHEN ar.punch_type = 'ON' THEN 1 ELSE 0 END) AS on_punches, | ||
| 147 | + SUM(CASE WHEN ar.punch_type = 'OFF' THEN 1 ELSE 0 END) AS off_punches, | ||
| 148 | + COUNT(DISTINCT DATE(ar.punch_time)) AS total_actual_days | ||
| 149 | + FROM staff_community sc | ||
| 150 | + INNER JOIN attendance_record ar ON sc.staff_id = ar.user_id AND sc.status_cd = '0' | ||
| 151 | + WHERE ar.punch_time >= #{startTime} AND ar.punch_time <= #{endTime} | ||
| 152 | + <if test="storeId != null and storeId != ''">AND sc.store_id = #{storeId}</if> | ||
| 153 | + GROUP BY sc.community_id, sc.community_name | ||
| 154 | + ORDER BY sc.community_name | ||
| 155 | + </select> | ||
| 156 | + | ||
| 157 | + <!-- 打卡统计:查询员工排班工作日数 --> | ||
| 158 | + <select id="queryScheduleWorkdays" parameterType="map" resultType="map"> | ||
| 159 | + SELECT | ||
| 160 | + scs.staff_id, | ||
| 161 | + COUNT(DISTINCT CONCAT(scd.day, '_', scd.week_flag)) AS schedule_days | ||
| 162 | + FROM schedule_classes_staff scs | ||
| 163 | + INNER JOIN schedule_classes_day scd ON scs.schedule_id = scd.schedule_id AND scd.status_cd = '0' | ||
| 164 | + WHERE scs.status_cd = '0' | ||
| 165 | + <if test="staffId != null and staffId != ''">AND scs.staff_id = #{staffId}</if> | ||
| 166 | + GROUP BY scs.staff_id | ||
| 61 | </select> | 167 | </select> |
| 62 | 168 | ||
| 63 | </mapper> | 169 | </mapper> |
springboot/src/main/java/com/java110/boot/BootApplicationStart.java