diff --git a/evo-admin/pom.xml b/evo-admin/pom.xml
index 170d4fc..3cc7edd 100644
--- a/evo-admin/pom.xml
+++ b/evo-admin/pom.xml
@@ -17,6 +17,19 @@
+
+
+ com.aliyun
+ dingtalk
+ 2.2.43
+
+
+
+ com.aliyun
+ alibaba-dingtalk-service-sdk
+ 2.0.0
+
+
com.baomidou
mybatis-plus-boot-starter
@@ -241,6 +254,12 @@
org.springframework.boot
spring-boot-starter-thymeleaf
+
+ org.springframework.boot
+ spring-boot-test
+ 3.3.3
+ test
+
diff --git a/evo-admin/src/main/java/com/evo/EvoApplication.java b/evo-admin/src/main/java/com/evo/EvoApplication.java
index d9284f8..d39027e 100644
--- a/evo-admin/src/main/java/com/evo/EvoApplication.java
+++ b/evo-admin/src/main/java/com/evo/EvoApplication.java
@@ -4,6 +4,7 @@ import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
@@ -14,6 +15,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@EnableScheduling
@MapperScan({"com.evo.**.mapper.**.**"})
+@EnableAsync // 开启异步支持
public class EvoApplication
{
public static void main(String[] args)
diff --git a/evo-admin/src/main/java/com/evo/attendance/processor/impl/KQDeviceExchangeProcessor.java b/evo-admin/src/main/java/com/evo/attendance/processor/impl/KQDeviceExchangeProcessor.java
index ef44732..3ea8187 100644
--- a/evo-admin/src/main/java/com/evo/attendance/processor/impl/KQDeviceExchangeProcessor.java
+++ b/evo-admin/src/main/java/com/evo/attendance/processor/impl/KQDeviceExchangeProcessor.java
@@ -149,7 +149,6 @@ public class KQDeviceExchangeProcessor implements PunchTheClockStrategyExchangeP
return initMessage(1, "当天已经打过加班卡");
}
-
//员工考勤记录
RzAttendance attendance = rzAttendanceMapper.queryNowDayAttendanceByStatisticalIdAndDate(Long.valueOf(userId),date);
//如果当前打卡是下班卡, 并且当前员工考勤上班时间为空, 则判定为隔天打下班卡
diff --git a/evo-admin/src/main/java/com/evo/attendance/service/IRzAttendanceService.java b/evo-admin/src/main/java/com/evo/attendance/service/IRzAttendanceService.java
index 34938e6..acfca3e 100644
--- a/evo-admin/src/main/java/com/evo/attendance/service/IRzAttendanceService.java
+++ b/evo-admin/src/main/java/com/evo/attendance/service/IRzAttendanceService.java
@@ -62,4 +62,6 @@ public interface IRzAttendanceService extends IService
RzAttendance selectRzAttendanceBySfIdAndNameAndTime(Long userId, String employeeName, String replacementTime,Boolean isAttendance);
public void sendAbnormalAttendance();
+
+ public void dingDing(String dingUserId, String bizId);
}
diff --git a/evo-admin/src/main/java/com/evo/attendance/service/impl/RzAttendanceServiceImpl.java b/evo-admin/src/main/java/com/evo/attendance/service/impl/RzAttendanceServiceImpl.java
index 4b69aef..8e4b220 100644
--- a/evo-admin/src/main/java/com/evo/attendance/service/impl/RzAttendanceServiceImpl.java
+++ b/evo-admin/src/main/java/com/evo/attendance/service/impl/RzAttendanceServiceImpl.java
@@ -1,5 +1,8 @@
package com.evo.attendance.service.impl;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -13,10 +16,12 @@ import com.evo.attendance.service.RzAttendanceDetailService;
import com.evo.common.annotation.DataScope;
import com.evo.common.core.domain.AjaxResult;
import com.evo.common.core.domain.entity.SysDept;
-import com.evo.common.utils.Collections;
-import com.evo.common.utils.DateUtils;
-import com.evo.common.utils.SecurityUtils;
-import com.evo.common.utils.StringUtils;
+import com.evo.common.utils.*;
+import com.evo.ding.DingUtils;
+import com.evo.ding.domain.DingShift;
+import com.evo.ding.domain.DingShiftGroup;
+import com.evo.ding.mapper.DingShiftGroupMapper;
+import com.evo.ding.mapper.DingShiftMapper;
import com.evo.equipment.service.IEqImagesService;
import com.evo.system.domain.SysStaff;
import com.evo.system.mapper.SysDeptMapper;
@@ -31,6 +36,7 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
+import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -56,6 +62,10 @@ public class RzAttendanceServiceImpl extends ServiceImpl"+result);
+ JSONObject resultJson = JSON.parseObject(result);
+ if(Integer.valueOf(0).equals(resultJson.getInteger("errcode"))) try {
+ {
+ JSONArray jsonArray = resultJson.getJSONArray("recordresult");
+ for (int i = 0; i < jsonArray.size(); i++) {
+ JSONObject ob = jsonArray.getJSONObject(i);
+ if(bizId.equals(ob.getString("bizId"))){
+ //打卡类型
+ String checkType = ob.getString("checkType");
+ //考勤组
+ String groupId = ob.getString("groupId");
+ //排班时间
+ Long planCheckTime = ob.getLong("planCheckTime");
+ // 格式化:先转Date对象,再格式化
+ String formatPlanCheckTime = DateUtils.parseDateToStr("HH:mm:ss", new Date(planCheckTime));
+ DingShiftGroup dingsShiftGroup = dingsShiftGroupMapper.selectByGroupIdAndTypeAndCheckTime(groupId, checkType, formatPlanCheckTime);
+ if(dingsShiftGroup != null){
+ //用户打卡时间
+ Long userCheckTime = ob.getLong("userCheckTime");
+ //工作时间 (这里指的是哪一天的卡)
+ Long workDate = ob.getLong("workDate");
+ /***
+ * 打卡结果
+ * Normal:正常
+ * Early:早退
+ * Late:迟到
+ * SeriousLate:严重迟到
+ * Absenteeism:旷工迟到
+ * NotSigned:未打卡
+ */
+ String timeResult = ob.getString("timeResult");
+ Date formatWorkDate = Date.from(Instant.ofEpochMilli(workDate));
+ Date formatUserCheckTime = Date.from(Instant.ofEpochMilli(userCheckTime));
+
+ //通过打卡类型, classId, 和排班打卡时间, 获取工作时长
+ //通过班次id拿到工作时长
+ DingShift dingsShift = dingsShiftMapper.selectOne(new LambdaQueryWrapper().eq(DingShift::getDingId, dingsShiftGroup.getShiftId()));
+ //通过workDate 获取哪一天的卡
+ RzAttendance attendance = getBaseMapper().queryNowDayAttendanceByStatisticalIdAndDate(sysStaff.getUserId(),formatWorkDate);
+ //生成打卡记录
+ rzAttendanceDetailService.addDetail(attendance, ("OnDuty".equals(checkType) ? "上班卡" : "下班卡"), "/", formatUserCheckTime, "钉钉打卡", "");
+ //上班
+ if("OnDuty".equals(checkType)){
+ attendance.setRules("上班卡");
+ attendance.setWorkStartTime(formatUserCheckTime);
+ if(!"Normal".equals(timeResult)){
+ attendance.setYcsFlag("1");
+ }
+ }else if("OffDuty".equals(checkType)){
+ attendance.setWorkEndTime(formatUserCheckTime);
+ if("Normal".equals(timeResult)){
+ attendance.setWorkSum(BigDecimal.valueOf(dingsShift.getWorkHour()));
+ }else{
+ //早退需要计算工时
+ Long hours = (userCheckTime - attendance.getWorkStartTime().getTime())/1000/60/60;
+ attendance.setWorkSum(BigDecimal.valueOf(hours));
+ attendance.setYcxFlag("1");
+ }
+ }else{
+ log.error("钉钉打卡=====>>>>>>>>>>>>>>>>>> 出现未知打卡类型, 当前打卡类型");
+ //终止继续执行
+ return;
+ }
+ //判断打卡时间, 添加夜班次数 打卡时间在晚上7点以后
+ if(dingsShift.getWorkHour() == 12 && attendance.getWorkSum().intValue() >= 12 && attendance.getWorkStartTime().getHours() > 12){
+ attendance.setNightNumber(1);
+ //必须是8小时倒班制or单班制, 并且工作时长>=8小时, 并且 上班打开时间在12点至21点 算作中班 -- 2026-02-04 因1月份张克龙中班考勤问题, 刘杰面谈
+ }else if(dingsShift.getWorkHour() == 8 && attendance.getWorkSum().intValue() >= 8 && attendance.getWorkStartTime().getHours() >= 12 && attendance.getWorkStartTime().getHours() < 21){
+ attendance.setMiddleShiftNumber(1);
+ }
+
+ getBaseMapper().updateRzAttendance(attendance);
+ //检查是否开启加班
+ if(dingsShift.getOvertimeWork()){
+ //如果开启了加班
+ Long hours = (userCheckTime - attendance.getWorkStartTime().getTime())/1000/60/60;
+ new BigDecimal(hours).subtract(new BigDecimal(dingsShift.getWorkHour())).subtract(BigDecimal.valueOf(1)).subtract(new BigDecimal(String.valueOf(DataUtils.findDefaultValue(dingsShift.getDeductionHour(), 0))));
+ }
+
+ }else{
+ log.info("未找到相关考勤班次数据, 疑似加班打卡=====>>>>> {}", result);
+ }
+ break;
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ log.error("处理钉钉打卡出现异常, 异常原因是==>>{}, 打卡结果是=====>>>>> {}", e.getMessage(), result);
+ }
+ }
+ }
@Async
diff --git a/evo-admin/src/main/java/com/evo/common/async/AsyncConfig.java b/evo-admin/src/main/java/com/evo/common/async/AsyncConfig.java
new file mode 100644
index 0000000..ab7a14d
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/common/async/AsyncConfig.java
@@ -0,0 +1,52 @@
+package com.evo.common.async;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+/**
+ * AsyncConfig
+ *
+ * @author andy.shi
+ * @ClassName:AsyncConfig
+ * @date: 2026年03月05日 8:48
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+@Configuration
+//@EnableAsync
+public class AsyncConfig {
+
+ @Value("${yt.async.corePoolSize:'5'}")
+ private Integer corePoolSize;
+ @Value("${yt.async.maxPoolSize:'10'}")
+ private Integer maxPoolSize;
+ @Value("${yt.async.queueCapacity:'20'}")
+ private Integer queueCapacity;
+ @Value("${yt.async.keepAliveSeconds:'60'}")
+ private Integer keepAliveSeconds;
+
+
+ @Bean("asyncExecutor") // 自定义线程池名称
+ public Executor asyncExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ // 核心线程数
+ executor.setCorePoolSize(corePoolSize);
+ // 最大线程数
+ executor.setMaxPoolSize(maxPoolSize);
+ // 队列容量
+ executor.setQueueCapacity(queueCapacity);
+ // 线程空闲超时时间
+ executor.setKeepAliveSeconds(keepAliveSeconds);
+ // 线程名称前缀
+ executor.setThreadNamePrefix("async-thread-");
+ // 拒绝策略(队列满+最大线程数满时的处理方式)
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ // 初始化
+ executor.initialize();
+ return executor;
+ }
+}
diff --git a/evo-admin/src/main/java/com/evo/common/controller/CallbackController.java b/evo-admin/src/main/java/com/evo/common/controller/CallbackController.java
new file mode 100644
index 0000000..691a5ed
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/common/controller/CallbackController.java
@@ -0,0 +1,95 @@
+package com.evo.common.controller;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.evo.attendance.processor.impl.KQDeviceExchangeProcessor;
+import com.evo.attendance.service.IRzAttendanceService;
+import com.evo.common.core.controller.BaseController;
+import com.evo.common.utils.StringUtils;
+import com.evo.ding.DingGlobalParams;
+import com.evo.ding.DingCallbackCrypto;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * CallbackController
+ *
+ * @author andy.shi
+ * @ClassName:CallbackController
+ * @date: 2026年03月04日 11:27
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+@RestController
+@RequestMapping("/callback")
+@Slf4j
+public class CallbackController extends BaseController {
+
+
+ @Autowired
+ IRzAttendanceService rzAttendanceService;
+
+ @PostMapping("/ding/clock")
+ public Map dingDingClock(@RequestBody Map params){
+ System.out.println(JSONObject.toJSONString(params));
+ try {
+ // 1. 从http请求中获取加解密参数
+
+ // 2. 使用加解密类型
+ // 2、调用订阅事件接口订阅的事件为企业级事件推送,此时OWNER_KEY为:开发者后台应用的Client ID(原企业内部应用 appKey )
+ DingCallbackCrypto callbackCrypto = new DingCallbackCrypto(DingGlobalParams.getToken(), DingGlobalParams.getAesKey(), DingGlobalParams.getClientId());
+ String encryptMsg = String.valueOf(params.get("encrypt"));
+ String decryptMsg = callbackCrypto.getDecryptMsg(String.valueOf(params.get("msg_signature")), String.valueOf(params.get("timestamp")), String.valueOf(params.get("nonce")), encryptMsg);
+
+ System.out.println("==============>"+decryptMsg);
+ // 3. 反序列化回调事件json数据
+ JSONObject eventJson = JSON.parseObject(decryptMsg);
+ String eventType = eventJson.getString("EventType");
+
+ // 4. 根据EventType分类处理
+ if ("check_url".equals(eventType)) {
+ // 测试回调url的正确性
+ log.info("测试回调url的正确性");
+ } else if ("attendance_check_record".equals(eventType)) { //打卡推送
+ JSONArray dataList = eventJson.getJSONArray("DataList");
+ if(dataList!= null && dataList.size() > 0){
+ JSONObject jsonObject = dataList.getJSONObject(0);
+ String userId = jsonObject.getString("userId");
+ if(StringUtils.isNotEmpty(userId)){
+ rzAttendanceService.dingDing(userId, jsonObject.getString("bizId"));
+ }
+ }
+ //加班审批订阅
+ }else if ("bpms_instance_change".equals(eventType)) {
+
+
+ // 处理通讯录用户增加事件
+ }else if ("user_add_org".equals(eventType)) {
+ // 处理通讯录用户增加事件
+ log.info("发生了:" + eventType + "事件");
+
+
+
+ } else {
+ // 添加其他已注册的
+ log.info("发生了:" + eventType + "事件");
+ }
+
+ // 5. 返回success的加密数据
+ Map successMap = callbackCrypto.getEncryptedMap("success");
+ return successMap;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+}
diff --git a/evo-admin/src/main/java/com/evo/common/controller/TestController.java b/evo-admin/src/main/java/com/evo/common/controller/TestController.java
index cb862c8..4cff262 100644
--- a/evo-admin/src/main/java/com/evo/common/controller/TestController.java
+++ b/evo-admin/src/main/java/com/evo/common/controller/TestController.java
@@ -11,6 +11,7 @@ import com.evo.common.config.Global;
import com.evo.common.constant.Constants;
import com.evo.common.core.domain.AjaxResult;
import com.evo.common.utils.Collections;
+import com.evo.ding.DingUtils;
import com.evo.personnelMatters.mapper.RzOverTimeDetailMapper;
import com.evo.personnelMatters.mapper.RzOverTimeMapper;
import com.evo.restaurant.service.IRzRestaurantStatisticsService;
@@ -68,6 +69,22 @@ public class TestController {
@Resource
private GZHAccessTokenService gzhAccessTokenService;
+ /**
+ * 清洗加班
+ */
+ @GetMapping("/dept")
+ public AjaxResult dept(Long deptId) throws ParseException {
+ return AjaxResult.success("");
+ }
+
+ /**
+ * 清洗加班
+ */
+ @GetMapping("/user")
+ public AjaxResult user(String phone) throws ParseException {
+ return AjaxResult.success(DingUtils.getUserIdByPhoto(phone));
+ }
+
/**
* 清洗加班
*/
diff --git a/evo-admin/src/main/java/com/evo/ding/DingCallbackCrypto.java b/evo-admin/src/main/java/com/evo/ding/DingCallbackCrypto.java
new file mode 100644
index 0000000..9de042a
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/DingCallbackCrypto.java
@@ -0,0 +1,400 @@
+package com.evo.ding;
+
+/**
+ * DingCallbackCrypto
+ *
+ * @author andy.shi
+ * @ClassName:DingCallbackCrypto
+ * @date: 2026年03月04日 11:55
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+
+import com.alibaba.fastjson2.JSON;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Field;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.Permission;
+import java.security.PermissionCollection;
+import java.security.Security;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+public class DingCallbackCrypto {
+
+ private static final Charset CHARSET = Charset.forName("utf-8");
+ private static final Base64 base64 = new Base64();
+ private byte[] aesKey;
+ private String token;
+ private String corpId;
+ /**
+ * ask getPaddingBytes key固定长度
+ **/
+ private static final Integer AES_ENCODE_KEY_LENGTH = 43;
+ /**
+ * 加密随机字符串字节长度
+ **/
+ private static final Integer RANDOM_LENGTH = 16;
+
+ /**
+ * 构造函数
+ *
+ * @param token 钉钉开放平台上,开发者设置的token
+ * @param encodingAesKey 钉钉开放台上,开发者设置的EncodingAESKey
+ * @param corpId 企业自建应用-事件订阅, 使用appKey
+ * 企业自建应用-注册回调地址, 使用corpId
+ * 第三方企业应用, 使用suiteKey
+ *
+ * @throws DingTalkEncryptException 执行失败,请查看该异常的错误码和具体的错误信息
+ */
+ public DingCallbackCrypto(String token, String encodingAesKey, String corpId) throws DingTalkEncryptException {
+ if (null == encodingAesKey || encodingAesKey.length() != AES_ENCODE_KEY_LENGTH) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.AES_KEY_ILLEGAL);
+ }
+ this.token = token;
+ this.corpId = corpId;
+ aesKey = Base64.decodeBase64(encodingAesKey + "=");
+ }
+
+ public Map getEncryptedMap(String plaintext) throws DingTalkEncryptException {
+ return getEncryptedMap(plaintext, System.currentTimeMillis(), Utils.getRandomStr(16));
+ }
+
+ /**
+ * 将和钉钉开放平台同步的消息体加密,返回加密Map
+ *
+ * @param plaintext 传递的消息体明文
+ * @param timeStamp 时间戳
+ * @param nonce 随机字符串
+ * @return
+ * @throws DingTalkEncryptException
+ */
+ public Map getEncryptedMap(String plaintext, Long timeStamp, String nonce)
+ throws DingTalkEncryptException {
+ if (null == plaintext) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_PLAINTEXT_ILLEGAL);
+ }
+ if (null == timeStamp) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_TIMESTAMP_ILLEGAL);
+ }
+ if (null == nonce) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.ENCRYPTION_NONCE_ILLEGAL);
+ }
+ // 加密
+ String encrypt = encrypt(Utils.getRandomStr(RANDOM_LENGTH), plaintext);
+ String signature = getSignature(token, String.valueOf(timeStamp), nonce, encrypt);
+ Map resultMap = new HashMap();
+ resultMap.put("msg_signature", signature);
+ resultMap.put("encrypt", encrypt);
+ resultMap.put("timeStamp", String.valueOf(timeStamp));
+ resultMap.put("nonce", nonce);
+ return resultMap;
+ }
+
+ /**
+ * 密文解密
+ *
+ * @param msgSignature 签名串
+ * @param timeStamp 时间戳
+ * @param nonce 随机串
+ * @param encryptMsg 密文
+ * @return 解密后的原文
+ * @throws DingTalkEncryptException
+ */
+ public String getDecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptMsg)
+ throws DingTalkEncryptException {
+ //校验签名
+ String signature = getSignature(token, timeStamp, nonce, encryptMsg);
+ if (!signature.equals(msgSignature)) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
+ }
+ // 解密
+ String result = decrypt(encryptMsg);
+ return result;
+ }
+
+ /*
+ * 对明文加密.
+ * @param text 需要加密的明文
+ * @return 加密后base64编码的字符串
+ */
+ private String encrypt(String random, String plaintext) throws DingTalkEncryptException {
+ try {
+ byte[] randomBytes = random.getBytes(CHARSET);
+ byte[] plainTextBytes = plaintext.getBytes(CHARSET);
+ byte[] lengthByte = Utils.int2Bytes(plainTextBytes.length);
+ byte[] corpidBytes = corpId.getBytes(CHARSET);
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+ byteStream.write(randomBytes);
+ byteStream.write(lengthByte);
+ byteStream.write(plainTextBytes);
+ byteStream.write(corpidBytes);
+ byte[] padBytes = PKCS7Padding.getPaddingBytes(byteStream.size());
+ byteStream.write(padBytes);
+ byte[] unencrypted = byteStream.toByteArray();
+ byteStream.close();
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
+ IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
+ byte[] encrypted = cipher.doFinal(unencrypted);
+ String result = base64.encodeToString(encrypted);
+ return result;
+ } catch (Exception e) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_ENCRYPT_TEXT_ERROR);
+ }
+ }
+
+ /*
+ * 对密文进行解密.
+ * @param text 需要解密的密文
+ * @return 解密得到的明文
+ */
+ private String decrypt(String text) throws DingTalkEncryptException {
+ byte[] originalArr;
+ try {
+ // 设置解密模式为AES的CBC模式
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
+ IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
+ // 使用BASE64对密文进行解码
+ byte[] encrypted = Base64.decodeBase64(text);
+ // 解密
+ originalArr = cipher.doFinal(encrypted);
+ } catch (Exception e) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_ERROR);
+ }
+
+ String plainText;
+ String fromCorpid;
+ try {
+ // 去除补位字符
+ byte[] bytes = PKCS7Padding.removePaddingBytes(originalArr);
+ // 分离16位随机字符串,网络字节序和corpId
+ byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+ int plainTextLegth = Utils.bytes2int(networkOrder);
+ plainText = new String(Arrays.copyOfRange(bytes, 20, 20 + plainTextLegth), CHARSET);
+ fromCorpid = new String(Arrays.copyOfRange(bytes, 20 + plainTextLegth, bytes.length), CHARSET);
+ } catch (Exception e) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_LENGTH_ERROR);
+ }
+
+ // corpid不相同的情况
+ if (!fromCorpid.equals(corpId)) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_DECRYPT_TEXT_CORPID_ERROR);
+ }
+ return plainText;
+ }
+
+ /**
+ * 数字签名
+ *
+ * @param token isv token
+ * @param timestamp 时间戳
+ * @param nonce 随机串
+ * @param encrypt 加密文本
+ * @return
+ * @throws DingTalkEncryptException
+ */
+ public String getSignature(String token, String timestamp, String nonce, String encrypt)
+ throws DingTalkEncryptException {
+ try {
+ String[] array = new String[] {token, timestamp, nonce, encrypt};
+ Arrays.sort(array);
+ System.out.println(JSON.toJSONString(array));
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < 4; i++) {
+ sb.append(array[i]);
+ }
+ String str = sb.toString();
+ System.out.println(str);
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(str.getBytes());
+ byte[] digest = md.digest();
+
+ StringBuffer hexstr = new StringBuffer();
+ String shaHex = "";
+ for (int i = 0; i < digest.length; i++) {
+ shaHex = Integer.toHexString(digest[i] & 0xFF);
+ if (shaHex.length() < 2) {
+ hexstr.append(0);
+ }
+ hexstr.append(shaHex);
+ }
+ return hexstr.toString();
+ } catch (Exception e) {
+ throw new DingTalkEncryptException(DingTalkEncryptException.COMPUTE_SIGNATURE_ERROR);
+ }
+ }
+
+ public static class Utils {
+ public Utils() {
+ }
+
+ public static String getRandomStr(int count) {
+ String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ Random random = new Random();
+ StringBuffer sb = new StringBuffer();
+
+ for (int i = 0; i < count; ++i) {
+ int number = random.nextInt(base.length());
+ sb.append(base.charAt(number));
+ }
+
+ return sb.toString();
+ }
+
+ public static byte[] int2Bytes(int count) {
+ byte[] byteArr = new byte[] {(byte)(count >> 24 & 255), (byte)(count >> 16 & 255), (byte)(count >> 8 & 255),
+ (byte)(count & 255)};
+ return byteArr;
+ }
+
+ public static int bytes2int(byte[] byteArr) {
+ int count = 0;
+
+ for (int i = 0; i < 4; ++i) {
+ count <<= 8;
+ count |= byteArr[i] & 255;
+ }
+
+ return count;
+ }
+ }
+
+ public static class PKCS7Padding {
+ private static final Charset CHARSET = Charset.forName("utf-8");
+ private static final int BLOCK_SIZE = 32;
+
+ public PKCS7Padding() {
+ }
+
+ public static byte[] getPaddingBytes(int count) {
+ int amountToPad = 32 - count % 32;
+ if (amountToPad == 0) {
+ amountToPad = 32;
+ }
+
+ char padChr = chr(amountToPad);
+ String tmp = new String();
+
+ for (int index = 0; index < amountToPad; ++index) {
+ tmp = tmp + padChr;
+ }
+
+ return tmp.getBytes(CHARSET);
+ }
+
+ public static byte[] removePaddingBytes(byte[] decrypted) {
+ int pad = decrypted[decrypted.length - 1];
+ if (pad < 1 || pad > 32) {
+ pad = 0;
+ }
+
+ return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
+ }
+
+ private static char chr(int a) {
+ byte target = (byte)(a & 255);
+ return (char)target;
+ }
+ }
+
+ public static class DingTalkEncryptException extends Exception {
+ public static final int SUCCESS = 0;
+ public static final int ENCRYPTION_PLAINTEXT_ILLEGAL = 900001;
+ public static final int ENCRYPTION_TIMESTAMP_ILLEGAL = 900002;
+ public static final int ENCRYPTION_NONCE_ILLEGAL = 900003;
+ public static final int AES_KEY_ILLEGAL = 900004;
+ public static final int SIGNATURE_NOT_MATCH = 900005;
+ public static final int COMPUTE_SIGNATURE_ERROR = 900006;
+ public static final int COMPUTE_ENCRYPT_TEXT_ERROR = 900007;
+ public static final int COMPUTE_DECRYPT_TEXT_ERROR = 900008;
+ public static final int COMPUTE_DECRYPT_TEXT_LENGTH_ERROR = 900009;
+ public static final int COMPUTE_DECRYPT_TEXT_CORPID_ERROR = 900010;
+ private static Map msgMap = new HashMap();
+ private Integer code;
+
+ static {
+ msgMap.put(0, "成功");
+ msgMap.put(900001, "加密明文文本非法");
+ msgMap.put(900002, "加密时间戳参数非法");
+ msgMap.put(900003, "加密随机字符串参数非法");
+ msgMap.put(900005, "签名不匹配");
+ msgMap.put(900006, "签名计算失败");
+ msgMap.put(900004, "不合法的aes key");
+ msgMap.put(900007, "计算加密文字错误");
+ msgMap.put(900008, "计算解密文字错误");
+ msgMap.put(900009, "计算解密文字长度不匹配");
+ msgMap.put(900010, "计算解密文字corpid不匹配");
+ }
+
+ public Integer getCode() {
+ return this.code;
+ }
+
+ public DingTalkEncryptException(Integer exceptionCode) {
+ super((String)msgMap.get(exceptionCode));
+ this.code = exceptionCode;
+ }
+ }
+ static {
+ try {
+ Security.setProperty("crypto.policy", "limited");
+ RemoveCryptographyRestrictions();
+ } catch (Exception var1) {
+ }
+
+ }
+ private static void RemoveCryptographyRestrictions() throws Exception {
+ Class> jceSecurity = getClazz("javax.crypto.JceSecurity");
+ Class> cryptoPermissions = getClazz("javax.crypto.CryptoPermissions");
+ Class> cryptoAllPermission = getClazz("javax.crypto.CryptoAllPermission");
+ if (jceSecurity != null) {
+ setFinalStaticValue(jceSecurity, "isRestricted", false);
+ PermissionCollection defaultPolicy = (PermissionCollection)getFieldValue(jceSecurity, "defaultPolicy", (Object)null, PermissionCollection.class);
+ if (cryptoPermissions != null) {
+ Map, ?> map = (Map)getFieldValue(cryptoPermissions, "perms", defaultPolicy, Map.class);
+ map.clear();
+ }
+
+ if (cryptoAllPermission != null) {
+ Permission permission = (Permission)getFieldValue(cryptoAllPermission, "INSTANCE", (Object)null, Permission.class);
+ defaultPolicy.add(permission);
+ }
+ }
+
+ }
+ private static Class> getClazz(String className) {
+ Class clazz = null;
+
+ try {
+ clazz = Class.forName(className);
+ } catch (Exception var3) {
+ }
+
+ return clazz;
+ }
+ private static void setFinalStaticValue(Class> srcClazz, String fieldName, Object newValue) throws Exception {
+ Field field = srcClazz.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(field, field.getModifiers() & -17);
+ field.set((Object)null, newValue);
+ }
+ private static T getFieldValue(Class> srcClazz, String fieldName, Object owner, Class dstClazz) throws Exception {
+ Field field = srcClazz.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return dstClazz.cast(field.get(owner));
+ }
+
+}
\ No newline at end of file
diff --git a/evo-admin/src/main/java/com/evo/ding/DingGlobalParams.java b/evo-admin/src/main/java/com/evo/ding/DingGlobalParams.java
new file mode 100644
index 0000000..f2f3283
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/DingGlobalParams.java
@@ -0,0 +1,91 @@
+package com.evo.ding;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * DingBase
+ *
+ * @author andy.shi
+ * @ClassName:DingBase
+ * @date: 2026年03月02日 8:48
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "ding")
+public class DingGlobalParams {
+
+ public static String baseUrl;
+
+ public static String url;
+
+ public static String clientId;
+
+ public static String clientSecret;
+
+ public static String aesKey;
+
+ public static String token;
+
+ public static String corpId;
+
+
+ public static String getBaseUrl() {
+ return baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ DingGlobalParams.baseUrl = baseUrl;
+ }
+
+ public static String getCorpId() {
+ return corpId;
+ }
+
+ public void setCorpId(String corpId) {
+ DingGlobalParams.corpId = corpId;
+ }
+
+ public static String getAesKey() {
+ return aesKey;
+ }
+
+ public void setAesKey(String aesKey) {
+ DingGlobalParams.aesKey = aesKey;
+ }
+
+ public static String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ DingGlobalParams.token = token;
+ }
+
+ public static String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ DingGlobalParams.url = url;
+ }
+
+ public static String getClientId() {
+ return DingGlobalParams.clientId;
+ }
+
+ public void setClientId(String clientId) {
+ DingGlobalParams.clientId = clientId;
+ }
+
+ public static String getClientSecret() {
+ return clientSecret;
+ }
+
+
+ public void setClientSecret(String clientSecret) {
+ DingGlobalParams.clientSecret = clientSecret;
+ }
+}
diff --git a/evo-admin/src/main/java/com/evo/ding/DingUtils.java b/evo-admin/src/main/java/com/evo/ding/DingUtils.java
new file mode 100644
index 0000000..ff0cc0f
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/DingUtils.java
@@ -0,0 +1,207 @@
+package com.evo.ding;
+
+import com.aliyun.dingtalkoauth2_1_0.Client;
+import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest;
+import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenResponse;
+import com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceResponseBody;
+import com.aliyun.tea.TeaException;
+import com.dingtalk.api.DefaultDingTalkClient;
+import com.dingtalk.api.DingTalkClient;
+import com.dingtalk.api.request.*;
+import com.dingtalk.api.response.*;
+import com.evo.common.core.redis.RedisCache;
+import com.evo.common.utils.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * DingUtils
+ *
+ * @author andy.shi
+ * @ClassName:DingUtils
+ * @date: 2026年03月02日 8:54
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+@Service
+@Slf4j
+public class DingUtils {
+
+ private static RedisCache redisCache;
+
+ private static String ACCESS_TOKEN_KEY = "ding_access_token";
+
+ @Autowired
+ public DingUtils(RedisCache redisCache) {
+ this.redisCache = redisCache;
+ }
+
+ private static Client createClient() throws Exception {
+ com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+ config.protocol = "https";
+ config.regionId = "central";
+ return new Client(config);
+ }
+
+ public static String getAccessToken() throws Exception {
+ String token = redisCache.getCacheObject(ACCESS_TOKEN_KEY);
+ if(StringUtils.isEmpty(token)){
+ Client client = createClient();
+ GetAccessTokenRequest getAccessTokenRequest = new GetAccessTokenRequest().setAppKey(DingGlobalParams.getClientId()).setAppSecret(DingGlobalParams.getClientSecret());
+ try {
+ GetAccessTokenResponse res = client.getAccessToken(getAccessTokenRequest);
+ token = res.getBody().getAccessToken();
+ //7200s- 10s,防止出现读取不超时, 请求超时
+ redisCache.setCacheObject(ACCESS_TOKEN_KEY, token, res.getBody().getExpireIn()-10);
+ } catch (TeaException err) {
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ // err 中含有 code 和 message 属性,可帮助开发定位问题
+ log.error("获取token出现 TeaException 异常, 异常编码{}, 异常原因{}", err.getCode(), err.getMessage());
+ }
+ } catch (Exception _err) {
+ TeaException err = new TeaException(_err.getMessage(), _err);
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ // err 中含有 code 和 message 属性,可帮助开发定位问题
+ log.error("获取token出现 Exception 异常, 异常编码{}, 异常原因{}", err.getCode(), err.getMessage());
+ }
+ }
+
+ }
+ return token;
+ }
+
+// public static String getDeptList(Long deptId){
+// try {
+// DingTalkClient client = new DefaultDingTalkClient(DingBase.getUrl()+"v2/department/listsub");
+// OapiV2DepartmentListsubRequest req = new OapiV2DepartmentListsubRequest();
+// req.setDeptId(deptId);
+// req.setLanguage("zh_CN");
+// OapiV2DepartmentListsubResponse rsp = client.execute(req, getAccessToken());
+// return rsp.getBody();
+// } catch (Exception e) {
+// e.printStackTrace();
+// }
+// return "";
+// }
+//
+// public static String getUserList(Long deptId){
+// try {
+// DingTalkClient client = new DefaultDingTalkClient(DingBase.getUrl()+"user/listid");
+// OapiUserListidRequest req = new OapiUserListidRequest();
+// req.setDeptId(deptId);
+// OapiUserListidResponse rsp = client.execute(req, getAccessToken());
+// return rsp.getBody();
+// } catch (Exception e) {
+// e.printStackTrace();
+// }
+// return "";
+// }
+ //根据手机号获取用户id
+ public static String getUserIdByPhoto(String photo){
+ try {
+ DingTalkClient client = new DefaultDingTalkClient(DingGlobalParams.getUrl()+"v2/user/getbymobile");
+ OapiV2UserGetbymobileRequest req = new OapiV2UserGetbymobileRequest();
+ req.setMobile(photo);
+ OapiV2UserGetbymobileResponse rsp = client.execute(req, getAccessToken());
+ return rsp.getBody();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+ /***
+ * 获取班次信息
+ *
+ */
+ public static String getShift(){
+ try {
+ DingTalkClient client = new DefaultDingTalkClient(DingGlobalParams.getUrl()+"attendance/shift/list");
+ OapiAttendanceShiftListRequest req = new OapiAttendanceShiftListRequest();
+ req.setOpUserId("03491015494921375480");
+ OapiAttendanceShiftListResponse rsp = client.execute(req, getAccessToken());
+ return rsp.getBody();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ /***
+ * 获取考勤组信息
+ *
+ */
+ public static String getAttendanceGroupList(){
+ try {
+ DingTalkClient client = new DefaultDingTalkClient(DingGlobalParams.getUrl()+"attendance/getsimplegroups");
+ OapiAttendanceGetsimplegroupsRequest req = new OapiAttendanceGetsimplegroupsRequest();
+ OapiAttendanceGetsimplegroupsResponse rsp = client.execute(req, getAccessToken());
+ return rsp.getBody();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+
+ /***
+ * 获取打卡详情
+ * @param dingUserIds 钉钉userId
+ * @param checkDateFrom 开始时间 yyyy-MM-dd HH:mm:ss
+ * @param checkDateTo 结束时间 yyyy-MM-dd HH:mm:ss
+ */
+ public static String getListRecord(List dingUserIds, String checkDateFrom, String checkDateTo){
+ try {
+ DingTalkClient client = new DefaultDingTalkClient(DingGlobalParams.getBaseUrl()+"attendance/listRecord");
+ OapiAttendanceListRecordRequest req = new OapiAttendanceListRecordRequest();
+ req.setUserIds(dingUserIds);
+ req.setCheckDateFrom(checkDateFrom);
+ req.setCheckDateTo(checkDateTo);
+ req.setIsI18n(false);
+ OapiAttendanceListRecordResponse rsp = client.execute(req, getAccessToken());
+ System.out.println(rsp.getBody());
+ return rsp.getBody();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ public static com.aliyun.dingtalkworkflow_1_0.Client createClient1() throws Exception {
+ com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+ config.protocol = "https";
+ config.regionId = "central";
+ return new com.aliyun.dingtalkworkflow_1_0.Client(config);
+ }
+
+ /***
+ * 获取审批详情
+ * @param processInstanceId 审批示例id
+ */
+ public static GetProcessInstanceResponseBody.GetProcessInstanceResponseBodyResult getProcessInstanceInfo(String processInstanceId) throws Exception {
+
+ com.aliyun.dingtalkworkflow_1_0.Client client = DingUtils.createClient1();
+ com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceHeaders getProcessInstanceHeaders = new com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceHeaders();
+ getProcessInstanceHeaders.xAcsDingtalkAccessToken = getAccessToken();
+ com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceRequest getProcessInstanceRequest = new com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceRequest();
+ getProcessInstanceRequest.setProcessInstanceId(processInstanceId);
+ try {
+ return client.getProcessInstanceWithOptions(getProcessInstanceRequest, getProcessInstanceHeaders, new com.aliyun.teautil.models.RuntimeOptions()).getBody().getResult();
+ } catch (TeaException err) {
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ // err 中含有 code 和 message 属性,可帮助开发定位问题
+ log.error("获取token出现 TeaException 异常, 异常编码{}, 异常原因{}", err.getCode(), err.getMessage());
+ }
+ } catch (Exception _err) {
+ TeaException err = new TeaException(_err.getMessage(), _err);
+ if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+ // err 中含有 code 和 message 属性,可帮助开发定位问题
+ log.error("获取token出现 Exception 异常, 异常编码{}, 异常原因{}", err.getCode(), err.getMessage());
+ }
+
+ }
+ return null;
+ }
+
+}
diff --git a/evo-admin/src/main/java/com/evo/ding/domain/DingGroup.java b/evo-admin/src/main/java/com/evo/ding/domain/DingGroup.java
new file mode 100644
index 0000000..0816658
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/domain/DingGroup.java
@@ -0,0 +1,25 @@
+package com.evo.ding.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * DingsShift
+ *
+ * @author andy.shi
+ * @ClassName:DingsShift
+ * @date: 2026年03月04日 13:50
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+@Data
+public class DingGroup implements Serializable {
+
+ /** 主键 */
+ private Long id;
+ /** 班次名称 */
+ public String name;
+ /** 钉钉的班次id */
+ public String dingId;
+
+}
diff --git a/evo-admin/src/main/java/com/evo/ding/domain/DingShift.java b/evo-admin/src/main/java/com/evo/ding/domain/DingShift.java
new file mode 100644
index 0000000..677c5f5
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/domain/DingShift.java
@@ -0,0 +1,31 @@
+package com.evo.ding.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * DingsShift
+ *
+ * @author andy.shi
+ * @ClassName:DingsShift
+ * @date: 2026年03月04日 13:50
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+@Data
+public class DingShift implements Serializable {
+
+ /** 主键 */
+ private Long id;
+ /** 班次名称 */
+ public String name;
+ /** 钉钉的班次id */
+ public String dingId;
+ /** 工作时长 */
+ public Integer workHour;
+ /** 是否加班 */
+ public Boolean overtimeWork;
+ /** 扣减时长 */
+ public Double deductionHour;
+
+}
diff --git a/evo-admin/src/main/java/com/evo/ding/domain/DingShiftGroup.java b/evo-admin/src/main/java/com/evo/ding/domain/DingShiftGroup.java
new file mode 100644
index 0000000..9e6ebe8
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/domain/DingShiftGroup.java
@@ -0,0 +1,29 @@
+package com.evo.ding.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * DingsShift
+ *
+ * @author andy.shi
+ * @ClassName:DingsShift
+ * @date: 2026年03月04日 13:50
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+@Data
+public class DingShiftGroup implements Serializable {
+
+ /** 主键 */
+ private Long id;
+ /** 钉钉的groupId */
+ public String groupId;
+ /** 钉钉的班次id */
+ public String shiftId;
+ /** type 打卡类型 */
+ public String type;
+ /** type 打卡类型 */
+ public String checkTime;
+
+}
diff --git a/evo-admin/src/main/java/com/evo/ding/mapper/DingShiftGroupMapper.java b/evo-admin/src/main/java/com/evo/ding/mapper/DingShiftGroupMapper.java
new file mode 100644
index 0000000..ae21f00
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/mapper/DingShiftGroupMapper.java
@@ -0,0 +1,19 @@
+package com.evo.ding.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.evo.ding.domain.DingShiftGroup;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * DingsShiftGroupMapper
+ *
+ * @author andy.shi
+ * @ClassName:DingsShiftGroupMapper
+ * @date: 2026年03月05日 14:47
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+public interface DingShiftGroupMapper extends BaseMapper {
+
+
+ DingShiftGroup selectByGroupIdAndTypeAndCheckTime(@Param("groupId") String groupId, @Param("type") String type, @Param("checkTime") String checkTime);
+}
diff --git a/evo-admin/src/main/java/com/evo/ding/mapper/DingShiftMapper.java b/evo-admin/src/main/java/com/evo/ding/mapper/DingShiftMapper.java
new file mode 100644
index 0000000..975b3d3
--- /dev/null
+++ b/evo-admin/src/main/java/com/evo/ding/mapper/DingShiftMapper.java
@@ -0,0 +1,17 @@
+package com.evo.ding.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.evo.ding.domain.DingShift;
+
+/**
+ * DingsShiftGroupMapper
+ *
+ * @author andy.shi
+ * @ClassName:DingsShiftGroupMapper
+ * @date: 2026年03月05日 14:47
+ * @remark: 开发人员联系方式 1042025947@qq.com/微信同步
+ */
+public interface DingShiftMapper extends BaseMapper {
+
+
+}
diff --git a/evo-admin/src/main/java/com/evo/framework/config/SecurityConfig.java b/evo-admin/src/main/java/com/evo/framework/config/SecurityConfig.java
index 254268a..68a9ce9 100644
--- a/evo-admin/src/main/java/com/evo/framework/config/SecurityConfig.java
+++ b/evo-admin/src/main/java/com/evo/framework/config/SecurityConfig.java
@@ -119,6 +119,7 @@ public class SecurityConfig
.antMatchers("/wechat/**").permitAll()
.antMatchers("/websocket/**").permitAll()
.antMatchers("/test/**").permitAll()
+ .antMatchers("/callback/**/**").permitAll()
.antMatchers("/api/v1/verify_user").permitAll()
.antMatchers("/api/v1/record/face").permitAll()
.antMatchers("/api/v2/verify_user_yt").permitAll()
diff --git a/evo-admin/src/main/java/com/evo/system/domain/SysStaff.java b/evo-admin/src/main/java/com/evo/system/domain/SysStaff.java
index edb0339..e084e96 100644
--- a/evo-admin/src/main/java/com/evo/system/domain/SysStaff.java
+++ b/evo-admin/src/main/java/com/evo/system/domain/SysStaff.java
@@ -215,6 +215,8 @@ public class SysStaff extends BaseEntity
private String extended;
+ private String dingUserId;
+
public List getSubsidyList() {
if(Collections.isEmpty(subsidyList) && StringUtils.isNotEmpty(subsidys)){
subsidyList = Collections.asList(subsidys.split(",")).stream().map(Long::valueOf).collect(Collectors.toList());
diff --git a/evo-admin/src/main/java/com/evo/system/mapper/SysStaffMapper.java b/evo-admin/src/main/java/com/evo/system/mapper/SysStaffMapper.java
index 2bb6608..e9b16c3 100644
--- a/evo-admin/src/main/java/com/evo/system/mapper/SysStaffMapper.java
+++ b/evo-admin/src/main/java/com/evo/system/mapper/SysStaffMapper.java
@@ -88,4 +88,12 @@ s * @return
*/
SysStaff selectLeaderSysStaffByDeptId(Long deptId);
+ /**
+ * 查询员工管理
+ *
+ * @param dingUserId 员工管理主键
+ * @return 员工管理
+ */
+ public SysStaff selectSysStaffByDingUserId(String dingUserId);
+
}
diff --git a/evo-admin/src/main/resources/application-local.yml b/evo-admin/src/main/resources/application-local.yml
index d839287..bd781f9 100644
--- a/evo-admin/src/main/resources/application-local.yml
+++ b/evo-admin/src/main/resources/application-local.yml
@@ -32,7 +32,7 @@ spring:
druid:
# 主库数据源
master:
- url: jdbc:mysql://localhost:3306/evo_cw_dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+ url: jdbc:mysql://localhost:3306/evo_cw_dev_ding?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: hbyt123456
#username: root
diff --git a/evo-admin/src/main/resources/application.yml b/evo-admin/src/main/resources/application.yml
index c8aa6aa..125763f 100644
--- a/evo-admin/src/main/resources/application.yml
+++ b/evo-admin/src/main/resources/application.yml
@@ -115,3 +115,16 @@ yt:
appid: wx268e32962db19f5f
app-secret: 84a6065165ec82862c5e03a010a6dc6c
abnormal-attendance-template-id: z9sy-38K-iC5MAWHbxcxwg1c-9oNTFWeCOoy6B6zdKY
+ async:
+ corePoolSize: 15
+ maxPoolSize: 30
+ queueCapacity: 60
+ keepAliveSeconds: 60
+ding:
+ base_url: https://oapi.dingtalk.com/
+ url: ${ding.base_url}topapi/
+ client_id: dingoilfezillv76k3mm
+ client_secret: _UsKmEIidy8U-VDxhb11-qxSGwg4JZqretet4zV5YYTF39lH1DLig5kIlQx4pMfd
+ aes_key: gCywKzw8AyGfnnh5SkIaRNnVSLzKka0mYjzrODqRzqD
+ token: J0kSLkXOVMqI3PSSrF1YS16r2pwpExzyEFDGl68SNbf
+ corp_id: dinga5c6eeb2f797ecdbf5bf40eda33b7ba0
diff --git a/evo-admin/src/main/resources/mapper/ding/DingShiftGroupMapper.xml b/evo-admin/src/main/resources/mapper/ding/DingShiftGroupMapper.xml
new file mode 100644
index 0000000..1ea3a38
--- /dev/null
+++ b/evo-admin/src/main/resources/mapper/ding/DingShiftGroupMapper.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ select id, group_id, shift_id, type, check_time from ding_shift_group
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/evo-admin/src/main/resources/mapper/system/SysStaffMapper.xml b/evo-admin/src/main/resources/mapper/system/SysStaffMapper.xml
index 358ba09..049ad30 100644
--- a/evo-admin/src/main/resources/mapper/system/SysStaffMapper.xml
+++ b/evo-admin/src/main/resources/mapper/system/SysStaffMapper.xml
@@ -50,6 +50,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+
@@ -58,7 +59,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
quit_date, contract_start, contract_end, contract_type, social_type, seniority, is_overtime_pay,
zs_flag, secrecy, injury, insurance, introducer, clock_in, status,
wages_ratio_date, remarks, del_flag, create_by, create_time, update_by,
- update_time, image_url,time_clock,subsidys, job_code, extended, openid
+ update_time, image_url,time_clock,subsidys, job_code, extended, openid, ding_user_id
from sys_staff
@@ -88,6 +89,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+
+