增加BUG任务的提醒

This commit is contained in:
dengqichen 2025-05-29 13:09:03 +08:00
parent 25f6988e65
commit b5b50bf0cb
9 changed files with 904 additions and 5 deletions

View File

@ -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. **启用定时任务**:系统会按配置的时间自动发送提醒
## 注意事项

View File

@ -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<Map<String, Object>> testZentaoBugConnection(@PathVariable String groupId) {
Map<String, Object> 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<ZentaoBug> 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<Map<String, Object>> sendZentaoBugReminder(@PathVariable String groupId) {
Map<String, Object> 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);
}
}

View File

@ -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 : "未知";
}
}
}

View File

@ -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);
}
/**
* 判断任务是否已过期
*/

View File

@ -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<ZentaoBug> 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<ZentaoBug> 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<ZentaoBug> 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<ZentaoBug> 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<ZentaoBug> parseBugsFromJsonNode(JsonNode rootNode) {
List<ZentaoBug> 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解析任务列表
*/

View File

@ -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<ZentaoBug> unresolvedBugs = zentaoApiService.getUnresolvedBugs(group);
if (unresolvedBugs.isEmpty()) {
logger.info("No unresolved bugs found for group: {}", group.getId());
sendNoBugsMessage(group, projectInfo);
return;
}
// 按负责人分组
Map<String, List<ZentaoBug>> bugsByAssignee = unresolvedBugs.stream()
.filter(bug -> bug.getAssignedTo() != null && !bug.getAssignedTo().isEmpty())
.collect(Collectors.groupingBy(ZentaoBug::getAssignedTo));
// 生成提醒消息并收集@人手机号
List<String> 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<String, List<ZentaoBug>> bugsByAssignee,
List<ZentaoBug> allBugs,
ProjectInfo projectInfo,
List<String> 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<String, List<ZentaoBug>> entry : bugsByAssignee.entrySet()) {
String assignedTo = entry.getKey(); // 这是用户名
List<ZentaoBug> 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<ZentaoBug> 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(); // 返回空列表
}
}
}

View File

@ -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:

View File

@ -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<String, String> 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());
}
}

129
test-bug-api.md Normal file
View File

@ -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处理时间统计