Compare commits

..

17 Commits

Author SHA1 Message Date
dengqichen
766bf2a4e2 增加延期信息 2025-07-02 14:25:26 +08:00
dengqichen
73b1085925 增加延期信息 2025-07-01 14:39:22 +08:00
dengqichen
38cbdf6c04 增加延期信息 2025-07-01 14:12:39 +08:00
dengqichen
5d48ce35ce 格式化 2025-05-30 16:00:35 +08:00
dengqichen
1975ff1ffc 格式化 2025-05-30 13:20:51 +08:00
dengqichen
4ca28f8657 格式化 2025-05-30 13:07:32 +08:00
dengqichen
65e9dc7b16 格式化 2025-05-30 13:06:45 +08:00
dengqichen
c5233719cb 生产环境使用PROD配置文件 2025-05-30 11:31:53 +08:00
dengqichen
89ccf9d555 增加环境变量配置,去除全局无用开关 2025-05-30 11:20:24 +08:00
dengqichen
48333f5b4a 增加了提醒的单独开关功能 2025-05-30 11:03:24 +08:00
dengqichen
b3d98ce025 每个提醒的类型都增加了枚举 2025-05-29 17:55:48 +08:00
dengqichen
55d8ad26c0 修复了提示信息不显示中文人名的问题 2025-05-29 13:57:22 +08:00
dengqichen
b5b50bf0cb 增加BUG任务的提醒 2025-05-29 13:09:03 +08:00
dengqichen
25f6988e65 修复了提示信息不显示中文人名的问题 2025-05-29 11:33:25 +08:00
dengqichen
405b481c9e 增加docker其他阶段。 2025-05-28 17:36:21 +08:00
dengqichen
42b9651392 增加@人功能 2025-05-28 17:20:46 +08:00
dengqichen
d71708bb65 增加禅道看板任务提醒功能 2025-05-28 16:26:42 +08:00
34 changed files with 3626 additions and 267 deletions

5
.gitignore vendored
View File

@ -54,11 +54,6 @@ buildNumber.properties
ehthumbs.db
Thumbs.db
# Spring Boot
application-local.yml
application-dev.yml
application-prod.yml
# Temporary files
*.tmp
*.temp

View File

@ -7,7 +7,7 @@ ENV TZ=Asia/Shanghai
EXPOSE 8080
## 启动后端项目
CMD java -Djava.security.egd=file:/dev/./urandom -jar app.jar
CMD java -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=prod -jar app.jar
# docker -H tcp://172.22.222.6:2375 build -t 172.22.222.100:28082/task-reminder:1.0.0 .

179
ZENTAO_INTEGRATION.md Normal file
View File

