Your Name d0cb8e800f feat: 群通知增加处理人信息和@功能
- 任务指派时发送群通知并@被指定的设计师
- 子任务组指派时发送群通知并@被指定的设计师
- 任务驳回时如果有处理人发送群通知并@处理人
- 子任务组驳回时发送群通知并@所有处理人
- 钉钉机器人支持通过手机号@群成员
2026-01-04 08:30:03 +08:00

840 lines
35 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;
}
}
/**
* 根据手机号获取userId
*/
public String getUserIdByMobile(String mobile) {
DingTalkConfig config = getConfig();
if (config == null) return null;
String accessToken = getAccessToken(config);
try {
String url = String.format("%s/topapi/v2/user/getbymobile?access_token=%s",
DINGTALK_API_BASE, accessToken);
Map<String, Object> request = new HashMap<>();
request.put("mobile", mobile);
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.warn("根据手机号获取userId失败: mobile={}, errcode={}, errmsg={}",
mobile, errcode, body.get("errmsg"));
}
}
return null;
} catch (Exception e) {
log.error("根据手机号获取userId异常", 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) {
return sendRobotMessage(title, content, null);
}
/**
* 发送机器人消息,支持@指定人(通过手机号)
* @param title 消息标题
* @param content 消息内容
* @param atMobiles 要@的手机号列表,null或空表示不@任何人
*/
public boolean sendRobotMessage(String title, String content, List<String> atMobiles) {
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);
// 添加@人功能
if (atMobiles != null && !atMobiles.isEmpty()) {
Map<String, Object> at = new HashMap<>();
at.put("atMobiles", atMobiles);
at.put("isAtAll", false);
message.put("at", at);
log.info("群消息将@手机号: {}", atMobiles);
}
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());
// 按照层级排序:父部门在前,子部门在后
// 通过多次遍历确保父部门先创建
Map<String, Long> dingDeptIdToSysIdMap = new HashMap<>(); // 缓存ding_dept_id到系统ID的映射
// 第一遍:创建所有部门(如果不存在),并设置ding_dept_id和parent_id为0
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");
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) {
// 部门不存在,自动创建(先不设置parent_id,等第二遍再设置)
try {
// 根据部门名称智能判断部门类型
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(0L); // 先设置为0,第二遍再更新
newDept.setStatus(1); // 启用
newDept.setSortOrder(0);
departmentMapper.insert(newDept);
sysDept = newDept;
log.info("自动创建部门(第一遍): id={}, name={}, ding_dept_id={}, type={}",
newDept.getId(), deptName, dingDeptId, deptType);
} catch (Exception e) {
log.error("自动创建部门失败: name={}, ding_dept_id={}", deptName, dingDeptId, e);
continue; // 跳过失败的部门
}
} else {
// 更新ding_dept_id(如果还没有)
if (!dingDeptId.equals(sysDept.getDingDeptId())) {
sysDept.setDingDeptId(dingDeptId);
departmentMapper.updateById(sysDept);
log.info("更新部门ding_dept_id: id={}, name={}, ding_dept_id={}",
sysDept.getId(), deptName, dingDeptId);
}
}
// 缓存映射关系
dingDeptIdToSysIdMap.put(dingDeptId, sysDept.getId());
}
// 第二遍:更新所有部门的parent_id
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) {
continue;
}
Long sysDeptId = dingDeptIdToSysIdMap.get(dingDeptId);
if (sysDeptId == null) {
continue; // 第一遍创建失败的部门跳过
}
// 获取系统部门
com.designhub.entity.Department sysDept = departmentMapper.selectById(sysDeptId);
if (sysDept == null) {
continue;
}
// 确定parent_id
Long parentId = 0L;
if (parentDingDeptId != null && !parentDingDeptId.equals("1")) {
// 从缓存中获取父部门的系统ID
Long parentSysId = dingDeptIdToSysIdMap.get(parentDingDeptId);
if (parentSysId != null) {
parentId = parentSysId;
}
}
// 更新parent_id(如果不同)
Long currentParentId = sysDept.getParentId() != null ? sysDept.getParentId() : 0L;
if (!parentId.equals(currentParentId)) {
sysDept.setParentId(parentId);
departmentMapper.updateById(sysDept);
log.info("更新部门父子关系: id={}, name={}, parentId: {} -> {}",
sysDept.getId(), deptName, currentParentId, parentId);
}
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;
}
/**
* 根据部门名称推断部门类型
* 可以根据实际业务规则调整判断逻辑
*/
private String inferDeptType(String deptName) {
if (deptName == null) {
return "demand"; // 默认需求部门
}
String name = deptName.toLowerCase();
// 常见的接单部门关键词
if (name.contains("设计") || name.contains("创意") || name.contains("美工") ||
name.contains("视觉") || name.contains("ui") || name.contains("ux")) {
return "receiving";
}
// 默认需求部门
return "demand";
}
/**
* 同步钉钉用户
*/
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);
}
}