1240 lines
54 KiB
Java
1240 lines
54 KiB
Java
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.entity.Department;
|
||
import com.designhub.entity.User;
|
||
import com.designhub.mapper.DepartmentMapper;
|
||
import com.designhub.mapper.DingTalkConfigMapper;
|
||
import com.designhub.mapper.UserMapper;
|
||
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 final UserMapper userMapper;
|
||
private final SystemConfigService systemConfigService;
|
||
|
||
private static final String DINGTALK_API_BASE = "https://oapi.dingtalk.com";
|
||
private static final String DEFAULT_SITE_URL = "https://your-domain.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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取配置的官网地址
|
||
*/
|
||
private String getSiteUrl() {
|
||
String siteUrl = systemConfigService.getConfigValue("site_url");
|
||
if (siteUrl == null || siteUrl.trim().isEmpty()) {
|
||
siteUrl = systemConfigService.getConfigValue("dingtalk_redirect_url");
|
||
}
|
||
if (siteUrl == null || siteUrl.trim().isEmpty()) {
|
||
siteUrl = DEFAULT_SITE_URL;
|
||
}
|
||
return siteUrl;
|
||
}
|
||
|
||
/**
|
||
* 构建钉钉跳转链接
|
||
* 如果配置了正确的corpId(以ding开头),使用应用内跳转;否则使用简单外链方式
|
||
* @param config 钉钉配置
|
||
* @param targetUrl 目标跳转地址
|
||
* @return 钉钉跳转链接
|
||
*/
|
||
private String buildDingTalkAppLink(DingTalkConfig config, String targetUrl) {
|
||
try {
|
||
String corpId = config.getCorpId();
|
||
String agentId = config.getAgentId();
|
||
|
||
// 检查corpId是否是有效的钉钉企业ID格式(以ding开头)
|
||
boolean validCorpId = corpId != null && corpId.trim().startsWith("ding");
|
||
boolean validAgentId = agentId != null && !agentId.trim().isEmpty();
|
||
|
||
if (validCorpId && validAgentId) {
|
||
// 使用应用内跳转
|
||
String appId = "0_" + agentId.trim();
|
||
String encodedUrl = URLEncoder.encode(targetUrl, StandardCharsets.UTF_8.toString());
|
||
String dingTalkLink = String.format(
|
||
"dingtalk://dingtalkclient/action/openapp?corpid=%s&container_type=work_platform&app_id=%s&redirect_type=jump&redirect_url=%s",
|
||
corpId.trim(), appId, encodedUrl
|
||
);
|
||
log.debug("使用钉钉应用内跳转: {}", dingTalkLink);
|
||
return dingTalkLink;
|
||
} else {
|
||
// corpId格式不正确,使用简单外链方式
|
||
log.info("corpId格式不正确或未配置,使用简单外链跳转。当前corpId: {}", corpId);
|
||
return targetUrl;
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("构建钉钉跳转链接失败", e);
|
||
return targetUrl;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送工作通知(卡片消息格式)
|
||
* 用户点击卡片可跳转到官网
|
||
*/
|
||
public boolean sendWorkNotification(List<String> userIds, String title, String content) {
|
||
return sendWorkNotification(userIds, title, content, null);
|
||
}
|
||
|
||
/**
|
||
* 发送工作通知(卡片消息格式,支持自定义跳转链接)
|
||
* @param userIds 接收用户ID列表
|
||
* @param title 消息标题
|
||
* @param content 消息内容(支持markdown格式)
|
||
* @param customUrl 自定义跳转链接,为空则使用官网地址
|
||
*/
|
||
public boolean sendWorkNotification(List<String> userIds, String title, String content, String customUrl) {
|
||
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);
|
||
|
||
// 获取目标跳转地址
|
||
String targetUrl = (customUrl != null && !customUrl.trim().isEmpty()) ? customUrl : getSiteUrl();
|
||
// 构建钉钉应用内跳转链接
|
||
String jumpUrl = buildDingTalkAppLink(config, targetUrl);
|
||
|
||
// 构建卡片消息
|
||
Map<String, Object> msg = new HashMap<>();
|
||
msg.put("msgtype", "action_card");
|
||
|
||
Map<String, String> actionCard = new HashMap<>();
|
||
actionCard.put("title", title);
|
||
// 构建 markdown 正文(卡片已有标题,内容中不再重复)
|
||
String markdown = content;
|
||
actionCard.put("markdown", markdown);
|
||
actionCard.put("single_title", "查看详情 »");
|
||
actionCard.put("single_url", jumpUrl);
|
||
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={}, title={}", userIds, 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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送机器人消息(卡片格式)
|
||
*/
|
||
public boolean sendRobotMessage(String title, String content) {
|
||
return sendRobotMessage(title, content, null);
|
||
}
|
||
|
||
/**
|
||
* 发送机器人消息(Markdown格式),支持@指定人(通过手机号)
|
||
* 注意:群机器人只支持 text/link/markdown/feedCard 类型,不支持 action_card
|
||
* @param title 消息标题
|
||
* @param content 消息内容(支持markdown格式)
|
||
* @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 + "×tamp=" + timestamp + "&sign=" + sign;
|
||
}
|
||
|
||
// 获取官网地址并构建钉钉应用内跳转链接
|
||
String siteUrl = getSiteUrl();
|
||
String jumpUrl = buildDingTalkAppLink(config, siteUrl);
|
||
|
||
// 构建 Markdown 消息(群机器人支持的格式)
|
||
Map<String, Object> message = new HashMap<>();
|
||
message.put("msgtype", "markdown");
|
||
|
||
Map<String, Object> markdown = new HashMap<>();
|
||
markdown.put("title", title);
|
||
|
||
// 构建 markdown 正文
|
||
StringBuilder markdownBuilder = new StringBuilder();
|
||
markdownBuilder.append("### ").append(title).append("\n\n");
|
||
markdownBuilder.append(content);
|
||
|
||
// 添加跳转链接
|
||
markdownBuilder.append("\n\n---\n\n");
|
||
markdownBuilder.append("[>>> 点击查看详情](").append(jumpUrl).append(")");
|
||
|
||
// 如果有@人,在内容中添加@标记
|
||
if (atMobiles != null && !atMobiles.isEmpty()) {
|
||
markdownBuilder.append("\n\n");
|
||
for (String mobile : atMobiles) {
|
||
markdownBuilder.append("@").append(mobile).append(" ");
|
||
}
|
||
}
|
||
|
||
markdown.put("text", markdownBuilder.toString());
|
||
|
||
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("钉钉机器人Markdown消息发送成功: 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());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过临时授权码获取用户信息(应用内免登专用)
|
||
* 使用 /topapi/v2/user/getuserinfo 接口
|
||
* 参考:https://open.dingtalk.com/document/orgapp/obtain-the-userid-of-a-user-by-using-the-log-free
|
||
*/
|
||
public Map<String, Object> getUserInfoByAuthCode(String authCode) {
|
||
DingTalkConfig config = getConfig();
|
||
if (!isConfigured()) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉登录未配置或未启用");
|
||
}
|
||
|
||
String accessToken = getAccessToken(config);
|
||
|
||
try {
|
||
String url = String.format("%s/topapi/v2/user/getuserinfo?access_token=%s",
|
||
DINGTALK_API_BASE, accessToken);
|
||
|
||
Map<String, Object> request = new HashMap<>();
|
||
request.put("code", authCode);
|
||
|
||
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);
|
||
|
||
if (body != null && Integer.valueOf(0).equals(body.get("errcode"))) {
|
||
Map<String, Object> result = (Map<String, Object>) body.get("result");
|
||
if (result != null) {
|
||
return result;
|
||
}
|
||
}
|
||
|
||
String errMsg = body != null ? String.valueOf(body.get("errmsg")) : "未知错误";
|
||
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取用户信息失败: " + errMsg);
|
||
} catch (BusinessException e) {
|
||
throw e;
|
||
} 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";
|
||
}
|
||
|
||
/**
|
||
* 同步钉钉组织架构(用户部门信息)
|
||
* 注意:只同步用户的部门信息,不覆盖管理员手动设置的部门负责人
|
||
*/
|
||
@Transactional
|
||
public Map<String, Object> syncOrganization() {
|
||
DingTalkConfig config = getConfig();
|
||
if (config == null || !isConfigured()) {
|
||
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉未配置或未启用");
|
||
}
|
||
|
||
log.info("开始同步钉钉组织架构...");
|
||
|
||
int updatedUsers = 0;
|
||
int skippedUsers = 0;
|
||
int errorUsers = 0;
|
||
int disabledUsers = 0; // 已离职被禁用的用户数
|
||
List<String> errors = new ArrayList<>();
|
||
|
||
try {
|
||
String accessToken = getAccessToken(config);
|
||
if (accessToken == null) {
|
||
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取钉钉访问令牌失败");
|
||
}
|
||
|
||
// 获取系统中所有有钉钉ID的用户
|
||
List<User> users = userMapper.selectList(
|
||
new LambdaQueryWrapper<User>()
|
||
.isNotNull(User::getDingUserId)
|
||
.ne(User::getDingUserId, "")
|
||
.eq(User::getStatus, 1)
|
||
);
|
||
|
||
log.info("找到 {} 个钉钉用户需要同步", users.size());
|
||
|
||
for (User user : users) {
|
||
try {
|
||
// 获取用户在钉钉中的部门信息
|
||
Map<String, Object> dingUserInfo = getDingUserDetail(accessToken, user.getDingUserId());
|
||
|
||
if (dingUserInfo != null) {
|
||
// 检查是否是"找不到用户"的错误(已离职)
|
||
Object errcode = dingUserInfo.get("_errcode");
|
||
if (errcode != null && "60121".equals(errcode.toString())) {
|
||
// 用户在钉钉中不存在(已离职),禁用该用户
|
||
user.setStatus(0);
|
||
userMapper.updateById(user);
|
||
disabledUsers++;
|
||
log.info("用户在钉钉中不存在(已离职),已禁用: userId={}, userName={}",
|
||
user.getId(), user.getRealName());
|
||
continue;
|
||
}
|
||
|
||
// 获取用户的主部门ID
|
||
Object deptIdListObj = dingUserInfo.get("dept_id_list");
|
||
if (deptIdListObj instanceof List && !((List<?>) deptIdListObj).isEmpty()) {
|
||
List<Integer> deptIdList = (List<Integer>) deptIdListObj;
|
||
Long dingDeptId = deptIdList.get(0).longValue(); // 取第一个作为主部门
|
||
|
||
// 查找系统中对应的部门
|
||
Department sysDept = departmentMapper.selectOne(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getDingDeptId, dingDeptId.toString())
|
||
.eq(Department::getStatus, 1)
|
||
);
|
||
|
||
if (sysDept != null) {
|
||
// 检查用户部门是否需要更新
|
||
if (!sysDept.getId().equals(user.getDeptId())) {
|
||
user.setDeptId(sysDept.getId());
|
||
user.setDeptName(sysDept.getDeptName());
|
||
userMapper.updateById(user);
|
||
updatedUsers++;
|
||
log.info("更新用户部门: userId={}, userName={}, newDeptId={}, newDeptName={}",
|
||
user.getId(), user.getRealName(), sysDept.getId(), sysDept.getDeptName());
|
||
} else {
|
||
skippedUsers++;
|
||
}
|
||
} else {
|
||
// 系统中没有对应部门,自动创建
|
||
Map<String, Object> dingDeptInfo = getDingDepartmentDetail(accessToken, dingDeptId);
|
||
if (dingDeptInfo != null) {
|
||
String deptName = (String) dingDeptInfo.get("name");
|
||
Object parentIdObj = dingDeptInfo.get("parent_id");
|
||
Long dingParentId = parentIdObj != null ? Long.parseLong(parentIdObj.toString()) : 1L;
|
||
|
||
// 创建新部门
|
||
Department newDept = createDepartmentFromDingTalk(accessToken, dingDeptId, deptName, dingParentId);
|
||
if (newDept != null) {
|
||
// 更新用户部门
|
||
user.setDeptId(newDept.getId());
|
||
user.setDeptName(newDept.getDeptName());
|
||
userMapper.updateById(user);
|
||
updatedUsers++;
|
||
log.info("自动创建部门并更新用户: userId={}, userName={}, newDeptId={}, newDeptName={}",
|
||
user.getId(), user.getRealName(), newDept.getId(), newDept.getDeptName());
|
||
} else {
|
||
errors.add(String.format("用户 %s 的钉钉部门 %s 创建失败", user.getRealName(), deptName));
|
||
errorUsers++;
|
||
}
|
||
} else {
|
||
errorUsers++;
|
||
}
|
||
}
|
||
} else {
|
||
skippedUsers++;
|
||
log.debug("用户 {} 在钉钉中没有部门信息", user.getRealName());
|
||
}
|
||
} else {
|
||
errorUsers++;
|
||
errors.add(String.format("无法获取用户 %s 的钉钉信息", user.getRealName()));
|
||
}
|
||
} catch (Exception e) {
|
||
errorUsers++;
|
||
errors.add(String.format("同步用户 %s 失败: %s", user.getRealName(), e.getMessage()));
|
||
log.error("同步用户 {} 失败", user.getRealName(), e);
|
||
}
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("同步钉钉组织架构失败", e);
|
||
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "同步失败: " + e.getMessage());
|
||
}
|
||
|
||
Map<String, Object> result = new HashMap<>();
|
||
result.put("updatedUsers", updatedUsers);
|
||
result.put("skippedUsers", skippedUsers);
|
||
result.put("errorUsers", errorUsers);
|
||
result.put("disabledUsers", disabledUsers);
|
||
result.put("errors", errors);
|
||
|
||
log.info("钉钉组织架构同步完成: 更新{}人, 跳过{}人, 禁用{}人(已离职), 失败{}人",
|
||
updatedUsers, skippedUsers, disabledUsers, errorUsers);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 获取钉钉用户详情
|
||
* 注意:如果用户不存在(errcode=60121),会返回包含 _errcode 字段的 Map
|
||
*/
|
||
private Map<String, Object> getDingUserDetail(String accessToken, String dingUserId) {
|
||
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", dingUserId);
|
||
|
||
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.warn("获取钉钉用户详情失败: userId={}, errcode={}, errmsg={}",
|
||
dingUserId, errcode, body.get("errmsg"));
|
||
// 如果是"找不到用户"的错误,返回包含错误码的 Map,便于调用方判断
|
||
if (errcode != null && ("60121".equals(errcode.toString()) || errcode.equals(60121))) {
|
||
Map<String, Object> errorResult = new HashMap<>();
|
||
errorResult.put("_errcode", "60121");
|
||
errorResult.put("_errmsg", "找不到该用户");
|
||
return errorResult;
|
||
}
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("获取钉钉用户详情异常: userId={}", dingUserId, e);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取钉钉部门详情
|
||
*/
|
||
private Map<String, Object> getDingDepartmentDetail(String accessToken, Long deptId) {
|
||
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", 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");
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("获取钉钉部门详情异常: deptId={}", deptId, e);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 从钉钉部门信息创建系统部门
|
||
* 注意:不设置部门负责人,保留给管理员手动设置
|
||
*/
|
||
private Department createDepartmentFromDingTalk(String accessToken, Long dingDeptId, String deptName, Long dingParentId) {
|
||
try {
|
||
// 查找父部门
|
||
Long sysParentId = 0L;
|
||
if (dingParentId != null && dingParentId > 1) {
|
||
Department parentDept = departmentMapper.selectOne(
|
||
new LambdaQueryWrapper<Department>()
|
||
.eq(Department::getDingDeptId, dingParentId.toString())
|
||
.eq(Department::getStatus, 1)
|
||
);
|
||
if (parentDept != null) {
|
||
sysParentId = parentDept.getId();
|
||
} else {
|
||
// 递归创建父部门
|
||
Map<String, Object> parentDingDeptInfo = getDingDepartmentDetail(accessToken, dingParentId);
|
||
if (parentDingDeptInfo != null) {
|
||
String parentName = (String) parentDingDeptInfo.get("name");
|
||
Object grandParentIdObj = parentDingDeptInfo.get("parent_id");
|
||
Long grandParentId = grandParentIdObj != null ? Long.parseLong(grandParentIdObj.toString()) : 1L;
|
||
Department createdParent = createDepartmentFromDingTalk(accessToken, dingParentId, parentName, grandParentId);
|
||
if (createdParent != null) {
|
||
sysParentId = createdParent.getId();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 推断部门类型
|
||
String deptType = inferDeptType(deptName);
|
||
|
||
// 如果有父部门,继承父部门的类型
|
||
if (sysParentId > 0) {
|
||
Department parentDept = departmentMapper.selectById(sysParentId);
|
||
if (parentDept != null && parentDept.getDeptType() != null) {
|
||
deptType = parentDept.getDeptType();
|
||
}
|
||
}
|
||
|
||
// 创建新部门
|
||
Department newDept = new Department();
|
||
newDept.setDeptName(deptName);
|
||
newDept.setDingDeptId(dingDeptId.toString());
|
||
newDept.setParentId(sysParentId);
|
||
newDept.setDeptType(deptType);
|
||
newDept.setStatus(1);
|
||
newDept.setSortOrder(0);
|
||
// 注意:不设置 leaderId,保留给管理员手动设置
|
||
|
||
departmentMapper.insert(newDept);
|
||
log.info("自动创建部门: id={}, name={}, dingDeptId={}, parentId={}, deptType={}",
|
||
newDept.getId(), deptName, dingDeptId, sysParentId, deptType);
|
||
|
||
return newDept;
|
||
} catch (Exception e) {
|
||
log.error("创建部门失败: dingDeptId={}, deptName={}", dingDeptId, deptName, e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查功能是否启用
|
||
*/
|
||
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);
|
||
}
|
||
}
|