增加@人功能

This commit is contained in:
dengqichen 2025-05-28 17:20:46 +08:00
parent d71708bb65
commit 42b9651392
5 changed files with 217 additions and 77 deletions

View File

@ -122,6 +122,61 @@ public class UserMappingService {
.toList(); .toList();
} }
/**
* 通过邮箱获取手机号
*/
public String getPhoneByEmail(TaskReminderConfig.Group group, String email) {
if (email == null || email.isEmpty()) {
return null;
}
Map<String, String> 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<String, String> 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<String, String> 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;
}
/** /**
* 获取所有配置的用户映射信息用于调试 * 获取所有配置的用户映射信息用于调试
*/ */

View File

@ -126,6 +126,25 @@ public class WechatWebhookService {
return message; return message;
} }
/**
* 创建带@人的Markdown消息体
*/
private Map<String, Object> createMarkdownMessageWithMentions(String content, List<String> mobileList) {
Map<String, Object> message = new HashMap<>();
message.put("msgtype", "markdown");
Map<String, Object> 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 * 发送Markdown消息到指定URL
*/ */
@ -139,6 +158,19 @@ public class WechatWebhookService {
} }
} }
/**
* 发送带@人的Markdown消息到指定URL
*/
public boolean sendMessageWithMentions(String webhookUrl, String markdownContent, List<String> mobileList) {
try {
Map<String, Object> messageBody = createMarkdownMessageWithMentions(markdownContent, mobileList);
return sendMessage(webhookUrl, messageBody);
} catch (Exception e) {
logger.error("发送企业微信@人消息失败, URL: {}", webhookUrl, e);
return false;
}
}
/** /**
* 发送消息到企业微信 * 发送消息到企业微信
*/ */

View File

@ -192,6 +192,91 @@ public class ZentaoApiService {
return Collections.emptyList(); 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 * 获取Session ID
*/ */

View File

@ -11,6 +11,7 @@ import org.springframework.stereotype.Service;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -73,11 +74,12 @@ public class ZentaoTaskReminderService {
.filter(task -> task.getAssignedTo() != null && !task.getAssignedTo().isEmpty()) .filter(task -> task.getAssignedTo() != null && !task.getAssignedTo().isEmpty())
.collect(Collectors.groupingBy(ZentaoTask::getAssignedTo)); .collect(Collectors.groupingBy(ZentaoTask::getAssignedTo));
// 生成提醒消息 // 生成提醒消息并收集@人手机号
String message = buildTaskReminderMessage(group, tasksByAssignee, incompleteTasks, projectInfo); List<String> 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()); logger.info("Zentao task reminder sent successfully for group: {}", group.getId());
@ -92,81 +94,55 @@ public class ZentaoTaskReminderService {
private String buildTaskReminderMessage(TaskReminderConfig.Group group, private String buildTaskReminderMessage(TaskReminderConfig.Group group,
Map<String, List<ZentaoTask>> tasksByAssignee, Map<String, List<ZentaoTask>> tasksByAssignee,
List<ZentaoTask> allTasks, List<ZentaoTask> allTasks,
ProjectInfo projectInfo) { ProjectInfo projectInfo,
List<String> mentionedMobiles) {
StringBuilder message = new StringBuilder(); StringBuilder message = new StringBuilder();
// 消息头部 // 简化消息头部
message.append("📋 **禅道任务提醒**\n"); message.append("**禅道任务提醒**\n");
message.append("项目: ").append(projectInfo.getName()); 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(); long overdueTasks = allTasks.stream().filter(ZentaoTask::isOverdue).count();
message.append("📊 **任务统计**\n"); message.append("未完成任务: ").append(allTasks.size()).append("");
message.append("- 未完成任务总数: ").append(allTasks.size()).append("\n"); if (overdueTasks > 0) {
message.append("- 已过期任务: ").append(overdueTasks).append("\n"); message.append(",已过期: ").append(overdueTasks).append("");
message.append("- 涉及人员: ").append(tasksByAssignee.size()).append("\n\n"); }
message.append("\n\n");
// 按人员列出任务
message.append("👥 **任务分配详情**\n");
// 任务详情 - 不在消息内容中@人只显示姓名
for (Map.Entry<String, List<ZentaoTask>> entry : tasksByAssignee.entrySet()) { for (Map.Entry<String, List<ZentaoTask>> entry : tasksByAssignee.entrySet()) {
String assignee = entry.getKey(); String assigneeRealName = entry.getKey();
List<ZentaoTask> tasks = entry.getValue(); List<ZentaoTask> tasks = entry.getValue();
// 获取企业微信@标识 // 收集手机号用于@人
String wechatMention = userMappingService.getWechatMention(group, assignee); String phone = userMappingService.getPhoneByRealName(group, assigneeRealName);
String displayName = wechatMention != null ? wechatMention : assignee; 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) { for (ZentaoTask task : tasks) {
message.append("- "); // 简化任务状态图标
String statusIcon = task.isOverdue() ? "🔴" : "";
// 任务状态图标 message.append("- ").append(statusIcon).append(" [").append(task.getId()).append("] ")
if (task.isOverdue()) { .append(task.getName());
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("]");
}
// 截止日期 // 截止日期
if (task.getDeadline() != null && !task.getDeadline().isEmpty() && !"0000-00-00".equals(task.getDeadline())) { if (task.getDeadline() != null && !task.getDeadline().isEmpty() &&
message.append(" (截止: ").append(task.getDeadline()).append(")"); !"0000-00-00".equals(task.getDeadline())) {
message.append(" (").append(task.getDeadline()).append(")");
} }
message.append("\n"); message.append("\n");
} }
message.append("\n");
} }
// 消息尾部 // 简化提示
message.append("\n💡 **温馨提示**\n"); message.append("请及时登录禅道系统更新任务状态");
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");
return message.toString(); return message.toString();
} }
@ -176,18 +152,9 @@ public class ZentaoTaskReminderService {
*/ */
private void sendNoTasksMessage(TaskReminderConfig.Group group, ProjectInfo projectInfo) { private void sendNoTasksMessage(TaskReminderConfig.Group group, ProjectInfo projectInfo) {
StringBuilder message = new StringBuilder(); StringBuilder message = new StringBuilder();
message.append("✅ **禅道任务提醒**\n"); message.append("**禅道任务提醒**\n");
message.append("项目: ").append(projectInfo.getName()); message.append("项目: ").append(projectInfo.getName()).append("\n\n");
message.append("恭喜!当前项目没有未完成的任务。");
// 如果项目状态不是进行中显示状态
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("继续保持,团队表现优秀!💪");
try { try {
wechatWebhookService.sendMessage(group.getWebhook().getUrl(), message.toString()); wechatWebhookService.sendMessage(group.getWebhook().getUrl(), message.toString());

View File

@ -27,9 +27,10 @@ task:
password: "Lianyu!@#~123456" # 请替换为实际的禅道密码 password: "Lianyu!@#~123456" # 请替换为实际的禅道密码
project-id: 38 # 项目ID project-id: 38 # 项目ID
kanban-id: 39 # 看板ID看板模式项目需要 kanban-id: 39 # 看板ID看板模式项目需要
# 用户映射:禅道用户名/邮箱 -> 企业微信手机号 # 用户映射:禅道用户名/邮箱/真实姓名 -> 企业微信手机号
user-mapping: user-mapping:
"dengqichen@iscmtech.com": "18525522818" "dengqichen": "18525522818"
"songwei": "15724574541"
# 也可以直接用用户名映射 # 也可以直接用用户名映射
# "zhangsan": "13600136000" # "zhangsan": "13600136000"
schedules: schedules: