增加@人功能
This commit is contained in:
parent
d71708bb65
commit
42b9651392
@ -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<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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有配置的用户映射信息(用于调试)
|
||||
*/
|
||||
@ -143,7 +198,7 @@ public class UserMappingService {
|
||||
for (Map.Entry<String, String> entry : userMapping.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
|
||||
if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) {
|
||||
validMappings++;
|
||||
} else {
|
||||
|
||||
@ -126,6 +126,25 @@ public class WechatWebhookService {
|
||||
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
|
||||
*/
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到企业微信
|
||||
*/
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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<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());
|
||||
|
||||
@ -92,81 +94,55 @@ public class ZentaoTaskReminderService {
|
||||
private String buildTaskReminderMessage(TaskReminderConfig.Group group,
|
||||
Map<String, List<ZentaoTask>> tasksByAssignee,
|
||||
List<ZentaoTask> allTasks,
|
||||
ProjectInfo projectInfo) {
|
||||
ProjectInfo projectInfo,
|
||||
List<String> 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<String, List<ZentaoTask>> entry : tasksByAssignee.entrySet()) {
|
||||
String assignee = entry.getKey();
|
||||
String assigneeRealName = entry.getKey();
|
||||
List<ZentaoTask> 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());
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user