From 42b9651392334f2ec2ed9b40da7a9b8f604b4172 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 28 May 2025 17:20:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0@=E4=BA=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reminder/service/UserMappingService.java | 63 +++++++++- .../service/WechatWebhookService.java | 32 +++++ .../reminder/service/ZentaoApiService.java | 85 ++++++++++++++ .../service/ZentaoTaskReminderService.java | 109 ++++++------------ src/main/resources/application.yml | 5 +- 5 files changed, 217 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/zeodao/reminder/service/UserMappingService.java b/src/main/java/com/zeodao/reminder/service/UserMappingService.java index 898d079..e641ee6 100644 --- a/src/main/java/com/zeodao/reminder/service/UserMappingService.java +++ b/src/main/java/com/zeodao/reminder/service/UserMappingService.java @@ -13,7 +13,7 @@ import java.util.Map; /** * 用户映射服务 * 负责将禅道用户映射到企业微信用户 - * + * * @author Zeodao * @version 2.0.0 */ @@ -27,7 +27,7 @@ public class UserMappingService { /** * 根据禅道用户名获取企业微信@标识 - * + * * @param group 群组配置 * @param zentaoUsername 禅道用户名 * @return 企业微信@标识,如 @13800138000 或 null @@ -105,7 +105,7 @@ public class UserMappingService { /** * 批量获取企业微信@标识 - * + * * @param group 群组配置 * @param zentaoUsernames 禅道用户名列表 * @return 企业微信@标识列表 @@ -122,6 +122,61 @@ public class UserMappingService { .toList(); } + /** + * 通过邮箱获取手机号 + */ + public String getPhoneByEmail(TaskReminderConfig.Group group, String email) { + if (email == null || email.isEmpty()) { + return null; + } + + Map userMapping = group.getUserMapping(); + if (userMapping != null && userMapping.containsKey(email)) { + String wechatId = userMapping.get(email); + // 如果是手机号格式,直接返回 + if (wechatId != null && wechatId.matches("^1[3-9]\\d{9}$")) { + return wechatId; + } + } + + logger.debug("No phone mapping found for email: {}", email); + return null; + } + + /** + * 通过真实姓名获取手机号 + */ + public String getPhoneByRealName(TaskReminderConfig.Group group, String realName) { + if (realName == null || realName.isEmpty()) { + return null; + } + + Map userMapping = group.getUserMapping(); + if (userMapping != null) { + // 1. 直接通过真实姓名查找 + if (userMapping.containsKey(realName)) { + String wechatId = userMapping.get(realName); + if (wechatId != null && wechatId.matches("^1[3-9]\\d{9}$")) { + return wechatId; + } + } + + // 2. 遍历所有映射,查找值为真实姓名的条目(支持反向映射) + for (Map.Entry entry : userMapping.entrySet()) { + if (realName.equals(entry.getValue())) { + String key = entry.getKey(); + // 如果key是手机号格式,返回key + if (key.matches("^1[3-9]\\d{9}$")) { + return key; + } + } + } + } + + logger.debug("No phone mapping found for realName: {}", realName); + return null; + } + /** * 获取所有配置的用户映射信息(用于调试) */ @@ -143,7 +198,7 @@ public class UserMappingService { for (Map.Entry entry : userMapping.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); - + if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) { validMappings++; } else { diff --git a/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java b/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java index 7fe3666..bacbe44 100644 --- a/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java +++ b/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java @@ -126,6 +126,25 @@ public class WechatWebhookService { return message; } + /** + * 创建带@人的Markdown消息体 + */ + private Map createMarkdownMessageWithMentions(String content, List mobileList) { + Map message = new HashMap<>(); + message.put("msgtype", "markdown"); + + Map markdown = new HashMap<>(); + markdown.put("content", content); + message.put("markdown", markdown); + + // 添加@人列表 + if (mobileList != null && !mobileList.isEmpty()) { + message.put("mentioned_mobile_list", mobileList); + } + + return message; + } + /** * 发送Markdown消息到指定URL */ @@ -139,6 +158,19 @@ public class WechatWebhookService { } } + /** + * 发送带@人的Markdown消息到指定URL + */ + public boolean sendMessageWithMentions(String webhookUrl, String markdownContent, List mobileList) { + try { + Map messageBody = createMarkdownMessageWithMentions(markdownContent, mobileList); + return sendMessage(webhookUrl, messageBody); + } catch (Exception e) { + logger.error("发送企业微信@人消息失败, URL: {}", webhookUrl, e); + return false; + } + } + /** * 发送消息到企业微信 */ diff --git a/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java b/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java index dce671a..35bfcca 100644 --- a/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java +++ b/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java @@ -192,6 +192,91 @@ public class ZentaoApiService { return Collections.emptyList(); } + /** + * 通过真实姓名获取用户邮箱 + */ + public String getUserEmailByRealName(String realName, TaskReminderConfig.Group group) { + String sessionId = getSessionId(group.getZentao()); + if (sessionId == null) { + return null; + } + + try (CloseableHttpClient client = HttpClients.createDefault()) { + // 获取所有用户 + String url = group.getZentao().getApiUrl() + "/api.php/v1/users"; + HttpGet request = new HttpGet(url); + request.setHeader("Cookie", "zentaosid=" + sessionId); + request.setHeader("Content-Type", "application/json"); + + try (CloseableHttpResponse response = client.execute(request)) { + String responseBody = EntityUtils.toString(response.getEntity()); + logger.debug("Zentao users API response: {}", responseBody); + + JsonNode rootNode = objectMapper.readTree(responseBody); + + // 检查是否有错误 + if (rootNode.has("error")) { + logger.debug("API returned error: {}", rootNode.get("error").asText()); + return null; + } + + // 解析用户数据 + JsonNode usersNode = null; + if (rootNode.has("users") && rootNode.get("users").isArray()) { + usersNode = rootNode.get("users"); + } else if (rootNode.has("data") && rootNode.get("data").isArray()) { + usersNode = rootNode.get("data"); + } else if (rootNode.isArray()) { + usersNode = rootNode; + } + + if (usersNode != null) { + for (JsonNode userNode : usersNode) { + String userRealName = getStringValueSafe(userNode, "realname"); + if (realName.equals(userRealName)) { + String email = getStringValueSafe(userNode, "email"); + logger.debug("Found email for {}: {}", realName, email); + return email; + } + } + } + } + } catch (Exception e) { + logger.debug("Error getting user email for realName: {}, error: {}", realName, e.getMessage()); + } + + return null; + } + + /** + * 安全地从JsonNode中提取字符串值 + */ + private String getStringValueSafe(JsonNode node, String fieldName) { + if (node == null || !node.has(fieldName)) { + return null; + } + + JsonNode fieldNode = node.get(fieldName); + if (fieldNode == null || fieldNode.isNull()) { + return null; + } + + // 如果是对象类型,尝试提取其中的字符串字段 + if (fieldNode.isObject()) { + if (fieldNode.has("account")) { + return fieldNode.get("account").asText(); + } else if (fieldNode.has("realname")) { + return fieldNode.get("realname").asText(); + } else if (fieldNode.has("id")) { + return fieldNode.get("id").asText(); + } + return null; + } + + // 对于基本类型,直接转换为字符串 + return fieldNode.asText(); + } + /** * 获取Session ID */ diff --git a/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java b/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java index 98699c9..200f559 100644 --- a/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java +++ b/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java @@ -11,6 +11,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -73,11 +74,12 @@ public class ZentaoTaskReminderService { .filter(task -> task.getAssignedTo() != null && !task.getAssignedTo().isEmpty()) .collect(Collectors.groupingBy(ZentaoTask::getAssignedTo)); - // 生成提醒消息 - String message = buildTaskReminderMessage(group, tasksByAssignee, incompleteTasks, projectInfo); + // 生成提醒消息并收集@人手机号 + List mentionedMobiles = new ArrayList<>(); + String message = buildTaskReminderMessage(group, tasksByAssignee, incompleteTasks, projectInfo, mentionedMobiles); - // 发送消息 - wechatWebhookService.sendMessage(group.getWebhook().getUrl(), message); + // 发送带@人的消息 + wechatWebhookService.sendMessageWithMentions(group.getWebhook().getUrl(), message, mentionedMobiles); logger.info("Zentao task reminder sent successfully for group: {}", group.getId()); @@ -92,81 +94,55 @@ public class ZentaoTaskReminderService { private String buildTaskReminderMessage(TaskReminderConfig.Group group, Map> tasksByAssignee, List allTasks, - ProjectInfo projectInfo) { + ProjectInfo projectInfo, + List mentionedMobiles) { StringBuilder message = new StringBuilder(); - // 消息头部 - message.append("📋 **禅道任务提醒**\n"); - message.append("项目: ").append(projectInfo.getName()); + // 简化消息头部 + message.append("**禅道任务提醒**\n"); + message.append("项目: ").append(projectInfo.getName()).append("\n\n"); - // 如果项目状态不是进行中,显示状态 - if (!"doing".equals(projectInfo.getStatus())) { - message.append(" [").append(projectInfo.getStatusDescription()).append("]"); - } - - message.append("\n"); - message.append("统计时间: ").append(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))).append("\n\n"); - - // 统计信息 + // 简化统计信息 long overdueTasks = allTasks.stream().filter(ZentaoTask::isOverdue).count(); - message.append("📊 **任务统计**\n"); - message.append("- 未完成任务总数: ").append(allTasks.size()).append("\n"); - message.append("- 已过期任务: ").append(overdueTasks).append("\n"); - message.append("- 涉及人员: ").append(tasksByAssignee.size()).append("\n\n"); - - // 按人员列出任务 - message.append("👥 **任务分配详情**\n"); + message.append("未完成任务: ").append(allTasks.size()).append("个"); + if (overdueTasks > 0) { + message.append(",已过期: ").append(overdueTasks).append("个"); + } + message.append("\n\n"); + // 任务详情 - 不在消息内容中@人,只显示姓名 for (Map.Entry> entry : tasksByAssignee.entrySet()) { - String assignee = entry.getKey(); + String assigneeRealName = entry.getKey(); List tasks = entry.getValue(); - // 获取企业微信@标识 - String wechatMention = userMappingService.getWechatMention(group, assignee); - String displayName = wechatMention != null ? wechatMention : assignee; + // 收集手机号用于@人 + String phone = userMappingService.getPhoneByRealName(group, assigneeRealName); + if (phone != null) { + mentionedMobiles.add(phone); + } - message.append("\n**").append(displayName).append("** (").append(tasks.size()).append("个任务)\n"); + message.append(assigneeRealName).append(" (").append(tasks.size()).append("个任务)\n"); for (ZentaoTask task : tasks) { - message.append("- "); + // 简化任务状态图标 + String statusIcon = task.isOverdue() ? "🔴" : "⚪"; - // 任务状态图标 - if (task.isOverdue()) { - message.append("🔴 "); - } else if ("doing".equals(task.getStatus())) { - message.append("🟡 "); - } else { - message.append("⚪ "); - } - - // 任务信息 - message.append("[").append(task.getId()).append("] "); - message.append(task.getName()); - - // 优先级 - if (!"3".equals(task.getPri())) { // 不是普通优先级 - message.append(" [").append(task.getPriorityDesc()).append("]"); - } + message.append("- ").append(statusIcon).append(" [").append(task.getId()).append("] ") + .append(task.getName()); // 截止日期 - if (task.getDeadline() != null && !task.getDeadline().isEmpty() && !"0000-00-00".equals(task.getDeadline())) { - message.append(" (截止: ").append(task.getDeadline()).append(")"); + if (task.getDeadline() != null && !task.getDeadline().isEmpty() && + !"0000-00-00".equals(task.getDeadline())) { + message.append(" (").append(task.getDeadline()).append(")"); } message.append("\n"); } + message.append("\n"); } - // 消息尾部 - message.append("\n💡 **温馨提示**\n"); - message.append("- 🔴 表示已过期任务,请优先处理\n"); - message.append("- 🟡 表示进行中任务\n"); - message.append("- ⚪ 表示其他状态任务\n"); - message.append("- 请及时登录禅道系统更新任务状态\n"); - - // 禅道链接 - String zentaoUrl = group.getZentao().getApiUrl().replace("/api.php", ""); - message.append("- [点击访问禅道系统](").append(zentaoUrl).append(")\n"); + // 简化提示 + message.append("请及时登录禅道系统更新任务状态"); return message.toString(); } @@ -176,18 +152,9 @@ public class ZentaoTaskReminderService { */ private void sendNoTasksMessage(TaskReminderConfig.Group group, ProjectInfo projectInfo) { StringBuilder message = new StringBuilder(); - message.append("✅ **禅道任务提醒**\n"); - message.append("项目: ").append(projectInfo.getName()); - - // 如果项目状态不是进行中,显示状态 - if (!"doing".equals(projectInfo.getStatus())) { - message.append(" [").append(projectInfo.getStatusDescription()).append("]"); - } - - message.append("\n"); - message.append("统计时间: ").append(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))).append("\n\n"); - message.append("🎉 恭喜!当前项目没有未完成的任务。\n"); - message.append("继续保持,团队表现优秀!💪"); + message.append("**禅道任务提醒**\n"); + message.append("项目: ").append(projectInfo.getName()).append("\n\n"); + message.append("恭喜!当前项目没有未完成的任务。"); try { wechatWebhookService.sendMessage(group.getWebhook().getUrl(), message.toString()); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index eedae02..f36269a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,9 +27,10 @@ task: password: "Lianyu!@#~123456" # 请替换为实际的禅道密码 project-id: 38 # 项目ID kanban-id: 39 # 看板ID(看板模式项目需要) - # 用户映射:禅道用户名/邮箱 -> 企业微信手机号 + # 用户映射:禅道用户名/邮箱/真实姓名 -> 企业微信手机号 user-mapping: - "dengqichen@iscmtech.com": "18525522818" + "dengqichen": "18525522818" + "songwei": "15724574541" # 也可以直接用用户名映射 # "zhangsan": "13600136000" schedules: