111
tjx
昨天 8e3309ef57424194ce9683175b59d3c9e8cb0b27
src/main/java/com/gs/dingtalk/service/WorkWXService.java
@@ -212,6 +212,37 @@
        }
    }
    public int syncUsersToQwStaff() throws IOException {
        List<WorkWXUser> userList = getUserList();
        if (userList == null || userList.isEmpty()) {
            log.warn("获取到的用户列表为空");
            return 0;
        }
        int insertCount = 0;
        for (WorkWXUser user : userList) {
            if (user.getUserid() == null || user.getUserid().isEmpty()) {
                continue;
            }
            qwStaffMapper.delete(
                new LambdaQueryWrapper<QwStaff>().eq(QwStaff::getAccount, user.getUserid())
            );
            QwStaff qwStaff = new QwStaff();
            qwStaff.setName(user.getName());
            qwStaff.setAccount(user.getUserid());
            qwStaff.setDept(user.getDepartment() != null ? user.getDepartment().toString() : null);
            qwStaffMapper.insert(qwStaff);
            insertCount++;
        }
        log.info("同步用户到QW_STAFF表完成,同步用户数: {}", insertCount);
        return insertCount;
    }
    public List<CheckinData> getCheckinData(long startTime, long endTime, List<String> useridList) throws IOException {
        String accessToken = getAccessToken();
        String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckindata?access_token=%s", accessToken);
@@ -283,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;
@@ -340,6 +658,144 @@
        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
    @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true)
    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;
@@ -357,7 +813,7 @@
        private String wifiname;
        private String notes;
        private String wifimac;
        private String mediaids;
        private List<String> mediaids;
        private Double lat;
        private Double lng;
        @JsonProperty("sch_checkin_time")