Compare commits
17 Commits
release/1.
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
766bf2a4e2 | ||
|
|
73b1085925 | ||
|
|
38cbdf6c04 | ||
|
|
5d48ce35ce | ||
|
|
1975ff1ffc | ||
|
|
4ca28f8657 | ||
|
|
65e9dc7b16 | ||
|
|
c5233719cb | ||
|
|
89ccf9d555 | ||
|
|
48333f5b4a | ||
|
|
b3d98ce025 | ||
|
|
55d8ad26c0 | ||
|
|
b5b50bf0cb | ||
|
|
25f6988e65 | ||
|
|
405b481c9e | ||
|
|
42b9651392 | ||
|
|
d71708bb65 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
179
ZENTAO_INTEGRATION.md
Normal 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` - 禅道用户实体类
|
||||
@ -13,7 +13,7 @@ services:
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- /opt/task-reminder/logs:/app/logs
|
||||
networks:
|
||||
default:
|
||||
external: true
|
||||
80
pom.xml
80
pom.xml
@ -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>
|
||||
@ -80,6 +88,66 @@
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>docker-stop</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>docker</executable>
|
||||
<arguments>
|
||||
<argument>-H</argument>
|
||||
<argument>tcp://172.22.222.6:2375</argument>
|
||||
<argument>stop</argument>
|
||||
<argument>task-reminder-app</argument>
|
||||
</arguments>
|
||||
<successCodes>
|
||||
<successCode>0</successCode>
|
||||
<successCode>1</successCode>
|
||||
</successCodes>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>docker-remove</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>docker</executable>
|
||||
<arguments>
|
||||
<argument>-H</argument>
|
||||
<argument>tcp://172.22.222.6:2375</argument>
|
||||
<argument>remove</argument>
|
||||
<argument>task-reminder-app</argument>
|
||||
</arguments>
|
||||
<successCodes>
|
||||
<successCode>0</successCode>
|
||||
<successCode>1</successCode>
|
||||
</successCodes>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>docker-rmi</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>docker</executable>
|
||||
<arguments>
|
||||
<argument>-H</argument>
|
||||
<argument>tcp://172.22.222.6:2375</argument>
|
||||
<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镜像 -->
|
||||
<execution>
|
||||
<id>docker-build</id>
|
||||
@ -97,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镜像 -->
|
||||
@ -114,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>
|
||||
@ -131,6 +207,10 @@
|
||||
<argument>up</argument>
|
||||
<argument>-d</argument>
|
||||
</arguments>
|
||||
<successCodes>
|
||||
<successCode>0</successCode>
|
||||
<successCode>1</successCode>
|
||||
</successCodes>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
|
||||
@ -7,7 +7,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* 任务调度器配置
|
||||
*
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 2.0.0
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -10,7 +11,7 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* 多群任务提醒配置类
|
||||
*
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 2.0.0
|
||||
*/
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 String groupId,
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
67
src/main/java/com/zeodao/reminder/enums/ScheduleType.java
Normal file
67
src/main/java/com/zeodao/reminder/enums/ScheduleType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
68
src/main/java/com/zeodao/reminder/enums/TaskSystemType.java
Normal file
68
src/main/java/com/zeodao/reminder/enums/TaskSystemType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
51
src/main/java/com/zeodao/reminder/model/ProjectInfo.java
Normal file
51
src/main/java/com/zeodao/reminder/model/ProjectInfo.java
Normal 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 "未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
244
src/main/java/com/zeodao/reminder/model/ZentaoBug.java
Normal file
244
src/main/java/com/zeodao/reminder/model/ZentaoBug.java
Normal 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 : "未知";
|
||||
}
|
||||
}
|
||||
}
|
||||
197
src/main/java/com/zeodao/reminder/model/ZentaoTask.java
Normal file
197
src/main/java/com/zeodao/reminder/model/ZentaoTask.java
Normal 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 "普通";
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/main/java/com/zeodao/reminder/model/ZentaoUser.java
Normal file
81
src/main/java/com/zeodao/reminder/model/ZentaoUser.java
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
@ -18,7 +20,7 @@ import java.util.concurrent.ScheduledFuture;
|
||||
/**
|
||||
* 动态任务调度器
|
||||
* 根据配置文件动态创建定时任务
|
||||
*
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 2.0.0
|
||||
*/
|
||||
@ -33,6 +35,9 @@ public class DynamicTaskScheduler {
|
||||
@Autowired
|
||||
private TaskReminderService taskReminderService;
|
||||
|
||||
@Autowired
|
||||
private EnabledCheckService enabledCheckService;
|
||||
|
||||
@Autowired
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
@ -41,11 +46,11 @@ public class DynamicTaskScheduler {
|
||||
@PostConstruct
|
||||
public void initScheduledTasks() {
|
||||
logger.info("=== 初始化动态定时任务 ===");
|
||||
|
||||
for (TaskReminderConfig.Group group : taskReminderConfig.getEnabledGroups()) {
|
||||
|
||||
for (TaskReminderConfig.Group group : taskReminderConfig.getGroups()) {
|
||||
createSchedulesForGroup(group);
|
||||
}
|
||||
|
||||
|
||||
logger.info("=== 动态定时任务初始化完成,共创建 {} 个任务 ===", scheduledTasks.size());
|
||||
}
|
||||
|
||||
@ -54,32 +59,45 @@ public class DynamicTaskScheduler {
|
||||
*/
|
||||
private void createSchedulesForGroup(TaskReminderConfig.Group group) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,10 +107,10 @@ public class DynamicTaskScheduler {
|
||||
*/
|
||||
public void reloadScheduledTasks() {
|
||||
logger.info("=== 重新加载定时任务 ===");
|
||||
|
||||
|
||||
// 取消所有现有任务
|
||||
cancelAllTasks();
|
||||
|
||||
|
||||
// 重新创建任务
|
||||
initScheduledTasks();
|
||||
}
|
||||
@ -102,17 +120,17 @@ public class DynamicTaskScheduler {
|
||||
*/
|
||||
private void cancelAllTasks() {
|
||||
logger.info("取消所有现有定时任务");
|
||||
|
||||
|
||||
for (Map.Entry<String, ScheduledFuture<?>> entry : scheduledTasks.entrySet()) {
|
||||
String taskKey = entry.getKey();
|
||||
ScheduledFuture<?> future = entry.getValue();
|
||||
|
||||
|
||||
if (future != null && !future.isCancelled()) {
|
||||
future.cancel(false);
|
||||
logger.debug("取消定时任务: {}", taskKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
scheduledTasks.clear();
|
||||
}
|
||||
|
||||
@ -133,28 +151,37 @@ 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");
|
||||
|
||||
|
||||
for (Map.Entry<String, TaskReminderConfig.Schedule> entry : group.getSchedules().entrySet()) {
|
||||
String scheduleType = entry.getKey();
|
||||
TaskReminderConfig.Schedule schedule = entry.getValue();
|
||||
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");
|
||||
}
|
||||
info.append("\n");
|
||||
}
|
||||
|
||||
|
||||
return info.toString();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧版本的方法
|
||||
*/
|
||||
@Deprecated
|
||||
public void sendEveningReminder() {
|
||||
sendAllEveningReminders();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -126,48 +136,81 @@ public class WechatWebhookService {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带@人的Markdown消息体
|
||||
*/
|
||||
private Map<String, Object> createMarkdownMessageWithMentions(String content, List<String> mobileList) {
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("msgtype", "markdown");
|
||||
|
||||
Map<String, Object> markdown = new HashMap<>();
|
||||
markdown.put("content", content);
|
||||
message.put("markdown", markdown);
|
||||
|
||||
// 添加@人列表
|
||||
if (mobileList != null && !mobileList.isEmpty()) {
|
||||
message.put("mentioned_mobile_list", mobileList);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送Markdown消息到指定URL
|
||||
*/
|
||||
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) throws IOException {
|
||||
int timeout = taskReminderConfig.getGlobal().getTimeout();
|
||||
|
||||
private boolean sendMessage(String webhookUrl, Map<String, Object> messageBody) {
|
||||
logger.info("准备发送消息到企业微信,URL: {}", webhookUrl);
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
HttpPost httpPost = new HttpPost(webhookUrl);
|
||||
|
||||
// 设置请求配置
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(timeout)
|
||||
.setSocketTimeout(timeout)
|
||||
.setConnectionRequestTimeout(timeout)
|
||||
.build();
|
||||
httpPost.setConfig(requestConfig);
|
||||
|
||||
try {
|
||||
// 设置请求头
|
||||
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
// 设置请求体
|
||||
String jsonBody = objectMapper.writeValueAsString(messageBody);
|
||||
StringEntity entity = new StringEntity(jsonBody, StandardCharsets.UTF_8);
|
||||
httpPost.setEntity(entity);
|
||||
// 创建请求实体
|
||||
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(messageBody, headers);
|
||||
|
||||
logger.debug("发送消息内容: {}", jsonBody);
|
||||
logger.debug("发送消息内容: {}", objectMapper.writeValueAsString(messageBody));
|
||||
|
||||
// 执行请求
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(webhookUrl, requestEntity, String.class);
|
||||
|
||||
if (statusCode == 200) {
|
||||
logger.info("企业微信消息发送成功,响应: {}", responseBody);
|
||||
return true;
|
||||
} else {
|
||||
logger.error("企业微信消息发送失败,状态码: {}, 响应: {}", statusCode, responseBody);
|
||||
return false;
|
||||
}
|
||||
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" +
|
||||
|
||||
618
src/main/java/com/zeodao/reminder/service/ZentaoApiService.java
Normal file
618
src/main/java/com/zeodao/reminder/service/ZentaoApiService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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(); // 返回空列表
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,7 +31,6 @@ public class HolidayUtil {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 初始化2025年节假日
|
||||
* 根据国务院办公厅2024年11月12日发布的《关于2025年部分节假日安排的通知》
|
||||
|
||||
@ -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:
|
||||
77
src/main/resources/application-prod.yml
Normal file
77
src/main/resources/application-prod.yml
Normal 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
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 节假日工具类测试
|
||||
*
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 2.0.0
|
||||
*/
|
||||
@ -94,7 +94,7 @@ class HolidayUtilTest {
|
||||
// 测试周末
|
||||
LocalDate saturday = LocalDate.of(2025, Month.JANUARY, 4); // 周六
|
||||
LocalDate sunday = LocalDate.of(2025, Month.JANUARY, 5); // 周日
|
||||
|
||||
|
||||
assertTrue(holidayUtil.isWeekend(saturday));
|
||||
assertTrue(holidayUtil.isWeekend(sunday));
|
||||
assertFalse(holidayUtil.isWorkday(saturday));
|
||||
|
||||
130
test-bug-api.md
Normal file
130
test-bug-api.md
Normal 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处理时间统计
|
||||
Loading…
Reference in New Issue
Block a user