diff --git a/backend/src/main/java/com/designhub/controller/DingTalkController.java b/backend/src/main/java/com/designhub/controller/DingTalkController.java index d3782d8..3f057bd 100644 --- a/backend/src/main/java/com/designhub/controller/DingTalkController.java +++ b/backend/src/main/java/com/designhub/controller/DingTalkController.java @@ -1,7 +1,10 @@ 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; @@ -10,6 +13,7 @@ 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; @@ -26,6 +30,7 @@ public class DingTalkController { private final DingTalkService dingTalkService; private final ObjectMapper objectMapper; + private final UserMapper userMapper; @Operation(summary = "获取钉钉配置") @GetMapping("/config") @@ -251,6 +256,165 @@ public class DingTalkController { return Result.success(dingTalkService.sendRobotMessage(title, content.toString())); } + @Operation(summary = "发送个人测试消息", description = "向指定用户发送钉钉工作通知") + @PostMapping("/test-message-personal") + public Result> sendPersonalTestMessage(@RequestBody Map 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() + .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 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>> getDepartmentList() { diff --git a/frontend/src/api/dingtalk.ts b/frontend/src/api/dingtalk.ts index eb424c0..099a086 100644 --- a/frontend/src/api/dingtalk.ts +++ b/frontend/src/api/dingtalk.ts @@ -50,7 +50,7 @@ export function testDingTalkConnection(config: DingTalkConfig): Promise } /** - * 发送测试消息 + * 发送测试消息(群机器人) * @param type 消息类型:simple(简单测试), task_publish(新任务发布), task_assign(任务指派), task_reject(任务驳回), subtask_publish(子任务组发布) */ export function sendTestMessage(type: string = 'simple'): Promise { @@ -58,6 +58,24 @@ export function sendTestMessage(type: string = 'simple'): Promise { .then(res => res.data) } +/** + * 发送个人测试消息(工作通知) + * @param userName 接收人用户名或真实姓名 + * @param type 消息类型 + */ +export interface PersonalMessageResult { + success: boolean + userName: string + dingUserId: string + messageType: string + error?: string +} + +export function sendPersonalTestMessage(userName: string, type: string = 'simple'): Promise { + return request.post>('/dingtalk/test-message-personal', { userName, type }) + .then(res => res.data) +} + /** * 获取钉钉部门列表 */ diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 93561c2..e06ad19 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -713,7 +713,7 @@ - 消息测试 + 群机器人消息测试 简单测试 @@ -726,9 +726,37 @@ - 发送测试消息 + 发送群消息 - 选择不同类型查看实际消息排版效果 + 发送到群机器人,群成员都能看到 + + + 个人工作通知测试 + + + + + + + 简单测试 + 新任务发布 + 任务指派 + 任务完成 + 积分变动 + + + + + + 发送个人消息 + + 仅接收人本人可见 部门同步 @@ -2508,7 +2536,7 @@ const goToScorePool = () => { } // =============== 钉钉集成 =============== -import { getDingTalkConfig, saveDingTalkConfig, testDingTalkConnection, sendTestMessage, syncDingTalkDepartments } from '@/api/dingtalk' +import { getDingTalkConfig, saveDingTalkConfig, testDingTalkConnection, sendTestMessage, sendPersonalTestMessage, syncDingTalkDepartments } from '@/api/dingtalk' const dingTalkConfig = reactive({ id: undefined as number | undefined, @@ -2613,6 +2641,11 @@ const handleTestDingTalk = async () => { // 测试消息类型 const testMessageType = ref('simple') +// 个人消息测试 +const personalMessageUser = ref('') +const personalMessageType = ref('simple') +const sendingPersonalMessage = ref(false) + const handleSendTestMessage = async () => { if (!dingTalkConfig.robotWebhook) { message.warning('请先配置机器人Webhook') @@ -2658,6 +2691,40 @@ const handleSyncDepartments = async () => { } } +// 发送个人测试消息 +const handleSendPersonalMessage = async () => { + if (!personalMessageUser.value.trim()) { + message.warning('请输入接收人用户名或真实姓名') + return + } + + if (!dingTalkConfig.appKey || !dingTalkConfig.appSecret) { + message.warning('请先配置AppKey和AppSecret') + return + } + + sendingPersonalMessage.value = true + try { + const result = await sendPersonalTestMessage(personalMessageUser.value.trim(), personalMessageType.value) + if (result.success) { + const typeNames: Record = { + 'simple': '简单测试', + 'task_publish': '新任务发布', + 'task_assign': '任务指派', + 'task_complete': '任务完成', + 'score_change': '积分变动' + } + message.success(`"${typeNames[personalMessageType.value]}"消息已发送给 ${result.userName}!`) + } else { + message.error(result.error || '消息发送失败') + } + } catch (error: any) { + message.error(error.message || '发送失败') + } finally { + sendingPersonalMessage.value = false + } +} + // ==================== 导航栏配置 ==================== const navbarConfig = ref({ menuItems: {