commit da23b31a1e55b8d83631c68969cc093a3c5624f4 Author: dengqichen Date: Wed May 28 09:50:20 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c07e1f --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3b894a --- /dev/null +++ b/README.md @@ -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 + +## 联系方式 + +如有问题,请联系开发团队。 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0d474d9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + com.zeodao + task-reminder + 1.0.0 + jar + + Zeodao Task Reminder + 企业微信任务提醒系统 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.14 + + + + + 21 + 21 + 21 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.apache.httpcomponents + httpclient + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + org.springframework.boot + spring-boot-starter-logging + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/com/zeodao/reminder/ZeodaoTaskReminderApplication.java b/src/main/java/com/zeodao/reminder/ZeodaoTaskReminderApplication.java new file mode 100644 index 0000000..cd47f1c --- /dev/null +++ b/src/main/java/com/zeodao/reminder/ZeodaoTaskReminderApplication.java @@ -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("================================="); + } +} diff --git a/src/main/java/com/zeodao/reminder/config/WechatConfig.java b/src/main/java/com/zeodao/reminder/config/WechatConfig.java new file mode 100644 index 0000000..90379b0 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/config/WechatConfig.java @@ -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 groups = new ArrayList<>(); + + public Global getGlobal() { + return global; + } + + public void setGlobal(Global global) { + this.global = global; + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } + + /** + * 根据群组ID获取群组配置 + */ + public Group getGroupById(String groupId) { + return groups.stream() + .filter(group -> group.getId().equals(groupId)) + .findFirst() + .orElse(null); + } + + /** + * 获取所有启用的群组 + */ + public List 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 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 getSchedules() { + return schedules; + } + + public void setSchedules(Map 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; + } + } +} diff --git a/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java b/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java new file mode 100644 index 0000000..3444001 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java @@ -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> health() { + Map 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> getReminderInfo() { + Map 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> sendTestMessage() { + Map 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> triggerMorningReminder() { + Map 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> triggerEveningReminder() { + Map 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); + } +} diff --git a/src/main/java/com/zeodao/reminder/scheduler/TaskReminderScheduler.java b/src/main/java/com/zeodao/reminder/scheduler/TaskReminderScheduler.java new file mode 100644 index 0000000..e3e2355 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/scheduler/TaskReminderScheduler.java @@ -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); + } +} diff --git a/src/main/java/com/zeodao/reminder/service/TaskReminderService.java b/src/main/java/com/zeodao/reminder/service/TaskReminderService.java new file mode 100644 index 0000000..75e67e0 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/service/TaskReminderService.java @@ -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(); + } +} diff --git a/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java b/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java new file mode 100644 index 0000000..aabef59 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java @@ -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 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 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 enabledGroups = taskReminderConfig.getEnabledGroups(); + if (enabledGroups.isEmpty()) { + logger.error("没有启用的群组配置"); + return false; + } + + return sendMarkdownMessage(enabledGroups.get(0).getId(), content); + } + + /** + * 创建文本消息体 + */ + private Map createTextMessage(String content) { + Map message = new HashMap<>(); + message.put("msgtype", "text"); + + Map text = new HashMap<>(); + text.put("content", content); + message.put("text", text); + + return message; + } + + /** + * 创建Markdown消息体 + */ + private Map createMarkdownMessage(String content) { + Map message = new HashMap<>(); + message.put("msgtype", "markdown"); + + Map markdown = new HashMap<>(); + markdown.put("content", content); + message.put("markdown", markdown); + + return message; + } + + /** + * 发送消息到企业微信 + */ + private boolean sendMessage(String webhookUrl, Map 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 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 "未知"; + } + } +} diff --git a/src/main/java/com/zeodao/reminder/util/HolidayUtil.java b/src/main/java/com/zeodao/reminder/util/HolidayUtil.java new file mode 100644 index 0000000..d023a87 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/util/HolidayUtil.java @@ -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 HOLIDAYS_2024 = new HashSet<>(); + + // 2025年法定节假日(可根据实际情况调整) + private static final Set 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); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..994aa31 --- /dev/null +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/test/java/com/zeodao/reminder/service/TaskReminderServiceTest.java b/src/test/java/com/zeodao/reminder/service/TaskReminderServiceTest.java new file mode 100644 index 0000000..8b8c927 --- /dev/null +++ b/src/test/java/com/zeodao/reminder/service/TaskReminderServiceTest.java @@ -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("今天是否工作日:是"); + } +} diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..b0f351d --- /dev/null +++ b/start.bat @@ -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