727 lines
31 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.designhub.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.designhub.common.ResultCode;
import com.designhub.entity.DingTalkConfig;
import com.designhub.exception.BusinessException;
import com.designhub.mapper.DepartmentMapper;
import com.designhub.mapper.DingTalkConfigMapper;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
/**
* 钉钉集成 Service
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DingTalkService {
private final DingTalkConfigMapper configMapper;
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate = new RestTemplate();
private final DepartmentMapper departmentMapper;
private static final String DINGTALK_API_BASE = "https://oapi.dingtalk.com";
/**
* 获取钉钉配置(优先获取启用的配置,如果没有则获取最新的配置)
*/
public DingTalkConfig getConfig() {
// 1. 优先获取启用的配置
LambdaQueryWrapper<DingTalkConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DingTalkConfig::getEnabled, 1)
.orderByDesc(DingTalkConfig::getUpdateTime)
.last("LIMIT 1");
DingTalkConfig config = configMapper.selectOne(wrapper);
// 2. 如果没有启用的配置,获取最新的配置(用于管理界面显示)
if (config == null) {
wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(DingTalkConfig::getUpdateTime)
.last("LIMIT 1");
config = configMapper.selectOne(wrapper);
}
return config;
}
/**
* 检查钉钉是否已配置且启用
*/
public boolean isConfigured() {
DingTalkConfig config = getConfig();
return config != null
&& config.getEnabled() == 1
&& config.getAppKey() != null
&& !config.getAppKey().isEmpty()
&& config.getAppSecret() != null
&& !config.getAppSecret().isEmpty();
}
/**
* 保存钉钉配置
*/
@Transactional
public DingTalkConfig saveConfig(DingTalkConfig config) {
// 如果enabledFeatures不是字符串格式,转换为JSON字符串
if (config.getEnabledFeatures() != null && !config.getEnabledFeatures().trim().startsWith("{")) {
// 已经是JSON字符串,不需要处理
}
if (config.getId() != null) {
configMapper.updateById(config);
} else {
config.setEnabled(1);
configMapper.insert(config);
}
return config;
}
/**
* 测试钉钉连接
*/
public boolean testConnection(DingTalkConfig config) {
try {
String accessToken = getAccessToken(config);
return accessToken != null && !accessToken.isEmpty();
} catch (Exception e) {
log.error("钉钉连接测试失败", e);
return false;
}
}
/**
* 获取 Access Token
*/
public String getAccessToken(DingTalkConfig config) {
// 检查缓存的token是否有效
if (config.getAccessToken() != null && config.getTokenExpireTime() != null
&& config.getTokenExpireTime().isAfter(LocalDateTime.now())) {
return config.getAccessToken();
}
try {
String url = String.format("%s/gettoken?appkey=%s&appsecret=%s",
DINGTALK_API_BASE, config.getAppKey(), config.getAppSecret());
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
// 安全地比较errcode(可能是Integer或Long类型)
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
String accessToken = (String) body.get("access_token");
Object expiresInObj = body.get("expires_in");
int expiresIn = expiresInObj instanceof Integer ? (Integer) expiresInObj :
expiresInObj instanceof Long ? ((Long) expiresInObj).intValue() : 7200;
// 缓存token(只有在config有id时才更新数据库)
if (config.getId() != null) {
config.setAccessToken(accessToken);
config.setTokenExpireTime(LocalDateTime.now().plusSeconds(expiresIn - 300)); // 提前5分钟过期
configMapper.updateById(config);
}
return accessToken;
} else {
log.error("获取钉钉AccessToken失败: errcode={}, errmsg={}, body={}",
errcode, body.get("errmsg"), body);
return null;
}
} else {
log.error("获取钉钉AccessToken失败: 响应体为空");
return null;
}
} catch (Exception e) {
log.error("获取钉钉AccessToken异常", e);
return null;
}
}
/**
* 发送工作通知
*/
public boolean sendWorkNotification(List<String> userIds, String title, String content) {
DingTalkConfig config = getConfig();
if (config == null) {
log.warn("钉钉未配置,跳过发送通知");
return false;
}
try {
String accessToken = getAccessToken(config);
if (accessToken == null) {
return false;
}
String url = String.format("%s/topapi/message/corpconversation/asyncsend_v2?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> msg = new HashMap<>();
msg.put("msgtype", "action_card");
Map<String, String> actionCard = new HashMap<>();
actionCard.put("title", title);
actionCard.put("markdown", content);
actionCard.put("single_title", "查看详情");
actionCard.put("single_url", "dingtalk://dingtalkclient/page/link?url=" +
URLEncoder.encode("https://your-domain.com", StandardCharsets.UTF_8.toString()));
msg.put("action_card", actionCard);
Map<String, Object> request = new HashMap<>();
request.put("agent_id", config.getAgentId());
request.put("userid_list", String.join(",", userIds));
request.put("msg", msg);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
log.info("钉钉工作通知发送成功: userIds={}", userIds);
return true;
} else {
log.error("钉钉工作通知发送失败: errcode={}, errmsg={}, body={}",
errcode, body.get("errmsg"), body);
return false;
}
} else {
log.error("钉钉工作通知发送失败: 响应体为空");
return false;
}
} catch (Exception e) {
log.error("发送钉钉工作通知异常", e);
return false;
}
}
/**
* 发送机器人消息
*/
public boolean sendRobotMessage(String title, String content) {
DingTalkConfig config = getConfig();
if (config == null || config.getRobotWebhook() == null) {
log.warn("钉钉机器人未配置");
return false;
}
try {
String webhook = config.getRobotWebhook();
// 如果有密钥,需要签名
if (config.getRobotSecret() != null && !config.getRobotSecret().isEmpty()) {
long timestamp = System.currentTimeMillis();
String sign = generateSign(timestamp, config.getRobotSecret());
webhook = webhook + "&timestamp=" + timestamp + "&sign=" + sign;
}
Map<String, Object> message = new HashMap<>();
message.put("msgtype", "markdown");
Map<String, String> markdown = new HashMap<>();
markdown.put("title", title);
markdown.put("text", "### " + title + "\n\n" + content);
message.put("markdown", markdown);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(message, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(webhook, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
log.info("钉钉机器人消息发送成功: title={}", title);
return true;
} else {
log.error("钉钉机器人消息发送失败: errcode={}, errmsg={}, body={}",
errcode, body.get("errmsg"), body);
return false;
}
} else {
log.error("钉钉机器人消息发送失败: 响应体为空");
return false;
}
} catch (Exception e) {
log.error("发送钉钉机器人消息异常", e);
return false;
}
}
/**
* 生成签名
*/
private String generateSign(long timestamp, String secret) throws Exception {
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return URLEncoder.encode(Base64.getEncoder().encodeToString(signData), StandardCharsets.UTF_8.toString());
}
/**
* 获取部门列表
*/
public List<Map<String, Object>> getDepartmentList() {
DingTalkConfig config = getConfig();
if (config == null) {
return Collections.emptyList();
}
try {
String accessToken = getAccessToken(config);
if (accessToken == null) {
return Collections.emptyList();
}
String url = String.format("%s/topapi/v2/department/listsub?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("dept_id", 1); // 根部门
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
return (List<Map<String, Object>>) body.get("result");
} else {
log.error("获取钉钉部门列表失败: errcode={}, errmsg={}", errcode, body.get("errmsg"));
}
}
return Collections.emptyList();
} catch (Exception e) {
log.error("获取钉钉部门列表异常", e);
return Collections.emptyList();
}
}
/**
* 钉钉OAuth登录 - 获取用户访问令牌
* 参考:https://open.dingtalk.com/document/orgapp/obtain-user-token
*/
public Map<String, Object> getUserAccessToken(String code) {
DingTalkConfig config = getConfig();
if (!isConfigured()) {
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉登录未配置或未启用,请联系管理员在系统设置中配置");
}
try {
String url = "https://api.dingtalk.com/v1.0/oauth2/userAccessToken";
Map<String, Object> request = new HashMap<>();
request.put("clientId", config.getAppKey());
request.put("clientSecret", config.getAppSecret());
request.put("code", code);
request.put("grantType", "authorization_code");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
log.info("获取用户访问令牌响应: {}", body);
return body;
} catch (Exception e) {
log.error("获取用户访问令牌失败", e);
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取钉钉用户令牌失败: " + e.getMessage());
}
}
/**
* 获取用户unionId
* 参考:https://open.dingtalk.com/document/orgapp/obtain-the-user-information-based-on-the-sns-temporary-authorization
*/
public Map<String, Object> getUserInfo(String accessToken) {
try {
String url = "https://api.dingtalk.com/v1.0/contact/users/me";
HttpHeaders headers = new HttpHeaders();
headers.set("x-acs-dingtalk-access-token", accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
Map<String, Object> body = response.getBody();
log.info("获取用户信息响应: {}", body);
return body;
} catch (Exception e) {
log.error("获取用户信息失败", e);
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取钉钉用户信息失败: " + e.getMessage());
}
}
/**
* 根据unionId获取userId
*/
public String getUserIdByUnionId(String unionId) {
DingTalkConfig config = getConfig();
String accessToken = getAccessToken(config);
try {
String url = String.format("%s/topapi/user/getbyunionid?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("unionid", unionId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
Map<String, Object> result = (Map<String, Object>) body.get("result");
return (String) result.get("userid");
} else {
log.error("根据unionId获取userId失败: errcode={}, errmsg={}", errcode, body.get("errmsg"));
}
}
return null;
} catch (Exception e) {
log.error("根据unionId获取userId失败", e);
return null;
}
}
/**
* 根据userId获取详细用户信息(包含部门信息)
*/
public Map<String, Object> getDetailedUserInfo(String userId) {
DingTalkConfig config = getConfig();
String accessToken = getAccessToken(config);
try {
String url = String.format("%s/topapi/v2/user/get?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("userid", userId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
Map<String, Object> result = (Map<String, Object>) body.get("result");
// 获取部门信息
Object deptIdListObj = result.get("dept_id_list");
if (deptIdListObj != null) {
List<Map<String, Object>> departments = new ArrayList<>();
List<?> deptIdList = (List<?>) deptIdListObj;
if (!deptIdList.isEmpty()) {
for (Object deptIdObj : deptIdList) {
// 处理Integer或Long类型
String deptIdStr = deptIdObj != null ? deptIdObj.toString() : null;
if (deptIdStr != null) {
Map<String, Object> deptInfo = getDepartmentInfo(deptIdStr);
if (deptInfo != null) {
departments.add(deptInfo);
}
}
}
result.put("departments", departments);
log.info("获取到用户部门信息: userId={}, departments={}", userId, departments.size());
}
}
return result;
} else {
log.error("获取详细用户信息失败: errcode={}, errmsg={}", errcode, body.get("errmsg"));
}
}
return null;
} catch (Exception e) {
log.error("获取详细用户信息失败", e);
return null;
}
}
/**
* 获取钉钉部门信息
*/
public Map<String, Object> getDepartmentInfo(String deptId) {
DingTalkConfig config = getConfig();
String accessToken = getAccessToken(config);
try {
String url = String.format("%s/topapi/v2/department/get?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("dept_id", Long.parseLong(deptId));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
return (Map<String, Object>) body.get("result");
} else {
log.error("获取钉钉部门信息失败: deptId={}, errcode={}, errmsg={}",
deptId, errcode, body.get("errmsg"));
}
}
return null;
} catch (Exception e) {
log.error("获取钉钉部门信息失败: deptId={}", deptId, e);
return null;
}
}
/**
* 同步钉钉部门到系统数据库
* 递归获取所有部门,保存或更新到数据库,并设置ding_dept_id
*/
@Transactional(rollbackFor = Exception.class)
public int syncDepartments() {
DingTalkConfig config = getConfig();
if (config == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉未配置");
}
log.info("开始同步钉钉部门...");
int syncCount = 0;
try {
String accessToken = getAccessToken(config);
if (accessToken == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "获取钉钉访问令牌失败");
}
// 递归获取所有部门(从根部门开始)
List<Map<String, Object>> allDepartments = getAllDepartmentsRecursive(accessToken, 1L);
log.info("从钉钉获取到 {} 个部门", allDepartments.size());
// 同步每个部门到数据库
for (Map<String, Object> dingDept : allDepartments) {
Object deptIdObj = dingDept.get("dept_id");
String dingDeptId = deptIdObj != null ? deptIdObj.toString() : null;
String deptName = (String) dingDept.get("name");
Object parentIdObj = dingDept.get("parent_id");
String parentDingDeptId = parentIdObj != null ? parentIdObj.toString() : null;
if (dingDeptId == null || deptName == null) {
log.warn("跳过无效的部门信息: {}", dingDept);
continue;
}
// 查找系统部门(通过ding_dept_id或名称)
com.designhub.entity.Department sysDept = departmentMapper.selectOne(
new LambdaQueryWrapper<com.designhub.entity.Department>()
.eq(com.designhub.entity.Department::getDingDeptId, dingDeptId)
.eq(com.designhub.entity.Department::getDeleted, 0)
.last("LIMIT 1")
);
if (sysDept == null) {
// 通过名称查找
sysDept = departmentMapper.selectOne(
new LambdaQueryWrapper<com.designhub.entity.Department>()
.eq(com.designhub.entity.Department::getDeptName, deptName)
.eq(com.designhub.entity.Department::getDeleted, 0)
.last("LIMIT 1")
);
}
if (sysDept != null) {
// 更新现有部门:设置ding_dept_id
sysDept.setDingDeptId(dingDeptId);
departmentMapper.updateById(sysDept);
log.info("更新部门: id={}, name={}, ding_dept_id={}", sysDept.getId(), deptName, dingDeptId);
} else {
// 部门不存在,自动创建
try {
Long parentId = 0L;
if (parentDingDeptId != null && !parentDingDeptId.equals("1")) {
// 查找父部门的系统ID
com.designhub.entity.Department parentDept = departmentMapper.selectOne(
new LambdaQueryWrapper<com.designhub.entity.Department>()
.eq(com.designhub.entity.Department::getDingDeptId, parentDingDeptId)
.eq(com.designhub.entity.Department::getDeleted, 0)
.last("LIMIT 1")
);
if (parentDept != null) {
parentId = parentDept.getId();
}
}
// 根据部门名称智能判断部门类型(可根据实际业务规则调整)
String deptType = inferDeptType(deptName);
// 创建新部门
com.designhub.entity.Department newDept = new com.designhub.entity.Department();
newDept.setDeptName(deptName);
newDept.setDingDeptId(dingDeptId);
newDept.setDeptType(deptType);
newDept.setParentId(parentId);
newDept.setStatus(1); // 启用
newDept.setSortOrder(0);
departmentMapper.insert(newDept);
// 初始化积分池会在DepartmentService.createDepartment中处理
// 这里直接调用createDepartment会触发积分池初始化
// 但为了避免循环依赖,我们直接使用mapper插入,积分池可以稍后初始化
// 如果需要积分池,可以在后台手动初始化,或者用户首次使用时会自动创建
log.info("自动创建部门: id={}, name={}, ding_dept_id={}, type={}, parentId={}",
newDept.getId(), deptName, dingDeptId, deptType, parentId);
} catch (Exception e) {
log.error("自动创建部门失败: name={}, ding_dept_id={}", deptName, dingDeptId, e);
// 创建失败不影响其他部门的同步,继续处理下一个
}
}
syncCount++;
}
log.info("同步钉钉部门完成,共处理 {} 个部门", syncCount);
return syncCount;
} catch (Exception e) {
log.error("同步钉钉部门失败", e);
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "同步钉钉部门失败: " + e.getMessage());
}
}
/**
* 递归获取所有部门
*/
private List<Map<String, Object>> getAllDepartmentsRecursive(String accessToken, Long parentDeptId) {
List<Map<String, Object>> allDepartments = new ArrayList<>();
try {
String url = String.format("%s/topapi/v2/department/listsub?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("dept_id", parentDeptId);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
Map<String, Object> body = response.getBody();
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
List<Map<String, Object>> subDepts = (List<Map<String, Object>>) body.get("result");
if (subDepts != null) {
for (Map<String, Object> dept : subDepts) {
allDepartments.add(dept);
// 递归获取子部门
Object deptIdObj = dept.get("dept_id");
if (deptIdObj != null) {
Long deptId = deptIdObj instanceof Long ? (Long) deptIdObj : Long.parseLong(deptIdObj.toString());
allDepartments.addAll(getAllDepartmentsRecursive(accessToken, deptId));
}
}
}
}
}
} catch (Exception e) {
log.error("获取部门列表失败: parentDeptId={}", parentDeptId, e);
}
return allDepartments;
}
/**
* 同步钉钉用户
*/
public int syncUsers() {
DingTalkConfig config = getConfig();
if (config == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉未配置");
}
// 获取用户列表并同步到系统
// 这里需要根据实际需求实现用户同步逻辑
log.info("开始同步钉钉用户...");
// TODO: 实现用户同步逻辑
return 0;
}
/**
* 检查功能是否启用
*/
public boolean isFeatureEnabled(String feature) {
DingTalkConfig config = getConfig();
// 检查配置是否存在、已启用,并且enabledFeatures不为空
if (config == null || config.getEnabled() == null || config.getEnabled() != 1
|| config.getEnabledFeatures() == null || config.getEnabledFeatures().trim().isEmpty()) {
return false;
}
try {
Map<String, Boolean> features = objectMapper.readValue(
config.getEnabledFeatures(), new TypeReference<Map<String, Boolean>>() {});
return features.getOrDefault(feature, false);
} catch (Exception e) {
log.warn("解析enabledFeatures失败: {}", e.getMessage());
return false;
}
}
/**
* 发送任务通知(如果启用钉钉通知)
*/
public void sendTaskNotification(String dingTalkUserId, String taskTitle, String action) {
if (!isFeatureEnabled("notification") || dingTalkUserId == null) {
return;
}
String title = "任务通知";
String content = String.format("您的任务「%s」%s", taskTitle, action);
sendWorkNotification(Collections.singletonList(dingTalkUserId), title, content);
}
}