Commit f987c5679f0794c581943f01bfc3cd4feea81434

Authored by 王彪总
1 parent 2527b568

feat(property): 完善考勤统计功能并优化数据查询

- 重构AttendanceRecordMapper.xml查询逻辑,新增多维度统计查询
- 添加打卡统计相关DAO方法实现数据访问层功能
- 新增QueryAttendanceStatsCmd命令类处理考勤统计业务逻辑
- 优化QueryTodayAttendanceDetailCmd添加地址信息展示
- 移除pom.xml中的时间戳构建配置项
... ... @@ -564,7 +564,6 @@
564 564 </dependencies>
565 565  
566 566 <build>
567   - <finalName>${project.artifactId}-${maven.build.timestamp}</finalName>
568 567 <plugins>
569 568 <plugin>
570 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 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 &gt;= #{startTime}
  79 + AND ar.punch_time &lt;= #{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 &gt;= #{startTime} AND ar.punch_time &lt;= #{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 &gt;= #{startTime} AND punch_time &lt;= #{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 &gt;= #{startTime} AND ar.punch_time &lt;= #{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>
... ...
springboot/src/main/java/com/java110/boot/BootApplicationStart.java
... ... @@ -263,4 +263,4 @@ public class BootApplicationStart {
263 263 }
264 264 }
265 265  
266   -}
267 266 \ No newline at end of file
  267 +}
... ...