feat: 添加钉钉个人消息测试功能

- 后端新增 /api/dingtalk/test-message-personal 接口
- 支持根据用户名/真实姓名查找用户并发送工作通知
- 前端Settings页面新增个人工作通知测试区域
- 支持多种消息类型:简单测试、新任务发布、任务指派、任务完成、积分变动
This commit is contained in:
Your Name 2026-01-05 00:03:34 +08:00
parent d82ae4e072
commit 1d33d453d3
3 changed files with 254 additions and 5 deletions

View File

@ -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<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() {

View File

@ -50,7 +50,7 @@ export function testDingTalkConnection(config: DingTalkConfig): Promise<boolean>
}
/**
* 发送测试消息
* 发送测试消息(群机器人)
* @param type 消息类型:simple(简单测试), task_publish(新任务发布), task_assign(任务指派), task_reject(任务驳回), subtask_publish(子任务组发布)
*/
export function sendTestMessage(type: string = 'simple'): Promise<boolean> {
@ -58,6 +58,24 @@ export function sendTestMessage(type: string = 'simple'): Promise<boolean> {
.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<PersonalMessageResult> {
return request.post<Result<PersonalMessageResult>>('/dingtalk/test-message-personal', { userName, type })
.then(res => res.data)
}
/**
* 获取钉钉部门列表
*/

View File

@ -713,7 +713,7 @@
</a-space>
</a-form-item>
<a-divider orientation="left">消息测试</a-divider>
<a-divider orientation="left">群机器人消息测试</a-divider>
<a-form-item label="选择消息类型">
<a-radio-group v-model:value="testMessageType" button-style="solid">
<a-radio-button value="simple">简单测试</a-radio-button>
@ -726,9 +726,37 @@
<a-form-item>
<a-button type="primary" @click="handleSendTestMessage" :loading="sendingTestMessage" :disabled="!dingTalkConfig.robotWebhook">
<template #icon><Icon name="send" size="sm" /></template>
发送测试消息
发送群消息
</a-button>
<span class="test-message-hint">选择不同类型查看实际消息排版效果</span>
<span class="test-message-hint">发送到群机器人,群成员都能看到</span>
</a-form-item>
<a-divider orientation="left">个人工作通知测试</a-divider>
<a-alert
message="发送给个人的工作通知"
description="工作通知是企业内部应用发送的消息,只有接收人本人能看到,适合任务提醒、积分变动等私密通知。"
type="info"
show-icon
style="margin-bottom: 16px;"
/>
<a-form-item label="接收人">
<a-input v-model:value="personalMessageUser" placeholder="输入用户名或真实姓名,如:南亭" style="width: 200px;" />
</a-form-item>
<a-form-item label="消息类型">
<a-radio-group v-model:value="personalMessageType" button-style="solid">
<a-radio-button value="simple">简单测试</a-radio-button>
<a-radio-button value="task_publish">新任务发布</a-radio-button>
<a-radio-button value="task_assign">任务指派</a-radio-button>
<a-radio-button value="task_complete">任务完成</a-radio-button>
<a-radio-button value="score_change">积分变动</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSendPersonalMessage" :loading="sendingPersonalMessage" :disabled="!personalMessageUser || !dingTalkConfig.appKey">
<template #icon><Icon name="send" size="sm" /></template>
发送个人消息
</a-button>
<span class="test-message-hint">仅接收人本人可见</span>
</a-form-item>
<a-divider orientation="left">部门同步</a-divider>
@ -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<string, string> = {
'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<NavbarConfig>({
menuItems: {