package com.gs.dingtalk.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.gs.dingtalk.config.DataAcquisitionConfiguration; import com.gs.dingtalk.entity.QwStaff; import com.gs.dingtalk.mapper.QwStaffMapper; import lombok.Data; import lombok.RequiredArgsConstructor; import okhttp3.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class WorkWXService { private static final Logger log = LoggerFactory.getLogger(WorkWXService.class); private final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(90, TimeUnit.SECONDS) .readTimeout(90, TimeUnit.SECONDS) .build(); private final ObjectMapper objectMapper = new ObjectMapper(); private final QwStaffMapper qwStaffMapper; public String getAccessToken() throws IOException { String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", DataAcquisitionConfiguration.CORPID, DataAcquisitionConfiguration.CORPSECRET); Request request = new Request.Builder() .url(url) .get() .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { log.error("获取企业微信access_token失败,HTTP状态码: {}", response.code()); throw new IOException("获取access_token失败: " + response.message()); } String responseBody = response.body().string(); WorkWXTokenResponse tokenResponse = objectMapper.readValue(responseBody, WorkWXTokenResponse.class); if (tokenResponse.getErrcode() != 0) { log.error("获取企业微信access_token失败,错误码: {}, 错误信息: {}", tokenResponse.getErrcode(), tokenResponse.getErrmsg()); throw new IOException("获取access_token失败: " + tokenResponse.getErrmsg()); } log.info("成功获取企业微信access_token,有效期: {}秒", tokenResponse.getExpiresIn()); log.info("access_token : {}", tokenResponse.getAccessToken()); return tokenResponse.getAccessToken(); } } public String getContactAccessToken() throws IOException { String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", DataAcquisitionConfiguration.CORPID, DataAcquisitionConfiguration.TXL_CORPSECRET); Request request = new Request.Builder() .url(url) .get() .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { log.error("获取企业微信通讯录access_token失败,HTTP状态码: {}", response.code()); throw new IOException("获取通讯录access_token失败: " + response.message()); } String responseBody = response.body().string(); WorkWXTokenResponse tokenResponse = objectMapper.readValue(responseBody, WorkWXTokenResponse.class); if (tokenResponse.getErrcode() != 0) { log.error("获取企业微信通讯录access_token失败,错误码: {}, 错误信息: {}", tokenResponse.getErrcode(), tokenResponse.getErrmsg()); throw new IOException("获取通讯录access_token失败: " + tokenResponse.getErrmsg()); } log.info("成功获取企业微信通讯录access_token,有效期: {}秒", tokenResponse.getExpiresIn()); return tokenResponse.getAccessToken(); } } public List getUserList() throws IOException { String accessToken = getContactAccessToken(); String url = String.format( "https://qyapi.weixin.qq.com/cgi-bin/user/list_id?access_token=%s", accessToken); List allUsers = new ArrayList<>(); String cursor = null; do { Map requestBody = new HashMap<>(); requestBody.put("limit", 10000); if (cursor != null) { requestBody.put("cursor", cursor); } 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(); log.info("获取用户列表响应: {}", responseBody); WorkWXUserListIdResponse userListResponse = objectMapper.readValue(responseBody, WorkWXUserListIdResponse.class); if (userListResponse.getErrcode() != 0) { log.error("获取企业微信用户列表失败,错误码: {}, 错误信息: {}", userListResponse.getErrcode(), userListResponse.getErrmsg()); throw new IOException("获取用户列表失败: " + userListResponse.getErrmsg()); } if (userListResponse.getDeptUser() != null && !userListResponse.getDeptUser().isEmpty()) { allUsers.addAll(userListResponse.getDeptUser()); } cursor = userListResponse.getNextCursor(); } catch (IOException e) { log.error("解析用户列表响应失败", e); throw e; } } while (cursor != null && !cursor.isEmpty()); log.info("成功获取企业微信用户列表,用户数量: {}", allUsers.size()); return allUsers; } public List getCheckinDataByQwStaff(long startTime, long endTime) throws IOException { List qwStaffList = qwStaffMapper.selectList(new LambdaQueryWrapper()); if (qwStaffList == null || qwStaffList.isEmpty()) { log.warn("QW_STAFF表中没有数据"); return new ArrayList<>(); } List 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 getCheckinData(startTime, endTime, useridList); } public WorkWXUserDetail getUserDetail(String userid) throws IOException { String accessToken = getContactAccessToken(); String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s", accessToken, userid); Request request = new Request.Builder() .url(url) .get() .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(); log.info("获取用户详情响应: {}", responseBody); WorkWXUserDetailResponse userDetailResponse = objectMapper.readValue(responseBody, WorkWXUserDetailResponse.class); if (userDetailResponse.getErrcode() != 0) { log.error("获取用户详情失败,错误码: {}, 错误信息: {}", userDetailResponse.getErrcode(), userDetailResponse.getErrmsg()); throw new IOException("获取用户详情失败: " + userDetailResponse.getErrmsg()); } log.info("成功获取用户 {} 的详情", userid); return userDetailResponse; } } public int syncUsersToQwStaff() throws IOException { List 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().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 getCheckinData(long startTime, long endTime, List useridList) throws IOException { String accessToken = getAccessToken(); String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckindata?access_token=%s", accessToken); List allCheckinData = 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 batchUserList = useridList.subList(fromIndex, toIndex); log.info("正在获取第 {}/{} 批打卡数据,用户数: {}", i + 1, batchCount, batchUserList.size()); Map requestBody = new HashMap<>(); requestBody.put("opencheckindatatype", 3); 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(); WorkWXCheckinResponse checkinResponse = objectMapper.readValue(responseBody, WorkWXCheckinResponse.class); if (checkinResponse.getErrcode() != 0) { log.error("获取打卡数据失败,错误码: {}, 错误信息: {}", checkinResponse.getErrcode(), checkinResponse.getErrmsg()); throw new IOException("获取打卡数据失败: " + checkinResponse.getErrmsg()); } if (checkinResponse.getCheckindata() != null) { allCheckinData.addAll(checkinResponse.getCheckindata()); log.info("第 {}/{} 批获取到打卡记录数: {}", i + 1, batchCount, checkinResponse.getCheckindata().size()); } } if (i < batchCount - 1) { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.warn("批次间等待被中断"); } } } log.info("打卡数据获取完成,总记录数: {}", allCheckinData.size()); return allCheckinData; } /** * 获取打卡日报数据 * 接口调用频率限制为100次/分钟 * @param startTime 开始时间戳(秒) * @param endTime 结束时间戳(秒) * @param useridList 用户ID列表 * @return 打卡日报数据列表 */ public List getCheckinDayData(long startTime, long endTime, List 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 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 batchUserList = useridList.subList(fromIndex, toIndex); log.info("正在获取第 {}/{} 批打卡日报数据,用户数: {}", i + 1, batchCount, batchUserList.size()); Map 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 getCheckinDayDataByQwStaff(long startTime, long endTime) throws IOException { List qwStaffList = qwStaffMapper.selectList(new LambdaQueryWrapper()); if (qwStaffList == null || qwStaffList.isEmpty()) { log.warn("QW_STAFF表中没有数据"); return new ArrayList<>(); } List 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 String errmsg; @JsonProperty("access_token") private String accessToken; @JsonProperty("expires_in") private Integer expiresIn; } @Data private static class WorkWXUserIdResponse { private Integer errcode; private String errmsg; private String userid; } @Data private static class WorkWXUserListIdResponse { private Integer errcode; private String errmsg; @JsonProperty("next_cursor") private String nextCursor; @JsonProperty("dept_user") private List deptUser; } @Data public static class WorkWXUser { private String userid; private Integer department; private String name; @JsonProperty("open_userid") private String openUserid; } @Data public static class WorkWXUserDetail { private Integer errcode; private String errmsg; private String userid; private String name; private String mobile; private String position; } @Data private static class WorkWXUserDetailResponse extends WorkWXUserDetail { } @Data private static class WorkWXCheckinResponse { private Integer errcode; private String errmsg; private List checkindata; } // ==================== 打卡日报数据响应结构 ==================== @Data private static class WorkWXCheckinDayDataResponse { private Integer errcode; private String errmsg; private List datas; } @Data public static class CheckinDayData { @JsonProperty("base_info") private BaseInfo baseInfo; @JsonProperty("summary_info") private SummaryInfo summaryInfo; @JsonProperty("holiday_infos") private List holidayInfos; @JsonProperty("exception_infos") private List exceptionInfos; @JsonProperty("ot_info") private OtInfo otInfo; @JsonProperty("sp_items") private List 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; } @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 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 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 groupname; @JsonProperty("checkin_type") private String checkinType; @JsonProperty("exception_type") private String exceptionType; @JsonProperty("checkin_time") private Long checkinTime; @JsonProperty("location_title") private String locationTitle; @JsonProperty("location_detail") private String locationDetail; private String wifiname; private String notes; private String wifimac; private List mediaids; private Double lat; private Double lng; @JsonProperty("sch_checkin_time") private Long schCheckinTime; private Integer groupid; @JsonProperty("schedule_id") private Integer scheduleId; @JsonProperty("timeline_id") private Integer timelineId; private String deviceid; } }