diff --git a/pom.xml b/pom.xml index e22fb79..656cb03 100755 --- a/pom.xml +++ b/pom.xml @@ -564,7 +564,6 @@ - ${project.artifactId}-${maven.build.timestamp} org.apache.maven.plugins diff --git a/service-user/src/main/java/com/java110/user/cmd/property/QueryAttendanceStatsCmd.java b/service-user/src/main/java/com/java110/user/cmd/property/QueryAttendanceStatsCmd.java new file mode 100644 index 0000000..1656913 --- /dev/null +++ b/service-user/src/main/java/com/java110/user/cmd/property/QueryAttendanceStatsCmd.java @@ -0,0 +1,191 @@ +/* + * Copyright 2017-2020 吴学文 and java110 team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.java110.user.cmd.property; + +import com.alibaba.fastjson.JSONObject; +import com.java110.core.annotation.Java110Cmd; +import com.java110.core.context.CmdContextUtils; +import com.java110.core.context.ICmdDataFlowContext; +import com.java110.core.event.cmd.Cmd; +import com.java110.core.event.cmd.CmdEvent; +import com.java110.user.dao.property.IAttendanceRecordV1ServiceDao; +import com.java110.utils.exception.CmdException; +import com.java110.vo.ResultVo; +import org.springframework.beans.factory.annotation.Autowired; + +import java.text.ParseException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Java110Cmd(serviceCode = "property.queryAttendanceStats") +public class QueryAttendanceStatsCmd extends Cmd { + + @Autowired + private IAttendanceRecordV1ServiceDao attendanceRecordV1ServiceDao; + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Override + public void validate(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException, ParseException { + String startTime = reqJson.getString("startTime"); + String endTime = reqJson.getString("endTime"); + if (startTime == null || startTime.isEmpty()) { + throw new CmdException("开始时间不能为空"); + } + if (endTime == null || endTime.isEmpty()) { + throw new CmdException("结束时间不能为空"); + } + } + + @Override + public void doCmd(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException, ParseException { + String storeId = CmdContextUtils.getStoreId(context); + String storeTypeCd = CmdContextUtils.getStoreTypeCd(context); + + Map params = buildQueryParams(reqJson, storeId); + String startTime = (String) params.get("startTime"); + String endTime = (String) params.get("endTime"); + + // 1. 按用户聚合统计(u_user 主表,LEFT JOIN 打卡记录,含无打卡员工) + List userStatsList = attendanceRecordV1ServiceDao.queryAttendanceStats(params); + + // 2. 异常打卡明细(SQL 直接检测:有上班无下班 / 有下班无上班) + List anomalyRecords = attendanceRecordV1ServiceDao.queryAttendanceRecordsForStats(params); + + // 3. 应出勤天数(查询时间范围内的总天数,按月即自然月) + int totalWorkdays = countDays(startTime, endTime); + + // 4. 按 user_id 汇总异常 + Map> anomalyMap = new HashMap<>(); + for (Map a : anomalyRecords) { + String uid = (String) a.get("user_id"); + anomalyMap.putIfAbsent(uid, new ArrayList<>()); + Map detail = new HashMap(); + detail.put("date", a.get("punch_date")); + detail.put("type", a.get("lack_desc")); + anomalyMap.get(uid).add(detail); + } + + // 5. 组装结果 + List resultList = new ArrayList<>(); + int totalShouldDays = 0, totalActualDays = 0, totalPunches = 0; + int totalAnomalyDays = 0, totalAbsenceDays = 0; + + for (Map stat : userStatsList) { + String userId = (String) stat.get("user_id"); + int actualDays = parseInt(stat.get("actual_days")); + int shouldDays = totalWorkdays; + + List userAnomalies = anomalyMap.getOrDefault(userId, new ArrayList<>()); + int anomalyCount = userAnomalies.size(); + int absenceCount = Math.max(0, shouldDays - actualDays); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < userAnomalies.size(); i++) { + if (i > 0) sb.append(";"); + Map an = userAnomalies.get(i); + sb.append(an.get("date")).append(" ").append(an.get("type")); + } + + Map result = new HashMap(); + result.put("userId", userId); + result.put("userName", stat.getOrDefault("user_name", "")); + result.put("address", stat.getOrDefault("address", "")); + result.put("shouldDays", shouldDays); + result.put("actualDays", actualDays); + result.put("totalPunches", parseInt(stat.get("total_punches"))); + result.put("onPunches", parseInt(stat.get("on_punches"))); + result.put("offPunches", parseInt(stat.get("off_punches"))); + result.put("anomalyDays", anomalyCount); + result.put("anomalyDetail", sb.toString()); + result.put("absenceDays", absenceCount); + + resultList.add(result); + + totalShouldDays += shouldDays; + totalActualDays += actualDays; + totalPunches += parseInt(stat.get("total_punches")); + totalAnomalyDays += anomalyCount; + totalAbsenceDays += absenceCount; + } + + // 6. 每日趋势 + List dailyStats = attendanceRecordV1ServiceDao.queryDailyAttendanceStats(params); + + // 7. 汇总卡片 + Map summary = new HashMap(); + summary.put("totalUsers", userStatsList.size()); + summary.put("totalShouldDays", totalShouldDays); + summary.put("totalActualDays", totalActualDays); + summary.put("totalPunches", totalPunches); + summary.put("totalAnomalyDays", totalAnomalyDays); + summary.put("totalAbsenceDays", totalAbsenceDays); + summary.put("totalWorkdays", totalWorkdays); + + // 8. 项目维度(仅物业) + List projectStats = new ArrayList<>(); + if ("800900000003".equals(storeTypeCd)) { + projectStats = attendanceRecordV1ServiceDao.queryProjectAttendanceStats(params); + } + + Map response = new HashMap(); + response.put("summary", summary); + response.put("dailyStats", dailyStats); + response.put("userStats", resultList); + response.put("projectStats", projectStats); + response.put("total", resultList.size()); + + context.setResponseEntity(ResultVo.createResponseEntity(resultList.size(), resultList.size(), response)); + } + + private Map buildQueryParams(JSONObject reqJson, String storeId) { + Map params = new HashMap<>(); + params.put("startTime", reqJson.getString("startTime")); + params.put("endTime", reqJson.getString("endTime")); + if (reqJson.containsKey("filterUserId") && !reqJson.getString("filterUserId").isEmpty()) { + params.put("userId", reqJson.getString("filterUserId")); + } + if (storeId != null && !storeId.isEmpty()) { + params.put("storeId", storeId); + } + return params; + } + + private int countDays(String startTime, String endTime) { + try { + String sd = startTime.length() >= 10 ? startTime.substring(0, 10) : startTime; + String ed = endTime.length() >= 10 ? endTime.substring(0, 10) : endTime; + LocalDate start = LocalDate.parse(sd, DATE_FMT); + LocalDate end = LocalDate.parse(ed, DATE_FMT); + return (int) java.time.temporal.ChronoUnit.DAYS.between(start, end) + 1; + } catch (Exception e) { + return 0; + } + } + + private int parseInt(Object value) { + if (value == null) return 0; + if (value instanceof Integer) return (Integer) value; + if (value instanceof Long) return ((Long) value).intValue(); + if (value instanceof java.math.BigDecimal) return ((java.math.BigDecimal) value).intValue(); + if (value instanceof Double) return ((Double) value).intValue(); + if (value instanceof String) { + try { return Integer.parseInt((String) value); } catch (NumberFormatException e) { return 0; } + } + return 0; + } +} diff --git a/service-user/src/main/java/com/java110/user/cmd/property/QueryTodayAttendanceDetailCmd.java b/service-user/src/main/java/com/java110/user/cmd/property/QueryTodayAttendanceDetailCmd.java index a005a22..3976a1b 100644 --- a/service-user/src/main/java/com/java110/user/cmd/property/QueryTodayAttendanceDetailCmd.java +++ b/service-user/src/main/java/com/java110/user/cmd/property/QueryTodayAttendanceDetailCmd.java @@ -52,10 +52,13 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { if ("OFF".equals(type)) pd.put("offTime", r.get("punch_time")); } - // 4. 构建全系统员工→store映射 + // 4. 构建全系统员工→store/address映射 Map userStoreMap = new HashMap<>(); + Map userAddressMap = new HashMap<>(); for (Map u : allUsers) { - userStoreMap.put((String) u.get("user_id"), (String) u.get("store_name")); + String uid = (String) u.get("user_id"); + userStoreMap.put(uid, (String) u.get("store_name")); + userAddressMap.put(uid, u.get("address") != null ? (String) u.get("address") : ""); } // 5. 构建结果 @@ -65,6 +68,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { for (Map.Entry e : punchMap.entrySet()) { Map data = e.getValue(); data.put("storeName", userStoreMap.getOrDefault(e.getKey(), "")); + data.put("address", userAddressMap.getOrDefault(e.getKey(), "")); result.add(buildTask(data)); seen.add(e.getKey()); } @@ -76,6 +80,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { m.put("staffId", uid); m.put("staffName", u.get("user_name")); m.put("storeName", u.get("store_name")); + m.put("address", u.get("address") != null ? u.get("address") : ""); m.put("onTime", null); m.put("offTime", null); result.add(buildTask(m)); @@ -91,6 +96,7 @@ public class QueryTodayAttendanceDetailCmd extends Cmd { task.put("staffId", data.get("staffId")); task.put("staffName", data.getOrDefault("staffName", "")); task.put("storeName", data.getOrDefault("storeName", "")); + task.put("address", data.getOrDefault("address", "")); boolean hasOn = data.get("onTime") != null; task.put("state", hasOn ? "30000" : "10000"); diff --git a/service-user/src/main/java/com/java110/user/dao/impl/AttendanceRecordV1ServiceDaoImpl.java b/service-user/src/main/java/com/java110/user/dao/impl/AttendanceRecordV1ServiceDaoImpl.java index 2a9b6cd..fb25403 100644 --- a/service-user/src/main/java/com/java110/user/dao/impl/AttendanceRecordV1ServiceDaoImpl.java +++ b/service-user/src/main/java/com/java110/user/dao/impl/AttendanceRecordV1ServiceDaoImpl.java @@ -53,4 +53,39 @@ public class AttendanceRecordV1ServiceDaoImpl extends BaseServiceDao implements public List queryAllAttendanceUsers(Map params) { return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAllAttendanceUsers", params); } + + @Override + public List queryAttendanceStats(Map params) { + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAttendanceStats", params); + } + + @Override + public int queryAttendanceStatsCount(Map params) { + List result = sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAttendanceStatsCount", params); + if (result == null || result.isEmpty()) return 0; + Object count = result.get(0).get("count"); + if (count instanceof Integer) return (Integer) count; + if (count instanceof Long) return ((Long) count).intValue(); + return Integer.parseInt(count.toString()); + } + + @Override + public List queryDailyAttendanceStats(Map params) { + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryDailyAttendanceStats", params); + } + + @Override + public List queryAttendanceRecordsForStats(Map params) { + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryAttendanceRecordsForStats", params); + } + + @Override + public List queryProjectAttendanceStats(Map params) { + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryProjectAttendanceStats", params); + } + + @Override + public List queryScheduleWorkdays(Map params) { + return sqlSessionTemplate.selectList("AttendanceRecordV1ServiceDaoImpl.queryScheduleWorkdays", params); + } } diff --git a/service-user/src/main/java/com/java110/user/dao/property/IAttendanceRecordV1ServiceDao.java b/service-user/src/main/java/com/java110/user/dao/property/IAttendanceRecordV1ServiceDao.java index 204d385..68f8cca 100644 --- a/service-user/src/main/java/com/java110/user/dao/property/IAttendanceRecordV1ServiceDao.java +++ b/service-user/src/main/java/com/java110/user/dao/property/IAttendanceRecordV1ServiceDao.java @@ -17,4 +17,17 @@ public interface IAttendanceRecordV1ServiceDao { /** 查询所有有打卡记录的用户列表 */ List queryAllAttendanceUsers(Map params); + + /** 打卡统计:按用户聚合 */ + List queryAttendanceStats(Map params); + /** 打卡统计:用户数 */ + int queryAttendanceStatsCount(Map params); + /** 打卡统计:每日出勤人数(图表用) */ + List queryDailyAttendanceStats(Map params); + /** 打卡统计:时间范围内打卡记录明细(用于异常分析) */ + List queryAttendanceRecordsForStats(Map params); + /** 打卡统计:项目维度聚合(物业端) */ + List queryProjectAttendanceStats(Map params); + /** 打卡统计:查询员工排班工作日数 */ + List queryScheduleWorkdays(Map params); } diff --git a/service-user/src/main/resources/mapper/property/AttendanceRecordMapper.xml b/service-user/src/main/resources/mapper/property/AttendanceRecordMapper.xml index a51ad78..345b522 100644 --- a/service-user/src/main/resources/mapper/property/AttendanceRecordMapper.xml +++ b/service-user/src/main/resources/mapper/property/AttendanceRecordMapper.xml @@ -52,12 +52,118 @@ + + + + + + + + + + + + + + + + + + diff --git a/springboot/src/main/java/com/java110/boot/BootApplicationStart.java b/springboot/src/main/java/com/java110/boot/BootApplicationStart.java index b5f90f2..f5c6e1a 100644 --- a/springboot/src/main/java/com/java110/boot/BootApplicationStart.java +++ b/springboot/src/main/java/com/java110/boot/BootApplicationStart.java @@ -263,4 +263,4 @@ public class BootApplicationStart { } } -} \ No newline at end of file +}