111
tjx
21 小时以前 8f1efdcd51a0a75d3b19704d1487502bf1b60942
111
已修改3个文件
657 ■■■■■ 文件已修改
src/main/java/com/gs/dingtalk/service/WorkWXService.java 424 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/gs/dingtalk/service/impl/QwCheckinDataServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/gs/dingtalk/DeviceReceivingApplicationTests.java 229 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/gs/dingtalk/service/WorkWXService.java
@@ -314,6 +314,293 @@
        return allCheckinData;
    }
    /**
     * 获取打卡日报数据
     * 接口调用频率限制为100次/分钟
     * @param startTime 开始时间戳(秒)
     * @param endTime 结束时间戳(秒)
     * @param useridList 用户ID列表
     * @return 打卡日报数据列表
     */
    public List<CheckinDayData> getCheckinDayData(long startTime, long endTime, List<String> useridList) throws IOException {
        String accessToken = getAccessToken();
        String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckin_daydata?access_token=%s", accessToken);
        List<CheckinDayData> allDayData = new ArrayList<>();
        int batchSize = 100;
        int totalUsers = useridList.size();
        int batchCount = (totalUsers + batchSize - 1) / batchSize;
        log.info("开始获取打卡日报数据,总用户数: {}, 分批数: {}, 时间范围: {} - {}", totalUsers, batchCount, startTime, endTime);
        for (int i = 0; i < batchCount; i++) {
            int fromIndex = i * batchSize;
            int toIndex = Math.min((i + 1) * batchSize, totalUsers);
            List<String> batchUserList = useridList.subList(fromIndex, toIndex);
            log.info("正在获取第 {}/{} 批打卡日报数据,用户数: {}", i + 1, batchCount, batchUserList.size());
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("starttime", startTime);
            requestBody.put("endtime", endTime);
            requestBody.put("useridlist", batchUserList);
            MediaType mediaType = MediaType.parse("application/json; charset=UTF-8");
            String jsonBody = objectMapper.writeValueAsString(requestBody);
            RequestBody body = RequestBody.create(mediaType, jsonBody);
            Request request = new Request.Builder()
                    .url(url)
                    .post(body)
                    .addHeader("Content-Type", "application/json; charset=UTF-8")
                    .build();
            try (Response response = client.newCall(request).execute()) {
                if (!response.isSuccessful()) {
                    log.error("获取打卡日报数据失败,HTTP状态码: {}", response.code());
                    throw new IOException("获取打卡日报数据失败: " + response.message());
                }
                String responseBody = response.body().string();
                WorkWXCheckinDayDataResponse dayDataResponse = objectMapper.readValue(responseBody, WorkWXCheckinDayDataResponse.class);
                if (dayDataResponse.getErrcode() != 0) {
                    log.error("获取打卡日报数据失败,错误码: {}, 错误信息: {}",
                            dayDataResponse.getErrcode(), dayDataResponse.getErrmsg());
                    throw new IOException("获取打卡日报数据失败: " + dayDataResponse.getErrmsg());
                }
                if (dayDataResponse.getDatas() != null) {
                    allDayData.addAll(dayDataResponse.getDatas());
                    log.info("第 {}/{} 批获取到打卡日报记录数: {}", i + 1, batchCount, dayDataResponse.getDatas().size());
                }
            }
            // 接口限制100次/分钟,批次间等待600ms确保不超限
            if (i < batchCount - 1) {
                try {
                    Thread.sleep(600);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    log.warn("批次间等待被中断");
                }
            }
        }
        log.info("打卡日报数据获取完成,总记录数: {}", allDayData.size());
        return allDayData;
    }
    /**
     * 根据QW_STAFF表获取打卡日报数据
     */
    public List<CheckinDayData> getCheckinDayDataByQwStaff(long startTime, long endTime) throws IOException {
        List<QwStaff> qwStaffList = qwStaffMapper.selectList(new LambdaQueryWrapper<QwStaff>());
        if (qwStaffList == null || qwStaffList.isEmpty()) {
            log.warn("QW_STAFF表中没有数据");
            return new ArrayList<>();
        }
        List<String> useridList = qwStaffList.stream()
                .map(QwStaff::getAccount)
                .filter(account -> account != null && !account.isEmpty())
                .collect(Collectors.toList());
        if (useridList.isEmpty()) {
            log.warn("QW_STAFF表中没有有效的account数据");
            return new ArrayList<>();
        }
        log.info("从QW_STAFF表获取到 {} 个用户account", useridList.size());
        return getCheckinDayData(startTime, endTime, useridList);
    }
    //获取打卡日报数据
    //请求方式: POST(HTTPS)
    //请求地址: https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckin_daydata?access_token=ACCESS_TOKEN
    //参数示例
    // {
    //    "starttime": 1599062400,
    //    "endtime": 1599062400,
    //    "useridlist": [
    //        "ZhangSan"
    //    ]
    //}
    //接口调用频率限制为100次/分钟。
    //返回结果
    //{
    //    "errcode":0,
    //    "errmsg":"ok",
    //    "datas":[
    //        {
    //            "base_info":{
    //                "date":1599062400,
    //                "record_type":1,
    //                "name":"张三",
    //                "name_ex":"Three Zhang",
    //                "departs_name":"有家企业/realempty;有家企业;有家企业/部门A4",
    //                "acctid":"ZhangSan",
    //                "rule_info":{
    //                    "groupid":10,
    //                    "groupname":"规则测试",
    //                    "scheduleid":0,
    //                    "schedulename":"",
    //                    "checkintime":[
    //                        {
    //                            "work_sec":38760,
    //                            "off_work_sec":38880
    //                        }
    //                    ]
    //                },
    //                "day_type":0
    //            },
    //            "summary_info":{
    //                "checkin_count":2,
    //                "regular_work_sec":31,
    //                "standard_work_sec":120,
    //                "earliest_time":38827,
    //                "lastest_time":38858
    //            },
    //            "holiday_infos":[
    //                {
    //                    "sp_description":{
    //                        "data":[
    //                            {
    //                                "lang":"zh_CN",
    //                                "text":"09/03 10:00~09/03 10:01"
    //                            }
    //                        ]
    //                    },
    //                    "sp_number":"202009030002",
    //                    "sp_title":{
    //                        "data":[
    //                            {
    //                                "lang":"zh_CN",
    //                                "text":"请假0.1小时"
    //                            }
    //                        ]
    //                    }
    //                },
    //                {
    //                    "sp_description":{
    //                        "data":[
    //                            {
    //                                "lang":"zh_CN",
    //                                "text":"08/25 14:37~09/10 14:37"
    //                            }
    //                        ]
    //                    },
    //                    "sp_number":"202008270004",
    //                    "sp_title":{
    //                        "data":[
    //                            {
    //                                "lang":"zh_CN",
    //                                "text":"加班17.0小时"
    //                            }
    //                        ]
    //                    }
    //                }
    //            ],
    //            "exception_infos":[
    //                {
    //                    "count":1,
    //                    "duration":60,
    //                    "exception":1
    //                },
    //                {
    //                    "count":1,
    //                    "duration":60,
    //                    "exception":2
    //                }
    //            ],
    //            "ot_info":{
    //                "ot_status":1,
    //                "ot_duration":3600,
    //                "exception_duration":[],
    //                "workday_over_as_money": 54000
    //            },
    //            "sp_items":[
    //                {
    //                    "count":1,
    //                    "duration":360,
    //                    "time_type":0,
    //                    "type":1,
    //                    "vacation_id":2,
    //                    "name":"年假"
    //                },
    //                {
    //                    "count":0,
    //                    "duration":0,
    //                    "time_type":0,
    //                    "type":100,
    //                    "vacation_id":0,
    //                    "name":"外勤次数"
    //                }
    //            ]
    //        }
    //    ]
    //}
    //参数说明:
    //errcode    int32    返回码
    //errmsg    string    错误码描述
    //datas    obj[]    日报数据列表
    //datas.base_info    obj    基础信息
    //datas.base_info.date    uint32    日报日期
    //datas.base_info.record_type    uint32    记录类型:1-固定上下班;2-外出(此报表中不会出现外出打卡数据);3-按班次上下班;4-自由签到;5-加班;7-无规则
    //datas.base_info.name    string    打卡人员姓名
    //datas.base_info.name_ex    string    打卡人员别名
    //datas.base_info.departs_name    string    打卡人员所在部门,会显示所有所在部门
    //datas.base_info.acctid    string    打卡人员账号,即userid
    //datas.base_info.rule_info    obj    打卡人员所属规则信息
    //datas.base_info.rule_info.groupid    int32    所属规则的id
    //datas.base_info.rule_info.groupname    string    打卡规则名
    //datas.base_info.rule_info.scheduleid    int32    当日所属班次id,仅按班次上下班才有值,显示在打卡日报-班次列
    //datas.base_info.rule_info.schedulename    string    当日所属班次名称,仅按班次上下班才有值,显示在打卡日报-班次列
    //datas.base_info.rule_info.checkintime    obj[]    当日打卡时间,仅固定上下班规则有值,显示在打卡日报-班次列
    //datas.base_info.rule_info.checkintime.work_sec    uint32    上班时间,为距离0点的时间差
    //datas.base_info.rule_info.checkintime.off_work_sec    uint32    下班时间,为距离0点的时间差
    //datas.base_info.day_type    uint32    日报类型:0-工作日日报;1-休息日日报
    //datas.summary_info    obj    汇总信息
    //datas.summary_info.checkin_count    int32    当日打卡次数
    //datas.summary_info.regular_work_sec    int32    当日实际工作时长,单位:秒
    //datas.summary_info.standard_work_sec    int32    当日标准工作时长,单位:秒
    //datas.summary_info.earliest_time    int32    当日最早打卡时间
    //datas.summary_info.lastest_time    int32    当日最晚打卡时间
    //datas.holiday_infos    obj[]    假勤相关信息
    //datas.holiday_infos.sp_number    string    假勤申请id,即当日关联的假勤审批单id
    //datas.holiday_infos.sp_title    obj    假勤信息摘要-标题信息
    //datas.holiday_infos.sp_title.data    obj[]    多种语言描述,目前只有中文一种
    //datas.holiday_infos.sp_title.data.text    string    假勤信息摘要-标题文本
    //datas.holiday_infos.sp_title.data.lang    string    语言类型:"zh_CN"
    //datas.holiday_infos.sp_description    obj    假勤信息摘要-描述信息
    //datas.holiday_infos.sp_description.data    obj[]    多种语言描述,目前只有中文一种
    //datas.holiday_infos.sp_description.data.text    string    假勤信息摘要-描述文本
    //datas.holiday_infos.sp_description.data.lang    string    语言类型:"zh_CN"
    //datas.exception_infos    obj[]    校准状态信息
    //datas.exception_infos.exception    uint32    校准状态类型:1-迟到;2-早退;3-缺卡;4-旷工;5-地点异常;6-设备异常
    //datas.exception_infos.count    int32    当日此异常的次数
    //datas.exception_infos.duration    int32    当日此异常的时长(迟到/早退/旷工才有值)
    //datas.ot_info    obj    加班信息
    //datas.ot_info.ot_status    uint32    状态:0-无加班;1-正常;2-缺时长
    //datas.ot_info.ot_duration    uint32    加班时长
    //datas.ot_info.exception_duration    uint32[]    ot_status为2下,加班不足的时长
    //datas.ot_info.workday_over_as_vacation    int32    工作日加班记为调休,单位秒
    //datas.ot_info.workday_over_as_money    int32    工作日加班记为加班费,单位秒
    //datas.ot_info.restday_over_as_vacation    int32    休息日加班记为调休,单位秒
    //datas.ot_info.restday_over_as_money    int32    休息日加班记为加班费,单位秒
    //datas.ot_info.holiday_over_as_vacation    int32    节假日加班记为调休,单位秒
    //datas.ot_info.holiday_over_as_money    int32    节假日加班记为加班费,单位秒
    //datas.sp_items    obj[]    假勤统计信息
    //datas.sp_items.type    uint32    类型:1-请假;2-补卡;3-出差;4-外出;15-审批打卡;100-外勤
    //datas.sp_items.vacation_id    uint32    具体请假类型,当type为1请假时,具体的请假类型id,可通过审批相关接口获取假期详情
    //datas.sp_items.count    uint32    当日假勤次数
    //datas.sp_items.duration    uint32    当日假勤时长秒数,时长单位为天直接除以86400即为天数,单位为小时直接除以3600即为小时数
    //datas.sp_items.time_type    uint32    时长单位:0-按天 1-按小时
    //datas.sp_items.name    string    统计项名称
    @Data
    private static class WorkWXTokenResponse {
        private Integer errcode;
@@ -371,6 +658,143 @@
        private List<CheckinData> checkindata;
    }
    // ==================== 打卡日报数据响应结构 ====================
    @Data
    private static class WorkWXCheckinDayDataResponse {
        private Integer errcode;
        private String errmsg;
        private List<CheckinDayData> datas;
    }
    @Data
    public static class CheckinDayData {
        @JsonProperty("base_info")
        private BaseInfo baseInfo;
        @JsonProperty("summary_info")
        private SummaryInfo summaryInfo;
        @JsonProperty("holiday_infos")
        private List<HolidayInfo> holidayInfos;
        @JsonProperty("exception_infos")
        private List<ExceptionInfo> exceptionInfos;
        @JsonProperty("ot_info")
        private OtInfo otInfo;
        @JsonProperty("sp_items")
        private List<SpItem> spItems;
    }
    @Data
    public static class BaseInfo {
        private Long date;
        @JsonProperty("record_type")
        private Integer recordType;
        private String name;
        @JsonProperty("name_ex")
        private String nameEx;
        @JsonProperty("departs_name")
        private String departsName;
        private String acctid;
        @JsonProperty("rule_info")
        private RuleInfo ruleInfo;
        @JsonProperty("day_type")
        private Integer dayType;
    }
    @Data
    public static class RuleInfo {
        private Integer groupid;
        private String groupname;
        private Integer scheduleid;
        private String schedulename;
        private List<CheckinTime> checkintime;
    }
    @Data
    public static class CheckinTime {
        @JsonProperty("work_sec")
        private Integer workSec;
        @JsonProperty("off_work_sec")
        private Integer offWorkSec;
    }
    @Data
    public static class SummaryInfo {
        @JsonProperty("checkin_count")
        private Integer checkinCount;
        @JsonProperty("regular_work_sec")
        private Integer regularWorkSec;
        @JsonProperty("standard_work_sec")
        private Integer standardWorkSec;
        @JsonProperty("earliest_time")
        private Integer earliestTime;
        @JsonProperty("lastest_time")
        private Integer lastestTime;
    }
    @Data
    public static class HolidayInfo {
        @JsonProperty("sp_number")
        private String spNumber;
        @JsonProperty("sp_title")
        private LangData spTitle;
        @JsonProperty("sp_description")
        private LangData spDescription;
    }
    @Data
    public static class LangData {
        private List<LangText> data;
    }
    @Data
    public static class LangText {
        private String lang;
        private String text;
    }
    @Data
    public static class ExceptionInfo {
        private Integer exception;  // 1-迟到;2-早退;3-缺卡;4-旷工;5-地点异常;6-设备异常
        private Integer count;
        private Integer duration;
    }
    @Data
    public static class OtInfo {
        @JsonProperty("ot_status")
        private Integer otStatus;  // 0-无加班;1-正常;2-缺时长
        @JsonProperty("ot_duration")
        private Integer otDuration;
        @JsonProperty("exception_duration")
        private List<Integer> exceptionDuration;
        @JsonProperty("workday_over_as_vacation")
        private Integer workdayOverAsVacation;
        @JsonProperty("workday_over_as_money")
        private Integer workdayOverAsMoney;
        @JsonProperty("restday_over_as_vacation")
        private Integer restdayOverAsVacation;
        @JsonProperty("restday_over_as_money")
        private Integer restdayOverAsMoney;
        @JsonProperty("holiday_over_as_vacation")
        private Integer holidayOverAsVacation;
        @JsonProperty("holiday_over_as_money")
        private Integer holidayOverAsMoney;
    }
    @Data
    public static class SpItem {
        private Integer type;  // 1-请假;2-补卡;3-出差;4-外出;15-审批打卡;100-外勤
        @JsonProperty("vacation_id")
        private Integer vacationId;
        private Integer count;
        private Integer duration;
        @JsonProperty("time_type")
        private Integer timeType;  // 0-按天 1-按小时
        private String name;
    }
    // ==================== 打卡原始数据结构 ====================
    @Data
    public static class CheckinData {
        private String userid;
src/main/java/com/gs/dingtalk/service/impl/QwCheckinDataServiceImpl.java
@@ -21,6 +21,7 @@
 */
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class QwCheckinDataServiceImpl extends ServiceImpl<QwCheckinDataMapper, QwCheckinData>
        implements QwCheckinDataService {
@@ -85,7 +86,6 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int saveCheckinDataBatch(List<WorkWXService.CheckinData> checkinDataList) {
        if (checkinDataList == null || checkinDataList.isEmpty()) {
            return 0;
@@ -112,7 +112,6 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int syncCheckinData(long startTime, long endTime) throws IOException {
        log.info("开始同步打卡数据,时间范围: {} - {}", new Date(startTime * 1000), new Date(endTime * 1000));
@@ -128,7 +127,6 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int syncYesterdayCheckinData() throws IOException {
        long currentTime = System.currentTimeMillis() / 1000;
        long oneDaySeconds = 86400;
src/test/java/com/gs/dingtalk/DeviceReceivingApplicationTests.java
@@ -72,68 +72,6 @@
        // 4. 标准打卡时间只对于固定排班和自定义排班两种类型有效
        // 5. 接口调用频率限制为600次/分钟(已在Service层实现批次间延迟)
        //接口返回的原始数据
        //{
        //   "errcode":0,
        //   "errmsg":"ok",
        //   "checkindata": [{
        //        "userid" : "james",
        //        "groupname" : "打卡一组",
        //        "checkin_type" : "上班打卡",
        //        "exception_type" : "地点异常",
        //        "checkin_time" : 1492617610,
        //        "location_title" : "依澜府",
        //        "location_detail" : "四川省成都市武侯区益州大道中段784号附近",
        //        "wifiname" : "办公一区",
        //        "notes" : "路上堵车,迟到了5分钟",
        //        "wifimac" : "3c:46:d8:0c:7a:70",
        //        "mediaids":["WWCISP_G8PYgRaOVHjXWUWFqchpBqqqUpGj0OyR9z6WTwhnMZGCPHxyviVstiv_2fTG8YOJq8L8zJT2T2OvTebANV-2MQ"],
        //        "sch_checkin_time" : 1492617610,
        //        "groupid" : 1,
        //        "schedule_id" : 0,
        //        "timeline_id" : 2
        //    },{
        //        "userid" : "paul",
        //        "groupname" : "打卡二组",
        //        "checkin_type" : "外出打卡",
        //        "exception_type" : "时间异常",
        //        "checkin_time" : 1492617620,
        //        "location_title" : "重庆出口加工区",
        //        "location_detail" : "重庆市渝北区金渝大道101号金渝大道",
        //        "wifiname" : "办公室二区",
        //        "notes" : "",
        //        "wifimac" : "3c:46:d8:0c:7a:71",
        //        "mediaids":["WWCISP_G8PYgRaOVHjXWUWFqchpBqqqUpGj0OyR9z6WTwhnMZGCPHxyviVstiv_2fTG8YOJq8L8zJT2T2OvTebANV-2MQ"],
        //        "lat": 30547645,
        //        "lng": 104063236,
        //        "deviceid":"E5FA89F6-3926-4972-BE4F-4A7ACF4701E2",
        //        "sch_checkin_time" : 1492617610,
        //        "groupid" : 2,
        //        "schedule_id" : 3,
        //        "timeline_id" : 1
        //    }]
        //}
        //checkindata的字段说明
        //userid    用户id
        //groupname    打卡规则名称
        //checkin_type    打卡类型。字符串,目前有:上班打卡,下班打卡,外出打卡,仅记录打卡时间和位置
        //exception_type    异常类型,字符串,包括:时间异常,地点异常,未打卡,wifi异常,非常用设备。如果有多个异常,以分号间隔
        //checkin_time    打卡时间。Unix时间戳
        //location_title    打卡地点title
        //location_detail    打卡地点详情
        //wifiname    打卡wifi名称
        //notes    打卡备注
        //wifimac    打卡的MAC地址/bssid
        //mediaids    打卡的附件media_id,可使用media/get获取附件
        //lat    位置打卡地点纬度,是实际纬度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
        //lng    位置打卡地点经度,是实际经度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
        //deviceid    打卡设备id
        //sch_checkin_time    标准打卡时间,指此次打卡时间对应的标准上班时间或标准下班时间
        //groupid    规则id,表示打卡记录所属规则的id
        //schedule_id    班次id,表示打卡记录所属规则中,所属班次的id
        //timeline_id    时段id,表示打卡记录所属规则中,某一班次中的某一时段的id,如上下班时间为9:00-12:00、13:00-18:00的班次中,9:00-12:00为其中一组时段
        try {
            long currentTime = System.currentTimeMillis() / 1000;
            long oneDaySeconds = 86400;
@@ -298,4 +236,171 @@
        System.out.println("=== 测试结束 ===");
    }
    /**
     * 测试获取打卡日报数据
     * 接口限制:100次/分钟,时间跨度不超过30天
     */
    @Test
    void testGetCheckinDayData() {
        System.out.println("=== 开始测试获取打卡日报数据 ===");
        try {
            long currentTime = System.currentTimeMillis() / 1000;
            long oneDaySeconds = 86400;
            // 获取昨天的日报数据(starttime和endtime需要是同一天)
            long yesterdayStart = ((currentTime / oneDaySeconds) - 1) * oneDaySeconds;
            long startTime = yesterdayStart;
            long endTime = yesterdayStart;  // 日报接口starttime和endtime需相同
            System.out.println("  - 查询日期: " + new java.util.Date(startTime * 1000));
            // 获取用户总数
            long totalUsers = qwStaffMapper.selectCount(new LambdaQueryWrapper<QwStaff>()
                    .isNotNull(QwStaff::getAccount)
                    .ne(QwStaff::getAccount, ""));
            System.out.println("  - QW_STAFF表用户总数: " + totalUsers);
            List<WorkWXService.CheckinDayData> dayDataList = workWXService.getCheckinDayDataByQwStaff(startTime, endTime);
            if (dayDataList != null && !dayDataList.isEmpty()) {
                System.out.println("✓ 成功获取打卡日报数据");
                System.out.println("  - 日报记录总数: " + dayDataList.size());
                System.out.println("  - 前5条数据:");
                dayDataList.stream().limit(5).forEach(data -> {
                    WorkWXService.BaseInfo baseInfo = data.getBaseInfo();
                    WorkWXService.SummaryInfo summaryInfo = data.getSummaryInfo();
                    System.out.println("    * 姓名: " + (baseInfo != null ? baseInfo.getName() : "N/A") +
                            ", 账号: " + (baseInfo != null ? baseInfo.getAcctid() : "N/A") +
                            ", 部门: " + (baseInfo != null ? baseInfo.getDepartsName() : "N/A") +
                            ", 打卡次数: " + (summaryInfo != null ? summaryInfo.getCheckinCount() : 0) +
                            ", 实际工时(秒): " + (summaryInfo != null ? summaryInfo.getRegularWorkSec() : 0) +
                            ", 标准工时(秒): " + (summaryInfo != null ? summaryInfo.getStandardWorkSec() : 0));
                    // 打印异常信息
                    if (data.getExceptionInfos() != null && !data.getExceptionInfos().isEmpty()) {
                        data.getExceptionInfos().forEach(ex -> {
                            String exType = switch (ex.getException()) {
                                case 1 -> "迟到";
                                case 2 -> "早退";
                                case 3 -> "缺卡";
                                case 4 -> "旷工";
                                case 5 -> "地点异常";
                                case 6 -> "设备异常";
                                default -> "未知";
                            };
                            System.out.println("      异常: " + exType + ", 次数: " + ex.getCount() + ", 时长(秒): " + ex.getDuration());
                        });
                    }
                });
            } else {
                System.out.println("✗ 获取的打卡日报数据为空");
            }
        } catch (IOException e) {
            System.out.println("✗ 获取打卡日报数据失败: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("=== 测试结束 ===");
    }
    /**
     * 测试获取指定员工的打卡日报数据
     */
    @Test
    void testGetCheckinDayDataById() {
        System.out.println("=== 开始测试获取指定员工打卡日报数据 ===");
        QwStaff qwStaff = qwStaffMapper.selectById(3);
        if (qwStaff == null || qwStaff.getAccount() == null || qwStaff.getAccount().isEmpty()) {
            System.out.println("✗ 未找到ID为3的员工或员工account为空");
            return;
        }
        System.out.println("  - 员工姓名: " + qwStaff.getName());
        System.out.println("  - 员工账号: " + qwStaff.getAccount());
        try {
            long currentTime = System.currentTimeMillis() / 1000;
            long oneDaySeconds = 86400;
            // 获取昨天的日报
            long yesterdayStart = ((currentTime / oneDaySeconds) - 1) * oneDaySeconds;
            long startTime = yesterdayStart;
            long endTime = yesterdayStart;
            System.out.println("  - 查询日期: " + new java.util.Date(startTime * 1000));
            List<String> useridList = new java.util.ArrayList<>();
            useridList.add(qwStaff.getAccount());
            List<WorkWXService.CheckinDayData> dayDataList = workWXService.getCheckinDayData(startTime, endTime, useridList);
            if (dayDataList != null && !dayDataList.isEmpty()) {
                System.out.println("✓ 成功获取打卡日报数据");
                dayDataList.forEach(data -> {
                    WorkWXService.BaseInfo baseInfo = data.getBaseInfo();
                    WorkWXService.SummaryInfo summaryInfo = data.getSummaryInfo();
                    System.out.println("  基础信息:");
                    System.out.println("    - 姓名: " + (baseInfo != null ? baseInfo.getName() : "N/A"));
                    System.out.println("    - 部门: " + (baseInfo != null ? baseInfo.getDepartsName() : "N/A"));
                    System.out.println("    - 规则: " + (baseInfo != null && baseInfo.getRuleInfo() != null ? baseInfo.getRuleInfo().getGroupname() : "N/A"));
                    System.out.println("  汇总信息:");
                    System.out.println("    - 打卡次数: " + (summaryInfo != null ? summaryInfo.getCheckinCount() : 0));
                    System.out.println("    - 实际工时: " + (summaryInfo != null ? formatSeconds(summaryInfo.getRegularWorkSec()) : "0"));
                    System.out.println("    - 标准工时: " + (summaryInfo != null ? formatSeconds(summaryInfo.getStandardWorkSec()) : "0"));
                    if (data.getExceptionInfos() != null && !data.getExceptionInfos().isEmpty()) {
                        System.out.println("  异常信息:");
                        data.getExceptionInfos().forEach(ex -> {
                            String exType = switch (ex.getException()) {
                                case 1 -> "迟到";
                                case 2 -> "早退";
                                case 3 -> "缺卡";
                                case 4 -> "旷工";
                                case 5 -> "地点异常";
                                case 6 -> "设备异常";
                                default -> "未知";
                            };
                            System.out.println("    - " + exType + ": " + ex.getCount() + "次, 时长: " + formatSeconds(ex.getDuration()));
                        });
                    }
                    if (data.getOtInfo() != null && data.getOtInfo().getOtStatus() != null && data.getOtInfo().getOtStatus() > 0) {
                        System.out.println("  加班信息:");
                        System.out.println("    - 加班时长: " + formatSeconds(data.getOtInfo().getOtDuration()));
                    }
                });
            } else {
                System.out.println("✗ 该员工在指定日期无打卡日报数据");
            }
        } catch (IOException e) {
            System.out.println("✗ 获取打卡日报数据失败: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("=== 测试结束 ===");
    }
    /**
     * 格式化秒数为时分秒
     */
    private String formatSeconds(Integer seconds) {
        if (seconds == null || seconds == 0) {
            return "0秒";
        }
        int hours = seconds / 3600;
        int minutes = (seconds % 3600) / 60;
        int secs = seconds % 60;
        StringBuilder sb = new StringBuilder();
        if (hours > 0) sb.append(hours).append("小时");
        if (minutes > 0) sb.append(minutes).append("分钟");
        if (secs > 0) sb.append(secs).append("秒");
        return sb.toString();
    }
}