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
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 | 52 | if ("OFF".equals(type)) pd.put("offTime", r.get("punch_time")); |
| 53 | 53 | } |
| 54 | 54 | |
| 55 | - // 4. 构建全系统员工→store映射 | |
| 55 | + // 4. 构建全系统员工→store/address映射 | |
| 56 | 56 | Map<String, String> userStoreMap = new HashMap<>(); |
| 57 | + Map<String, String> userAddressMap = new HashMap<>(); | |
| 57 | 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 | 64 | // 5. 构建结果 |
| ... | ... | @@ -65,6 +68,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { |
| 65 | 68 | for (Map.Entry<String, Map> e : punchMap.entrySet()) { |
| 66 | 69 | Map data = e.getValue(); |
| 67 | 70 | data.put("storeName", userStoreMap.getOrDefault(e.getKey(), "")); |
| 71 | + data.put("address", userAddressMap.getOrDefault(e.getKey(), "")); | |
| 68 | 72 | result.add(buildTask(data)); |
| 69 | 73 | seen.add(e.getKey()); |
| 70 | 74 | } |
| ... | ... | @@ -76,6 +80,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { |
| 76 | 80 | m.put("staffId", uid); |
| 77 | 81 | m.put("staffName", u.get("user_name")); |
| 78 | 82 | m.put("storeName", u.get("store_name")); |
| 83 | + m.put("address", u.get("address") != null ? u.get("address") : ""); | |
| 79 | 84 | m.put("onTime", null); |
| 80 | 85 | m.put("offTime", null); |
| 81 | 86 | result.add(buildTask(m)); |
| ... | ... | @@ -91,6 +96,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { |
| 91 | 96 | task.put("staffId", data.get("staffId")); |
| 92 | 97 | task.put("staffName", data.getOrDefault("staffName", "")); |
| 93 | 98 | task.put("storeName", data.getOrDefault("storeName", "")); |
| 99 | + task.put("address", data.getOrDefault("address", "")); | |
| 94 | 100 | boolean hasOn = data.get("onTime") != null; |
| 95 | 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 | 53 | public List<Map> queryAllAttendanceUsers(Map params) { |
| 54 | 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 | 17 | |
| 18 | 18 | /** 查询所有有打卡记录的用户列表 */ |
| 19 | 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 | 52 | </select> |
| 53 | 53 | |
| 54 | 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 | 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 | 167 | </select> |
| 62 | 168 | |
| 63 | 169 | </mapper> | ... | ... |