feat: 添加钉钉个人消息测试功能
- 后端新增 /api/dingtalk/test-message-personal 接口 - 支持根据用户名/真实姓名查找用户并发送工作通知 - 前端Settings页面新增个人工作通知测试区域 - 支持多种消息类型:简单测试、新任务发布、任务指派、任务完成、积分变动
This commit is contained in:
parent
d82ae4e072
commit
1d33d453d3
@ -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() {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取钉钉部门列表
|
||||
*/
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user