diff --git a/ZENTAO_INTEGRATION.md b/ZENTAO_INTEGRATION.md index 0cbd0d7..bd515b2 100644 --- a/ZENTAO_INTEGRATION.md +++ b/ZENTAO_INTEGRATION.md @@ -24,7 +24,7 @@ task: api-url: "https://zentao.iscmtech.com" # 禅道地址 username: "your-username" # 禅道用户名 password: "your-password" # 禅道密码 - project-id: 26 # 项目ID + project-id: 26 # 项目ID(同时用于获取任务和BUG) # 用户映射:禅道邮箱 -> 企业微信手机号 user-mapping: "liguiling@iscmtech.com": "13800138000" @@ -63,7 +63,7 @@ user-mapping: ## API接口 -### 测试禅道连接 +### 测试禅道任务连接 ``` GET /api/reminder/zentao/test/{groupId} ``` @@ -73,6 +73,16 @@ GET /api/reminder/zentao/test/{groupId} POST /api/reminder/zentao/reminder/{groupId} ``` +### 测试禅道BUG连接 +``` +GET /api/reminder/zentao/bugs/test/{groupId} +``` + +### 手动触发禅道BUG提醒 +``` +POST /api/reminder/zentao/bugs/reminder/{groupId} +``` + ### 手动触发指定群组提醒 ``` POST /api/reminder/groups/{groupId}/morning @@ -81,21 +91,33 @@ POST /api/reminder/groups/{groupId}/evening ## 消息格式 +### 任务提醒消息格式 + 系统会发送包含以下信息的详细任务提醒: - 📊 任务统计(总数、过期数、涉及人员) - 👥 按人员分组的任务列表 - 🔴 过期任务标识 -- 🟡 进行中任务标识 - ⚪ 其他状态任务标识 - 📋 任务优先级和截止日期 - 🔗 禅道系统访问链接 +### BUG提醒消息格式 + +系统会发送包含以下信息的详细BUG提醒: + +- 📊 BUG统计(总数、过期数、涉及人员) +- 👥 按人员分组的BUG列表 +- 🔴 过期BUG标识 +- 🐛 未解决BUG标识 +- 📋 BUG严重程度和截止日期 +- 🔗 禅道系统访问链接 + ## 使用步骤 1. **配置禅道信息**:在配置文件中填入正确的禅道地址、用户名、密码和项目ID 2. **配置用户映射**:将团队成员的禅道邮箱映射到企业微信手机号 -3. **测试连接**:调用测试API确认禅道连接正常 +3. **测试连接**:调用测试API确认禅道任务和BUG连接正常 4. **启用定时任务**:系统会按配置的时间自动发送提醒 ## 注意事项 diff --git a/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java b/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java index 329e84f..31e5379 100644 --- a/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java +++ b/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java @@ -2,6 +2,7 @@ package com.zeodao.reminder.controller; import com.zeodao.reminder.config.TaskReminderConfig; import com.zeodao.reminder.model.ZentaoTask; +import com.zeodao.reminder.model.ZentaoBug; import com.zeodao.reminder.scheduler.DynamicTaskScheduler; import com.zeodao.reminder.service.TaskReminderService; import com.zeodao.reminder.service.ZentaoTaskReminderService; @@ -270,4 +271,72 @@ public class TaskReminderController { return ResponseEntity.ok(response); } + + /** + * 测试禅道BUG连接 + */ + @GetMapping("/zentao/bugs/test/{groupId}") + public ResponseEntity> testZentaoBugConnection(@PathVariable String groupId) { + Map response = new HashMap<>(); + + try { + TaskReminderConfig.Group group = taskReminderConfig.getGroupById(groupId); + if (group == null) { + response.put("success", false); + response.put("message", "群组不存在:" + groupId); + return ResponseEntity.ok(response); + } + + if (!"zentao".equals(group.getTaskSystem())) { + response.put("success", false); + response.put("message", "该群组不是禅道类型"); + return ResponseEntity.ok(response); + } + + List bugs = zentaoTaskReminderService.getBugsForTesting(group); + response.put("success", true); + response.put("message", "禅道BUG API连接成功"); + response.put("bugCount", bugs.size()); + response.put("bugs", bugs); + } catch (Exception e) { + logger.error("测试禅道BUG连接失败: {}", groupId, e); + response.put("success", false); + response.put("message", "测试禅道BUG连接失败:" + e.getMessage()); + } + + return ResponseEntity.ok(response); + } + + /** + * 手动触发禅道BUG提醒 + */ + @PostMapping("/zentao/bugs/reminder/{groupId}") + public ResponseEntity> sendZentaoBugReminder(@PathVariable String groupId) { + Map response = new HashMap<>(); + + try { + TaskReminderConfig.Group group = taskReminderConfig.getGroupById(groupId); + if (group == null) { + response.put("success", false); + response.put("message", "群组不存在:" + groupId); + return ResponseEntity.ok(response); + } + + if (!"zentao".equals(group.getTaskSystem())) { + response.put("success", false); + response.put("message", "该群组不是禅道类型"); + return ResponseEntity.ok(response); + } + + zentaoTaskReminderService.sendBugReminder(group); + response.put("success", true); + response.put("message", "禅道BUG提醒已发送"); + } catch (Exception e) { + logger.error("发送禅道BUG提醒失败: {}", groupId, e); + response.put("success", false); + response.put("message", "发送禅道BUG提醒失败:" + e.getMessage()); + } + + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/zeodao/reminder/model/ZentaoBug.java b/src/main/java/com/zeodao/reminder/model/ZentaoBug.java new file mode 100644 index 0000000..4ebdc2f --- /dev/null +++ b/src/main/java/com/zeodao/reminder/model/ZentaoBug.java @@ -0,0 +1,244 @@ +package com.zeodao.reminder.model; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; + +/** + * 禅道BUG实体类 - 使用JsonNode实现动态字段解析 + * + * @author Zeodao + * @version 1.0.0 + */ +@Data +public class ZentaoBug { + + private String id; + + private String title; + + private String type; + + private String status; + + private String severity; + + private String pri; + + private String assignedTo; + + private String assignedToRealName; + + private String assignedDate; + + private String deadline; + + private String openedBy; + + private String openedDate; + + private String resolvedBy; + + private String resolvedDate; + + private String closedBy; + + private String closedDate; + + private String lastEditedBy; + + private String lastEditedDate; + + private String product; + + private String project; + + private String execution; + + private String module; + + private String keywords; + + private String os; + + private String browser; + + private String steps; + + private String confirmed; + + private String activatedCount; + + private String activatedDate; + + private String deleted; + + /** + * 从JsonNode创建ZentaoBug对象,安全地提取字段 + */ + public static ZentaoBug fromJsonNode(JsonNode node) { + ZentaoBug bug = new ZentaoBug(); + + // 安全地提取字段,如果字段不存在或类型不匹配,使用默认值 + bug.setId(getStringValue(node, "id")); + bug.setTitle(getStringValue(node, "title")); + bug.setType(getStringValue(node, "type")); + bug.setStatus(getStringValue(node, "status")); + bug.setSeverity(getStringValue(node, "severity")); + bug.setPri(getStringValue(node, "pri")); + bug.setAssignedTo(getStringValue(node, "assignedTo")); + bug.setAssignedToRealName(getStringValue(node, "assignedToRealName")); + bug.setAssignedDate(getStringValue(node, "assignedDate")); + bug.setDeadline(getStringValue(node, "deadline")); + bug.setOpenedBy(getStringValue(node, "openedBy")); + bug.setOpenedDate(getStringValue(node, "openedDate")); + bug.setResolvedBy(getStringValue(node, "resolvedBy")); + bug.setResolvedDate(getStringValue(node, "resolvedDate")); + bug.setClosedBy(getStringValue(node, "closedBy")); + bug.setClosedDate(getStringValue(node, "closedDate")); + bug.setLastEditedBy(getStringValue(node, "lastEditedBy")); + bug.setLastEditedDate(getStringValue(node, "lastEditedDate")); + bug.setProduct(getStringValue(node, "product")); + bug.setProject(getStringValue(node, "project")); + bug.setExecution(getStringValue(node, "execution")); + bug.setModule(getStringValue(node, "module")); + bug.setKeywords(getStringValue(node, "keywords")); + bug.setOs(getStringValue(node, "os")); + bug.setBrowser(getStringValue(node, "browser")); + bug.setSteps(getStringValue(node, "steps")); + bug.setConfirmed(getStringValue(node, "confirmed")); + bug.setActivatedCount(getStringValue(node, "activatedCount")); + bug.setActivatedDate(getStringValue(node, "activatedDate")); + bug.setDeleted(getStringValue(node, "deleted")); + + return bug; + } + + /** + * 安全地从JsonNode获取字符串值 + */ + private static String getStringValue(JsonNode node, String fieldName) { + if (node == null || !node.has(fieldName)) { + return null; + } + + JsonNode fieldNode = node.get(fieldName); + if (fieldNode.isNull()) { + return null; + } + + // 处理嵌套对象(如status可能是对象) + if (fieldNode.isObject()) { + if (fieldNode.has("code")) { + return fieldNode.get("code").asText(); + } + if (fieldNode.has("account")) { + return fieldNode.get("account").asText(); + } + if (fieldNode.has("realname")) { + return fieldNode.get("realname").asText(); + } + return fieldNode.toString(); + } + + // 对于基本类型,直接转换为字符串 + return fieldNode.asText(); + } + + /** + * 判断BUG是否未解决 + */ + public boolean isUnresolved() { + return !"resolved".equals(status) && !"closed".equals(status); + } + + /** + * 判断BUG是否已过期 + */ + public boolean isOverdue() { + if (deadline == null || deadline.isEmpty() || "0000-00-00".equals(deadline)) { + return false; + } + // 简单的日期比较,实际项目中应该使用更严格的日期处理 + return deadline.compareTo(java.time.LocalDate.now().toString()) < 0; + } + + /** + * 获取严重程度描述 + */ + public String getSeverityDesc() { + switch (severity) { + case "1": + return "致命"; + case "2": + return "严重"; + case "3": + return "一般"; + case "4": + return "轻微"; + default: + return "未知"; + } + } + + /** + * 获取优先级描述 + */ + public String getPriorityDesc() { + switch (pri) { + case "1": + return "高"; + case "2": + return "中"; + case "3": + return "低"; + case "4": + return "最低"; + default: + return "普通"; + } + } + + /** + * 获取BUG类型描述 + */ + public String getTypeDesc() { + switch (type) { + case "codeerror": + return "代码错误"; + case "config": + return "配置相关"; + case "install": + return "安装部署"; + case "security": + return "安全相关"; + case "performance": + return "性能问题"; + case "standard": + return "标准规范"; + case "automation": + return "测试脚本"; + case "designdefect": + return "设计缺陷"; + case "others": + return "其他"; + default: + return type != null ? type : "未知"; + } + } + + /** + * 获取状态描述 + */ + public String getStatusDesc() { + switch (status) { + case "active": + return "激活"; + case "resolved": + return "已解决"; + case "closed": + return "已关闭"; + default: + return status != null ? status : "未知"; + } + } +} diff --git a/src/main/java/com/zeodao/reminder/model/ZentaoTask.java b/src/main/java/com/zeodao/reminder/model/ZentaoTask.java index be885c2..fd9435e 100644 --- a/src/main/java/com/zeodao/reminder/model/ZentaoTask.java +++ b/src/main/java/com/zeodao/reminder/model/ZentaoTask.java @@ -127,6 +127,13 @@ public class ZentaoTask { return !"done".equals(status) && !"closed".equals(status) && !"cancel".equals(status); } + /** + * 判断BUG是否未解决(用于BUG类型的数据) + */ + public boolean isBugUnresolved() { + return !"resolved".equals(status) && !"closed".equals(status); + } + /** * 判断任务是否已过期 */ diff --git a/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java b/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java index 35bfcca..c087d19 100644 --- a/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java +++ b/src/main/java/com/zeodao/reminder/service/ZentaoApiService.java @@ -7,6 +7,7 @@ import com.zeodao.reminder.config.TaskReminderConfig; import com.zeodao.reminder.exception.ProjectNotFoundException; import com.zeodao.reminder.model.ProjectInfo; import com.zeodao.reminder.model.ZentaoTask; +import com.zeodao.reminder.model.ZentaoBug; import com.zeodao.reminder.model.ZentaoUser; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; @@ -82,6 +83,35 @@ public class ZentaoApiService { return Collections.emptyList(); } + /** + * 获取项目下的未解决BUG + */ + public List getUnresolvedBugs(TaskReminderConfig.Group group) { + try { + String sessionId = getSessionId(group.getZentao()); + if (sessionId == null) { + logger.error("Failed to get session for group: {}", group.getId()); + return Collections.emptyList(); + } + + // 直接使用项目ID作为产品ID(在大多数情况下项目ID和产品ID是相同的) + String productId = String.valueOf(group.getZentao().getProjectId()); + + // 禅道开源版BUG API路径 + String url = group.getZentao().getApiUrl() + "/api.php/v1/products/" + productId + "/bugs"; + logger.debug("Trying to get bugs from URL: {}", url); + + List bugs = tryGetBugsFromUrl(url, sessionId, group); + logger.info("Found {} unresolved bugs for group: {}", bugs.size(), group.getId()); + return bugs; + + } catch (Exception e) { + logger.error("Error fetching bugs from Zentao for group: {}", group.getId(), e); + } + + return Collections.emptyList(); + } + /** * 检查项目是否存在并获取项目信息 */ @@ -380,6 +410,84 @@ public class ZentaoApiService { return Collections.emptyList(); } + /** + * 尝试从指定URL获取BUG列表 + */ + private List tryGetBugsFromUrl(String url, String sessionId, TaskReminderConfig.Group group) { + try (CloseableHttpClient client = HttpClients.createDefault()) { + 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 BUG API response for {}: {}", url, responseBody); + + JsonNode rootNode = objectMapper.readTree(responseBody); + + // 检查是否有错误 + if (rootNode.has("error")) { + logger.debug("API returned error for {}: {}", url, rootNode.get("error").asText()); + return Collections.emptyList(); + } + + // 使用安全的JsonNode解析方法 + List allBugs = parseBugsFromJsonNode(rootNode); + + logger.debug("Found {} bugs, filtering unresolved ones", allBugs.size()); + for (ZentaoBug bug : allBugs) { + logger.debug("Bug: {} - Status: {} - Unresolved: {}", + bug.getTitle(), bug.getStatus(), bug.isUnresolved()); + } + + return allBugs.stream() + .filter(ZentaoBug::isUnresolved) + .toList(); + } + } catch (Exception e) { + logger.debug("Error trying URL {}: {}", url, e.getMessage()); + } + + return Collections.emptyList(); + } + + /** + * 安全地从JsonNode解析BUG列表 + */ + private List parseBugsFromJsonNode(JsonNode rootNode) { + List bugs = new ArrayList<>(); + + try { + // 尝试从bugs字段获取BUG数组 + if (rootNode.has("bugs") && rootNode.get("bugs").isArray()) { + JsonNode bugsArray = rootNode.get("bugs"); + for (JsonNode bugNode : bugsArray) { + ZentaoBug bug = ZentaoBug.fromJsonNode(bugNode); + if (bug != null) { + bugs.add(bug); + } + } + return bugs; + } + + // 如果根节点直接是数组 + if (rootNode.isArray()) { + for (JsonNode bugNode : rootNode) { + ZentaoBug bug = ZentaoBug.fromJsonNode(bugNode); + if (bug != null) { + bugs.add(bug); + } + } + return bugs; + } + + } catch (Exception e) { + logger.debug("Error parsing bugs from JsonNode: {}", e.getMessage()); + } + + return bugs; + } + /** * 安全地从JsonNode解析任务列表 */ diff --git a/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java b/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java index 5fc3ce6..a0c5cb2 100644 --- a/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java +++ b/src/main/java/com/zeodao/reminder/service/ZentaoTaskReminderService.java @@ -4,6 +4,7 @@ import com.zeodao.reminder.config.TaskReminderConfig; import com.zeodao.reminder.exception.ProjectNotFoundException; import com.zeodao.reminder.model.ProjectInfo; import com.zeodao.reminder.model.ZentaoTask; +import com.zeodao.reminder.model.ZentaoBug; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -88,6 +89,58 @@ public class ZentaoTaskReminderService { } } + /** + * 发送禅道BUG提醒 + */ + public void sendBugReminder(TaskReminderConfig.Group group) { + try { + logger.info("Starting Zentao bug reminder for group: {}", group.getId()); + + // 首先检查项目是否存在 + ProjectInfo projectInfo; + try { + projectInfo = zentaoApiService.getProjectInfo(group); + logger.info("Project found: {} (ID: {}, Status: {})", + projectInfo.getName(), projectInfo.getId(), projectInfo.getStatus()); + } catch (ProjectNotFoundException e) { + logger.error("Project validation failed for group {}: {}", group.getId(), e.getMessage()); + // 项目不存在时只记录日志,不发送企业微信消息 + throw new RuntimeException("禅道项目配置错误: " + e.getMessage(), e); + } + + // 验证用户映射配置 + if (!userMappingService.validateUserMapping(group)) { + logger.warn("User mapping validation failed for group: {}", group.getId()); + return; + } + + // 获取未解决BUG + List unresolvedBugs = zentaoApiService.getUnresolvedBugs(group); + if (unresolvedBugs.isEmpty()) { + logger.info("No unresolved bugs found for group: {}", group.getId()); + sendNoBugsMessage(group, projectInfo); + return; + } + + // 按负责人分组 + Map> bugsByAssignee = unresolvedBugs.stream() + .filter(bug -> bug.getAssignedTo() != null && !bug.getAssignedTo().isEmpty()) + .collect(Collectors.groupingBy(ZentaoBug::getAssignedTo)); + + // 生成提醒消息并收集@人手机号 + List mentionedMobiles = new ArrayList<>(); + String message = buildBugReminderMessage(group, bugsByAssignee, unresolvedBugs, projectInfo, mentionedMobiles); + + // 发送带@人的消息 + wechatWebhookService.sendMessageWithMentions(group.getWebhook().getUrl(), message, mentionedMobiles); + + logger.info("Zentao bug reminder sent successfully for group: {}", group.getId()); + + } catch (Exception e) { + logger.error("Error sending Zentao bug reminder for group: {}", group.getId(), e); + } + } + /** * 构建任务提醒消息 */ @@ -150,6 +203,89 @@ public class ZentaoTaskReminderService { return message.toString(); } + /** + * 构建BUG提醒消息 + */ + private String buildBugReminderMessage(TaskReminderConfig.Group group, + Map> bugsByAssignee, + List allBugs, + ProjectInfo projectInfo, + List mentionedMobiles) { + StringBuilder message = new StringBuilder(); + + // 简化消息头部 + message.append("**禅道BUG提醒**\n"); + message.append("项目: ").append(projectInfo.getName()).append("\n\n"); + + // 简化统计信息 + long overdueBugs = allBugs.stream().filter(ZentaoBug::isOverdue).count(); + message.append("未解决BUG: ").append(allBugs.size()).append("个"); + if (overdueBugs > 0) { + message.append(",已过期: ").append(overdueBugs).append("个"); + } + message.append("\n\n"); + + // BUG详情 - 显示真实姓名,但用用户名匹配手机号 + for (Map.Entry> entry : bugsByAssignee.entrySet()) { + String assignedTo = entry.getKey(); // 这是用户名 + List bugs = entry.getValue(); + + // 用用户名匹配手机号 + String phone = userMappingService.getPhoneByRealName(group, assignedTo); + if (phone != null) { + mentionedMobiles.add(phone); + } + + // 显示真实姓名(从BUG中获取) + String realName = bugs.get(0).getAssignedToRealName(); + String displayName = (realName != null && !realName.isEmpty()) ? realName : assignedTo; + message.append(displayName).append(" (").append(bugs.size()).append("个BUG)\n"); + + for (ZentaoBug bug : bugs) { + // 简化BUG状态图标 + String statusIcon = bug.isOverdue() ? "🔴" : "🐛"; + + message.append("- ").append(statusIcon).append(" [").append(bug.getId()).append("] ") + .append(bug.getTitle()); + + // 严重程度 + if (bug.getSeverity() != null && !bug.getSeverity().isEmpty()) { + message.append(" [").append(bug.getSeverityDesc()).append("]"); + } + + // 截止日期 + if (bug.getDeadline() != null && !bug.getDeadline().isEmpty() && + !"0000-00-00".equals(bug.getDeadline())) { + message.append(" (").append(bug.getDeadline()).append(")"); + } + + message.append("\n"); + } + message.append("\n"); + } + + // 简化提示 + message.append("请及时登录禅道系统处理BUG"); + + return message.toString(); + } + + /** + * 发送无BUG消息 + */ + private void sendNoBugsMessage(TaskReminderConfig.Group group, ProjectInfo projectInfo) { + StringBuilder message = new StringBuilder(); + message.append("**禅道BUG提醒**\n"); + message.append("项目: ").append(projectInfo.getName()).append("\n\n"); + message.append("恭喜!当前项目没有未解决的BUG。"); + + try { + wechatWebhookService.sendMessage(group.getWebhook().getUrl(), message.toString()); + } catch (Exception e) { + logger.error("Error sending no bugs message for group: {}", group.getId(), e); + } + } + /** * 发送无任务消息 */ @@ -181,4 +317,18 @@ public class ZentaoTaskReminderService { return List.of(); // 返回空列表 } } + + /** + * 获取BUG详情(用于测试) + */ + public List getBugsForTesting(TaskReminderConfig.Group group) { + try { + // 先检查项目是否存在 + zentaoApiService.getProjectInfo(group); + return zentaoApiService.getUnresolvedBugs(group); + } catch (ProjectNotFoundException e) { + logger.error("Project not found during testing: {}", e.getMessage()); + return List.of(); // 返回空列表 + } + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f500615..557d73a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,7 +25,7 @@ task: api-url: "https://zentao.iscmtech.com" username: "admin" # 请替换为实际的禅道用户名 password: "Lianyu!@#~123456" # 请替换为实际的禅道密码 - project-id: 38 # 项目ID + project-id: 38 # 项目ID(同时用于获取任务和BUG) kanban-id: 39 # 看板ID(看板模式项目需要) # 用户映射:禅道用户名 -> 企业微信手机号(显示使用真实姓名) user-mapping: diff --git a/src/test/java/com/zeodao/reminder/service/ZentaoBugReminderTest.java b/src/test/java/com/zeodao/reminder/service/ZentaoBugReminderTest.java new file mode 100644 index 0000000..f8cff44 --- /dev/null +++ b/src/test/java/com/zeodao/reminder/service/ZentaoBugReminderTest.java @@ -0,0 +1,170 @@ +package com.zeodao.reminder.service; + +import com.zeodao.reminder.config.TaskReminderConfig; +import com.zeodao.reminder.model.ZentaoBug; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 禅道BUG提醒服务测试 + * + * @author Zeodao + * @version 1.0.0 + */ +@SpringBootTest +@TestPropertySource(locations = "classpath:application-test.properties") +public class ZentaoBugReminderTest { + + private TaskReminderConfig.Group testGroup; + + @BeforeEach + void setUp() { + // 创建测试群组配置 + testGroup = new TaskReminderConfig.Group(); + testGroup.setId("test-group"); + testGroup.setName("测试群组"); + testGroup.setEnabled(true); + testGroup.setTaskSystem("zentao"); + + TaskReminderConfig.Webhook webhook = new TaskReminderConfig.Webhook(); + webhook.setUrl("https://test.webhook.url"); + testGroup.setWebhook(webhook); + + TaskReminderConfig.Zentao zentao = new TaskReminderConfig.Zentao(); + zentao.setApiUrl("https://test.zentao.com"); + zentao.setUsername("testuser"); + zentao.setPassword("testpass"); + zentao.setProjectId(1); + testGroup.setZentao(zentao); + + Map userMapping = new HashMap<>(); + userMapping.put("testuser", "13800138000"); + testGroup.setUserMapping(userMapping); + } + + @Test + void testZentaoBugModelCreation() { + ZentaoBug bug = new ZentaoBug(); + bug.setId("1"); + bug.setTitle("测试BUG"); + bug.setStatus("active"); + bug.setSeverity("2"); + bug.setPri("1"); + bug.setAssignedTo("testuser"); + bug.setDeadline("2025-06-01"); + + assertNotNull(bug); + assertEquals("1", bug.getId()); + assertEquals("测试BUG", bug.getTitle()); + assertEquals("active", bug.getStatus()); + assertTrue(bug.isUnresolved()); + assertFalse(bug.isOverdue()); // 假设今天是2025年5月28日之前 + assertEquals("严重", bug.getSeverityDesc()); + assertEquals("高", bug.getPriorityDesc()); + } + + @Test + void testBugStatusLogic() { + ZentaoBug activeBug = new ZentaoBug(); + activeBug.setStatus("active"); + assertTrue(activeBug.isUnresolved()); + + ZentaoBug resolvedBug = new ZentaoBug(); + resolvedBug.setStatus("resolved"); + assertFalse(resolvedBug.isUnresolved()); + + ZentaoBug closedBug = new ZentaoBug(); + closedBug.setStatus("closed"); + assertFalse(closedBug.isUnresolved()); + } + + @Test + void testBugOverdueLogic() { + ZentaoBug overdueBug = new ZentaoBug(); + overdueBug.setDeadline("2020-01-01"); + assertTrue(overdueBug.isOverdue()); + + ZentaoBug futureBug = new ZentaoBug(); + futureBug.setDeadline("2030-01-01"); + assertFalse(futureBug.isOverdue()); + + ZentaoBug noDeadlineBug = new ZentaoBug(); + noDeadlineBug.setDeadline(null); + assertFalse(noDeadlineBug.isOverdue()); + + ZentaoBug zeroDeadlineBug = new ZentaoBug(); + zeroDeadlineBug.setDeadline("0000-00-00"); + assertFalse(zeroDeadlineBug.isOverdue()); + } + + @Test + void testBugDescriptions() { + ZentaoBug bug = new ZentaoBug(); + + // 测试严重程度描述 + bug.setSeverity("1"); + assertEquals("致命", bug.getSeverityDesc()); + + bug.setSeverity("2"); + assertEquals("严重", bug.getSeverityDesc()); + + bug.setSeverity("3"); + assertEquals("一般", bug.getSeverityDesc()); + + bug.setSeverity("4"); + assertEquals("轻微", bug.getSeverityDesc()); + + bug.setSeverity("unknown"); + assertEquals("未知", bug.getSeverityDesc()); + + // 测试优先级描述 + bug.setPri("1"); + assertEquals("高", bug.getPriorityDesc()); + + bug.setPri("2"); + assertEquals("中", bug.getPriorityDesc()); + + bug.setPri("3"); + assertEquals("低", bug.getPriorityDesc()); + + bug.setPri("4"); + assertEquals("最低", bug.getPriorityDesc()); + + bug.setPri("unknown"); + assertEquals("普通", bug.getPriorityDesc()); + + // 测试类型描述 + bug.setType("codeerror"); + assertEquals("代码错误", bug.getTypeDesc()); + + bug.setType("config"); + assertEquals("配置相关", bug.getTypeDesc()); + + bug.setType("security"); + assertEquals("安全相关", bug.getTypeDesc()); + + bug.setType("unknown"); + assertEquals("unknown", bug.getTypeDesc()); + + // 测试状态描述 + bug.setStatus("active"); + assertEquals("激活", bug.getStatusDesc()); + + bug.setStatus("resolved"); + assertEquals("已解决", bug.getStatusDesc()); + + bug.setStatus("closed"); + assertEquals("已关闭", bug.getStatusDesc()); + + bug.setStatus("unknown"); + assertEquals("unknown", bug.getStatusDesc()); + } +} diff --git a/test-bug-api.md b/test-bug-api.md new file mode 100644 index 0000000..650b7ce --- /dev/null +++ b/test-bug-api.md @@ -0,0 +1,129 @@ +# 禅道BUG提醒功能测试指南 + +## 新增功能概述 + +已成功为禅道任务提醒系统添加了BUG延期通知功能,现在系统支持: + +1. **任务延期通知** - 原有功能 +2. **BUG延期通知** - 新增功能 + +## 新增的API接口 + +### 1. 测试禅道BUG连接 +``` +GET /api/reminder/zentao/bugs/test/{groupId} +``` + +**功能**: 测试禅道BUG API连接,获取未解决的BUG列表 + +**响应示例**: +```json +{ + "success": true, + "message": "禅道BUG API连接成功", + "bugCount": 5, + "bugs": [ + { + "id": "123", + "title": "登录页面显示异常", + "status": "active", + "severity": "2", + "assignedTo": "zhangsan", + "deadline": "2025-06-01" + } + ] +} +``` + +### 2. 手动触发禅道BUG提醒 +``` +POST /api/reminder/zentao/bugs/reminder/{groupId} +``` + +**功能**: 手动发送禅道BUG提醒到企业微信群 + +**响应示例**: +```json +{ + "success": true, + "message": "禅道BUG提醒已发送" +} +``` + +## 配置说明 + +BUG功能直接使用现有的 `project-id` 配置,无需额外配置: + +```yaml +zentao: + api-url: "https://zentao.iscmtech.com" + username: "admin" + password: "password" + project-id: 38 # 同时用于获取任务和BUG +``` + +## 消息格式 + +### BUG提醒消息示例: +``` +**禅道BUG提醒** +项目: 一站式平台 + +未解决BUG: 3个,已过期: 1个 + +张三 (2个BUG) +- 🔴 [238] 测试任务2 [严重] (2025-05-28) +- 🐛 [236] 测试任务 [一般] (2025-05-28) + +请及时登录禅道系统处理BUG +``` + +## 技术实现 + +### 新增文件: +1. `ZentaoBug.java` - BUG数据模型 +2. `ZentaoBugReminderTest.java` - BUG功能测试 + +### 修改文件: +1. `ZentaoApiService.java` - 添加获取BUG的API方法 +2. `ZentaoTaskReminderService.java` - 添加BUG提醒逻辑 +3. `TaskReminderController.java` - 添加BUG相关API接口 +4. `TaskReminderConfig.java` - 添加productId配置字段 +5. `application.yml` - 添加产品ID配置 +6. `ZENTAO_INTEGRATION.md` - 更新文档 + +## 测试步骤 + +1. **启动应用程序** +2. **测试BUG连接**: + ```bash + curl http://localhost:8080/api/reminder/zentao/bugs/test/zentao-team + ``` + +3. **手动触发BUG提醒**: + ```bash + curl -X POST http://localhost:8080/api/reminder/zentao/bugs/reminder/zentao-team + ``` + +## 特性说明 + +- **自动过滤**: 只显示未解决的BUG(状态不是 resolved 或 closed) +- **过期标识**: 🔴 表示已过期的BUG,🐛 表示普通未解决BUG +- **严重程度**: 显示BUG的严重程度(致命、严重、一般、轻微) +- **@人功能**: 支持根据用户映射配置@相关负责人 +- **统计信息**: 显示总BUG数和过期BUG数 + +## 注意事项 + +1. **项目ID复用**: 系统直接使用 `project-id` 作为产品ID来获取BUG(在大多数禅道配置中,项目ID和产品ID是相同的) +2. **API兼容性**: 使用禅道开源版 RESTful API v1 +3. **权限要求**: 配置的禅道用户需要有产品的BUG查看权限 +4. **数据格式**: BUG数据结构与任务数据结构不同,使用专门的ZentaoBug模型处理 + +## 后续扩展 + +可以考虑添加: +- 定时BUG提醒(类似任务提醒的定时功能) +- BUG优先级过滤 +- BUG类型分类显示 +- BUG处理时间统计