diff --git a/README.md b/README.md index d3b894a..8f00818 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ -# Zeodao任务提醒系统 +# Zeodao任务提醒系统 v2.0 -一个基于Spring Boot的企业微信任务提醒系统,专门用于提醒团队成员及时在禅道系统中刷新任务状态。系统会在工作日的早上9点和下午5:30自动发送提醒消息,帮助团队养成良好的任务状态更新习惯。 +一个基于Spring Boot的多群企业微信任务提醒系统,支持多个团队群组、多种任务管理系统和自定义提醒时间。系统可以同时为使用禅道、智能表格、Jira等不同任务管理工具的团队提供个性化的任务状态提醒服务。 ## 功能特性 -- ✅ 自动在工作日早上9:00发送禅道任务状态提醒 -- ✅ 自动在工作日下午17:30发送任务完成状态提醒 -- ✅ 自动排除周末和法定节假日 -- ✅ 支持企业微信Webhook消息推送 -- ✅ 支持Markdown格式消息,包含操作指引 -- ✅ 提供REST API进行手动测试和触发 -- ✅ 完整的日志记录和错误处理 -- ✅ 可配置的消息内容和时间 +- ✅ **多群组支持** - 同时支持多个企业微信群组 +- ✅ **多任务系统** - 支持禅道、智能表格、Jira、Trello等任务管理系统 +- ✅ **自定义时间** - 每个群组可配置不同的提醒时间 +- ✅ **个性化消息** - 根据任务管理系统生成专属操作指引 +- ✅ **动态调度** - 根据配置文件动态创建定时任务 +- ✅ **自动排除节假日** - 智能跳过周末和法定节假日 +- ✅ **企业微信集成** - 支持Webhook消息推送和Markdown格式 +- ✅ **REST API** - 提供完整的管理和监控接口 +- ✅ **实时监控** - 任务状态监控和日志记录 ## 技术栈 diff --git a/src/main/java/com/zeodao/reminder/config/SchedulerConfig.java b/src/main/java/com/zeodao/reminder/config/SchedulerConfig.java new file mode 100644 index 0000000..8bbf7e0 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/config/SchedulerConfig.java @@ -0,0 +1,26 @@ +package com.zeodao.reminder.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * 任务调度器配置 + * + * @author Zeodao + * @version 2.0.0 + */ +@Configuration +public class SchedulerConfig { + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(10); + scheduler.setThreadNamePrefix("task-reminder-"); + scheduler.setWaitForTasksToCompleteOnShutdown(true); + scheduler.setAwaitTerminationSeconds(30); + return scheduler; + } +} diff --git a/src/main/java/com/zeodao/reminder/config/WechatConfig.java b/src/main/java/com/zeodao/reminder/config/TaskReminderConfig.java similarity index 99% rename from src/main/java/com/zeodao/reminder/config/WechatConfig.java rename to src/main/java/com/zeodao/reminder/config/TaskReminderConfig.java index 90379b0..e991e3b 100644 --- a/src/main/java/com/zeodao/reminder/config/WechatConfig.java +++ b/src/main/java/com/zeodao/reminder/config/TaskReminderConfig.java @@ -10,7 +10,7 @@ import java.util.Map; /** * 多群任务提醒配置类 - * + * * @author Zeodao * @version 2.0.0 */ diff --git a/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java b/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java index 3444001..12720f4 100644 --- a/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java +++ b/src/main/java/com/zeodao/reminder/controller/TaskReminderController.java @@ -1,5 +1,7 @@ package com.zeodao.reminder.controller; +import com.zeodao.reminder.config.TaskReminderConfig; +import com.zeodao.reminder.scheduler.DynamicTaskScheduler; import com.zeodao.reminder.service.TaskReminderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -8,12 +10,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; +import java.util.List; import java.util.Map; /** * 任务提醒控制器 * 提供手动触发提醒和系统状态查询的API - * + * * @author Zeodao * @version 1.0.0 */ @@ -26,6 +29,12 @@ public class TaskReminderController { @Autowired private TaskReminderService taskReminderService; + @Autowired + private TaskReminderConfig taskReminderConfig; + + @Autowired + private DynamicTaskScheduler dynamicTaskScheduler; + /** * 系统健康检查 */ @@ -36,28 +45,53 @@ public class TaskReminderController { 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() { + public ResponseEntity> getSystemInfo() { Map response = new HashMap<>(); - + try { - String info = taskReminderService.getNextReminderInfo(); + String taskInfo = dynamicTaskScheduler.getTaskStatusInfo(); + List enabledGroups = taskReminderConfig.getEnabledGroups(); + response.put("success", true); - response.put("info", info); - response.put("message", "获取提醒信息成功"); + response.put("taskInfo", taskInfo); + response.put("enabledGroupCount", enabledGroups.size()); + response.put("activeTaskCount", dynamicTaskScheduler.getActiveTaskCount()); + response.put("message", "获取系统信息成功"); } catch (Exception e) { - logger.error("获取提醒信息失败", e); + logger.error("获取系统信息失败", e); response.put("success", false); - response.put("message", "获取提醒信息失败:" + e.getMessage()); + response.put("message", "获取系统信息失败:" + e.getMessage()); } - + + return ResponseEntity.ok(response); + } + + /** + * 获取所有群组配置 + */ + @GetMapping("/groups") + public ResponseEntity> getGroups() { + Map response = new HashMap<>(); + + try { + List groups = taskReminderConfig.getGroups(); + response.put("success", true); + response.put("groups", groups); + response.put("message", "获取群组配置成功"); + } catch (Exception e) { + logger.error("获取群组配置失败", e); + response.put("success", false); + response.put("message", "获取群组配置失败:" + e.getMessage()); + } + return ResponseEntity.ok(response); } @@ -67,7 +101,7 @@ public class TaskReminderController { @PostMapping("/test") public ResponseEntity> sendTestMessage() { Map response = new HashMap<>(); - + try { boolean success = taskReminderService.sendTestMessage(); response.put("success", success); @@ -77,7 +111,7 @@ public class TaskReminderController { response.put("success", false); response.put("message", "发送测试消息失败:" + e.getMessage()); } - + return ResponseEntity.ok(response); } @@ -87,7 +121,7 @@ public class TaskReminderController { @PostMapping("/morning") public ResponseEntity> triggerMorningReminder() { Map response = new HashMap<>(); - + try { taskReminderService.sendMorningReminder(); response.put("success", true); @@ -97,7 +131,7 @@ public class TaskReminderController { response.put("success", false); response.put("message", "触发早上提醒失败:" + e.getMessage()); } - + return ResponseEntity.ok(response); } @@ -107,17 +141,60 @@ public class TaskReminderController { @PostMapping("/evening") public ResponseEntity> triggerEveningReminder() { Map response = new HashMap<>(); - + try { - taskReminderService.sendEveningReminder(); + taskReminderService.sendAllEveningReminders(); response.put("success", true); - response.put("message", "晚上提醒已触发"); + response.put("message", "所有群组晚上提醒已触发"); } catch (Exception e) { logger.error("触发晚上提醒失败", e); response.put("success", false); response.put("message", "触发晚上提醒失败:" + e.getMessage()); } - + + return ResponseEntity.ok(response); + } + + /** + * 手动触发指定群组的提醒 + */ + @PostMapping("/groups/{groupId}/{scheduleType}") + public ResponseEntity> triggerGroupReminder( + @PathVariable String groupId, + @PathVariable String scheduleType) { + Map response = new HashMap<>(); + + try { + taskReminderService.sendReminder(groupId, scheduleType); + response.put("success", true); + response.put("message", String.format("群组 %s 的 %s 提醒已触发", groupId, scheduleType)); + } catch (Exception e) { + logger.error("触发群组提醒失败: {} - {}", groupId, scheduleType, e); + response.put("success", false); + response.put("message", "触发群组提醒失败:" + e.getMessage()); + } + + return ResponseEntity.ok(response); + } + + /** + * 重新加载定时任务 + */ + @PostMapping("/reload") + public ResponseEntity> reloadTasks() { + Map response = new HashMap<>(); + + try { + dynamicTaskScheduler.reloadScheduledTasks(); + response.put("success", true); + response.put("message", "定时任务重新加载成功"); + response.put("activeTaskCount", dynamicTaskScheduler.getActiveTaskCount()); + } 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/DynamicTaskScheduler.java b/src/main/java/com/zeodao/reminder/scheduler/DynamicTaskScheduler.java new file mode 100644 index 0000000..84de575 --- /dev/null +++ b/src/main/java/com/zeodao/reminder/scheduler/DynamicTaskScheduler.java @@ -0,0 +1,166 @@ +package com.zeodao.reminder.scheduler; + +import com.zeodao.reminder.config.TaskReminderConfig; +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.TaskScheduler; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; + +/** + * 动态任务调度器 + * 根据配置文件动态创建定时任务 + * + * @author Zeodao + * @version 2.0.0 + */ +@Component +public class DynamicTaskScheduler { + + private static final Logger logger = LoggerFactory.getLogger(DynamicTaskScheduler.class); + + @Autowired + private TaskReminderConfig taskReminderConfig; + + @Autowired + private TaskReminderService taskReminderService; + + @Autowired + private TaskScheduler taskScheduler; + + private final Map> scheduledTasks = new HashMap<>(); + + @PostConstruct + public void initScheduledTasks() { + logger.info("=== 初始化动态定时任务 ==="); + + for (TaskReminderConfig.Group group : taskReminderConfig.getEnabledGroups()) { + createSchedulesForGroup(group); + } + + logger.info("=== 动态定时任务初始化完成,共创建 {} 个任务 ===", scheduledTasks.size()); + } + + /** + * 为指定群组创建定时任务 + */ + private void createSchedulesForGroup(TaskReminderConfig.Group group) { + logger.info("为群组 {} 创建定时任务", group.getName()); + + for (Map.Entry entry : group.getSchedules().entrySet()) { + String scheduleType = entry.getKey(); + TaskReminderConfig.Schedule schedule = entry.getValue(); + + String taskKey = group.getId() + "-" + scheduleType; + + try { + CronTrigger cronTrigger = new CronTrigger(schedule.getTime()); + + Runnable task = () -> { + try { + logger.info("=== 执行定时任务: {} - {} ===", group.getName(), scheduleType); + taskReminderService.sendReminder(group.getId(), scheduleType); + } catch (Exception e) { + logger.error("定时任务执行失败: {} - {}", group.getName(), scheduleType, e); + } + }; + + ScheduledFuture scheduledFuture = taskScheduler.schedule(task, cronTrigger); + scheduledTasks.put(taskKey, scheduledFuture); + + logger.info("创建定时任务成功: {} - {} ({})", group.getName(), scheduleType, schedule.getTime()); + + } catch (Exception e) { + logger.error("创建定时任务失败: {} - {} ({})", group.getName(), scheduleType, schedule.getTime(), e); + } + } + } + + /** + * 重新加载所有定时任务 + */ + public void reloadScheduledTasks() { + logger.info("=== 重新加载定时任务 ==="); + + // 取消所有现有任务 + cancelAllTasks(); + + // 重新创建任务 + initScheduledTasks(); + } + + /** + * 取消所有定时任务 + */ + private void cancelAllTasks() { + logger.info("取消所有现有定时任务"); + + for (Map.Entry> entry : scheduledTasks.entrySet()) { + String taskKey = entry.getKey(); + ScheduledFuture future = entry.getValue(); + + if (future != null && !future.isCancelled()) { + future.cancel(false); + logger.debug("取消定时任务: {}", taskKey); + } + } + + scheduledTasks.clear(); + } + + /** + * 获取当前活跃的任务数量 + */ + public int getActiveTaskCount() { + return (int) scheduledTasks.values().stream() + .filter(future -> future != null && !future.isCancelled()) + .count(); + } + + /** + * 获取任务状态信息 + */ + public String getTaskStatusInfo() { + StringBuilder info = new StringBuilder(); + info.append("=== 定时任务状态 ===\n"); + info.append("总任务数: ").append(scheduledTasks.size()).append("\n"); + info.append("活跃任务数: ").append(getActiveTaskCount()).append("\n"); + info.append("启用群组数: ").append(taskReminderConfig.getEnabledGroups().size()).append("\n\n"); + + info.append("=== 群组配置详情 ===\n"); + for (TaskReminderConfig.Group group : taskReminderConfig.getEnabledGroups()) { + info.append("群组: ").append(group.getName()).append(" (").append(group.getId()).append(")\n"); + info.append("任务系统: ").append(group.getTaskSystem()).append("\n"); + info.append("定时任务:\n"); + + for (Map.Entry entry : group.getSchedules().entrySet()) { + String scheduleType = entry.getKey(); + TaskReminderConfig.Schedule schedule = entry.getValue(); + String taskKey = group.getId() + "-" + scheduleType; + + ScheduledFuture future = scheduledTasks.get(taskKey); + String status = (future != null && !future.isCancelled()) ? "运行中" : "已停止"; + + info.append(" - ").append(scheduleType).append(": ").append(schedule.getTime()) + .append(" (").append(status).append(")\n"); + } + info.append("\n"); + } + + return info.toString(); + } + + @PreDestroy + public void destroy() { + logger.info("=== 销毁动态定时任务调度器 ==="); + cancelAllTasks(); + } +} diff --git a/src/main/java/com/zeodao/reminder/scheduler/TaskReminderScheduler.java b/src/main/java/com/zeodao/reminder/scheduler/TaskReminderScheduler.java deleted file mode 100644 index e3e2355..0000000 --- a/src/main/java/com/zeodao/reminder/scheduler/TaskReminderScheduler.java +++ /dev/null @@ -1,74 +0,0 @@ -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 index 75e67e0..1d423b3 100644 --- a/src/main/java/com/zeodao/reminder/service/TaskReminderService.java +++ b/src/main/java/com/zeodao/reminder/service/TaskReminderService.java @@ -28,52 +28,85 @@ public class TaskReminderService { @Autowired private HolidayUtil holidayUtil; - @Value("${task.reminder.morning.message}") - private String morningMessage; - - @Value("${task.reminder.evening.message}") - private String eveningMessage; + @Autowired + private TaskReminderConfig taskReminderConfig; /** - * 发送早上任务提醒 + * 发送指定群组和时间段的任务提醒 */ - public void sendMorningReminder() { + public void sendReminder(String groupId, String scheduleType) { if (!shouldSendReminder()) { - logger.info("今天是节假日或周末,跳过早上任务提醒"); + logger.info("今天是节假日或周末,跳过任务提醒"); return; } - logger.info("开始发送早上任务提醒"); + TaskReminderConfig.Group group = taskReminderConfig.getGroupById(groupId); + if (group == null) { + logger.error("未找到群组配置: {}", groupId); + return; + } - String message = wechatWebhookService.createTaskReminderMessage(morningMessage, "早上提醒"); - boolean success = wechatWebhookService.sendMarkdownMessage(message); + if (!group.isEnabled()) { + logger.info("群组 {} 已禁用,跳过提醒", groupId); + return; + } + + TaskReminderConfig.Schedule schedule = group.getSchedules().get(scheduleType); + if (schedule == null) { + logger.error("群组 {} 未配置 {} 提醒时间", groupId, scheduleType); + return; + } + + logger.info("开始发送任务提醒 - 群组: {}, 类型: {}", group.getName(), scheduleType); + + String message = wechatWebhookService.createTaskReminderMessage(groupId, schedule.getMessage(), scheduleType + "提醒"); + boolean success = wechatWebhookService.sendMarkdownMessage(groupId, message); if (success) { - logger.info("早上任务提醒发送成功"); + logger.info("任务提醒发送成功 - 群组: {}, 类型: {}", group.getName(), scheduleType); } else { - logger.error("早上任务提醒发送失败"); + logger.error("任务提醒发送失败 - 群组: {}, 类型: {}", group.getName(), scheduleType); } } /** - * 发送晚上任务提醒 + * 发送所有启用群组的早上提醒 */ + public void sendAllMorningReminders() { + List enabledGroups = taskReminderConfig.getEnabledGroups(); + for (TaskReminderConfig.Group group : enabledGroups) { + if (group.getSchedules().containsKey("morning")) { + sendReminder(group.getId(), "morning"); + } + } + } + + /** + * 发送所有启用群组的晚上提醒 + */ + public void sendAllEveningReminders() { + List enabledGroups = taskReminderConfig.getEnabledGroups(); + for (TaskReminderConfig.Group group : enabledGroups) { + if (group.getSchedules().containsKey("evening")) { + sendReminder(group.getId(), "evening"); + } + } + } + + /** + * 兼容旧版本的方法 + */ + @Deprecated + public void sendMorningReminder() { + sendAllMorningReminders(); + } + + /** + * 兼容旧版本的方法 + */ + @Deprecated 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("晚上任务提醒发送失败"); - } + sendAllEveningReminders(); } /** diff --git a/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java b/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java index aabef59..20d8b3d 100644 --- a/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java +++ b/src/main/java/com/zeodao/reminder/service/WechatWebhookService.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -192,15 +193,13 @@ public class WechatWebhookService { 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("**📝 提醒类型:** ").append(timeType).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*"); + message.append("💡 **温馨提示:** 及时更新任务状态有助于团队协作和项目管理"); return message.toString(); }