feat: 钉钉通知改为卡片消息格式,支持跳转官网

This commit is contained in:
Your Name 2026-01-07 09:36:40 +08:00
parent a520890284
commit a62afec617
4 changed files with 131 additions and 17 deletions

View File

@ -35,8 +35,10 @@ public class DingTalkService {
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate = new RestTemplate();
private final DepartmentMapper departmentMapper;
private final SystemConfigService systemConfigService;
private static final String DINGTALK_API_BASE = "https://oapi.dingtalk.com";
private static final String DEFAULT_SITE_URL = "https://your-domain.com";
/**
* 获取钉钉配置(优先获取启用的配置,如果没有则获取最新的配置)
@ -195,9 +197,35 @@ public class DingTalkService {
}
/**
* 发送工作通知
* 获取配置的官网地址
*/
private String getSiteUrl() {
String siteUrl = systemConfigService.getConfigValue("site_url");
if (siteUrl == null || siteUrl.trim().isEmpty()) {
siteUrl = systemConfigService.getConfigValue("dingtalk_redirect_url");
}
if (siteUrl == null || siteUrl.trim().isEmpty()) {
siteUrl = DEFAULT_SITE_URL;
}
return siteUrl;
}
/**
* 发送工作通知(卡片消息格式)
* 用户点击卡片可跳转到官网
*/
public boolean sendWorkNotification(List<String> userIds, String title, String content) {
return sendWorkNotification(userIds, title, content, null);
}
/**
* 发送工作通知(卡片消息格式,支持自定义跳转链接)
* @param userIds 接收用户ID列表
* @param title 消息标题
* @param content 消息内容(支持markdown格式)
* @param customUrl 自定义跳转链接,为空则使用官网地址
*/
public boolean sendWorkNotification(List<String> userIds, String title, String content, String customUrl) {
DingTalkConfig config = getConfig();
if (config == null) {
log.warn("钉钉未配置,跳过发送通知");
@ -213,14 +241,21 @@ public class DingTalkService {
String url = String.format("%s/topapi/message/corpconversation/asyncsend_v2?access_token=%s",
DINGTALK_API_BASE, accessToken);
// 获取跳转链接
String jumpUrl = (customUrl != null && !customUrl.trim().isEmpty()) ? customUrl : getSiteUrl();
// 构建卡片消息
Map<String, Object> msg = new HashMap<>();
msg.put("msgtype", "action_card");
Map<String, String> actionCard = new HashMap<>();
actionCard.put("title", title);
actionCard.put("markdown", content);
actionCard.put("single_title", "查看详情");
// 构建 markdown 正文,包含标题和内容
String markdown = "### " + title + "\n\n" + content + "\n\n---\n*点击下方按钮查看详情*";
actionCard.put("markdown", markdown);
actionCard.put("single_title", "查看详情 »");
actionCard.put("single_url", "dingtalk://dingtalkclient/page/link?url=" +
URLEncoder.encode("https://your-domain.com", StandardCharsets.UTF_8.toString()));
URLEncoder.encode(jumpUrl, StandardCharsets.UTF_8.toString()));
msg.put("action_card", actionCard);
Map<String, Object> request = new HashMap<>();
@ -238,7 +273,7 @@ public class DingTalkService {
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
log.info("钉钉工作通知发送成功: userIds={}", userIds);
log.info("钉钉工作通知发送成功: userIds={}, title={}", userIds, title);
return true;
} else {
log.error("钉钉工作通知发送失败: errcode={}, errmsg={}, body={}",
@ -256,16 +291,17 @@ public class DingTalkService {
}
/**
* 发送机器人消息
* 发送机器人消息(卡片格式)
*/
public boolean sendRobotMessage(String title, String content) {
return sendRobotMessage(title, content, null);
}
/**
* 发送机器人消息,支持@指定人(通过手机号)
* 发送机器人消息(卡片格式),支持@指定人(通过手机号)
* 使用整体跳转ActionCard样式,点击可跳转到官网
* @param title 消息标题
* @param content 消息内容
* @param content 消息内容(支持markdown格式)
* @param atMobiles 要@的手机号列表,null或空表示不@任何人
*/
public boolean sendRobotMessage(String title, String content, List<String> atMobiles) {
@ -285,14 +321,36 @@ public class DingTalkService {
webhook = webhook + "&timestamp=" + timestamp + "&sign=" + sign;
}
Map<String, Object> message = new HashMap<>();
message.put("msgtype", "markdown");
Map<String, String> markdown = new HashMap<>();
markdown.put("title", title);
markdown.put("text", "### " + title + "\n\n" + content);
message.put("markdown", markdown);
// 获取官网地址
String siteUrl = getSiteUrl();
// 添加@人功能
// 构建卡片消息(ActionCard格式)
Map<String, Object> message = new HashMap<>();
message.put("msgtype", "action_card");
Map<String, Object> actionCard = new HashMap<>();
actionCard.put("title", title);
// 构建 markdown 正文
StringBuilder markdownBuilder = new StringBuilder();
markdownBuilder.append("### ").append(title).append("\n\n");
markdownBuilder.append(content);
// 如果有@人,在内容中添加@标记
if (atMobiles != null && !atMobiles.isEmpty()) {
markdownBuilder.append("\n\n");
for (String mobile : atMobiles) {
markdownBuilder.append("@").append(mobile).append(" ");
}
}
actionCard.put("markdown", markdownBuilder.toString());
actionCard.put("single_title", "查看详情 »");
actionCard.put("single_url", siteUrl);
message.put("action_card", actionCard);
// 添加@人功能(对于action_card,@功能可能有限制)
if (atMobiles != null && !atMobiles.isEmpty()) {
Map<String, Object> at = new HashMap<>();
at.put("atMobiles", atMobiles);
@ -311,7 +369,7 @@ public class DingTalkService {
if (body != null) {
Object errcode = body.get("errcode");
if (errcode != null && (errcode.equals(0) || errcode.equals(0L) || "0".equals(errcode.toString()))) {
log.info("钉钉机器人消息发送成功: title={}", title);
log.info("钉钉机器人卡片消息发送成功: title={}", title);
return true;
} else {
log.error("钉钉机器人消息发送失败: errcode={}, errmsg={}, body={}",

View File

@ -0,0 +1,5 @@
-- 添加官网地址配置(用于钉钉卡片消息跳转)
INSERT INTO `sys_config` (`config_key`, `config_value`, `config_group`, `config_name`, `description`, `value_type`, `built_in`, `sort_order`, `create_time`, `update_time`, `deleted`)
VALUES ('site_url', '', 'system', '官网地址', '系统官网地址,用于钉钉通知卡片跳转,例如:https://example.com', 'string', 1, 10, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE `description` = '系统官网地址,用于钉钉通知卡片跳转,例如:https://example.com';

View File

@ -62,6 +62,18 @@
</div>
</a-form-item>
<a-form-item label="官网地址">
<a-input
v-model:value="siteSettings.siteUrl"
placeholder="请输入官网地址,例如:https://example.com"
size="large"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
<Icon name="info" size="sm" color="#888" style="margin-right: 4px;" />
钉钉卡片通知将跳转到此地址
</div>
</a-form-item>
<a-divider orientation="left">功能开关</a-divider>
<a-form-item label="注册功能">
<a-switch
@ -2060,9 +2072,11 @@ const router = useRouter()
const siteSettings = reactive({
siteName: 'DesignHub',
logoUrl: '',
siteUrl: '',
registerEnabled: true,
siteNameConfigId: null as number | null,
logoConfigId: null as number | null,
siteUrlConfigId: null as number | null,
registerEnabledConfigId: null as number | null
})
@ -3776,6 +3790,10 @@ const loadSiteSettings = async () => {
const response = await getSystemConfigsByGroup('site')
const configs = response.data || []
// 同时获取system分组的配置(包含site_url)
const systemResponse = await getSystemConfigsByGroup('system')
const systemConfigs = systemResponse.data || []
// 查找网站名称和Logo配置
const siteNameConfig = configs.find((c: any) => c.configKey === 'site.name')
const logoConfig = configs.find((c: any) => c.configKey === 'site.logo')
@ -3794,6 +3812,15 @@ const loadSiteSettings = async () => {
siteSettings.logoUrl = siteSettingsStore.logoUrl
}
// 查找官网地址配置(在system分组中)
const siteUrlConfig = systemConfigs.find((c: any) => c.configKey === 'site_url')
if (siteUrlConfig) {
siteSettings.siteUrl = siteUrlConfig.configValue || ''
siteSettings.siteUrlConfigId = siteUrlConfig.id
} else {
siteSettings.siteUrl = ''
}
// 查找注册功能开关配置
const registerConfig = configs.find((c: any) => c.configKey === 'site.register.enabled')
if (registerConfig) {
@ -3807,6 +3834,7 @@ const loadSiteSettings = async () => {
// 如果API失败,尝试从store读取
siteSettings.siteName = siteSettingsStore.siteName
siteSettings.logoUrl = siteSettingsStore.logoUrl
siteSettings.siteUrl = ''
siteSettings.registerEnabled = true // 默认开启
} finally {
loadingSiteSettings.value = false
@ -3906,6 +3934,29 @@ const handleSaveSiteSettings = async () => {
}
}
// 保存官网地址
if (siteSettings.siteUrlConfigId) {
// 如果配置已存在,更新
await updateSystemConfig(siteSettings.siteUrlConfigId, {
configValue: siteSettings.siteUrl
})
} else if (siteSettings.siteUrl) {
// 如果配置不存在但有值,创建
const response = await createSystemConfig({
configKey: 'site_url',
configValue: siteSettings.siteUrl,
configGroup: 'system',
configName: '官网地址',
description: '系统官网地址,用于钉钉通知卡片跳转',
valueType: 'string',
builtIn: 1,
sortOrder: 10
})
if (response.data && response.data.id) {
siteSettings.siteUrlConfigId = response.data.id
}
}
// 更新全局store(这会自动更新所有使用store的组件)
siteSettingsStore.updateSiteSettings(siteSettings.siteName, siteSettings.logoUrl)

View File

@ -15,7 +15,7 @@
### 调用流程
```
1. 任务系统生成任务号(如 TASK202601030184)
1. 任务系统生成任务编号(如 TASK202601030184)
2. 员工在素材平台上传素材时填入任务号
3. 员工在任务系统点击「完成」
4. 任务系统调用查询接口获取素材详情和下载链接