QueryAttendanceStatsCmd.java 8.13 KB
/*
 * 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<Map> userStatsList = attendanceRecordV1ServiceDao.queryAttendanceStats(params);

        // 2. 异常打卡明细(SQL 直接检测:有上班无下班 / 有下班无上班)
        List<Map> anomalyRecords = attendanceRecordV1ServiceDao.queryAttendanceRecordsForStats(params);

        // 3. 应出勤天数(查询时间范围内的总天数,按月即自然月)
        int totalWorkdays = countDays(startTime, endTime);

        // 4. 按 user_id 汇总异常
        Map<String, List<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<Map> 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<Map> 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<Map> 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<Map> 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;
    }
}