| | |
| | | } |
| | | } |
| | | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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") |