Your Name 1d33d453d3 feat: 添加钉钉个人消息测试功能
- 后端新增 /api/dingtalk/test-message-personal 接口
- 支持根据用户名/真实姓名查找用户并发送工作通知
- 前端Settings页面新增个人工作通知测试区域
- 支持多种消息类型:简单测试、新任务发布、任务指派、任务完成、积分变动
2026-01-05 00:03:34 +08:00

496 lines
24 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.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());
}
}