/* * 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; } }