@ -0,0 +1,179 @@
# 禅道集成功能说明
## 功能概述
本系统已集成禅道开源版20.8的API可以自动查询项目中的未完成任务并通过企业微信发送详细的任务提醒支持@相关负责人。
## 配置说明
### 1. 禅道配置
`application.yml` 中配置禅道相关信息:
```yaml
task:
reminder:
groups:
- id: "zentao-team"
name: "禅道开发团队"
webhook:
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-webhook-key"
task-system: "zentao" # 必须设置为 zentao
# 禅道配置
zentao:
api-url: "https://zentao.iscmtech.com" # 禅道地址
username: "your-username" # 禅道用户名
password: "your-password" # 禅道密码
project-id: 26 # 项目ID同时用于获取任务和BUG
# 用户映射:禅道邮箱 -> 企业微信手机号
user-mapping:
"liguiling@iscmtech.com": "13800138000"
"zhongweiyue@iscmtech.com": "13900139000"
"meya@iscmtech.com": "13700137000"
schedules:
morning:
time: "0 0 9 * * MON-FRI"
message: "早上好!请及时更新禅道任务状态"
evening:
time: "0 30 17 * * MON-FRI"
message: "下班前提醒:请更新今日任务完成情况"
enabled: true
```
### 2. 用户映射配置
用户映射支持两种方式:
#### 方式一:邮箱映射(推荐)
```yaml
user-mapping:
"user@iscmtech.com": "13800138000" # 禅道用户邮箱 -> 手机号
```
#### 方式二:用户名映射
```yaml
user-mapping:
"zhangsan": "13900139000" # 禅道用户名 -> 手机号
```
### 3. 企业微信@人格式
- 手机号:`13800138000`(系统会自动添加@前缀)
- @所有人`all`(系统会转换为@all
## API接口
### 测试禅道项目状态(推荐)
```
GET /api/reminder/zentao/status/test/{groupId}
```
**功能**: 测试禅道连接同时获取任务和BUG数据
### 手动触发禅道项目状态提醒(推荐)
```
POST /api/reminder/zentao/status/reminder/{groupId}
```
**功能**: 发送统一的项目状态提醒包含任务和BUG
### 测试禅道任务连接(兼容性)
```
GET /api/reminder/zentao/test/{groupId}
```
### 手动触发禅道任务提醒(兼容性)
```
POST /api/reminder/zentao/reminder/{groupId}
```
**注意**: 现在也会包含BUG信息
### 测试禅道BUG连接单独测试
```
GET /api/reminder/zentao/bugs/test/{groupId}
```
### 手动触发禅道BUG提醒单独发送
```
POST /api/reminder/zentao/bugs/reminder/{groupId}
```
### 手动触发指定群组提醒
```
POST /api/reminder/groups/{groupId}/morning
POST /api/reminder/groups/{groupId}/evening
```
## 消息格式
### 项目状态提醒消息格式(推荐)
系统会发送包含以下信息的统一项目状态提醒:
- 📊 项目概况任务和BUG的总数、过期数
- 👥 按人员分组的工作项列表(任务+BUG
- 🔴 过期事项标识
- ⚪ 未完成任务标识
- 🐛 未解决BUG标识
- 📋 任务优先级和BUG严重程度
- 📅 截止日期信息
**示例消息**
```
**禅道项目状态提醒**
项目: [2025-06-08]一站式平台
📊 项目概况
未完成任务: 2个已过期: 2个
未解决BUG: 1个
邓启辰 (3个事项)
- 🔴 [任务238] 测试任务2 (2025-05-28)
- 🔴 [任务236] 测试任务 (2025-05-28)
- 🐛 [BUG911] 测试BUG [一般] (2025-05-29)
请及时登录禅道系统更新状态
```
### 单独提醒消息格式(兼容性)
如果使用单独的任务或BUG提醒接口消息格式保持原样。
## 使用步骤
1. **配置禅道信息**在配置文件中填入正确的禅道地址、用户名、密码和项目ID
2. **配置用户映射**:将团队成员的禅道邮箱映射到企业微信手机号
3. **测试连接**调用测试API确认禅道任务和BUG连接正常
4. **启用定时任务**:系统会按配置的时间自动发送提醒
## 注意事项
1. **禅道版本**目前支持禅道开源版20.8
2. **API认证**:使用用户名密码方式认证
3. **项目权限**:配置的禅道用户需要有项目访问权限
4. **网络连接**:确保服务器能访问禅道系统
5. **手机号格式**必须是11位中国大陆手机号
## 故障排查
### 1. 禅道连接失败
- 检查禅道地址是否正确
- 检查用户名密码是否正确
- 检查网络连接是否正常
### 2. 无法获取任务
- 检查项目ID是否正确
- 检查用户是否有项目访问权限
- 查看日志中的详细错误信息
### 3. @人不生效
- 检查用户映射配置是否正确
- 确认手机号格式是否正确
- 确认企业微信群中是否有对应用户
## 开发说明
相关代码文件:
- `ZentaoApiService.java` - 禅道API调用服务
- `ZentaoTaskReminderService.java` - 禅道任务提醒服务
- `UserMappingService.java` - 用户映射服务
- `ZentaoTask.java` - 禅道任务实体类
- `ZentaoUser.java` - 禅道用户实体类

View File

@ -13,7 +13,7 @@ services:
environment:
- TZ=Asia/Shanghai
volumes:
- ./logs:/app/logs
- /opt/task-reminder/logs:/app/logs
networks:
default:
external: true

32
pom.xml
View File

@ -65,6 +65,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
@ -94,6 +102,10 @@
<argument>stop</argument>
<argument>task-reminder-app</argument>
</arguments>
<successCodes>
<successCode>0</successCode>
<successCode>1</successCode>
</successCodes>
</configuration>
</execution>
<execution>
@ -110,6 +122,10 @@
<argument>remove</argument>
<argument>task-reminder-app</argument>
</arguments>
<successCodes>
<successCode>0</successCode>
<successCode>1</successCode>
</successCodes>
</configuration>
</execution>
<execution>
@ -126,6 +142,10 @@
<argument>rmi</argument>
<argument>172.22.222.100:28082/task-reminder:1.0.0</argument>
</arguments>
<successCodes>
<successCode>0</successCode>
<successCode>1</successCode>
</successCodes>
</configuration>
</execution>
<!-- 在install阶段构建Docker镜像 -->
@ -145,6 +165,10 @@
<argument>172.22.222.100:28082/task-reminder:1.0.0</argument>
<argument>.</argument>
</arguments>
<successCodes>
<successCode>0</successCode>
<successCode>1</successCode>
</successCodes>
</configuration>
</execution>
<!-- 在install阶段推送Docker镜像 -->
@ -162,6 +186,10 @@
<argument>push</argument>
<argument>172.22.222.100:28082/task-reminder:1.0.0</argument>
</arguments>
<successCodes>
<successCode>0</successCode>
<successCode>1</successCode>
</successCodes>
</configuration>
</execution>
<execution>
@ -179,6 +207,10 @@
<argument>up</argument>
<argument>-d</argument>
</arguments>
<successCodes>
<successCode>0</successCode>
<successCode>1</successCode>
</successCodes>
</configuration>
</execution>
</executions>

View File

@ -1,5 +1,6 @@
package com.zeodao.reminder.config;
import com.zeodao.reminder.enums.TaskSystemType;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@ -47,14 +48,7 @@ public class TaskReminderConfig {
.orElse(null);
}
/**
* 获取所有启用的群组
*/
public List<Group> getEnabledGroups() {
return groups.stream()
.filter(Group::isEnabled)
.toList();
}
public static class Global {
private int timeout = 5000;
@ -72,9 +66,11 @@ public class TaskReminderConfig {
private String id;
private String name;
private Webhook webhook = new Webhook();
private String taskSystem;
private TaskSystemType taskSystem;
private Map<String, Schedule> schedules = new HashMap<>();
private boolean enabled = true;
private Zentao zentao = new Zentao();
private Map<String, String> userMapping = new HashMap<>();
public String getId() {
return id;
@ -100,14 +96,28 @@ public class TaskReminderConfig {
this.webhook = webhook;
}
public String getTaskSystem() {
public TaskSystemType getTaskSystem() {
return taskSystem;
}
public void setTaskSystem(String taskSystem) {
public void setTaskSystem(TaskSystemType taskSystem) {
this.taskSystem = taskSystem;
}
/**
* 设置任务系统类型字符串版本用于配置文件兼容
*/
public void setTaskSystem(String taskSystemCode) {
this.taskSystem = TaskSystemType.fromCode(taskSystemCode);
}
/**
* 获取任务系统代码字符串版本用于向后兼容
*/
public String getTaskSystemCode() {
return taskSystem != null ? taskSystem.getCode() : null;
}
public Map<String, Schedule> getSchedules() {
return schedules;
}
@ -116,12 +126,22 @@ public class TaskReminderConfig {
this.schedules = schedules;
}
public boolean isEnabled() {
return enabled;
public Zentao getZentao() {
return zentao;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
public void setZentao(Zentao zentao) {
this.zentao = zentao;
}
public Map<String, String> getUserMapping() {
return userMapping;
}
public void setUserMapping(Map<String, String> userMapping) {
this.userMapping = userMapping;
}
}
@ -140,6 +160,7 @@ public class TaskReminderConfig {
public static class Schedule {
private String time;
private String message;
private boolean enabled = true; // 默认启用
public String getTime() {
return time;
@ -156,5 +177,70 @@ public class TaskReminderConfig {
public void setMessage(String message) {
this.message = message;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
public static class Zentao {
private String apiUrl;
private String token;
private String username;
private String password;
private Integer projectId;
private Integer kanbanId;
public String getApiUrl() {
return apiUrl;
}
public void setApiUrl(String apiUrl) {
this.apiUrl = apiUrl;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getProjectId() {
return projectId;
}
public void setProjectId(Integer projectId) {
this.projectId = projectId;
}
public Integer getKanbanId() {
return kanbanId;
}
public void setKanbanId(Integer kanbanId) {
this.kanbanId = kanbanId;
}
}
}

View File

@ -1,8 +1,13 @@
package com.zeodao.reminder.controller;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -35,6 +40,9 @@ public class TaskReminderController {
@Autowired
private DynamicTaskScheduler dynamicTaskScheduler;
@Autowired
private ZentaoTaskReminderService zentaoTaskReminderService;
/**
* 系统健康检查
*/
@ -58,11 +66,11 @@ public class TaskReminderController {
try {
String taskInfo = dynamicTaskScheduler.getTaskStatusInfo();
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
List<TaskReminderConfig.Group> allGroups = taskReminderConfig.getGroups();
response.put("success", true);
response.put("taskInfo", taskInfo);
response.put("enabledGroupCount", enabledGroups.size());
response.put("totalGroupCount", allGroups.size());
response.put("activeTaskCount", dynamicTaskScheduler.getActiveTaskCount());
response.put("message", "获取系统信息成功");
} catch (Exception e) {
@ -95,26 +103,6 @@ public class TaskReminderController {
return ResponseEntity.ok(response);
}
/**
* 手动发送测试消息
*/
@PostMapping("/test")
public ResponseEntity<Map<String, Object>> sendTestMessage() {
Map<String, Object> response = new HashMap<>();
try {
boolean success = taskReminderService.sendTestMessage();
response.put("success", success);
response.put("message", success ? "测试消息发送成功" : "测试消息发送失败");
} catch (Exception e) {
logger.error("发送测试消息失败", e);
response.put("success", false);
response.put("message", "发送测试消息失败:" + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 手动触发早上提醒
*/
@ -123,7 +111,7 @@ public class TaskReminderController {
Map<String, Object> response = new HashMap<>();
try {
taskReminderService.sendMorningReminder();
taskReminderService.sendAllMorningReminders();
response.put("success", true);
response.put("message", "早上提醒已触发");
} catch (Exception e) {
@ -155,13 +143,33 @@ public class TaskReminderController {
return ResponseEntity.ok(response);
}
/**
* 手动触发逾期任务提醒
*/
@PostMapping("/overdue-reminder")
public ResponseEntity<Map<String, Object>> triggerOverdueReminder() {
Map<String, Object> response = new HashMap<>();
try {
taskReminderService.sendAllOverdueReminders();
response.put("success", true);
response.put("message", "所有群组逾期任务提醒已触发");
} catch (Exception e) {
logger.error("触发逾期任务提醒失败", e);
response.put("success", false);
response.put("message", "触发逾期任务提醒失败:" + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 手动触发指定群组的提醒
*/
@PostMapping("/groups/{groupId}/{scheduleType}")
public ResponseEntity<Map<String, Object>> triggerGroupReminder(
@PathVariable String groupId,
@PathVariable String scheduleType) {
@PathVariable ScheduleType scheduleType) {
Map<String, Object> response = new HashMap<>();
try {
@ -197,4 +205,213 @@ public class TaskReminderController {
return ResponseEntity.ok(response);
}
/**
* 测试禅道API连接
*/
@GetMapping("/zentao/test/{groupId}")
public ResponseEntity<Map<String, Object>> testZentaoConnection(@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 (group.getTaskSystem() != TaskSystemType.ZENTAO) {
response.put("success", false);
response.put("message", "该群组不是禅道类型");
return ResponseEntity.ok(response);
}
List<ZentaoTask> tasks = zentaoTaskReminderService.getTasksForTesting(group);
response.put("success", true);
response.put("message", "禅道API连接成功");
response.put("taskCount", tasks.size());
response.put("tasks", tasks);
} catch (Exception e) {
logger.error("测试禅道连接失败: {}", groupId, e);
response.put("success", false);
response.put("message", "测试禅道连接失败:" + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 手动触发禅道任务提醒
*/
@PostMapping("/zentao/reminder/{groupId}")
public ResponseEntity<Map<String, Object>> triggerZentaoReminder(@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 (group.getTaskSystem() != TaskSystemType.ZENTAO) {
response.put("success", false);
response.put("message", "该群组不是禅道类型");
return ResponseEntity.ok(response);
}
zentaoTaskReminderService.sendTaskReminder(group);
response.put("success", true);
response.put("message", "禅道任务提醒已发送");
} catch (Exception e) {
logger.error("发送禅道任务提醒失败: {}", groupId, e);
response.put("success", false);
response.put("message", "发送禅道任务提醒失败:" + e.getMessage());
}
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 (group.getTaskSystem() != TaskSystemType.ZENTAO) {
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 (group.getTaskSystem() != TaskSystemType.ZENTAO) {
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);
}
/**
* 测试禅道项目状态任务+BUG
*/
@GetMapping("/zentao/status/test/{groupId}")
public ResponseEntity<Map<String, Object>> testZentaoProjectStatus(@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 (group.getTaskSystem() != TaskSystemType.ZENTAO) {
response.put("success", false);
response.put("message", "该群组不是禅道类型");
return ResponseEntity.ok(response);
}
List<ZentaoTask> tasks = zentaoTaskReminderService.getTasksForTesting(group);
List<ZentaoBug> bugs = zentaoTaskReminderService.getBugsForTesting(group);
response.put("success", true);
response.put("message", "禅道项目状态API连接成功");
response.put("taskCount", tasks.size());
response.put("bugCount", bugs.size());
response.put("tasks", tasks);
response.put("bugs", bugs);
} catch (Exception e) {
logger.error("测试禅道项目状态失败: {}", groupId, e);
response.put("success", false);
response.put("message", "测试禅道项目状态失败:" + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 手动触发禅道项目状态提醒任务+BUG统一通知
*/
@PostMapping("/zentao/status/reminder/{groupId}")
public ResponseEntity<Map<String, Object>> sendZentaoProjectStatusReminder(@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 (group.getTaskSystem() != TaskSystemType.ZENTAO) {
response.put("success", false);
response.put("message", "该群组不是禅道类型");
return ResponseEntity.ok(response);
}
// 使用现有的sendTaskReminder方法它现在已经包含了任务和BUG
zentaoTaskReminderService.sendTaskReminder(group);
response.put("success", true);
response.put("message", "禅道项目状态提醒已发送包含任务和BUG");
} catch (Exception e) {
logger.error("发送禅道项目状态提醒失败: {}", groupId, e);
response.put("success", false);
response.put("message", "发送禅道项目状态提醒失败:" + e.getMessage());
}
return ResponseEntity.ok(response);
}
}

View File

@ -0,0 +1,67 @@
package com.zeodao.reminder.enums;
/**
* 提醒类型枚举
*
* @author Zeodao
* @version 2.0.0
*/
public enum ScheduleType {
/**
* 早上提醒
*/
MORNING("morning", "早上提醒"),
/**
* 晚上提醒
*/
EVENING("evening", "晚上提醒"),
/**
* 逾期任务提醒
*/
OVERDUE_REMINDER("overdue-reminder", "逾期任务提醒");
private final String code;
private final String description;
ScheduleType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码获取枚举
*/
public static ScheduleType fromCode(String code) {
for (ScheduleType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
return null;
}
/**
* 判断是否为文本提醒类型
*/
public boolean isTextReminder() {
return this == MORNING || this == EVENING;
}
/**
* 判断是否为逾期提醒类型
*/
public boolean isOverdueReminder() {
return this == OVERDUE_REMINDER;
}
}

View File

@ -0,0 +1,68 @@
package com.zeodao.reminder.enums;
/**
* 任务系统类型枚举
*
* @author Zeodao
* @version 2.0.0
*/
public enum TaskSystemType {
/**
* 禅道
*/
ZENTAO("zentao", "禅道"),
/**
* 智能表格
*/
SMARTSHEET("smartsheet", "智能表格"),
/**
* Jira
*/
JIRA("jira", "Jira"),
/**
* Trello
*/
TRELLO("trello", "Trello"),
/**
* Asana
*/
ASANA("asana", "Asana"),
/**
* Notion
*/
NOTION("notion", "Notion");
private final String code;
private final String description;
TaskSystemType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码获取枚举
*/
public static TaskSystemType fromCode(String code) {
for (TaskSystemType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
return null;
}
}

View File

@ -0,0 +1,18 @@
package com.zeodao.reminder.exception;
/**
* 项目不存在异常
*
* @author Zeodao
* @version 2.0.0
*/
public class ProjectNotFoundException extends Exception {
public ProjectNotFoundException(String message) {
super(message);
}
public ProjectNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,51 @@
package com.zeodao.reminder.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 项目信息实体类
*
* @author Zeodao
* @version 2.0.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProjectInfo {
private Integer id;
private String name;
private String status;
private boolean exists;
/**
* 检查项目是否处于活跃状态
*/
public boolean isActive() {
return exists && !"closed".equals(status) && !"suspended".equals(status);
}
/**
* 获取项目状态描述
*/
public String getStatusDescription() {
switch (status) {
case "wait":
return "未开始";
case "doing":
return "进行中";
case "suspended":
return "已挂起";
case "closed":
return "已关闭";
default:
return "未知状态";
}
}
}

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

@ -0,0 +1,197 @@
package com.zeodao.reminder.model;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
/**
* 禅道任务实体类 - 使用JsonNode实现动态字段解析
*
* @author Zeodao
* @version 2.0.0
*/
@Data
public class ZentaoTask {
private String id;
private String name;
private String type;
private String status;
private String pri;
private String assignedTo;
private String assignedToRealName;
private String assignedDate;
private String deadline;
private String estStarted;
private String realStarted;
private String finishedBy;
private String finishedDate;
private String canceledBy;
private String canceledDate;
private String closedBy;
private String closedDate;
private String closedReason;
private String lastEditedBy;
private String lastEditedDate;
private String openedBy;
private String openedDate;
private String desc;
private String project;
private String story;
private String storyTitle;
private String module;
private String estimate;
private String consumed;
private String left;
private String progress;
private String color;
private String deleted;
/**
* 从JsonNode创建ZentaoTask对象安全地提取字段
*/
public static ZentaoTask fromJsonNode(JsonNode node) {
ZentaoTask task = new ZentaoTask();
// 安全地提取字段如果字段不存在或类型不匹配使用默认值
task.setId(getStringValue(node, "id"));
task.setName(getStringValue(node, "name"));
task.setType(getStringValue(node, "type"));
task.setStatus(getStringValue(node, "status"));
task.setPri(getStringValue(node, "pri"));
task.setAssignedTo(getStringValue(node, "assignedTo"));
task.setAssignedToRealName(getStringValue(node, "assignedToRealName"));
task.setAssignedDate(getStringValue(node, "assignedDate"));
task.setDeadline(getStringValue(node, "deadline"));
task.setEstStarted(getStringValue(node, "estStarted"));
task.setRealStarted(getStringValue(node, "realStarted"));
task.setFinishedBy(getStringValue(node, "finishedBy"));
task.setFinishedDate(getStringValue(node, "finishedDate"));
task.setCanceledBy(getStringValue(node, "canceledBy"));
task.setCanceledDate(getStringValue(node, "canceledDate"));
task.setClosedBy(getStringValue(node, "closedBy"));
task.setClosedDate(getStringValue(node, "closedDate"));
task.setClosedReason(getStringValue(node, "closedReason"));
task.setLastEditedBy(getStringValue(node, "lastEditedBy"));
task.setLastEditedDate(getStringValue(node, "lastEditedDate"));
task.setOpenedBy(getStringValue(node, "openedBy"));
task.setOpenedDate(getStringValue(node, "openedDate"));
task.setDesc(getStringValue(node, "desc"));
task.setProject(getStringValue(node, "project"));
task.setStory(getStringValue(node, "story"));
task.setStoryTitle(getStringValue(node, "storyTitle"));
task.setModule(getStringValue(node, "module"));
task.setEstimate(getStringValue(node, "estimate"));
task.setConsumed(getStringValue(node, "consumed"));
task.setLeft(getStringValue(node, "left"));
task.setProgress(getStringValue(node, "progress"));
task.setColor(getStringValue(node, "color"));
task.setDeleted(getStringValue(node, "deleted"));
return task;
}
/**
* 安全地从JsonNode中提取字符串值
*/
private static String getStringValue(JsonNode node, String fieldName) {
if (node == null || !node.has(fieldName)) {
return null;
}
JsonNode fieldNode = node.get(fieldName);
if (fieldNode == null || fieldNode.isNull()) {
return null;
}
// 如果是对象类型如openedBy尝试提取其中的字符串字段
if (fieldNode.isObject()) {
// 对于用户对象尝试提取account或realname
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();
}
/**
* 判断任务是否未完成
*/
public boolean isIncomplete() {
return !"done".equals(status) && !"closed".equals(status) && !"cancel".equals(status);
}
/**
* 判断BUG是否未解决用于BUG类型的数据
*/
public boolean isBugUnresolved() {
return !"resolved".equals(status) && !"closed".equals(status);
}
/**
* 判断任务是否已过期
*/
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 getPriorityDesc() {
switch (pri) {
case "1":
return "";
case "2":
return "";
case "3":
return "";
case "4":
return "最低";
default:
return "普通";
}
}
}

View File

@ -0,0 +1,81 @@
package com.zeodao.reminder.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
/**
* 禅道用户实体类
*
* @author Zeodao
* @version 2.0.0
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ZentaoUser {
private String id;
private String account;
private String realname;
private String nickname;
private String avatar;
private String birthday;
private String gender;
private String email;
private String skype;
private String qq;
private String yahoo;
private String gtalk;
private String wangwang;
private String mobile;
private String phone;
private String address;
private String zipcode;
private String join;
private String visits;
private String ip;
private String last;
private String fails;
private String locked;
private String feedback;
private String mail;
private String clientStatus;
private String clientLang;
private String dept;
private String role;
private String type;
private String groups;
private String view;
private String deleted;
}

View File

@ -1,6 +1,8 @@
package com.zeodao.reminder.scheduler;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.service.EnabledCheckService;
import com.zeodao.reminder.service.TaskReminderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,6 +35,9 @@ public class DynamicTaskScheduler {
@Autowired
private TaskReminderService taskReminderService;
@Autowired
private EnabledCheckService enabledCheckService;
@Autowired
private TaskScheduler taskScheduler;
@ -42,7 +47,7 @@ public class DynamicTaskScheduler {
public void initScheduledTasks() {
logger.info("=== 初始化动态定时任务 ===");
for (TaskReminderConfig.Group group : taskReminderConfig.getEnabledGroups()) {
for (TaskReminderConfig.Group group : taskReminderConfig.getGroups()) {
createSchedulesForGroup(group);
}
@ -56,30 +61,43 @@ public class DynamicTaskScheduler {
logger.info("为群组 {} 创建定时任务", group.getName());
for (Map.Entry<String, TaskReminderConfig.Schedule> entry : group.getSchedules().entrySet()) {
String scheduleType = entry.getKey();
String scheduleTypeCode = entry.getKey();
TaskReminderConfig.Schedule schedule = entry.getValue();
String taskKey = group.getId() + "-" + scheduleType;
// 将字符串转换为枚举
ScheduleType scheduleType = ScheduleType.fromCode(scheduleTypeCode);
if (scheduleType == null) {
logger.warn("无效的提醒类型代码: {} - 跳过", scheduleTypeCode);
continue;
}
// 检查该提醒类型是否启用
if (!enabledCheckService.isReminderEnabled(group, scheduleType)) {
logger.debug("跳过禁用的提醒类型: {} - {}", group.getName(), scheduleType.getDescription());
continue;
}
String taskKey = group.getId() + "-" + scheduleTypeCode;
try {
CronTrigger cronTrigger = new CronTrigger(schedule.getTime());
Runnable task = () -> {
try {
logger.info("=== 执行定时任务: {} - {} ===", group.getName(), scheduleType);
logger.info("=== 执行定时任务: {} - {} ===", group.getName(), scheduleType.getDescription());
taskReminderService.sendReminder(group.getId(), scheduleType);
} catch (Exception e) {
logger.error("定时任务执行失败: {} - {}", group.getName(), scheduleType, e);
logger.error("定时任务执行失败: {} - {}", group.getName(), scheduleType.getDescription(), e);
}
};
ScheduledFuture<?> scheduledFuture = taskScheduler.schedule(task, cronTrigger);
scheduledTasks.put(taskKey, scheduledFuture);
logger.info("创建定时任务成功: {} - {} ({})", group.getName(), scheduleType, schedule.getTime());
logger.info("创建定时任务成功: {} - {} ({})", group.getName(), scheduleType.getDescription(), schedule.getTime());
} catch (Exception e) {
logger.error("创建定时任务失败: {} - {} ({})", group.getName(), scheduleType, schedule.getTime(), e);
logger.error("创建定时任务失败: {} - {} ({})", group.getName(), scheduleType.getDescription(), schedule.getTime(), e);
}
}
}
@ -133,10 +151,10 @@ public class DynamicTaskScheduler {
info.append("=== 定时任务状态 ===\n");
info.append("总任务数: ").append(scheduledTasks.size()).append("\n");
info.append("活跃任务数: ").append(getActiveTaskCount()).append("\n");
info.append("启用群组数: ").append(taskReminderConfig.getEnabledGroups().size()).append("\n\n");
info.append("配置群组数: ").append(taskReminderConfig.getGroups().size()).append("\n\n");
info.append("=== 群组配置详情 ===\n");
for (TaskReminderConfig.Group group : taskReminderConfig.getEnabledGroups()) {
for (TaskReminderConfig.Group group : taskReminderConfig.getGroups()) {
info.append("群组: ").append(group.getName()).append(" (").append(group.getId()).append(")\n");
info.append("任务系统: ").append(group.getTaskSystem()).append("\n");
info.append("定时任务:\n");
@ -147,7 +165,16 @@ public class DynamicTaskScheduler {
String taskKey = group.getId() + "-" + scheduleType;
ScheduledFuture<?> future = scheduledTasks.get(taskKey);
String status = (future != null && !future.isCancelled()) ? "运行中" : "已停止";
boolean isEnabled = enabledCheckService.isReminderEnabled(group, scheduleType);
String status;
if (!isEnabled) {
status = "已禁用";
} else if (future != null && !future.isCancelled()) {
status = "运行中";
} else {
status = "已停止";
}
info.append(" - ").append(scheduleType).append(": ").append(schedule.getTime())
.append(" (").append(status).append(")\n");

View File

@ -0,0 +1,61 @@
package com.zeodao.reminder.service;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* 启用状态检查服务
* 简化版本直接检查提醒类型的启用状态
*
* @author Zeodao
* @version 2.0.0
*/
@Service
public class EnabledCheckService {
private static final Logger logger = LoggerFactory.getLogger(EnabledCheckService.class);
/**
* 检查群组的特定提醒类型是否启用
*
* @param group 群组配置
* @param scheduleType 提醒类型
* @return 是否启用
*/
public boolean isReminderEnabled(TaskReminderConfig.Group group, ScheduleType scheduleType) {
TaskReminderConfig.Schedule schedule = group.getSchedules().get(scheduleType.getCode());
if (schedule == null) {
logger.debug("群组 {} 未配置 {} 提醒", group.getName(), scheduleType.getDescription());
return false;
}
boolean enabled = schedule.isEnabled();
if (!enabled) {
logger.debug("群组 {} 的 {} 提醒已禁用", group.getName(), scheduleType.getDescription());
} else {
logger.debug("群组 {} 的 {} 提醒已启用", group.getName(), scheduleType.getDescription());
}
return enabled;
}
/**
* 检查群组的特定提醒类型是否启用字符串版本用于兼容
*
* @param group 群组配置
* @param scheduleTypeCode 提醒类型代码
* @return 是否启用
*/
public boolean isReminderEnabled(TaskReminderConfig.Group group, String scheduleTypeCode) {
ScheduleType scheduleType = ScheduleType.fromCode(scheduleTypeCode);
if (scheduleType == null) {
logger.warn("无效的提醒类型代码: {}", scheduleTypeCode);
return false;
}
return isReminderEnabled(group, scheduleType);
}
}

View File

@ -1,6 +1,10 @@
package com.zeodao.reminder.service;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
import com.zeodao.reminder.strategy.ReminderHandler;
import com.zeodao.reminder.strategy.ReminderHandlerFactory;
import com.zeodao.reminder.util.HolidayUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,10 +35,16 @@ public class TaskReminderService {
@Autowired
private TaskReminderConfig taskReminderConfig;
@Autowired
private ReminderHandlerFactory reminderHandlerFactory;
@Autowired
private EnabledCheckService enabledCheckService;
/**
* 发送指定群组和时间段的任务提醒
* 发送指定群组和时间段的任务提醒字符串版本用于向后兼容
*/
public void sendReminder(String groupId, String scheduleType) {
public void sendReminder(String groupId, ScheduleType scheduleType) {
if (!shouldSendReminder()) {
logger.info("今天是节假日或周末,跳过任务提醒");
return;
@ -46,12 +56,13 @@ public class TaskReminderService {
return;
}
if (!group.isEnabled()) {
logger.info("群组 {} 已禁用,跳过提醒", groupId);
// 使用启用状态检查服务进行多级检查
if (!enabledCheckService.isReminderEnabled(group, scheduleType)) {
logger.info("群组 {} 的 {} 提醒已禁用,跳过提醒", group.getName(), scheduleType);
return;
}
TaskReminderConfig.Schedule schedule = group.getSchedules().get(scheduleType);
TaskReminderConfig.Schedule schedule = group.getSchedules().get(scheduleType.getCode());
if (schedule == null) {
logger.error("群组 {} 未配置 {} 提醒时间", groupId, scheduleType);
return;
@ -59,13 +70,20 @@ public class TaskReminderService {
logger.info("开始发送任务提醒 - 群组: {}, 类型: {}", group.getName(), scheduleType);
String message = wechatWebhookService.createTaskReminderMessage(groupId, schedule.getMessage(), scheduleType + "提醒");
boolean success = wechatWebhookService.sendMarkdownMessage(groupId, message);
// 转换字符串类型为枚举类型
TaskSystemType taskSystemTypeEnum = group.getTaskSystem();
if (success) {
logger.info("任务提醒发送成功 - 群组: {}, 类型: {}", group.getName(), scheduleType);
if (taskSystemTypeEnum == null) {
logger.error("无效的任务系统类型: {}", group.getTaskSystemCode());
return;
}
// 使用策略模式选择合适的处理器
ReminderHandler handler = reminderHandlerFactory.getHandler(scheduleType, taskSystemTypeEnum);
if (handler != null) {
handler.handleReminder(group, scheduleType, schedule);
} else {
logger.error("任务提醒发送失败 - 群组: {}, 类型: {}", group.getName(), scheduleType);
logger.error("未找到合适的提醒处理器 - 群组: {}, 类型: {}, 任务系统: {}", group.getName(), scheduleType.getDescription(), taskSystemTypeEnum.getDescription());
}
}
@ -73,10 +91,10 @@ public class TaskReminderService {
* 发送所有启用群组的早上提醒
*/
public void sendAllMorningReminders() {
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
for (TaskReminderConfig.Group group : enabledGroups) {
if (group.getSchedules().containsKey("morning")) {
sendReminder(group.getId(), "morning");
List<TaskReminderConfig.Group> allGroups = taskReminderConfig.getGroups();
for (TaskReminderConfig.Group group : allGroups) {
if (enabledCheckService.isReminderEnabled(group, ScheduleType.MORNING)) {
sendReminder(group.getId(), ScheduleType.MORNING);
}
}
}
@ -85,28 +103,24 @@ public class TaskReminderService {
* 发送所有启用群组的晚上提醒
*/
public void sendAllEveningReminders() {
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
for (TaskReminderConfig.Group group : enabledGroups) {
if (group.getSchedules().containsKey("evening")) {
sendReminder(group.getId(), "evening");
List<TaskReminderConfig.Group> allGroups = taskReminderConfig.getGroups();
for (TaskReminderConfig.Group group : allGroups) {
if (enabledCheckService.isReminderEnabled(group, ScheduleType.EVENING)) {
sendReminder(group.getId(), ScheduleType.EVENING);
}
}
}
/**
* 兼容旧版本的方法
* 发送所有启用群组的逾期任务提醒
*/
@Deprecated
public void sendMorningReminder() {
sendAllMorningReminders();
public void sendAllOverdueReminders() {
List<TaskReminderConfig.Group> allGroups = taskReminderConfig.getGroups();
for (TaskReminderConfig.Group group : allGroups) {
if (enabledCheckService.isReminderEnabled(group, ScheduleType.OVERDUE_REMINDER)) {
sendReminder(group.getId(), ScheduleType.OVERDUE_REMINDER);
}
}
/**
* 兼容旧版本的方法
*/
@Deprecated
public void sendEveningReminder() {
sendAllEveningReminders();
}
/**
@ -131,40 +145,4 @@ public class TaskReminderService {
return true;
}
/**
* 手动发送测试消息
*/
public boolean sendTestMessage() {
logger.info("发送测试消息");
String testMessage = wechatWebhookService.createTaskReminderMessage(
"这是一条测试消息用于验证企业微信webhook是否正常工作。",
"测试消息"
);
boolean success = wechatWebhookService.sendMarkdownMessage(testMessage);
if (success) {
logger.info("测试消息发送成功");
} else {
logger.error("测试消息发送失败");
}
return success;
}
/**
* 获取下次提醒时间信息
*/
public String getNextReminderInfo() {
LocalDateTime now = LocalDateTime.now();
StringBuilder info = new StringBuilder();
info.append("当前时间:").append(now.toString()).append("\n");
info.append("今天是否工作日:").append(shouldSendReminder() ? "" : "").append("\n");
info.append("早上提醒时间:每个工作日 09:00\n");
info.append("晚上提醒时间:每个工作日 17:30\n");
return info.toString();
}
}

View File

@ -0,0 +1,212 @@
package com.zeodao.reminder.service;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.model.ZentaoUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 用户映射服务
* 负责将禅道用户映射到企业微信用户
*
* @author Zeodao
* @version 2.0.0
*/
@Service
public class UserMappingService {
private static final Logger logger = LoggerFactory.getLogger(UserMappingService.class);
@Autowired
private ZentaoApiService zentaoApiService;
/**
* 根据禅道用户名获取企业微信@标识
*
* @param group 群组配置
* @param zentaoUsername 禅道用户名
* @return 企业微信@标识 @13800138000 null
*/
public String getWechatMention(TaskReminderConfig.Group group, String zentaoUsername) {
if (zentaoUsername == null || zentaoUsername.isEmpty()) {
return null;
}
// 1. 首先尝试从配置的用户映射中查找
Map<String, String> userMapping = group.getUserMapping();
if (userMapping != null && !userMapping.isEmpty()) {
// 直接用户名映射
if (userMapping.containsKey(zentaoUsername)) {
String wechatId = userMapping.get(zentaoUsername);
return formatWechatMention(wechatId);
}
// 尝试通过邮箱映射
String email = getUserEmailByUsername(group, zentaoUsername);
if (email != null && userMapping.containsKey(email)) {
String wechatId = userMapping.get(email);
return formatWechatMention(wechatId);
}
}
logger.debug("No WeChat mapping found for Zentao user: {}", zentaoUsername);
return null;
}
/**
* 根据禅道用户名获取用户邮箱
*/
private String getUserEmailByUsername(TaskReminderConfig.Group group, String username) {
try {
List<ZentaoUser> users = zentaoApiService.getUsers(group);
return users.stream()
.filter(user -> username.equals(user.getAccount()))
.map(ZentaoUser::getEmail)
.filter(email -> email != null && !email.isEmpty())
.findFirst()
.orElse(null);
} catch (Exception e) {
logger.error("Error getting user email for username: {}", username, e);
return null;
}
}
/**
* 格式化企业微信@标识
*/
private String formatWechatMention(String wechatId) {
if (wechatId == null || wechatId.isEmpty()) {
return null;
}
// 如果已经是@格式直接返回
if (wechatId.startsWith("@")) {
return wechatId;
}
// 如果是手机号格式添加@前缀
if (wechatId.matches("^1[3-9]\\d{9}$")) {
return "@" + wechatId;
}
// 如果是特殊标识如all添加@前缀
if ("all".equals(wechatId)) {
return "@all";
}
// 其他情况当作用户ID处理
return "@" + wechatId;
}
/**
* 批量获取企业微信@标识
*
* @param group 群组配置
* @param zentaoUsernames 禅道用户名列表
* @return 企业微信@标识列表
*/
public List<String> getWechatMentions(TaskReminderConfig.Group group, List<String> zentaoUsernames) {
if (zentaoUsernames == null || zentaoUsernames.isEmpty()) {
return List.of();
}
return zentaoUsernames.stream()
.map(username -> getWechatMention(group, username))
.filter(mention -> mention != null)
.distinct()
.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;
}
/**
* 获取所有配置的用户映射信息用于调试
*/
public Map<String, String> getAllUserMappings(TaskReminderConfig.Group group) {
return group.getUserMapping();
}
/**
* 验证用户映射配置
*/
public boolean validateUserMapping(TaskReminderConfig.Group group) {
Map<String, String> userMapping = group.getUserMapping();
if (userMapping == null || userMapping.isEmpty()) {
logger.warn("No user mapping configured for group: {}", group.getId());
return false;
}
int validMappings = 0;
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 {
logger.warn("Invalid user mapping: {} -> {}", key, value);
}
}
logger.info("Group {} has {} valid user mappings", group.getId(), validMappings);
return validMappings > 0;
}
}

View File

@ -2,20 +2,16 @@ package com.zeodao.reminder.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zeodao.reminder.config.TaskReminderConfig;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import com.zeodao.reminder.enums.TaskSystemType;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
@ -33,10 +29,24 @@ public class WechatWebhookService {
private static final Logger logger = LoggerFactory.getLogger(WechatWebhookService.class);
@Autowired
private TaskReminderConfig taskReminderConfig;
private final TaskReminderConfig taskReminderConfig;
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestTemplate restTemplate;
public WechatWebhookService(TaskReminderConfig taskReminderConfig) {
this.taskReminderConfig = taskReminderConfig;
// 根据配置设置超时时间如果配置为空则使用默认值
int timeoutSeconds = taskReminderConfig != null && taskReminderConfig.getGlobal() != null
? taskReminderConfig.getGlobal().getTimeout() / 1000
: 10;
this.restTemplate = new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(timeoutSeconds))
.setReadTimeout(Duration.ofSeconds(timeoutSeconds))
.build();
}
/**
* 发送文本消息到指定群组
@ -89,13 +99,13 @@ public class WechatWebhookService {
*/
@Deprecated
public boolean sendMarkdownMessage(String content) {
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
if (enabledGroups.isEmpty()) {
logger.error("没有启用的群组配置");
List<TaskReminderConfig.Group> allGroups = taskReminderConfig.getGroups();
if (allGroups.isEmpty()) {
logger.error("没有配置的群组");
return false;
}
return sendMarkdownMessage(enabledGroups.get(0).getId(), content);
return sendMarkdownMessage(allGroups.get(0).getId(), content);
}
/**
@ -127,47 +137,80 @@ public class WechatWebhookService {
}
/**
* 发送消息到企业微信
* 创建带@人的Markdown消息体
*/
private boolean sendMessage(String webhookUrl, Map<String, Object> messageBody) throws IOException {
int timeout = taskReminderConfig.getGlobal().getTimeout();
private Map<String, Object> createMarkdownMessageWithMentions(String content, List<String> mobileList) {
Map<String, Object> message = new HashMap<>();
message.put("msgtype", "markdown");
logger.info("准备发送消息到企业微信URL: {}", webhookUrl);
Map<String, Object> markdown = new HashMap<>();
markdown.put("content", content);
message.put("markdown", markdown);
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(webhookUrl);
// 添加@人列表
if (mobileList != null && !mobileList.isEmpty()) {
message.put("mentioned_mobile_list", mobileList);
}
// 设置请求配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.build();
httpPost.setConfig(requestConfig);
return message;
}
// 设置请求头
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
// 设置请求体
String jsonBody = objectMapper.writeValueAsString(messageBody);
StringEntity entity = new StringEntity(jsonBody, StandardCharsets.UTF_8);
httpPost.setEntity(entity);
logger.debug("发送消息内容: {}", jsonBody);
// 执行请求
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (statusCode == 200) {
logger.info("企业微信消息发送成功,响应: {}", responseBody);
return true;
} else {
logger.error("企业微信消息发送失败,状态码: {}, 响应: {}", statusCode, responseBody);
/**
* 发送Markdown消息到指定URL
*/
public boolean sendMessage(String webhookUrl, String markdownContent) {
try {
Map<String, Object> messageBody = createMarkdownMessage(markdownContent);
return sendMessage(webhookUrl, messageBody);
} catch (Exception e) {
logger.error("发送企业微信消息失败, URL: {}", webhookUrl, e);
return false;
}
}
/**
* 发送带@人的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;
}
}
/**
* 发送消息到企业微信
*/
private boolean sendMessage(String webhookUrl, Map<String, Object> messageBody) {
logger.info("准备发送消息到企业微信URL: {}", webhookUrl);
try {
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 创建请求实体
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(messageBody, headers);
logger.debug("发送消息内容: {}", objectMapper.writeValueAsString(messageBody));
// 发送请求
ResponseEntity<String> response = restTemplate.postForEntity(webhookUrl, requestEntity, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
logger.info("企业微信消息发送成功,响应: {}", response.getBody());
return true;
} else {
logger.error("企业微信消息发送失败,状态码: {}, 响应: {}",
response.getStatusCode(), response.getBody());
return false;
}
} catch (Exception e) {
logger.error("发送企业微信消息异常URL: {}", webhookUrl, e);
return false;
}
}
@ -188,57 +231,35 @@ public class WechatWebhookService {
StringBuilder message = new StringBuilder();
// 根据任务管理系统类型生成不同的标题和操作指引
String systemName = getTaskSystemDisplayName(group.getTaskSystem());
String systemIcon = getTaskSystemIcon(group.getTaskSystem());
TaskSystemType taskSystemType = group.getTaskSystem();
String systemName = taskSystemType != null ? taskSystemType.getDescription() : "任务管理系统";
String systemIcon = getTaskSystemIcon(taskSystemType);
message.append("## ").append(systemIcon).append(" ").append(systemName).append("任务状态提醒\n\n");
message.append("⏰ **时间:** ").append(timestamp).append(" (").append(dayOfWeek).append(")\n\n");
message.append("📢 ").append(baseMessage).append("\n\n");
message.append("🔗 **操作指引:**\n");
message.append(getTaskSystemInstructions(group.getTaskSystem()));
message.append(getTaskSystemInstructions(taskSystemType));
message.append("💡 及时更新任务状态有助于团队协作和项目管理");
return message.toString();
}
/**
* 兼容旧版本的方法
*/
@Deprecated
public String createTaskReminderMessage(String baseMessage, String timeType) {
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
if (enabledGroups.isEmpty()) {
return baseMessage;
}
return createTaskReminderMessage(enabledGroups.get(0).getId(), baseMessage, timeType);
}
/**
* 获取任务管理系统的显示名称
*/
private String getTaskSystemDisplayName(String taskSystem) {
switch (taskSystem.toLowerCase()) {
case "zentao": return "禅道";
case "smartsheet": return "智能表格";
case "jira": return "Jira";
case "trello": return "Trello";
case "asana": return "Asana";
case "notion": return "Notion";
default: return "任务管理系统";
}
}
/**
* 获取任务管理系统的图标
*/
private String getTaskSystemIcon(String taskSystem) {
switch (taskSystem.toLowerCase()) {
case "zentao": return "📋";
case "smartsheet": return "📊";
case "jira": return "🎯";
case "trello": return "📌";
case "asana": return "";
case "notion": return "📝";
private String getTaskSystemIcon(TaskSystemType taskSystemType) {
if (taskSystemType == null) {
return "📋";
}
switch (taskSystemType) {
case ZENTAO: return "📋";
case SMARTSHEET: return "📊";
case JIRA: return "🎯";
case TRELLO: return "📌";
case ASANA: return "";
case NOTION: return "📝";
default: return "📋";
}
}
@ -246,24 +267,31 @@ public class WechatWebhookService {
/**
* 获取任务管理系统的操作指引
*/
private String getTaskSystemInstructions(String taskSystem) {
switch (taskSystem.toLowerCase()) {
case "zentao":
private String getTaskSystemInstructions(TaskSystemType taskSystemType) {
if (taskSystemType == null) {
return "1. 登录任务管理系统\n" +
"2. 查看分配给自己的任务\n" +
"3. 更新任务状态和进度\n" +
"4. 添加必要的工作记录\n\n";
}
switch (taskSystemType) {
case ZENTAO:
return "1. 登录禅道系统\n" +
"2. 查看分配给自己的任务\n" +
"3. 更新任务状态和进度\n" +
"4. 添加必要的工作日志\n\n";
case "smartsheet":
case SMARTSHEET:
return "1. 打开智能表格\n" +
"2. 找到自己负责的任务行\n" +
"3. 更新任务状态和完成百分比\n" +
"4. 添加备注说明进展情况\n\n";
case "jira":
case JIRA:
return "1. 登录Jira系统\n" +
"2. 查看分配给自己的Issue\n" +
"3. 更新Issue状态\n" +
"4. 记录工作日志和时间\n\n";
case "trello":
case TRELLO:
return "1. 打开Trello看板\n" +
"2. 找到自己的任务卡片\n" +
"3. 移动卡片到对应状态列\n" +

View File

@ -0,0 +1,618 @@
package com.zeodao.reminder.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 禅道API服务类
* 支持禅道开源版20.8
*
* @author Zeodao
* @version 2.0.0
*/
@Service
public class ZentaoApiService {
private static final Logger logger = LoggerFactory.getLogger(ZentaoApiService.class);
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, String> sessionCache = new HashMap<>();
/**
* 获取项目下的未完成任务
*/
public List<ZentaoTask> getIncompleteTasks(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();
}
// 尝试多个API路径来获取任务
List<String> urlsToTry = new ArrayList<>();
if (group.getZentao().getKanbanId() != null) {
// 看板模式尝试多个可能的API路径
urlsToTry.add(group.getZentao().getApiUrl() + "/api.php/v1/executions/" + group.getZentao().getProjectId() + "/tasks");
} else {
// 传统项目模式
urlsToTry.add(group.getZentao().getApiUrl() + "/api.php/v1/projects/" + group.getZentao().getProjectId() + "/tasks");
}
// 尝试每个URL直到找到有效的
for (String url : urlsToTry) {
logger.debug("Trying URL: {}", url);
List<ZentaoTask> tasks = tryGetTasksFromUrl(url, sessionId, group);
if (!tasks.isEmpty()) {
return tasks;
}
}
} catch (Exception e) {
logger.error("Error fetching tasks from Zentao for group: {}", group.getId(), e);
}
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();
}
/**
* 检查项目是否存在并获取项目信息
*/
public ProjectInfo getProjectInfo(TaskReminderConfig.Group group) throws ProjectNotFoundException {
try {
String sessionId = getSessionId(group.getZentao());
if (sessionId == null) {
throw new ProjectNotFoundException("无法获取禅道会话,请检查用户名密码是否正确");
}
// 禅道开源版API路径
String url = group.getZentao().getApiUrl() + "/api.php/v1/projects/" + group.getZentao().getProjectId();
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 project API response: {}", responseBody);
JsonNode rootNode = objectMapper.readTree(responseBody);
// 检查是否有错误信息
if (rootNode.has("error")) {
String errorMsg = rootNode.get("error").asText();
throw new ProjectNotFoundException("项目不存在或已被删除:" + errorMsg);
}
// 禅道开源版直接返回项目对象不是包装在data字段中
if (rootNode.has("id") && rootNode.has("name")) {
// 检查项目是否被删除
if (rootNode.has("deleted") && rootNode.get("deleted").asBoolean()) {
throw new ProjectNotFoundException("项目ID " + group.getZentao().getProjectId() + " 已被删除");
}
// 检查项目状态
String status = rootNode.has("status") ? rootNode.get("status").asText() : "unknown";
if ("closed".equals(status)) {
logger.warn("项目ID {} 已关闭", group.getZentao().getProjectId());
}
String projectName = rootNode.get("name").asText();
return new ProjectInfo(group.getZentao().getProjectId(), projectName, status, true);
} else {
throw new ProjectNotFoundException("项目ID " + group.getZentao().getProjectId() + " 不存在或无权限访问");
}
}
}
} catch (ProjectNotFoundException e) {
throw e; // 重新抛出项目不存在异常
} catch (Exception e) {
logger.error("Error fetching project info from Zentao for group: {}", group.getId(), e);
throw new ProjectNotFoundException("获取项目信息失败:" + e.getMessage());
}
}
/**
* 获取项目名称兼容旧方法
*/
@Deprecated
public String getProjectName(TaskReminderConfig.Group group) {
try {
ProjectInfo projectInfo = getProjectInfo(group);
return projectInfo.getName();
} catch (ProjectNotFoundException e) {
logger.error("项目不存在: {}", e.getMessage());
return "项目ID: " + group.getZentao().getProjectId() + " (不存在)";
}
}
/**
* 获取用户信息
*/
public List<ZentaoUser> getUsers(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();
}
String url = buildApiUrl(group.getZentao().getApiUrl(), "user", "list", null);
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 users API response: {}", responseBody);
JsonNode rootNode = objectMapper.readTree(responseBody);
if (rootNode.has("data")) {
JsonNode dataNode = rootNode.get("data");
return objectMapper.convertValue(
dataNode, new TypeReference<List<ZentaoUser>>() {
});
}
}
}
} catch (Exception e) {
logger.error("Error fetching users from Zentao for group: {}", group.getId(), e);
}
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
*/
private String getSessionId(TaskReminderConfig.Zentao zentaoConfig) {
String cacheKey = zentaoConfig.getApiUrl() + ":" + zentaoConfig.getUsername();
// 检查缓存
if (sessionCache.containsKey(cacheKey)) {
return sessionCache.get(cacheKey);
}
try {
String loginUrl = zentaoConfig.getApiUrl() + "/api.php/v1/tokens";
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpPost request = new HttpPost(loginUrl);
request.setHeader("Content-Type", "application/json");
// 构建登录请求体
Map<String, String> loginData = new HashMap<>();
loginData.put("account", zentaoConfig.getUsername());
loginData.put("password", zentaoConfig.getPassword());
String jsonBody = objectMapper.writeValueAsString(loginData);
request.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));
try (CloseableHttpResponse response = client.execute(request)) {
String responseBody = EntityUtils.toString(response.getEntity());
logger.debug("Zentao login response: {}", responseBody);
JsonNode rootNode = objectMapper.readTree(responseBody);
if (rootNode.has("token")) {
String sessionId = rootNode.get("token").asText();
sessionCache.put(cacheKey, sessionId);
return sessionId;
}
}
}
} catch (Exception e) {
logger.error("Error getting session from Zentao", e);
}
return null;
}
/**
* 构建API URL
*/
private String buildApiUrl(String baseUrl, String module, String method, String params) {
StringBuilder url = new StringBuilder(baseUrl);
if (!baseUrl.endsWith("/")) {
url.append("/");
}
url.append("api.php/v1/").append(module).append("/").append(method);
if (params != null && !params.isEmpty()) {
url.append("/").append(params);
}
return url.toString();
}
/**
* 尝试从指定URL获取任务
*/
private List<ZentaoTask> tryGetTasksFromUrl(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 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<ZentaoTask> allTasks = parseTasksFromJsonNode(rootNode);
logger.debug("Found {} tasks, filtering incomplete ones", allTasks.size());
for (ZentaoTask task : allTasks) {
logger.debug("Task: {} - Status: {} - Incomplete: {}",
task.getName(), task.getStatus(), task.isIncomplete());
}
return allTasks.stream()
.filter(ZentaoTask::isIncomplete)
.toList();
}
} catch (Exception e) {
logger.debug("Error trying URL {}: {}", url, e.getMessage());
}
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解析任务列表
*/
private List<ZentaoTask> parseTasksFromJsonNode(JsonNode rootNode) {
List<ZentaoTask> tasks = new ArrayList<>();
try {
// 尝试从tasks字段获取任务数组
if (rootNode.has("tasks") && rootNode.get("tasks").isArray()) {
JsonNode tasksArray = rootNode.get("tasks");
for (JsonNode taskNode : tasksArray) {
ZentaoTask task = ZentaoTask.fromJsonNode(taskNode);
if (task != null) {
tasks.add(task);
}
}
return tasks;
}
// 尝试从data字段获取任务
if (rootNode.has("data")) {
JsonNode dataNode = rootNode.get("data");
// 如果data是数组直接解析
if (dataNode.isArray()) {
for (JsonNode taskNode : dataNode) {
ZentaoTask task = ZentaoTask.fromJsonNode(taskNode);
if (task != null) {
tasks.add(task);
}
}
return tasks;
}
// 如果data是对象且包含tasks数组
if (dataNode.has("tasks") && dataNode.get("tasks").isArray()) {
JsonNode tasksArray = dataNode.get("tasks");
for (JsonNode taskNode : tasksArray) {
ZentaoTask task = ZentaoTask.fromJsonNode(taskNode);
if (task != null) {
tasks.add(task);
}
}
return tasks;
}
}
// 如果根节点直接是数组
if (rootNode.isArray()) {
for (JsonNode taskNode : rootNode) {
ZentaoTask task = ZentaoTask.fromJsonNode(taskNode);
if (task != null) {
tasks.add(task);
}
}
return tasks;
}
} catch (Exception e) {
logger.debug("Error parsing tasks from JsonNode: {}", e.getMessage());
}
return tasks;
}
/**
* 解析看板任务数据
*/
private List<ZentaoTask> parseKanbanTasks(JsonNode rootNode) {
List<ZentaoTask> tasks = new ArrayList<>();
try {
// 看板API返回的是泳道数据需要遍历每个泳道中的卡片
if (rootNode.isArray()) {
for (JsonNode laneNode : rootNode) {
if (laneNode.has("cards") && laneNode.get("cards").isArray()) {
for (JsonNode cardNode : laneNode.get("cards")) {
ZentaoTask task = parseKanbanCard(cardNode);
if (task != null && task.isIncomplete()) {
tasks.add(task);
}
}
}
}
}
} catch (Exception e) {
logger.error("Error parsing kanban tasks", e);
}
return tasks;
}
/**
* 解析看板卡片为任务对象
*/
private ZentaoTask parseKanbanCard(JsonNode cardNode) {
try {
ZentaoTask task = new ZentaoTask();
if (cardNode.has("id")) {
task.setId(String.valueOf(cardNode.get("id").asInt()));
}
if (cardNode.has("title")) {
task.setName(cardNode.get("title").asText());
}
if (cardNode.has("assignedTo")) {
task.setAssignedTo(cardNode.get("assignedTo").asText());
}
if (cardNode.has("status")) {
task.setStatus(cardNode.get("status").asText());
}
if (cardNode.has("deadline")) {
task.setDeadline(cardNode.get("deadline").asText());
}
return task;
} catch (Exception e) {
logger.error("Error parsing kanban card", e);
return null;
}
}
/**
* 清除Session缓存
*/
public void clearSessionCache() {
sessionCache.clear();
}
}

View File

@ -0,0 +1,529 @@
package com.zeodao.reminder.service;
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;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 禅道任务提醒服务
*
* @author Zeodao
* @version 2.0.0
*/
@Service
public class ZentaoTaskReminderService {
private static final Logger logger = LoggerFactory.getLogger(ZentaoTaskReminderService.class);
@Autowired
private ZentaoApiService zentaoApiService;
@Autowired
private UserMappingService userMappingService;
@Autowired
private WechatWebhookService wechatWebhookService;
/**
* 发送禅道项目状态提醒包含任务和BUG
*/
public void sendTaskReminder(TaskReminderConfig.Group group) {
try {
logger.info("Starting Zentao project status 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<ZentaoTask> incompleteTasks = zentaoApiService.getIncompleteTasks(group);
List<ZentaoBug> unresolvedBugs = zentaoApiService.getUnresolvedBugs(group);
// 如果任务和BUG都为空发送无事项消息
if (incompleteTasks.isEmpty() && unresolvedBugs.isEmpty()) {
logger.info("No incomplete tasks or unresolved bugs found for group: {}", group.getId());
// sendNoItemsMessage(group, projectInfo);
return;
}
// 生成统一的项目状态提醒消息
List<String> mentionedMobiles = new ArrayList<>();
String message = buildProjectStatusMessage(group, incompleteTasks, unresolvedBugs, projectInfo, mentionedMobiles);
// 发送带@人的消息
wechatWebhookService.sendMessageWithMentions(group.getWebhook().getUrl(), message, mentionedMobiles);
logger.info("Zentao project status reminder sent successfully for group: {}", group.getId());
} catch (Exception e) {
logger.error("Error sending Zentao project status reminder for group: {}", group.getId(), e);
}
}
/**
* 发送禅道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);
}
}
/**
* 构建统一的项目状态提醒消息包含任务和BUG
*/
private String buildProjectStatusMessage(TaskReminderConfig.Group group,
List<ZentaoTask> incompleteTasks,
List<ZentaoBug> unresolvedBugs,
ProjectInfo projectInfo,
List<String> mentionedMobiles) {
StringBuilder message = new StringBuilder();
// 消息头部
message.append("**禅道项目状态提醒**\n");
message.append("项目: ").append(projectInfo.getName()).append("\n\n");
// 统计信息
long overdueTasks = incompleteTasks.stream().filter(ZentaoTask::isOverdue).count();
long overdueBugs = unresolvedBugs.stream().filter(ZentaoBug::isOverdue).count();
message.append("📊 **项目概况**\n");
message.append("未完成TASK: ").append(incompleteTasks.size()).append("");
if (overdueTasks > 0) {
message.append(",已过期: ").append(overdueTasks).append("");
}
message.append("\n");
message.append("未解决BUGFIX: ").append(unresolvedBugs.size()).append("");
if (overdueBugs > 0) {
message.append(",已过期: ").append(overdueBugs).append("");
}
message.append("\n\n");
// 按负责人合并任务和BUG
Map<String, ProjectItems> itemsByAssignee = mergeTasksAndBugs(incompleteTasks, unresolvedBugs);
// 生成每个人的工作项详情
for (Map.Entry<String, ProjectItems> entry : itemsByAssignee.entrySet()) {
String assignedTo = entry.getKey();
ProjectItems items = entry.getValue();
// 用用户名匹配手机号
String phone = userMappingService.getPhoneByRealName(group, assignedTo);
if (phone != null) {
mentionedMobiles.add(phone);
}
// 显示真实姓名
String displayName = getDisplayName(items, assignedTo);
int totalItems = items.tasks.size() + items.bugs.size();
message.append("## ").append(displayName).append(" (").append(totalItems).append("个事项)\n");
// 显示任务
for (ZentaoTask task : items.tasks) {
String statusIcon = task.isOverdue() ? "<font color=\"warning\">" : "<font color=\"common\">";
message.append(statusIcon).append(" [TASK-").append(task.getId()).append("] ").append(task.getName());
if (task.getDeadline() != null && !task.getDeadline().isEmpty() && !"0000-00-00".equals(task.getDeadline())) {
message.append(" \n截止日期").append(task.getDeadline());
if (task.isOverdue()) {
long overdueDays = getOverdueDays(task.getDeadline());
if (overdueDays > 0) {
message.append(",已延期").append(overdueDays).append("");
}
}
}
message.append("</font>\n");
}
// 显示BUG
for (ZentaoBug bug : items.bugs) {
String statusIcon = bug.isOverdue() ? "<font color=\"warning\">🔴" : "<font color=\"common\">🐛";
message.append(statusIcon).append(" [BUGFIX-").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(" \n截止日期").append(bug.getDeadline());
if (bug.isOverdue()) {
long overdueDays = getOverdueDays(bug.getDeadline());
if (overdueDays > 0) {
message.append(",已延期").append(overdueDays).append("");
}
}
}
message.append("</font>\n");
}
message.append("\n");
}
// 提示信息
message.append("\n请及时登录禅道系统更新状态");
return message.toString();
}
/**
* 计算延期天数
*/
private long getOverdueDays(String deadline) {
if (deadline == null || deadline.isEmpty() || "0000-00-00".equals(deadline)) {
return 0;
}
try {
LocalDate deadlineDate = LocalDate.parse(deadline);
LocalDate today = LocalDate.now();
return today.toEpochDay() - deadlineDate.toEpochDay();
} catch (Exception e) {
logger.debug("Failed to parse deadline: {}", deadline);
return 0;
}
}
/**
* 合并任务和BUG按负责人分组
*/
private Map<String, ProjectItems> mergeTasksAndBugs(List<ZentaoTask> tasks, List<ZentaoBug> bugs) {
Map<String, ProjectItems> result = new HashMap<>();
// 添加任务
for (ZentaoTask task : tasks) {
if (task.getAssignedTo() != null && !task.getAssignedTo().isEmpty()) {
result.computeIfAbsent(task.getAssignedTo(), k -> new ProjectItems()).tasks.add(task);
}
}
// 添加BUG
for (ZentaoBug bug : bugs) {
if (bug.getAssignedTo() != null && !bug.getAssignedTo().isEmpty()) {
result.computeIfAbsent(bug.getAssignedTo(), k -> new ProjectItems()).bugs.add(bug);
}
}
return result;
}
/**
* 获取显示名称优先使用真实姓名
*/
private String getDisplayName(ProjectItems items, String assignedTo) {
// 从任务中获取真实姓名
if (!items.tasks.isEmpty()) {
String realName = items.tasks.get(0).getAssignedToRealName();
if (realName != null && !realName.isEmpty()) {
return realName;
}
}
// 从BUG中获取真实姓名
if (!items.bugs.isEmpty()) {
String realName = items.bugs.get(0).getAssignedToRealName();
if (realName != null && !realName.isEmpty()) {
return realName;
}
}
// 默认返回用户名
return assignedTo;
}
/**
* 项目工作项容器类
*/
private static class ProjectItems {
List<ZentaoTask> tasks = new ArrayList<>();
List<ZentaoBug> bugs = new ArrayList<>();
}
/**
* 构建任务提醒消息保留原方法以兼容性
*/
private String buildTaskReminderMessage(TaskReminderConfig.Group group,
Map<String, List<ZentaoTask>> tasksByAssignee,
List<ZentaoTask> allTasks,
ProjectInfo projectInfo,
List<String> mentionedMobiles) {
StringBuilder message = new StringBuilder();
// 简化消息头部
message.append("**禅道任务提醒**\n");
message.append("项目: ").append(projectInfo.getName()).append("\n\n");
// 简化统计信息
long overdueTasks = allTasks.stream().filter(ZentaoTask::isOverdue).count();
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 assignedTo = entry.getKey(); // 这是用户名
List<ZentaoTask> tasks = entry.getValue();
// 用用户名匹配手机号
String phone = userMappingService.getPhoneByRealName(group, assignedTo);
if (phone != null) {
mentionedMobiles.add(phone);
}
// 显示真实姓名从任务中获取
String realName = tasks.get(0).getAssignedToRealName();
String displayName = (realName != null && !realName.isEmpty()) ? realName : assignedTo;
message.append(displayName).append(" (").append(tasks.size()).append("个任务)\n");
for (ZentaoTask task : tasks) {
// 简化任务状态图标
String statusIcon = task.isOverdue() ? "🔴" : "";
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());
if (task.isOverdue()) {
long overdueDays = getOverdueDays(task.getDeadline());
if (overdueDays > 0) {
message.append(",已延期").append(overdueDays).append("");
}
}
message.append(")");
}
message.append("\n");
}
message.append("\n");
}
// 简化提示
message.append("请及时登录禅道系统更新任务状态");
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());
if (bug.isOverdue()) {
long overdueDays = getOverdueDays(bug.getDeadline());
if (overdueDays > 0) {
message.append(",已延期").append(overdueDays).append("");
}
}
message.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);
}
}
/**
* 发送无事项消息既没有任务也没有BUG
*/
private void sendNoItemsMessage(TaskReminderConfig.Group group, ProjectInfo projectInfo) {
StringBuilder message = new StringBuilder();
message.append("**禅道项目状态提醒**\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 items message for group: {}", group.getId(), e);
}
}
/**
* 发送无任务消息保留原方法以兼容性
*/
private void sendNoTasksMessage(TaskReminderConfig.Group group, ProjectInfo projectInfo) {
StringBuilder message = new StringBuilder();
message.append("**禅道任务提醒**\n");
message.append("项目: ").append(projectInfo.getName()).append("\n\n");
message.append("恭喜!当前项目没有未完成的任务。");
try {
wechatWebhookService.sendMessage(group.getWebhook().getUrl(), message.toString());
} catch (Exception e) {
logger.error("Error sending no tasks message for group: {}", group.getId(), e);
}
}
/**
* 获取任务详情用于测试
*/
public List<ZentaoTask> getTasksForTesting(TaskReminderConfig.Group group) {
try {
// 先检查项目是否存在
zentaoApiService.getProjectInfo(group);
return zentaoApiService.getIncompleteTasks(group);
} catch (ProjectNotFoundException e) {
logger.error("Project not found during testing: {}", e.getMessage());
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

@ -0,0 +1,33 @@
package com.zeodao.reminder.strategy;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
/**
* 提醒处理器接口
* 使用策略模式处理不同类型的提醒
*
* @author Zeodao
* @version 2.0.0
*/
public interface ReminderHandler {
/**
* 判断是否支持处理指定的提醒类型
*
* @param scheduleType 提醒类型
* @param taskSystemType 任务系统类型
* @return 是否支持
*/
boolean supports(ScheduleType scheduleType, TaskSystemType taskSystemType);
/**
* 处理提醒
*
* @param group 群组配置
* @param scheduleType 提醒类型
* @param schedule 提醒配置
*/
void handleReminder(TaskReminderConfig.Group group, ScheduleType scheduleType, TaskReminderConfig.Schedule schedule);
}

View File

@ -0,0 +1,66 @@
package com.zeodao.reminder.strategy;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 提醒处理器工厂
* 根据提醒类型和任务系统选择合适的处理器
*
* @author Zeodao
* @version 2.0.0
*/
@Component
public class ReminderHandlerFactory {
private static final Logger logger = LoggerFactory.getLogger(ReminderHandlerFactory.class);
@Autowired
private List<ReminderHandler> reminderHandlers;
/**
* 获取合适的提醒处理器
*
* @param scheduleType 提醒类型
* @param taskSystemType 任务系统类型
* @return 提醒处理器如果没有找到则返回null
*/
public ReminderHandler getHandler(ScheduleType scheduleType, TaskSystemType taskSystemType) {
for (ReminderHandler handler : reminderHandlers) {
if (handler.supports(scheduleType, taskSystemType)) {
logger.debug("找到处理器: {} 用于处理 scheduleType={}, taskSystemType={}",
handler.getClass().getSimpleName(), scheduleType.getDescription(), taskSystemType.getDescription());
return handler;
}
}
logger.warn("未找到合适的处理器 - scheduleType: {}, taskSystemType: {}",
scheduleType.getDescription(), taskSystemType.getDescription());
return null;
}
/**
* 获取合适的提醒处理器字符串参数版本用于兼容
*
* @param scheduleTypeCode 提醒类型代码
* @param taskSystemCode 任务系统类型代码
* @return 提醒处理器如果没有找到则返回null
*/
public ReminderHandler getHandler(String scheduleTypeCode, String taskSystemCode) {
ScheduleType scheduleType = ScheduleType.fromCode(scheduleTypeCode);
TaskSystemType taskSystemType = TaskSystemType.fromCode(taskSystemCode);
if (scheduleType == null || taskSystemType == null) {
logger.warn("无效的类型代码 - scheduleTypeCode: {}, taskSystemCode: {}", scheduleTypeCode, taskSystemCode);
return null;
}
return getHandler(scheduleType, taskSystemType);
}
}

View File

@ -0,0 +1,53 @@
package com.zeodao.reminder.strategy;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
import com.zeodao.reminder.service.WechatWebhookService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 文本提醒处理器
* 处理 morning evening 类型的文本提醒
*
* @author Zeodao
* @version 2.0.0
*/
@Component
public class TextReminderHandler implements ReminderHandler {
private static final Logger logger = LoggerFactory.getLogger(TextReminderHandler.class);
@Autowired
private WechatWebhookService wechatWebhookService;
@Override
public boolean supports(ScheduleType scheduleType, TaskSystemType taskSystemType) {
// 支持所有任务系统的文本提醒类型
return scheduleType.isTextReminder();
}
@Override
public void handleReminder(TaskReminderConfig.Group group, ScheduleType scheduleType, TaskReminderConfig.Schedule schedule) {
logger.info("处理文本提醒 - 群组: {}, 类型: {}", group.getName(), scheduleType.getDescription());
// 生成带格式的提醒消息
String message = wechatWebhookService.createTaskReminderMessage(
group.getId(),
schedule.getMessage(),
scheduleType.getDescription()
);
// 发送消息
boolean success = wechatWebhookService.sendMarkdownMessage(group.getId(), message);
if (success) {
logger.info("文本提醒发送成功 - 群组: {}, 类型: {}", group.getName(), scheduleType.getDescription());
} else {
logger.error("文本提醒发送失败 - 群组: {}, 类型: {}", group.getName(), scheduleType.getDescription());
}
}
}

View File

@ -0,0 +1,45 @@
package com.zeodao.reminder.strategy;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
import com.zeodao.reminder.service.ZentaoTaskReminderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 禅道逾期任务提醒处理器
* 处理禅道系统的 overdue-reminder 类型提醒
*
* @author Zeodao
* @version 2.0.0
*/
@Component
public class ZentaoOverdueReminderHandler implements ReminderHandler {
private static final Logger logger = LoggerFactory.getLogger(ZentaoOverdueReminderHandler.class);
@Autowired
private ZentaoTaskReminderService zentaoTaskReminderService;
@Override
public boolean supports(ScheduleType scheduleType, TaskSystemType taskSystemType) {
// 只支持禅道系统的逾期提醒
return scheduleType.isOverdueReminder() && taskSystemType == TaskSystemType.ZENTAO;
}
@Override
public void handleReminder(TaskReminderConfig.Group group, ScheduleType scheduleType, TaskReminderConfig.Schedule schedule) {
logger.info("处理禅道逾期任务提醒 - 群组: {}", group.getName());
try {
// 发送禅道任务状态统计提醒包含任务和BUG详情
zentaoTaskReminderService.sendTaskReminder(group);
logger.info("禅道逾期任务提醒发送成功 - 群组: {}", group.getName());
} catch (Exception e) {
logger.error("禅道逾期任务提醒发送失败 - 群组: {}", group.getName(), e);
}
}
}

View File

@ -31,7 +31,6 @@ public class HolidayUtil {
}
/**
* 初始化2025年节假日
* 根据国务院办公厅2024年11月12日发布的关于2025年部分节假日安排的通知

View File

@ -1,5 +1,5 @@
server:
port: 8080
port: 8081
spring:
application:
@ -15,34 +15,53 @@ task:
# 群组配置列表
groups:
# 禅道团队群
- id: "zentao-team"
name: "禅道开发团队"
- id: "chainhub"
name: "一站式平台"
webhook:
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=614b110b-8957-4be8-95b9-4eca84c15028"
task-system: "zentao" # 任务管理系统类型zentao, smartsheet, jira, trello 等
# 禅道配置
zentao:
api-url: "https://zentao.iscmtech.com"
username: "admin" # 请替换为实际的禅道用户名
password: "Lianyu!@#~123456" # 请替换为实际的禅道密码
project-id: 38 # 项目ID同时用于获取任务和BUG
kanban-id: 39 # 看板ID看板模式项目需要
# 用户映射:禅道用户名 -> 企业微信手机号(显示使用真实姓名)
user-mapping:
"dengqichen": "18525522818"
"songwei": "15724574541"
# 也可以直接用用户名映射
# "zhangsan": "13600136000"
schedules:
morning:
time: "0 0 9 * * MON-FRI" # 工作日早上9点
message: "早上好!新的一天开始了,请大家及时登录禅道系统刷新今日的任务状态,确保任务进度准确反映当前工作情况。"
enabled: true # 启用早上提醒
evening:
time: "0 30 17 * * MON-FRI" # 工作日下午5:30
message: "下班前提醒:请大家登录禅道系统,及时更新今日任务的完成状态和进度,为明天的工作安排做好准备!"
enabled: true
enabled: true # 启用晚上提醒
overdue-reminder:
time: "0 30 17 * * MON-FRI" # 工作日上午10点
message: "禅道逾期任务提醒" # 这个消息会被系统动态生成包含具体的任务和BUG统计信息
enabled: true # 启用逾期提醒
# 智能表格团队群
- id: "smartsheet-team"
name: "智能表格团队"
- id: "longi-scp"
name: "隆基需求计划"
webhook:
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ed54908b-0f58-46b7-beb6-8facca4438d6"
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=614b110b-8957-4be8-95b9-4eca84c15028"
task-system: "smartsheet"
schedules:
morning:
time: "0 50 8 * * MON-FRI" # 工作日早上8点50分
message: "早上好!请大家及时更新智能表格中的任务状态,确保项目进度信息准确无误。"
enabled: true # 启用早上提醒
evening:
time: "0 20 17 * * MON-FRI" # 工作日下午5点20分
message: "下班前提醒:请在智能表格中更新今日工作完成情况,为明天做好准备!"
enabled: true
enabled: true # 禁用晚上提醒(示例:智能表格团队不需要晚上提醒)
# 日志配置
logging:

View File

@ -0,0 +1,77 @@
server:
port: 8080
spring:
application:
name: zeodao-task-reminder
# 多群任务提醒配置
task:
reminder:
# 全局默认配置
global:
timeout: 5000 # 超时时间(毫秒)
# 群组配置列表
groups:
# 禅道团队群
- id: "chainhub"
name: "一站式平台"
webhook:
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=6c33fed6-c883-493d-a1c3-a7a23b9b4175"
task-system: "zentao" # 任务管理系统类型zentao, smartsheet, jira, trello 等
# 禅道配置
zentao:
api-url: "https://zentao.iscmtech.com"
username: "admin" # 请替换为实际的禅道用户名
password: "Lianyu!@#~123456" # 请替换为实际的禅道密码
project-id: 38 # 项目ID同时用于获取任务和BUG
kanban-id: 39 # 看板ID看板模式项目需要
# 用户映射:禅道用户名 -> 企业微信手机号(显示使用真实姓名)
user-mapping:
"dengqichen": "18525522818"
"songwei": "15724574541"
# 也可以直接用用户名映射
# "zhangsan": "13600136000"
schedules:
morning:
time: "0 0 9 * * MON-FRI" # 工作日早上9点
message: "早上好!新的一天开始了,请大家及时登录禅道系统刷新今日的任务状态,确保任务进度准确反映当前工作情况。"
enabled: true # 启用早上提醒
evening:
time: "0 50 17 * * MON-FRI" # 工作日下午5:30
message: "下班前提醒:请大家登录禅道系统,及时更新今日任务的完成状态和进度,为明天的工作安排做好准备!"
enabled: true # 启用晚上提醒
overdue-reminder:
time: "0 30 17 * * MON-FRI" # 工作日上午10点
message: "禅道逾期任务提醒" # 这个消息会被系统动态生成包含具体的任务和BUG统计信息
enabled: true # 启用逾期提醒
# 智能表格团队群
- id: "longi-scp"
name: "隆基需求计划"
webhook:
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=ed54908b-0f58-46b7-beb6-8facca4438d6"
task-system: "smartsheet"
schedules:
morning:
time: "0 50 8 * * MON-FRI" # 工作日早上8点50分
message: "早上好!请大家及时更新智能表格中的任务状态,确保项目进度信息准确无误。"
enabled: true # 启用早上提醒
evening:
time: "0 20 17 * * MON-FRI" # 工作日下午5点20分
message: "下班前提醒:请在智能表格中更新今日工作完成情况,为明天做好准备!"
enabled: true # 禁用晚上提醒(示例:智能表格团队不需要晚上提醒)
# 日志配置
logging:
level:
com.zeodao.reminder: DEBUG
org.springframework.web: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/task-reminder.log
max-size: 10MB
max-history: 30

View File

@ -1,6 +1,10 @@
package com.zeodao.reminder.service;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
import com.zeodao.reminder.strategy.ReminderHandler;
import com.zeodao.reminder.strategy.ReminderHandlerFactory;
import com.zeodao.reminder.util.HolidayUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -37,6 +41,15 @@ class TaskReminderServiceTest {
@Mock
private TaskReminderConfig taskReminderConfig;
@Mock
private EnabledCheckService enabledCheckService;
@Mock
private ReminderHandlerFactory reminderHandlerFactory;
@Mock
private ReminderHandler reminderHandler;
@InjectMocks
private TaskReminderService taskReminderService;
@ -48,7 +61,8 @@ class TaskReminderServiceTest {
testGroup = new TaskReminderConfig.Group();
testGroup.setId("test-group");
testGroup.setName("测试群组");
testGroup.setEnabled(true);
testGroup.setTaskSystem(TaskSystemType.ZENTAO);
TaskReminderConfig.Webhook webhook = new TaskReminderConfig.Webhook();
webhook.setUrl("https://test.webhook.url");
@ -59,11 +73,13 @@ class TaskReminderServiceTest {
TaskReminderConfig.Schedule morningSchedule = new TaskReminderConfig.Schedule();
morningSchedule.setTime("0 0 9 * * MON-FRI");
morningSchedule.setMessage("测试早上消息");
morningSchedule.setEnabled(true);
schedules.put("morning", morningSchedule);
TaskReminderConfig.Schedule eveningSchedule = new TaskReminderConfig.Schedule();
eveningSchedule.setTime("0 30 17 * * MON-FRI");
eveningSchedule.setMessage("测试晚上消息");
eveningSchedule.setEnabled(true);
schedules.put("evening", eveningSchedule);
testGroup.setSchedules(schedules);
@ -75,14 +91,14 @@ class TaskReminderServiceTest {
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(false);
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(false);
when(taskReminderConfig.getGroupById("test-group")).thenReturn(testGroup);
when(wechatWebhookService.createTaskReminderMessage(anyString(), anyString(), anyString())).thenReturn("测试消息");
when(wechatWebhookService.sendMarkdownMessage(anyString(), anyString())).thenReturn(true);
when(enabledCheckService.isReminderEnabled(testGroup, "morning")).thenReturn(true);
when(reminderHandlerFactory.getHandler(ScheduleType.MORNING, TaskSystemType.ZENTAO)).thenReturn(reminderHandler);
// 执行测试
taskReminderService.sendReminder("test-group", "morning");
taskReminderService.sendReminder("test-group", ScheduleType.MORNING);
// 验证
verify(wechatWebhookService, times(1)).sendMarkdownMessage(eq("test-group"), anyString());
verify(reminderHandler, times(1)).handleReminder(eq(testGroup), eq(ScheduleType.MORNING), any(TaskReminderConfig.Schedule.class));
}
@Test
@ -91,7 +107,7 @@ class TaskReminderServiceTest {
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(true);
// 执行测试
taskReminderService.sendReminder("test-group", "morning");
taskReminderService.sendReminder("test-group", ScheduleType.MORNING);
// 验证不发送消息
verify(wechatWebhookService, never()).sendMarkdownMessage(anyString(), anyString());
@ -104,7 +120,7 @@ class TaskReminderServiceTest {
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(true);
// 执行测试
taskReminderService.sendReminder("test-group", "morning");
taskReminderService.sendReminder("test-group", ScheduleType.MORNING);
// 验证不发送消息
verify(wechatWebhookService, never()).sendMarkdownMessage(anyString(), anyString());
@ -118,16 +134,17 @@ class TaskReminderServiceTest {
// 模拟工作日
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(false);
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(false);
when(taskReminderConfig.getEnabledGroups()).thenReturn(Arrays.asList(testGroup));
when(taskReminderConfig.getGroups()).thenReturn(Arrays.asList(testGroup));
when(taskReminderConfig.getGroupById("test-group")).thenReturn(testGroup);
when(wechatWebhookService.createTaskReminderMessage(anyString(), anyString(), anyString())).thenReturn("测试消息");
when(wechatWebhookService.sendMarkdownMessage(anyString(), anyString())).thenReturn(true);
when(enabledCheckService.isReminderEnabled(testGroup, ScheduleType.MORNING)).thenReturn(true);
when(enabledCheckService.isReminderEnabled(testGroup, "morning")).thenReturn(true);
when(reminderHandlerFactory.getHandler(ScheduleType.MORNING, TaskSystemType.ZENTAO)).thenReturn(reminderHandler);
// 执行测试
taskReminderService.sendAllMorningReminders();
// 验证
verify(wechatWebhookService, times(1)).sendMarkdownMessage(eq("test-group"), anyString());
verify(reminderHandler, times(1)).handleReminder(eq(testGroup), eq(ScheduleType.MORNING), any(TaskReminderConfig.Schedule.class));
}
@Test
@ -135,16 +152,17 @@ class TaskReminderServiceTest {
// 模拟工作日
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(false);
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(false);
when(taskReminderConfig.getEnabledGroups()).thenReturn(Arrays.asList(testGroup));
when(taskReminderConfig.getGroups()).thenReturn(Arrays.asList(testGroup));
when(taskReminderConfig.getGroupById("test-group")).thenReturn(testGroup);
when(wechatWebhookService.createTaskReminderMessage(anyString(), anyString(), anyString())).thenReturn("测试消息");
when(wechatWebhookService.sendMarkdownMessage(anyString(), anyString())).thenReturn(true);
when(enabledCheckService.isReminderEnabled(testGroup, ScheduleType.EVENING)).thenReturn(true);
when(enabledCheckService.isReminderEnabled(testGroup, "evening")).thenReturn(true);
when(reminderHandlerFactory.getHandler(ScheduleType.EVENING, TaskSystemType.ZENTAO)).thenReturn(reminderHandler);
// 执行测试
taskReminderService.sendAllEveningReminders();
// 验证
verify(wechatWebhookService, times(1)).sendMarkdownMessage(eq("test-group"), anyString());
verify(reminderHandler, times(1)).handleReminder(eq(testGroup), eq(ScheduleType.EVENING), any(TaskReminderConfig.Schedule.class));
}
@Test
@ -153,20 +171,20 @@ class TaskReminderServiceTest {
when(taskReminderConfig.getGroupById("invalid-group")).thenReturn(null);
// 执行测试
taskReminderService.sendReminder("invalid-group", "morning");
taskReminderService.sendReminder("invalid-group", ScheduleType.MORNING);
// 验证不发送消息
verify(wechatWebhookService, never()).sendMarkdownMessage(anyString(), anyString());
}
@Test
void testSendReminderWithDisabledGroup() {
// 创建禁用的群组
testGroup.setEnabled(false);
void testSendReminderWithDisabledReminder() {
// 模拟提醒被禁用
when(taskReminderConfig.getGroupById("test-group")).thenReturn(testGroup);
when(enabledCheckService.isReminderEnabled(testGroup, "morning")).thenReturn(false);
// 执行测试
taskReminderService.sendReminder("test-group", "morning");
taskReminderService.sendReminder("test-group", ScheduleType.MORNING);
// 验证不发送消息
verify(wechatWebhookService, never()).sendMarkdownMessage(anyString(), anyString());

View File

@ -0,0 +1,166 @@
package com.zeodao.reminder.service;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.enums.TaskSystemType;
import com.zeodao.reminder.model.ZentaoBug;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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
*/
public class ZentaoBugReminderTest {
private TaskReminderConfig.Group testGroup;
@BeforeEach
void setUp() {
// 创建测试群组配置
testGroup = new TaskReminderConfig.Group();
testGroup.setId("test-group");
testGroup.setName("测试群组");
testGroup.setTaskSystem(TaskSystemType.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());
}
}

View File

@ -0,0 +1,65 @@
package com.zeodao.reminder.strategy;
import com.zeodao.reminder.enums.ScheduleType;
import com.zeodao.reminder.enums.TaskSystemType;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
/**
* 提醒处理器工厂测试
*
* @author Zeodao
* @version 2.0.0
*/
@SpringBootTest
public class ReminderHandlerFactoryTest {
@Autowired
private ReminderHandlerFactory reminderHandlerFactory;
@Test
public void testGetTextReminderHandler() {
// 测试文本提醒处理器
ReminderHandler handler = reminderHandlerFactory.getHandler(ScheduleType.MORNING, TaskSystemType.ZENTAO);
assertNotNull(handler);
assertTrue(handler instanceof TextReminderHandler);
handler = reminderHandlerFactory.getHandler(ScheduleType.EVENING, TaskSystemType.SMARTSHEET);
assertNotNull(handler);
assertTrue(handler instanceof TextReminderHandler);
}
@Test
public void testGetZentaoOverdueReminderHandler() {
// 测试禅道逾期提醒处理器
ReminderHandler handler = reminderHandlerFactory.getHandler(ScheduleType.OVERDUE_REMINDER, TaskSystemType.ZENTAO);
assertNotNull(handler);
assertTrue(handler instanceof ZentaoOverdueReminderHandler);
}
@Test
public void testUnsupportedCombination() {
// 测试不支持的组合
ReminderHandler handler = reminderHandlerFactory.getHandler(ScheduleType.OVERDUE_REMINDER, TaskSystemType.SMARTSHEET);
assertNull(handler);
}
@Test
public void testStringParameterVersion() {
// 测试字符串参数版本
ReminderHandler handler = reminderHandlerFactory.getHandler("morning", "zentao");
assertNotNull(handler);
assertTrue(handler instanceof TextReminderHandler);
handler = reminderHandlerFactory.getHandler("overdue-reminder", "zentao");
assertNotNull(handler);
assertTrue(handler instanceof ZentaoOverdueReminderHandler);
// 测试无效代码
handler = reminderHandlerFactory.getHandler("unknown-type", "zentao");
assertNull(handler);
}
}

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

@ -0,0 +1,130 @@
# 禅道统一项目状态提醒功能测试指南
## 功能概述
已成功实现禅道统一项目状态提醒功能,现在系统支持:
1. **统一项目状态提醒** - 一个通知包含任务和BUG推荐
2. **单独任务提醒** - 仅任务通知(兼容性)
3. **单独BUG提醒** - 仅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处理时间统计