- 后端新增 /api/dingtalk/test-message-personal 接口 - 支持根据用户名/真实姓名查找用户并发送工作通知 - 前端Settings页面新增个人工作通知测试区域 - 支持多种消息类型:简单测试、新任务发布、任务指派、任务完成、积分变动
496 lines
24 KiB
Java
496 lines
24 KiB
Java
package com.designhub.controller;
|
||
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
import com.designhub.common.Result;
|
||
import com.designhub.entity.DingTalkConfig;
|
||
import com.designhub.entity.User;
|
||
import com.designhub.mapper.UserMapper;
|
||
import com.designhub.service.DingTalkService;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import io.swagger.v3.oas.annotations.Operation;
|
||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.web.bind.annotation.*;
|
||
|
||
import java.util.Collections;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* 钉钉集成 Controller
|
||
*/
|
||
@Slf4j
|
||
@Tag(name = "钉钉集成", description = "钉钉集成管理API")
|
||
@RestController
|
||
@RequestMapping("/api/dingtalk")
|
||
@RequiredArgsConstructor
|
||
public class DingTalkController {
|
||
|
||
private final DingTalkService dingTalkService;
|
||
private final ObjectMapper objectMapper;
|
||
private final UserMapper userMapper;
|
||
|
||
@Operation(summary = "获取钉钉配置")
|
||
@GetMapping("/config")
|
||
public Result<DingTalkConfig> getConfig() {
|
||
DingTalkConfig config = dingTalkService.getConfig();
|
||
|
||
// 如果没有配置,返回默认配置(仅用于前端显示)
|
||
if (config == null) {
|
||
config = new DingTalkConfig();
|
||
config.setName("默认配置");
|
||
config.setEnabled(0);
|
||
}
|
||
|
||
// 隐藏敏感信息
|
||
if (config.getAppSecret() != null && !config.getAppSecret().isEmpty()) {
|
||
config.setAppSecret("******");
|
||
}
|
||
if (config.getRobotSecret() != null && !config.getRobotSecret().isEmpty()) {
|
||
config.setRobotSecret("******");
|
||
}
|
||
config.setAccessToken(null);
|
||
|
||
return Result.success(config);
|
||
}
|
||
|
||
@Operation(summary = "保存钉钉配置")
|
||
@PostMapping("/config")
|
||
public Result<DingTalkConfig> saveConfig(@RequestBody Map<String, Object> configMap) {
|
||
try {
|
||
// 处理enabledFeatures字段
|
||
Object enabledFeaturesObj = configMap.get("enabledFeatures");
|
||
String enabledFeaturesJson = "{}"; // 默认为空JSON对象
|
||
if (enabledFeaturesObj != null) {
|
||
if (enabledFeaturesObj instanceof String) {
|
||
enabledFeaturesJson = (String) enabledFeaturesObj;
|
||
} else if (enabledFeaturesObj instanceof Map && !((Map<?, ?>) enabledFeaturesObj).isEmpty()) {
|
||
// 转换为JSON字符串
|
||
enabledFeaturesJson = objectMapper.writeValueAsString(enabledFeaturesObj);
|
||
} else {
|
||
// 如果是空对象或null,使用空JSON对象
|
||
enabledFeaturesJson = "{}";
|
||
}
|
||
}
|
||
|
||
// 构建配置对象
|
||
DingTalkConfig config = new DingTalkConfig();
|
||
config.setId(configMap.get("id") != null ? ((Number) configMap.get("id")).longValue() : null);
|
||
config.setName((String) configMap.get("name"));
|
||
config.setAgentId((String) configMap.get("agentId"));
|
||
config.setAppKey((String) configMap.get("appKey"));
|
||
config.setAppSecret((String) configMap.get("appSecret"));
|
||
config.setCorpId((String) configMap.get("corpId"));
|
||
config.setRobotWebhook((String) configMap.get("robotWebhook"));
|
||
config.setRobotSecret((String) configMap.get("robotSecret"));
|
||
config.setEnabled(configMap.get("enabled") != null ? ((Number) configMap.get("enabled")).intValue() : 1);
|
||
config.setEnabledFeatures(enabledFeaturesJson);
|
||
|
||
// 如果前端传来的appSecret是占位符,则保留原有的secret
|
||
if ("******".equals(config.getAppSecret())) {
|
||
DingTalkConfig existingConfig = dingTalkService.getConfig();
|
||
if (existingConfig != null) {
|
||
config.setAppSecret(existingConfig.getAppSecret());
|
||
}
|
||
}
|
||
if ("******".equals(config.getRobotSecret())) {
|
||
DingTalkConfig existingConfig = dingTalkService.getConfig();
|
||
if (existingConfig != null) {
|
||
config.setRobotSecret(existingConfig.getRobotSecret());
|
||
}
|
||
}
|
||
|
||
DingTalkConfig saved = dingTalkService.saveConfig(config);
|
||
|
||
// 返回时隐藏敏感信息
|
||
if (saved.getAppSecret() != null && !saved.getAppSecret().isEmpty()) {
|
||
saved.setAppSecret("******");
|
||
}
|
||
if (saved.getRobotSecret() != null && !saved.getRobotSecret().isEmpty()) {
|
||
saved.setRobotSecret("******");
|
||
}
|
||
saved.setAccessToken(null);
|
||
|
||
return Result.success(saved);
|
||
} catch (Exception e) {
|
||
return Result.error("保存配置失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "测试钉钉连接")
|
||
@PostMapping("/test")
|
||
public Result<Boolean> testConnection(@RequestBody DingTalkConfig config) {
|
||
try {
|
||
// 如果前端传来的appSecret是占位符,则从数据库获取真实的secret
|
||
if ("******".equals(config.getAppSecret())) {
|
||
DingTalkConfig existingConfig = dingTalkService.getConfig();
|
||
if (existingConfig != null && existingConfig.getAppSecret() != null) {
|
||
config.setAppSecret(existingConfig.getAppSecret());
|
||
} else {
|
||
return Result.error("请先保存配置后再测试连接");
|
||
}
|
||
}
|
||
// 如果appKey或appSecret为空,返回错误
|
||
if (config.getAppKey() == null || config.getAppKey().isEmpty()
|
||
|| config.getAppSecret() == null || config.getAppSecret().isEmpty()) {
|
||
return Result.error("AppKey和AppSecret不能为空");
|
||
}
|
||
return Result.success(dingTalkService.testConnection(config));
|
||
} catch (Exception e) {
|
||
return Result.error("测试连接失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "发送测试消息")
|
||
@PostMapping("/test-message")
|
||
public Result<Boolean> sendTestMessage(@RequestBody Map<String, String> params) {
|
||
String messageType = params.getOrDefault("type", "simple");
|
||
String title;
|
||
StringBuilder content = new StringBuilder();
|
||
|
||
switch (messageType) {
|
||
case "task_publish":
|
||
// 新任务发布样式
|
||
title = "新任务发布(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 新 任 务 发 布 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("任务名称:示例任务标题 \n");
|
||
content.append("任务编号:TSK20260104001 \n");
|
||
content.append("任务积分:100 分 \n");
|
||
content.append("任务数量:5 个 \n");
|
||
content.append("任务需求类型:3D模型 \n");
|
||
content.append("品牌:示例品牌 \n");
|
||
content.append("类目:示例类目 \n");
|
||
content.append("平台:天猫 \n");
|
||
content.append("SPU信息:SPU12345 \n");
|
||
content.append("预估工时:8 小时(接单后开始计时) \n");
|
||
content.append(" \n");
|
||
content.append("发起人:张三 \n");
|
||
content.append("发起部门:市场部 \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("前往抢单:qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
case "task_assign":
|
||
// 任务指派样式
|
||
title = "任务已指派(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 任 务 已 指 派 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("任务名称:示例任务标题 \n");
|
||
content.append("任务编号:TSK20260104001 \n");
|
||
content.append("任务积分:100 分 \n");
|
||
content.append("指派给:李四 \n");
|
||
content.append("预估工时:8 小时 \n");
|
||
content.append("发起人:张三 \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("请尽快处理!qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
case "task_reject":
|
||
// 任务驳回样式
|
||
title = "任务已驳回(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 任 务 已 驳 回 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("任务名称:示例任务标题 \n");
|
||
content.append("任务编号:TSK20260104001 \n");
|
||
content.append("原处理人:李四 \n");
|
||
content.append("驳回原因:设计稿不符合要求,请重新修改 \n");
|
||
content.append(" \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
case "subtask_publish":
|
||
// 子任务组发布样式
|
||
title = "子任务组发布(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 子任务组发布 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("主任务:示例主任务标题 \n");
|
||
content.append("任务编号:TSK20260104001 \n");
|
||
content.append("当前组号:第 2 组 \n");
|
||
content.append("子任务数:3 个 \n");
|
||
content.append("组积分:60 分 \n");
|
||
content.append("任务需求类型:平面设计 \n");
|
||
content.append(" \n");
|
||
content.append("子任务列表: \n");
|
||
content.append(" 1. 首页Banner(20分) \n");
|
||
content.append(" 2. 详情页设计(25分) \n");
|
||
content.append(" 3. 主图设计(15分) \n");
|
||
content.append(" \n");
|
||
content.append("发起人:张三 \n");
|
||
content.append("发起部门:市场部 \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("前往抢单:qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
default:
|
||
// 简单测试消息
|
||
title = params.getOrDefault("title", "系统测试消息");
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 系 统 测 试 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("这是一条来自 DesignHub 系统的测试消息 \n");
|
||
content.append(" \n");
|
||
content.append("如果您收到此消息,说明钉钉机器人配置成功! \n");
|
||
content.append(" \n");
|
||
content.append("测试时间:").append(java.time.LocalDateTime.now().format(
|
||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append(" \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("访问系统:qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
}
|
||
|
||
return Result.success(dingTalkService.sendRobotMessage(title, content.toString()));
|
||
}
|
||
|
||
@Operation(summary = "发送个人测试消息", description = "向指定用户发送钉钉工作通知")
|
||
@PostMapping("/test-message-personal")
|
||
public Result<Map<String, Object>> sendPersonalTestMessage(@RequestBody Map<String, String> params) {
|
||
String userName = params.get("userName");
|
||
String messageType = params.getOrDefault("type", "simple");
|
||
|
||
if (userName == null || userName.trim().isEmpty()) {
|
||
return Result.error("请指定接收人");
|
||
}
|
||
|
||
// 根据用户名或真实姓名查找用户
|
||
User user = userMapper.selectOne(
|
||
new LambdaQueryWrapper<User>()
|
||
.and(w -> w.eq(User::getUsername, userName.trim())
|
||
.or().eq(User::getRealName, userName.trim()))
|
||
.eq(User::getDeleted, 0)
|
||
.last("LIMIT 1")
|
||
);
|
||
|
||
if (user == null) {
|
||
return Result.error("未找到用户: " + userName);
|
||
}
|
||
|
||
// 检查用户是否绑定了钉钉ID
|
||
String dingUserId = user.getDingUserId();
|
||
if (dingUserId == null || dingUserId.isEmpty()) {
|
||
// 尝试通过手机号获取钉钉ID
|
||
if (user.getPhone() != null && !user.getPhone().isEmpty()) {
|
||
dingUserId = dingTalkService.getUserIdByMobile(user.getPhone());
|
||
if (dingUserId != null) {
|
||
// 更新用户的钉钉ID
|
||
user.setDingUserId(dingUserId);
|
||
userMapper.updateById(user);
|
||
log.info("通过手机号自动绑定钉钉ID: userId={}, phone={}, dingUserId={}",
|
||
user.getId(), user.getPhone(), dingUserId);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (dingUserId == null || dingUserId.isEmpty()) {
|
||
return Result.error("用户未绑定钉钉,请先在钉钉中绑定或确保手机号正确");
|
||
}
|
||
|
||
// 构建消息内容
|
||
String title;
|
||
StringBuilder content = new StringBuilder();
|
||
|
||
switch (messageType) {
|
||
case "task_publish":
|
||
title = "新任务发布(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 新 任 务 发 布 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("任务名称:示例任务标题 \n");
|
||
content.append("任务编号:TSK20260104001 \n");
|
||
content.append("任务积分:100 分 \n");
|
||
content.append("任务数量:5 个 \n");
|
||
content.append("任务需求类型:3D模型 \n");
|
||
content.append("品牌:示例品牌 \n");
|
||
content.append("类目:示例类目 \n");
|
||
content.append("平台:天猫 \n");
|
||
content.append("SPU信息:SPU12345 \n");
|
||
content.append("预估工时:8 小时(接单后开始计时) \n");
|
||
content.append(" \n");
|
||
content.append("发起人:张三 \n");
|
||
content.append("发起部门:市场部 \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("前往抢单:qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
case "task_assign":
|
||
title = "任务已指派(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 任 务 已 指 派 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("任务名称:示例任务标题 \n");
|
||
content.append("任务编号:TSK20260104001 \n");
|
||
content.append("任务积分:100 分 \n");
|
||
content.append("指派给:").append(user.getRealName() != null ? user.getRealName() : user.getUsername()).append(" \n");
|
||
content.append("预估工时:8 小时 \n");
|
||
content.append("发起人:张三 \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("请尽快处理!qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
case "task_complete":
|
||
title = "任务已完成(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 任 务 已 完 成 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("任务名称:示例任务标题 \n");
|
||
content.append("任务编号:TSK20260104001 \n");
|
||
content.append("任务积分:100 分 \n");
|
||
content.append("处理人:").append(user.getRealName() != null ? user.getRealName() : user.getUsername()).append(" \n");
|
||
content.append("处理时长:6 小时 30 分钟 \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("请前往验收!qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
case "score_change":
|
||
title = "积分变动通知(测试)";
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 积 分 变 动 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("变动类型:任务完成奖励 \n");
|
||
content.append("变动积分:+100 分 \n");
|
||
content.append("当前积分:1,500 分 \n");
|
||
content.append("变动时间:").append(java.time.LocalDateTime.now().format(
|
||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append(" \n");
|
||
content.append(" \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
|
||
default:
|
||
title = params.getOrDefault("title", "系统测试消息");
|
||
content.append("-------------------------------- \n");
|
||
content.append(" 【 系 统 测 试 】 \n");
|
||
content.append("-------------------------------- \n\n");
|
||
content.append("亲爱的 ").append(user.getRealName() != null ? user.getRealName() : user.getUsername()).append(": \n\n");
|
||
content.append("这是一条来自 DesignHub 系统的测试消息 \n");
|
||
content.append(" \n");
|
||
content.append("如果您收到此消息,说明钉钉个人通知配置成功! \n");
|
||
content.append(" \n");
|
||
content.append("测试时间:").append(java.time.LocalDateTime.now().format(
|
||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append(" \n");
|
||
content.append(" \n");
|
||
content.append("-------------------------------- \n");
|
||
content.append("访问系统:qd.yydd.ai \n");
|
||
content.append("--------------------------------");
|
||
break;
|
||
}
|
||
|
||
// 发送工作通知
|
||
boolean success = dingTalkService.sendWorkNotification(
|
||
Collections.singletonList(dingUserId),
|
||
title,
|
||
content.toString()
|
||
);
|
||
|
||
Map<String, Object> result = new HashMap<>();
|
||
result.put("success", success);
|
||
result.put("userName", user.getRealName() != null ? user.getRealName() : user.getUsername());
|
||
result.put("dingUserId", dingUserId);
|
||
result.put("messageType", messageType);
|
||
|
||
if (success) {
|
||
return Result.success(result);
|
||
} else {
|
||
result.put("error", "消息发送失败,请检查钉钉配置");
|
||
return Result.error("消息发送失败");
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "获取钉钉部门列表")
|
||
@GetMapping("/departments")
|
||
public Result<List<Map<String, Object>>> getDepartmentList() {
|
||
return Result.success(dingTalkService.getDepartmentList());
|
||
}
|
||
|
||
@Operation(summary = "同步钉钉部门")
|
||
@PostMapping("/sync-departments")
|
||
public Result<Integer> syncDepartments() {
|
||
try {
|
||
return Result.success(dingTalkService.syncDepartments());
|
||
} catch (Exception e) {
|
||
log.error("同步钉钉部门失败", e);
|
||
return Result.error("同步钉钉部门失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "同步钉钉用户")
|
||
@PostMapping("/sync-users")
|
||
public Result<Integer> syncUsers() {
|
||
return Result.success(dingTalkService.syncUsers());
|
||
}
|
||
|
||
@Operation(summary = "检查功能状态")
|
||
@GetMapping("/feature/{feature}")
|
||
public Result<Boolean> isFeatureEnabled(@PathVariable String feature) {
|
||
return Result.success(dingTalkService.isFeatureEnabled(feature));
|
||
}
|
||
|
||
@Operation(summary = "钉钉OAuth登录发起", description = "生成钉钉登录URL")
|
||
@GetMapping("/oauth/login")
|
||
public Result<Map<String, String>> initiateLogin(@RequestParam(required = false) String redirectUrl) {
|
||
try {
|
||
// 检查是否配置
|
||
if (!dingTalkService.isConfigured()) {
|
||
return Result.error("钉钉登录未配置或未启用,请联系管理员在系统设置中配置");
|
||
}
|
||
|
||
// 检查登录功能是否启用
|
||
if (!dingTalkService.isFeatureEnabled("login")) {
|
||
return Result.error("钉钉登录功能未启用,请在系统设置中启用钉钉登录功能");
|
||
}
|
||
|
||
DingTalkConfig config = dingTalkService.getConfig();
|
||
if (config == null) {
|
||
return Result.error("钉钉配置不存在");
|
||
}
|
||
|
||
if (config.getAppKey() == null || config.getAppKey().isEmpty()) {
|
||
return Result.error("钉钉AppKey未配置");
|
||
}
|
||
|
||
// 构建回调URL
|
||
String callbackUrl = redirectUrl != null ? redirectUrl : "http://localhost:3000/auth/dingtalk/callback";
|
||
|
||
// 构建钉钉OAuth URL
|
||
String authUrl = String.format(
|
||
"https://login.dingtalk.com/oauth2/auth?redirect_uri=%s&response_type=code&client_id=%s&scope=openid&state=STATE&prompt=consent",
|
||
java.net.URLEncoder.encode(callbackUrl, java.nio.charset.StandardCharsets.UTF_8),
|
||
config.getAppKey()
|
||
);
|
||
|
||
Map<String, String> result = new HashMap<>();
|
||
result.put("authUrl", authUrl);
|
||
result.put("appKey", config.getAppKey());
|
||
return Result.success(result);
|
||
} catch (Exception e) {
|
||
log.error("获取钉钉登录地址失败", e);
|
||
return Result.error("获取钉钉登录地址失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "检查钉钉登录是否已配置")
|
||
@GetMapping("/is-configured")
|
||
public Result<Boolean> isConfigured() {
|
||
return Result.success(dingTalkService.isConfigured());
|
||
}
|
||
}
|