first commit

This commit is contained in:
dengqichen 2025-05-28 09:50:20 +08:00
commit da23b31a1e
13 changed files with 1578 additions and 0 deletions

71
.gitignore vendored Normal file
View 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
View 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
View 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>

View File

@ -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("=================================");
}
}

View 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View 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);
}
}

View 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

View File

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