727 lines
31 KiB
Java
727 lines
31 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.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 + "×tamp=" + 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);
|
||
}
|
||
}
|