first commit
This commit is contained in:
commit
da23b31a1e
71
.gitignore
vendored
Normal file
71
.gitignore
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
.vscode/
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Spring Boot
|
||||
application-local.yml
|
||||
application-dev.yml
|
||||
application-prod.yml
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Application specific
|
||||
logs/
|
||||
*.pid
|
||||
180
README.md
Normal file
180
README.md
Normal file
@ -0,0 +1,180 @@
|
||||
# Zeodao任务提醒系统
|
||||
|
||||
一个基于Spring Boot的企业微信任务提醒系统,专门用于提醒团队成员及时在禅道系统中刷新任务状态。系统会在工作日的早上9点和下午5:30自动发送提醒消息,帮助团队养成良好的任务状态更新习惯。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 自动在工作日早上9:00发送禅道任务状态提醒
|
||||
- ✅ 自动在工作日下午17:30发送任务完成状态提醒
|
||||
- ✅ 自动排除周末和法定节假日
|
||||
- ✅ 支持企业微信Webhook消息推送
|
||||
- ✅ 支持Markdown格式消息,包含操作指引
|
||||
- ✅ 提供REST API进行手动测试和触发
|
||||
- ✅ 完整的日志记录和错误处理
|
||||
- ✅ 可配置的消息内容和时间
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Java 21
|
||||
- Spring Boot 2.7.14
|
||||
- Maven
|
||||
- 企业微信Webhook API
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 环境要求
|
||||
|
||||
- JDK 21 或更高版本
|
||||
- Maven 3.6+
|
||||
|
||||
### 2. 配置企业微信Webhook
|
||||
|
||||
在 `src/main/resources/application.yml` 中配置您的企业微信Webhook地址:
|
||||
|
||||
```yaml
|
||||
wechat:
|
||||
webhook:
|
||||
url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_WEBHOOK_KEY
|
||||
```
|
||||
|
||||
### 3. 编译和运行
|
||||
|
||||
```bash
|
||||
# 编译项目
|
||||
mvn clean compile
|
||||
|
||||
# 运行项目
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
或者打包后运行:
|
||||
|
||||
```bash
|
||||
# 打包
|
||||
mvn clean package
|
||||
|
||||
# 运行jar包
|
||||
java -jar target/task-reminder-1.0.0.jar
|
||||
```
|
||||
|
||||
### 4. 验证系统
|
||||
|
||||
系统启动后,可以通过以下API进行测试:
|
||||
|
||||
```bash
|
||||
# 健康检查
|
||||
curl http://localhost:8080/api/reminder/health
|
||||
|
||||
# 发送测试消息
|
||||
curl -X POST http://localhost:8080/api/reminder/test
|
||||
|
||||
# 手动触发早上提醒
|
||||
curl -X POST http://localhost:8080/api/reminder/morning
|
||||
|
||||
# 手动触发晚上提醒
|
||||
curl -X POST http://localhost:8080/api/reminder/evening
|
||||
|
||||
# 获取提醒信息
|
||||
curl http://localhost:8080/api/reminder/info
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 时间配置
|
||||
|
||||
在 `application.yml` 中可以自定义提醒时间:
|
||||
|
||||
```yaml
|
||||
task:
|
||||
reminder:
|
||||
morning:
|
||||
time: "0 0 9 * * MON-FRI" # Cron表达式:工作日早上9点
|
||||
message: "早上好!请及时更新您的任务状态,开始新的一天工作!"
|
||||
evening:
|
||||
time: "0 30 17 * * MON-FRI" # Cron表达式:工作日下午5:30
|
||||
message: "下班前提醒:请更新今日任务完成状态,为明天做好准备!"
|
||||
```
|
||||
|
||||
### 节假日配置
|
||||
|
||||
系统内置了2024年和2025年的法定节假日,可以在 `HolidayUtil.java` 中修改或添加自定义节假日。
|
||||
|
||||
## API文档
|
||||
|
||||
### 健康检查
|
||||
- **URL**: `GET /api/reminder/health`
|
||||
- **描述**: 检查系统运行状态
|
||||
|
||||
### 发送测试消息
|
||||
- **URL**: `POST /api/reminder/test`
|
||||
- **描述**: 发送一条测试消息到企业微信群
|
||||
|
||||
### 手动触发早上提醒
|
||||
- **URL**: `POST /api/reminder/morning`
|
||||
- **描述**: 手动触发早上任务提醒
|
||||
|
||||
### 手动触发晚上提醒
|
||||
- **URL**: `POST /api/reminder/evening`
|
||||
- **描述**: 手动触发晚上任务提醒
|
||||
|
||||
### 获取提醒信息
|
||||
- **URL**: `GET /api/reminder/info`
|
||||
- **描述**: 获取下次提醒时间和系统状态信息
|
||||
|
||||
## 日志
|
||||
|
||||
系统会在 `logs/task-reminder.log` 文件中记录详细的运行日志,包括:
|
||||
- 定时任务执行记录
|
||||
- 消息发送状态
|
||||
- 错误信息
|
||||
- 系统健康检查
|
||||
|
||||
## 部署建议
|
||||
|
||||
### 生产环境部署
|
||||
|
||||
1. 修改 `application.yml` 中的日志级别为 `INFO`
|
||||
2. 配置合适的JVM参数
|
||||
3. 使用systemd或其他进程管理工具管理应用
|
||||
4. 配置日志轮转策略
|
||||
|
||||
### Docker部署
|
||||
|
||||
可以创建Dockerfile进行容器化部署:
|
||||
|
||||
```dockerfile
|
||||
FROM openjdk:21-jre-slim
|
||||
COPY target/task-reminder-1.0.0.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "/app.jar"]
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **消息发送失败**
|
||||
- 检查企业微信Webhook地址是否正确
|
||||
- 检查网络连接是否正常
|
||||
- 查看日志文件中的错误信息
|
||||
|
||||
2. **定时任务不执行**
|
||||
- 检查Cron表达式是否正确
|
||||
- 确认当前时间是否为工作日
|
||||
- 查看系统日志确认定时任务是否启动
|
||||
|
||||
3. **节假日判断错误**
|
||||
- 检查 `HolidayUtil.java` 中的节假日配置
|
||||
- 确认系统时间是否正确
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request来改进这个项目。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请联系开发团队。
|
||||
78
pom.xml
Normal file
78
pom.xml
Normal file
@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.zeodao</groupId>
|
||||
<artifactId>task-reminder</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Zeodao Task Reminder</name>
|
||||
<description>企业微信任务提醒系统</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.14</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starter Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Configuration Processor -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP Client -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,24 @@
|
||||
package com.zeodao.reminder;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* Zeodao任务提醒系统主启动类
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class ZeodaoTaskReminderApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ZeodaoTaskReminderApplication.class, args);
|
||||
System.out.println("=================================");
|
||||
System.out.println("Zeodao任务提醒系统启动成功!");
|
||||
System.out.println("系统将在工作日早上9:00和下午17:30发送任务提醒");
|
||||
System.out.println("=================================");
|
||||
}
|
||||
}
|
||||
160
src/main/java/com/zeodao/reminder/config/WechatConfig.java
Normal file
160
src/main/java/com/zeodao/reminder/config/WechatConfig.java
Normal file
@ -0,0 +1,160 @@
|
||||
package com.zeodao.reminder.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 多群任务提醒配置类
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 2.0.0
|
||||
*/
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "task.reminder")
|
||||
public class TaskReminderConfig {
|
||||
|
||||
private Global global = new Global();
|
||||
private List<Group> groups = new ArrayList<>();
|
||||
|
||||
public Global getGlobal() {
|
||||
return global;
|
||||
}
|
||||
|
||||
public void setGlobal(Global global) {
|
||||
this.global = global;
|
||||
}
|
||||
|
||||
public List<Group> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void setGroups(List<Group> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据群组ID获取群组配置
|
||||
*/
|
||||
public Group getGroupById(String groupId) {
|
||||
return groups.stream()
|
||||
.filter(group -> group.getId().equals(groupId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的群组
|
||||
*/
|
||||
public List<Group> getEnabledGroups() {
|
||||
return groups.stream()
|
||||
.filter(Group::isEnabled)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static class Global {
|
||||
private int timeout = 5000;
|
||||
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Group {
|
||||
private String id;
|
||||
private String name;
|
||||
private Webhook webhook = new Webhook();
|
||||
private String taskSystem;
|
||||
private Map<String, Schedule> schedules = new HashMap<>();
|
||||
private boolean enabled = true;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Webhook getWebhook() {
|
||||
return webhook;
|
||||
}
|
||||
|
||||
public void setWebhook(Webhook webhook) {
|
||||
this.webhook = webhook;
|
||||
}
|
||||
|
||||
public String getTaskSystem() {
|
||||
return taskSystem;
|
||||
}
|
||||
|
||||
public void setTaskSystem(String taskSystem) {
|
||||
this.taskSystem = taskSystem;
|
||||
}
|
||||
|
||||
public Map<String, Schedule> getSchedules() {
|
||||
return schedules;
|
||||
}
|
||||
|
||||
public void setSchedules(Map<String, Schedule> schedules) {
|
||||
this.schedules = schedules;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Webhook {
|
||||
private String url;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Schedule {
|
||||
private String time;
|
||||
private String message;
|
||||
|
||||
public String getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(String time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
package com.zeodao.reminder.controller;
|
||||
|
||||
import com.zeodao.reminder.service.TaskReminderService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 任务提醒控制器
|
||||
* 提供手动触发提醒和系统状态查询的API
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/reminder")
|
||||
public class TaskReminderController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskReminderController.class);
|
||||
|
||||
@Autowired
|
||||
private TaskReminderService taskReminderService;
|
||||
|
||||
/**
|
||||
* 系统健康检查
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<Map<String, Object>> health() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("status", "UP");
|
||||
response.put("service", "Zeodao Task Reminder");
|
||||
response.put("version", "1.0.0");
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下次提醒信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public ResponseEntity<Map<String, Object>> getReminderInfo() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
String info = taskReminderService.getNextReminderInfo();
|
||||
response.put("success", true);
|
||||
response.put("info", info);
|
||||
response.put("message", "获取提醒信息成功");
|
||||
} catch (Exception e) {
|
||||
logger.error("获取提醒信息失败", e);
|
||||
response.put("success", false);
|
||||
response.put("message", "获取提醒信息失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发早上提醒
|
||||
*/
|
||||
@PostMapping("/morning")
|
||||
public ResponseEntity<Map<String, Object>> triggerMorningReminder() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
taskReminderService.sendMorningReminder();
|
||||
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("/evening")
|
||||
public ResponseEntity<Map<String, Object>> triggerEveningReminder() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
try {
|
||||
taskReminderService.sendEveningReminder();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package com.zeodao.reminder.scheduler;
|
||||
|
||||
import com.zeodao.reminder.service.TaskReminderService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 任务提醒定时调度器
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TaskReminderScheduler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskReminderScheduler.class);
|
||||
|
||||
@Autowired
|
||||
private TaskReminderService taskReminderService;
|
||||
|
||||
/**
|
||||
* 早上9点任务提醒
|
||||
* Cron表达式:0 0 9 * * MON-FRI
|
||||
* 含义:每个工作日(周一到周五)的早上9点0分0秒执行
|
||||
*/
|
||||
@Scheduled(cron = "${task.reminder.morning.time}")
|
||||
public void morningReminder() {
|
||||
logger.info("=== 早上任务提醒定时任务开始执行 ===");
|
||||
try {
|
||||
taskReminderService.sendMorningReminder();
|
||||
} catch (Exception e) {
|
||||
logger.error("早上任务提醒执行失败", e);
|
||||
}
|
||||
logger.info("=== 早上任务提醒定时任务执行完成 ===");
|
||||
}
|
||||
|
||||
/**
|
||||
* 下午5:30任务提醒
|
||||
* Cron表达式:0 30 17 * * MON-FRI
|
||||
* 含义:每个工作日(周一到周五)的下午5点30分0秒执行
|
||||
*/
|
||||
@Scheduled(cron = "${task.reminder.evening.time}")
|
||||
public void eveningReminder() {
|
||||
logger.info("=== 晚上任务提醒定时任务开始执行 ===");
|
||||
try {
|
||||
taskReminderService.sendEveningReminder();
|
||||
} catch (Exception e) {
|
||||
logger.error("晚上任务提醒执行失败", e);
|
||||
}
|
||||
logger.info("=== 晚上任务提醒定时任务执行完成 ===");
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统健康检查(每小时执行一次)
|
||||
* 用于记录系统运行状态
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * *")
|
||||
public void healthCheck() {
|
||||
logger.debug("系统健康检查 - 任务提醒系统运行正常");
|
||||
}
|
||||
|
||||
/**
|
||||
* 每天早上8点记录当天的提醒计划
|
||||
*/
|
||||
@Scheduled(cron = "0 0 8 * * *")
|
||||
public void logDailyPlan() {
|
||||
logger.info("=== 今日提醒计划 ===");
|
||||
String info = taskReminderService.getNextReminderInfo();
|
||||
logger.info(info);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
package com.zeodao.reminder.service;
|
||||
|
||||
import com.zeodao.reminder.config.TaskReminderConfig;
|
||||
import com.zeodao.reminder.util.HolidayUtil;
|
||||
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.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 任务提醒服务
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Service
|
||||
public class TaskReminderService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TaskReminderService.class);
|
||||
|
||||
@Autowired
|
||||
private WechatWebhookService wechatWebhookService;
|
||||
|
||||
@Autowired
|
||||
private HolidayUtil holidayUtil;
|
||||
|
||||
@Value("${task.reminder.morning.message}")
|
||||
private String morningMessage;
|
||||
|
||||
@Value("${task.reminder.evening.message}")
|
||||
private String eveningMessage;
|
||||
|
||||
/**
|
||||
* 发送早上任务提醒
|
||||
*/
|
||||
public void sendMorningReminder() {
|
||||
if (!shouldSendReminder()) {
|
||||
logger.info("今天是节假日或周末,跳过早上任务提醒");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("开始发送早上任务提醒");
|
||||
|
||||
String message = wechatWebhookService.createTaskReminderMessage(morningMessage, "早上提醒");
|
||||
boolean success = wechatWebhookService.sendMarkdownMessage(message);
|
||||
|
||||
if (success) {
|
||||
logger.info("早上任务提醒发送成功");
|
||||
} else {
|
||||
logger.error("早上任务提醒发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送晚上任务提醒
|
||||
*/
|
||||
public void sendEveningReminder() {
|
||||
if (!shouldSendReminder()) {
|
||||
logger.info("今天是节假日或周末,跳过晚上任务提醒");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("开始发送晚上任务提醒");
|
||||
|
||||
String message = wechatWebhookService.createTaskReminderMessage(eveningMessage, "下班提醒");
|
||||
boolean success = wechatWebhookService.sendMarkdownMessage(message);
|
||||
|
||||
if (success) {
|
||||
logger.info("晚上任务提醒发送成功");
|
||||
} else {
|
||||
logger.error("晚上任务提醒发送失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该发送提醒
|
||||
* 排除周末和节假日
|
||||
*/
|
||||
private boolean shouldSendReminder() {
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// 检查是否是周末
|
||||
if (holidayUtil.isWeekend(today)) {
|
||||
logger.debug("今天是周末,不发送提醒");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是节假日
|
||||
if (holidayUtil.isHoliday(today)) {
|
||||
logger.debug("今天是节假日,不发送提醒");
|
||||
return false;
|
||||
}
|
||||
|
||||
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,298 @@
|
||||
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 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.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 企业微信Webhook服务
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Service
|
||||
public class WechatWebhookService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WechatWebhookService.class);
|
||||
|
||||
@Autowired
|
||||
private TaskReminderConfig taskReminderConfig;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 发送文本消息到指定群组
|
||||
*
|
||||
* @param groupId 群组ID
|
||||
* @param message 消息内容
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendTextMessage(String groupId, String message) {
|
||||
try {
|
||||
TaskReminderConfig.Group group = taskReminderConfig.getGroupById(groupId);
|
||||
if (group == null) {
|
||||
logger.error("未找到群组配置: {}", groupId);
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<String, Object> messageBody = createTextMessage(message);
|
||||
return sendMessage(group.getWebhook().getUrl(), messageBody);
|
||||
} catch (Exception e) {
|
||||
logger.error("发送企业微信消息失败, 群组: {}", groupId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送Markdown格式消息到指定群组
|
||||
*
|
||||
* @param groupId 群组ID
|
||||
* @param content Markdown内容
|
||||
* @return 是否发送成功
|
||||
*/
|
||||
public boolean sendMarkdownMessage(String groupId, String content) {
|
||||
try {
|
||||
TaskReminderConfig.Group group = taskReminderConfig.getGroupById(groupId);
|
||||
if (group == null) {
|
||||
logger.error("未找到群组配置: {}", groupId);
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<String, Object> messageBody = createMarkdownMessage(content);
|
||||
return sendMessage(group.getWebhook().getUrl(), messageBody);
|
||||
} catch (Exception e) {
|
||||
logger.error("发送企业微信Markdown消息失败, 群组: {}", groupId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧版本的方法 - 发送到第一个启用的群组
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean sendMarkdownMessage(String content) {
|
||||
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
|
||||
if (enabledGroups.isEmpty()) {
|
||||
logger.error("没有启用的群组配置");
|
||||
return false;
|
||||
}
|
||||
|
||||
return sendMarkdownMessage(enabledGroups.get(0).getId(), content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文本消息体
|
||||
*/
|
||||
private Map<String, Object> createTextMessage(String content) {
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("msgtype", "text");
|
||||
|
||||
Map<String, Object> text = new HashMap<>();
|
||||
text.put("content", content);
|
||||
message.put("text", text);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Markdown消息体
|
||||
*/
|
||||
private Map<String, Object> createMarkdownMessage(String content) {
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("msgtype", "markdown");
|
||||
|
||||
Map<String, Object> markdown = new HashMap<>();
|
||||
markdown.put("content", content);
|
||||
message.put("markdown", markdown);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到企业微信
|
||||
*/
|
||||
private boolean sendMessage(String webhookUrl, Map<String, Object> messageBody) throws IOException {
|
||||
int timeout = taskReminderConfig.getGlobal().getTimeout();
|
||||
|
||||
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);
|
||||
|
||||
// 设置请求头
|
||||
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
// 设置请求体
|
||||
String jsonBody = objectMapper.writeValueAsString(messageBody);
|
||||
StringEntity entity = new StringEntity(jsonBody, StandardCharsets.UTF_8);
|
||||
httpPost.setEntity(entity);
|
||||
|
||||
logger.debug("发送消息内容: {}", jsonBody);
|
||||
|
||||
// 执行请求
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
|
||||
if (statusCode == 200) {
|
||||
logger.info("企业微信消息发送成功,响应: {}", responseBody);
|
||||
return true;
|
||||
} else {
|
||||
logger.error("企业微信消息发送失败,状态码: {}, 响应: {}", statusCode, responseBody);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带时间戳的任务提醒消息
|
||||
*/
|
||||
public String createTaskReminderMessage(String groupId, String baseMessage, String timeType) {
|
||||
TaskReminderConfig.Group group = taskReminderConfig.getGroupById(groupId);
|
||||
if (group == null) {
|
||||
logger.error("未找到群组配置: {}", groupId);
|
||||
return baseMessage;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String timestamp = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
String dayOfWeek = getDayOfWeekInChinese(now.getDayOfWeek().getValue());
|
||||
|
||||
StringBuilder message = new StringBuilder();
|
||||
|
||||
// 根据任务管理系统类型生成不同的标题和操作指引
|
||||
String systemName = getTaskSystemDisplayName(group.getTaskSystem());
|
||||
String systemIcon = getTaskSystemIcon(group.getTaskSystem());
|
||||
|
||||
message.append("## ").append(systemIcon).append(" ").append(systemName).append("任务状态提醒\n\n");
|
||||
message.append("**⏰ 时间:** ").append(timestamp).append(" (").append(dayOfWeek).append(")\n");
|
||||
message.append("**📝 提醒类型:** ").append(timeType).append("\n");
|
||||
message.append("**👥 群组:** ").append(group.getName()).append("\n\n");
|
||||
message.append("### 📢 重要提醒\n");
|
||||
message.append(baseMessage).append("\n\n");
|
||||
message.append("### 🔗 操作指引\n");
|
||||
message.append(getTaskSystemInstructions(group.getTaskSystem()));
|
||||
message.append("---\n");
|
||||
message.append("💡 **温馨提示:** 及时更新任务状态有助于团队协作和项目管理\n\n");
|
||||
message.append("*Zeodao任务提醒系统 v2.0*");
|
||||
|
||||
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 "📝";
|
||||
default: return "📋";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务管理系统的操作指引
|
||||
*/
|
||||
private String getTaskSystemInstructions(String taskSystem) {
|
||||
switch (taskSystem.toLowerCase()) {
|
||||
case "zentao":
|
||||
return "1. 登录禅道系统\n" +
|
||||
"2. 查看分配给自己的任务\n" +
|
||||
"3. 更新任务状态和进度\n" +
|
||||
"4. 添加必要的工作日志\n\n";
|
||||
case "smartsheet":
|
||||
return "1. 打开智能表格\n" +
|
||||
"2. 找到自己负责的任务行\n" +
|
||||
"3. 更新任务状态和完成百分比\n" +
|
||||
"4. 添加备注说明进展情况\n\n";
|
||||
case "jira":
|
||||
return "1. 登录Jira系统\n" +
|
||||
"2. 查看分配给自己的Issue\n" +
|
||||
"3. 更新Issue状态\n" +
|
||||
"4. 记录工作日志和时间\n\n";
|
||||
case "trello":
|
||||
return "1. 打开Trello看板\n" +
|
||||
"2. 找到自己的任务卡片\n" +
|
||||
"3. 移动卡片到对应状态列\n" +
|
||||
"4. 添加评论记录进展\n\n";
|
||||
default:
|
||||
return "1. 登录任务管理系统\n" +
|
||||
"2. 查看分配给自己的任务\n" +
|
||||
"3. 更新任务状态和进度\n" +
|
||||
"4. 添加必要的工作记录\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中文星期
|
||||
*/
|
||||
private String getDayOfWeekInChinese(int dayOfWeek) {
|
||||
switch (dayOfWeek) {
|
||||
case 1: return "星期一";
|
||||
case 2: return "星期二";
|
||||
case 3: return "星期三";
|
||||
case 4: return "星期四";
|
||||
case 5: return "星期五";
|
||||
case 6: return "星期六";
|
||||
case 7: return "星期日";
|
||||
default: return "未知";
|
||||
}
|
||||
}
|
||||
}
|
||||
205
src/main/java/com/zeodao/reminder/util/HolidayUtil.java
Normal file
205
src/main/java/com/zeodao/reminder/util/HolidayUtil.java
Normal file
@ -0,0 +1,205 @@
|
||||
package com.zeodao.reminder.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 节假日工具类
|
||||
* 用于判断是否为节假日或周末
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component
|
||||
public class HolidayUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(HolidayUtil.class);
|
||||
|
||||
// 2024年法定节假日(可根据实际情况调整)
|
||||
private static final Set<LocalDate> HOLIDAYS_2024 = new HashSet<>();
|
||||
|
||||
// 2025年法定节假日(可根据实际情况调整)
|
||||
private static final Set<LocalDate> HOLIDAYS_2025 = new HashSet<>();
|
||||
|
||||
static {
|
||||
// 初始化2024年节假日
|
||||
initHolidays2024();
|
||||
// 初始化2025年节假日
|
||||
initHolidays2025();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化2024年节假日
|
||||
*/
|
||||
private static void initHolidays2024() {
|
||||
// 元旦:1月1日
|
||||
HOLIDAYS_2024.add(LocalDate.of(2024, Month.JANUARY, 1));
|
||||
|
||||
// 春节:2月10日-17日
|
||||
for (int day = 10; day <= 17; day++) {
|
||||
HOLIDAYS_2024.add(LocalDate.of(2024, Month.FEBRUARY, day));
|
||||
}
|
||||
|
||||
// 清明节:4月4日-6日
|
||||
for (int day = 4; day <= 6; day++) {
|
||||
HOLIDAYS_2024.add(LocalDate.of(2024, Month.APRIL, day));
|
||||
}
|
||||
|
||||
// 劳动节:5月1日-5日
|
||||
for (int day = 1; day <= 5; day++) {
|
||||
HOLIDAYS_2024.add(LocalDate.of(2024, Month.MAY, day));
|
||||
}
|
||||
|
||||
// 端午节:6月10日
|
||||
HOLIDAYS_2024.add(LocalDate.of(2024, Month.JUNE, 10));
|
||||
|
||||
// 中秋节:9月15日-17日
|
||||
for (int day = 15; day <= 17; day++) {
|
||||
HOLIDAYS_2024.add(LocalDate.of(2024, Month.SEPTEMBER, day));
|
||||
}
|
||||
|
||||
// 国庆节:10月1日-7日
|
||||
for (int day = 1; day <= 7; day++) {
|
||||
HOLIDAYS_2024.add(LocalDate.of(2024, Month.OCTOBER, day));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化2025年节假日
|
||||
*/
|
||||
private static void initHolidays2025() {
|
||||
// 元旦:1月1日
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.JANUARY, 1));
|
||||
|
||||
// 春节:1月28日-2月3日(预估,实际以国务院公布为准)
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.JANUARY, 28));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.JANUARY, 29));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.JANUARY, 30));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.JANUARY, 31));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.FEBRUARY, 1));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.FEBRUARY, 2));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.FEBRUARY, 3));
|
||||
|
||||
// 清明节:4月5日-7日(预估)
|
||||
for (int day = 5; day <= 7; day++) {
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.APRIL, day));
|
||||
}
|
||||
|
||||
// 劳动节:5月1日-5日(预估)
|
||||
for (int day = 1; day <= 5; day++) {
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.MAY, day));
|
||||
}
|
||||
|
||||
// 端午节:5月31日-6月2日(预估)
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.MAY, 31));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.JUNE, 1));
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.JUNE, 2));
|
||||
|
||||
// 中秋节:10月6日(预估)
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.OCTOBER, 6));
|
||||
|
||||
// 国庆节:10月1日-7日
|
||||
for (int day = 1; day <= 7; day++) {
|
||||
HOLIDAYS_2025.add(LocalDate.of(2025, Month.OCTOBER, day));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定日期是否为周末
|
||||
*
|
||||
* @param date 日期
|
||||
* @return 是否为周末
|
||||
*/
|
||||
public boolean isWeekend(LocalDate date) {
|
||||
DayOfWeek dayOfWeek = date.getDayOfWeek();
|
||||
boolean isWeekend = dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
|
||||
|
||||
if (isWeekend) {
|
||||
logger.debug("日期 {} 是周末", date);
|
||||
}
|
||||
|
||||
return isWeekend;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定日期是否为法定节假日
|
||||
*
|
||||
* @param date 日期
|
||||
* @return 是否为节假日
|
||||
*/
|
||||
public boolean isHoliday(LocalDate date) {
|
||||
boolean isHoliday = false;
|
||||
|
||||
int year = date.getYear();
|
||||
switch (year) {
|
||||
case 2024:
|
||||
isHoliday = HOLIDAYS_2024.contains(date);
|
||||
break;
|
||||
case 2025:
|
||||
isHoliday = HOLIDAYS_2025.contains(date);
|
||||
break;
|
||||
default:
|
||||
// 对于其他年份,可以考虑调用外部API或使用默认规则
|
||||
logger.warn("未配置年份 {} 的节假日信息,默认不是节假日", year);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isHoliday) {
|
||||
logger.debug("日期 {} 是法定节假日", date);
|
||||
}
|
||||
|
||||
return isHoliday;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定日期是否为工作日
|
||||
*
|
||||
* @param date 日期
|
||||
* @return 是否为工作日
|
||||
*/
|
||||
public boolean isWorkday(LocalDate date) {
|
||||
return !isWeekend(date) && !isHoliday(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个工作日
|
||||
*
|
||||
* @param date 起始日期
|
||||
* @return 下一个工作日
|
||||
*/
|
||||
public LocalDate getNextWorkday(LocalDate date) {
|
||||
LocalDate nextDay = date.plusDays(1);
|
||||
while (!isWorkday(nextDay)) {
|
||||
nextDay = nextDay.plusDays(1);
|
||||
}
|
||||
return nextDay;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义节假日
|
||||
*
|
||||
* @param date 节假日日期
|
||||
*/
|
||||
public void addCustomHoliday(LocalDate date) {
|
||||
int year = date.getYear();
|
||||
switch (year) {
|
||||
case 2024:
|
||||
HOLIDAYS_2024.add(date);
|
||||
break;
|
||||
case 2025:
|
||||
HOLIDAYS_2025.add(date);
|
||||
break;
|
||||
default:
|
||||
logger.warn("暂不支持添加年份 {} 的自定义节假日", year);
|
||||
break;
|
||||
}
|
||||
logger.info("添加自定义节假日:{}", date);
|
||||
}
|
||||
}
|
||||
58
src/main/resources/application.yml
Normal file
58
src/main/resources/application.yml
Normal file
@ -0,0 +1,58 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: zeodao-task-reminder
|
||||
|
||||
# 多群任务提醒配置
|
||||
task:
|
||||
reminder:
|
||||
# 全局默认配置
|
||||
global:
|
||||
timeout: 5000 # 超时时间(毫秒)
|
||||
|
||||
# 群组配置列表
|
||||
groups:
|
||||
# 禅道团队群
|
||||
- id: "zentao-team"
|
||||
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 等
|
||||
schedules:
|
||||
morning:
|
||||
time: "0 0 9 * * MON-FRI" # 工作日早上9点
|
||||
message: "早上好!新的一天开始了,请大家及时登录禅道系统刷新今日的任务状态,确保任务进度准确反映当前工作情况。"
|
||||
evening:
|
||||
time: "0 30 17 * * MON-FRI" # 工作日下午5:30
|
||||
message: "下班前提醒:请大家登录禅道系统,及时更新今日任务的完成状态和进度,为明天的工作安排做好准备!"
|
||||
enabled: true
|
||||
|
||||
# 智能表格团队群
|
||||
- id: "smartsheet-team"
|
||||
name: "智能表格团队"
|
||||
webhook:
|
||||
url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=496db13a-d92f-4686-98fa-5ff05f49616e"
|
||||
task-system: "smartsheet"
|
||||
schedules:
|
||||
morning:
|
||||
time: "0 0 8 * * MON-FRI" # 工作日早上8点
|
||||
message: "早上好!请大家及时更新智能表格中的任务状态,确保项目进度信息准确无误。"
|
||||
evening:
|
||||
time: "0 0 18 * * MON-FRI" # 工作日下午6点
|
||||
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
|
||||
@ -0,0 +1,130 @@
|
||||
package com.zeodao.reminder.service;
|
||||
|
||||
import com.zeodao.reminder.util.HolidayUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 任务提醒服务测试类
|
||||
*
|
||||
* @author Zeodao
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TaskReminderServiceTest {
|
||||
|
||||
@Mock
|
||||
private WechatWebhookService wechatWebhookService;
|
||||
|
||||
@Mock
|
||||
private HolidayUtil holidayUtil;
|
||||
|
||||
@InjectMocks
|
||||
private TaskReminderService taskReminderService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 设置测试用的消息内容
|
||||
ReflectionTestUtils.setField(taskReminderService, "morningMessage", "测试早上消息");
|
||||
ReflectionTestUtils.setField(taskReminderService, "eveningMessage", "测试晚上消息");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendMorningReminderOnWorkday() {
|
||||
// 模拟工作日
|
||||
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(false);
|
||||
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(false);
|
||||
when(wechatWebhookService.createTaskReminderMessage(anyString(), anyString())).thenReturn("测试消息");
|
||||
when(wechatWebhookService.sendMarkdownMessage(anyString())).thenReturn(true);
|
||||
|
||||
// 执行测试
|
||||
taskReminderService.sendMorningReminder();
|
||||
|
||||
// 验证
|
||||
verify(wechatWebhookService, times(1)).sendMarkdownMessage(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendMorningReminderOnWeekend() {
|
||||
// 模拟周末
|
||||
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(true);
|
||||
// 注意:当isWeekend返回true时,不会调用isHoliday方法,所以不需要mock
|
||||
|
||||
// 执行测试
|
||||
taskReminderService.sendMorningReminder();
|
||||
|
||||
// 验证不发送消息
|
||||
verify(wechatWebhookService, never()).sendMarkdownMessage(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendMorningReminderOnHoliday() {
|
||||
// 模拟节假日(不是周末,但是节假日)
|
||||
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(false);
|
||||
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(true);
|
||||
|
||||
// 执行测试
|
||||
taskReminderService.sendMorningReminder();
|
||||
|
||||
// 验证不发送消息
|
||||
verify(wechatWebhookService, never()).sendMarkdownMessage(anyString());
|
||||
// 验证两个方法都被调用了
|
||||
verify(holidayUtil, times(1)).isWeekend(any(LocalDate.class));
|
||||
verify(holidayUtil, times(1)).isHoliday(any(LocalDate.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendEveningReminderOnWorkday() {
|
||||
// 模拟工作日
|
||||
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(false);
|
||||
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(false);
|
||||
when(wechatWebhookService.createTaskReminderMessage(anyString(), anyString())).thenReturn("测试消息");
|
||||
when(wechatWebhookService.sendMarkdownMessage(anyString())).thenReturn(true);
|
||||
|
||||
// 执行测试
|
||||
taskReminderService.sendEveningReminder();
|
||||
|
||||
// 验证
|
||||
verify(wechatWebhookService, times(1)).sendMarkdownMessage(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendTestMessage() {
|
||||
// 模拟成功发送
|
||||
when(wechatWebhookService.createTaskReminderMessage(anyString(), anyString())).thenReturn("测试消息");
|
||||
when(wechatWebhookService.sendMarkdownMessage(anyString())).thenReturn(true);
|
||||
|
||||
// 执行测试
|
||||
boolean result = taskReminderService.sendTestMessage();
|
||||
|
||||
// 验证
|
||||
assert result;
|
||||
verify(wechatWebhookService, times(1)).sendMarkdownMessage(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetNextReminderInfo() {
|
||||
// 模拟工作日
|
||||
when(holidayUtil.isWeekend(any(LocalDate.class))).thenReturn(false);
|
||||
when(holidayUtil.isHoliday(any(LocalDate.class))).thenReturn(false);
|
||||
|
||||
// 执行测试
|
||||
String info = taskReminderService.getNextReminderInfo();
|
||||
|
||||
// 验证
|
||||
assert info != null;
|
||||
assert info.contains("当前时间");
|
||||
assert info.contains("今天是否工作日:是");
|
||||
}
|
||||
}
|
||||
40
start.bat
Normal file
40
start.bat
Normal file
@ -0,0 +1,40 @@
|
||||
@echo off
|
||||
echo ================================
|
||||
echo Zeodao任务提醒系统启动脚本
|
||||
echo ================================
|
||||
|
||||
echo 正在检查Java环境...
|
||||
java -version
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误:未找到Java环境,请确保已安装JDK 21或更高版本
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 正在检查Maven环境...
|
||||
mvn -version
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误:未找到Maven环境,请确保已安装Maven 3.6+
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 正在编译项目...
|
||||
mvn clean compile
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误:项目编译失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 正在启动应用...
|
||||
echo 应用将在 http://localhost:8080 启动
|
||||
echo 按 Ctrl+C 可以停止应用
|
||||
echo.
|
||||
|
||||
mvn spring-boot:run
|
||||
|
||||
pause
|
||||
Loading…
Reference in New Issue
Block a user