钉钉相关业务调整
This commit is contained in:
parent
5be8266d4a
commit
31f481a78b
@ -17,6 +17,19 @@
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- 新版 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dingtalk</artifactId>
|
||||
<version>2.2.43</version>
|
||||
</dependency>
|
||||
<!-- 旧版 -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
@ -241,6 +254,12 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-test</artifactId>
|
||||
<version>3.3.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -149,7 +149,6 @@ public class KQDeviceExchangeProcessor implements PunchTheClockStrategyExchangeP
|
||||
return initMessage(1, "当天已经打过加班卡");
|
||||
}
|
||||
|
||||
|
||||
//员工考勤记录
|
||||
RzAttendance attendance = rzAttendanceMapper.queryNowDayAttendanceByStatisticalIdAndDate(Long.valueOf(userId),date);
|
||||
//如果当前打卡是下班卡, 并且当前员工考勤上班时间为空, 则判定为隔天打下班卡
|
||||
|
||||
@ -62,4 +62,6 @@ public interface IRzAttendanceService extends IService<RzAttendance>
|
||||
RzAttendance selectRzAttendanceBySfIdAndNameAndTime(Long userId, String employeeName, String replacementTime,Boolean isAttendance);
|
||||
|
||||
public void sendAbnormalAttendance();
|
||||
|
||||
public void dingDing(String dingUserId, String bizId);
|
||||
}
|
||||
|
||||
@ -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<RzAttendanceMapper, RzA
|
||||
private SendClientService sendClientService;
|
||||
@Autowired
|
||||
private SysStaffMapper sysStaffMapper;
|
||||
@Autowired
|
||||
private DingShiftGroupMapper dingsShiftGroupMapper;
|
||||
@Autowired
|
||||
private DingShiftMapper dingsShiftMapper;
|
||||
|
||||
private static final String MORNING_CARD_SINGLE = "上班卡(单班制)";
|
||||
private static final String MORNING_CARD_DOUBLE = "上班卡(双班制)";
|
||||
@ -505,7 +515,107 @@ public class RzAttendanceServiceImpl extends ServiceImpl<RzAttendanceMapper, RzA
|
||||
log.error("查询前一天的打卡情况报错了");
|
||||
}
|
||||
}
|
||||
@Override
|
||||
@Async("asyncExecutor")
|
||||
public void dingDing(String dingUserId, String bizId) {
|
||||
//此处需要保证, 所有的员工必须存在dingUserId
|
||||
SysStaff sysStaff = sysStaffMapper.selectSysStaffByDingUserId(dingUserId);//打卡记录信息
|
||||
if(sysStaff != null){
|
||||
String result = DingUtils.getListRecord(Collections.asList(dingUserId), DateUtils.parseDateToStr("yyyy-MM-dd", new Date())+" 00:00:00", DateUtils.parseDateToStr("yyyy-MM-dd", new Date())+" 23:59:59");
|
||||
System.out.println("打卡结果===>"+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<DingShift>().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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<String, String> dingDingClock(@RequestBody Map<String, Object> 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<String, String> successMap = callbackCrypto.getEncryptedMap("success");
|
||||
return successMap;
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗加班
|
||||
*/
|
||||
|
||||
400
evo-admin/src/main/java/com/evo/ding/DingCallbackCrypto.java
Normal file
400
evo-admin/src/main/java/com/evo/ding/DingCallbackCrypto.java
Normal file
@ -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<String, String> 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<String, String> 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<String, String> resultMap = new HashMap<String, String>();
|
||||
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<Integer, String> 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> T getFieldValue(Class<?> srcClazz, String fieldName, Object owner, Class<T> dstClazz) throws Exception {
|
||||
Field field = srcClazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return dstClazz.cast(field.get(owner));
|
||||
}
|
||||
|
||||
}
|
||||
91
evo-admin/src/main/java/com/evo/ding/DingGlobalParams.java
Normal file
91
evo-admin/src/main/java/com/evo/ding/DingGlobalParams.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
207
evo-admin/src/main/java/com/evo/ding/DingUtils.java
Normal file
207
evo-admin/src/main/java/com/evo/ding/DingUtils.java
Normal file
@ -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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
25
evo-admin/src/main/java/com/evo/ding/domain/DingGroup.java
Normal file
25
evo-admin/src/main/java/com/evo/ding/domain/DingGroup.java
Normal file
@ -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;
|
||||
|
||||
}
|
||||
31
evo-admin/src/main/java/com/evo/ding/domain/DingShift.java
Normal file
31
evo-admin/src/main/java/com/evo/ding/domain/DingShift.java
Normal file
@ -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;
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
|
||||
DingShiftGroup selectByGroupIdAndTypeAndCheckTime(@Param("groupId") String groupId, @Param("type") String type, @Param("checkTime") String checkTime);
|
||||
}
|
||||
@ -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<DingShift> {
|
||||
|
||||
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -215,6 +215,8 @@ public class SysStaff extends BaseEntity
|
||||
|
||||
private String extended;
|
||||
|
||||
private String dingUserId;
|
||||
|
||||
public List<Long> getSubsidyList() {
|
||||
if(Collections.isEmpty(subsidyList) && StringUtils.isNotEmpty(subsidys)){
|
||||
subsidyList = Collections.asList(subsidys.split(",")).stream().map(Long::valueOf).collect(Collectors.toList());
|
||||
|
||||
@ -88,4 +88,12 @@ s * @return
|
||||
*/
|
||||
SysStaff selectLeaderSysStaffByDeptId(Long deptId);
|
||||
|
||||
/**
|
||||
* 查询员工管理
|
||||
*
|
||||
* @param dingUserId 员工管理主键
|
||||
* @return 员工管理
|
||||
*/
|
||||
public SysStaff selectSysStaffByDingUserId(String dingUserId);
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.evo.ding.mapper.DingShiftGroupMapper">
|
||||
|
||||
<resultMap type="DingShiftGroup" id="DingShiftGroupResult">
|
||||
<result property="id" column="id" />
|
||||
<result property="num" column="num" />
|
||||
<result property="name" column="name" />
|
||||
<result property="image" column="image" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="remarks" column="remarks" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectVo">
|
||||
select id, group_id, shift_id, type, check_time from ding_shift_group
|
||||
</sql>
|
||||
|
||||
<select id="selectByGroupIdAndTypeAndCheckTime" resultMap="DingShiftGroupResult">
|
||||
<include refid="selectVo"/>
|
||||
where group_id =#{groupId} and type = #{type} and check_time = #{checkTime}
|
||||
</select>
|
||||
|
||||
|
||||
<!-- <select id="selectEqButtonList" parameterType="EqButton" resultMap="EqButtonResult">-->
|
||||
<!-- <include refid="selectEqButtonVo"/>-->
|
||||
<!-- <where>-->
|
||||
<!-- del_flag = '0'-->
|
||||
<!-- <if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>-->
|
||||
<!-- </where>-->
|
||||
<!-- order by num asc-->
|
||||
<!-- </select>-->
|
||||
|
||||
<!-- <select id="selectEqButtonById" parameterType="Long" resultMap="EqButtonResult">-->
|
||||
<!-- <include refid="selectEqButtonVo"/>-->
|
||||
<!-- where id = #{id}-->
|
||||
<!-- </select>-->
|
||||
|
||||
<!-- <insert id="insertEqButton" parameterType="EqButton" useGeneratedKeys="true" keyProperty="id">-->
|
||||
<!-- insert into eq_button-->
|
||||
<!-- <trim prefix="(" suffix=")" suffixOverrides=",">-->
|
||||
<!-- <if test="num != null">num,</if>-->
|
||||
<!-- <if test="name != null">name,</if>-->
|
||||
<!-- <if test="image != null">image,</if>-->
|
||||
<!-- <if test="delFlag != null">del_flag,</if>-->
|
||||
<!-- <if test="remarks != null">remarks,</if>-->
|
||||
<!-- <if test="createBy != null">create_by,</if>-->
|
||||
<!-- <if test="createTime != null">create_time,</if>-->
|
||||
<!-- <if test="updateBy != null">update_by,</if>-->
|
||||
<!-- <if test="updateTime != null">update_time,</if>-->
|
||||
<!-- </trim>-->
|
||||
<!-- <trim prefix="values (" suffix=")" suffixOverrides=",">-->
|
||||
<!-- <if test="num != null">#{num},</if>-->
|
||||
<!-- <if test="name != null">#{name},</if>-->
|
||||
<!-- <if test="image != null">#{image},</if>-->
|
||||
<!-- <if test="delFlag != null">#{delFlag},</if>-->
|
||||
<!-- <if test="remarks != null">#{remarks},</if>-->
|
||||
<!-- <if test="createBy != null">#{createBy},</if>-->
|
||||
<!-- <if test="createTime != null">#{createTime},</if>-->
|
||||
<!-- <if test="updateBy != null">#{updateBy},</if>-->
|
||||
<!-- <if test="updateTime != null">#{updateTime},</if>-->
|
||||
<!-- </trim>-->
|
||||
<!-- </insert>-->
|
||||
|
||||
<!-- <update id="updateEqButton" parameterType="EqButton">-->
|
||||
<!-- update eq_button-->
|
||||
<!-- <trim prefix="SET" suffixOverrides=",">-->
|
||||
<!-- <if test="num != null">num = #{num},</if>-->
|
||||
<!-- <if test="name != null">name = #{name},</if>-->
|
||||
<!-- <if test="image != null">image = #{image},</if>-->
|
||||
<!-- <if test="delFlag != null">del_flag = #{delFlag},</if>-->
|
||||
<!-- <if test="remarks != null">remarks = #{remarks},</if>-->
|
||||
<!-- <if test="createBy != null">create_by = #{createBy},</if>-->
|
||||
<!-- <if test="createTime != null">create_time = #{createTime},</if>-->
|
||||
<!-- <if test="updateBy != null">update_by = #{updateBy},</if>-->
|
||||
<!-- <if test="updateTime != null">update_time = #{updateTime},</if>-->
|
||||
<!-- </trim>-->
|
||||
<!-- where id = #{id}-->
|
||||
<!-- </update>-->
|
||||
<!-- <!– 根据名称查询信息 –>-->
|
||||
<!-- <select id="selectEqButtonByName" parameterType="String" resultMap="EqButtonResult">-->
|
||||
<!-- <include refid="selectEqButtonVo"/>-->
|
||||
<!-- where del_flag = '0' and name = #{name}-->
|
||||
<!-- </select>-->
|
||||
</mapper>
|
||||
@ -50,6 +50,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="imageUrl" column="image_url" />
|
||||
<result property="timeClock" column="time_clock" />
|
||||
<result property="extended" column="extended" />
|
||||
<result property="dingUserId" column="ding_user_id" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectSysStaffVo">
|
||||
@ -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
|
||||
</sql>
|
||||
|
||||
@ -88,6 +89,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<select id="selectSysStaffByDingUserId" parameterType="String" resultMap="SysStaffResult">
|
||||
<include refid="selectSysStaffVo"/>
|
||||
where ding_user_id = #{dingUserId}
|
||||
</select>
|
||||
|
||||
<select id="selectSysStaffByUserId" parameterType="Long" resultMap="SysStaffResult">
|
||||
<include refid="selectSysStaffVo"/>
|
||||
where user_id = #{userId}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user