增加多群通知

This commit is contained in:
dengqichen 2025-05-28 10:10:37 +08:00
parent da23b31a1e
commit d165f2b043
8 changed files with 365 additions and 137 deletions

View File

@ -1,17 +1,18 @@
# Zeodao任务提醒系统 # Zeodao任务提醒系统 v2.0
一个基于Spring Boot的企业微信任务提醒系统专门用于提醒团队成员及时在禅道系统中刷新任务状态。系统会在工作日的早上9点和下午5:30自动发送提醒消息帮助团队养成良好的任务状态更新习惯 一个基于Spring Boot的多群企业微信任务提醒系统支持多个团队群组、多种任务管理系统和自定义提醒时间。系统可以同时为使用禅道、智能表格、Jira等不同任务管理工具的团队提供个性化的任务状态提醒服务
## 功能特性 ## 功能特性
- ✅ 自动在工作日早上9:00发送禅道任务状态提醒 - ✅ **多群组支持** - 同时支持多个企业微信群组
- ✅ 自动在工作日下午17:30发送任务完成状态提醒 - ✅ **多任务系统** - 支持禅道、智能表格、Jira、Trello等任务管理系统
- ✅ 自动排除周末和法定节假日 - ✅ **自定义时间** - 每个群组可配置不同的提醒时间
- ✅ 支持企业微信Webhook消息推送 - ✅ **个性化消息** - 根据任务管理系统生成专属操作指引
- ✅ 支持Markdown格式消息包含操作指引 - ✅ **动态调度** - 根据配置文件动态创建定时任务
- ✅ 提供REST API进行手动测试和触发 - ✅ **自动排除节假日** - 智能跳过周末和法定节假日
- ✅ 完整的日志记录和错误处理 - ✅ **企业微信集成** - 支持Webhook消息推送和Markdown格式
- ✅ 可配置的消息内容和时间 - ✅ **REST API** - 提供完整的管理和监控接口
- ✅ **实时监控** - 任务状态监控和日志记录
## 技术栈 ## 技术栈

View File

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

View File

@ -1,5 +1,7 @@
package com.zeodao.reminder.controller; package com.zeodao.reminder.controller;
import com.zeodao.reminder.config.TaskReminderConfig;
import com.zeodao.reminder.scheduler.DynamicTaskScheduler;
import com.zeodao.reminder.service.TaskReminderService; import com.zeodao.reminder.service.TaskReminderService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -8,6 +10,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -26,6 +29,12 @@ public class TaskReminderController {
@Autowired @Autowired
private TaskReminderService taskReminderService; private TaskReminderService taskReminderService;
@Autowired
private TaskReminderConfig taskReminderConfig;
@Autowired
private DynamicTaskScheduler dynamicTaskScheduler;
/** /**
* 系统健康检查 * 系统健康检查
*/ */
@ -41,21 +50,46 @@ public class TaskReminderController {
} }
/** /**
* 获取下次提醒信息 * 获取系统信息
*/ */
@GetMapping("/info") @GetMapping("/info")
public ResponseEntity<Map<String, Object>> getReminderInfo() { public ResponseEntity<Map<String, Object>> getSystemInfo() {
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
try { try {
String info = taskReminderService.getNextReminderInfo(); String taskInfo = dynamicTaskScheduler.getTaskStatusInfo();
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
response.put("success", true); response.put("success", true);
response.put("info", info); response.put("taskInfo", taskInfo);
response.put("message", "获取提醒信息成功"); response.put("enabledGroupCount", enabledGroups.size());
response.put("activeTaskCount", dynamicTaskScheduler.getActiveTaskCount());
response.put("message", "获取系统信息成功");
} catch (Exception e) { } catch (Exception e) {
logger.error("获取提醒信息失败", e); logger.error("获取系统信息失败", e);
response.put("success", false); response.put("success", false);
response.put("message", "获取提醒信息失败:" + e.getMessage()); response.put("message", "获取系统信息失败:" + e.getMessage());
}
return ResponseEntity.ok(response);
}
/**
* 获取所有群组配置
*/
@GetMapping("/groups")
public ResponseEntity<Map<String, Object>> getGroups() {
Map<String, Object> response = new HashMap<>();
try {
List<TaskReminderConfig.Group> 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); return ResponseEntity.ok(response);
@ -109,9 +143,9 @@ public class TaskReminderController {
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
try { try {
taskReminderService.sendEveningReminder(); taskReminderService.sendAllEveningReminders();
response.put("success", true); response.put("success", true);
response.put("message", "晚上提醒已触发"); response.put("message", "所有群组晚上提醒已触发");
} catch (Exception e) { } catch (Exception e) {
logger.error("触发晚上提醒失败", e); logger.error("触发晚上提醒失败", e);
response.put("success", false); response.put("success", false);
@ -120,4 +154,47 @@ public class TaskReminderController {
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
/**
* 手动触发指定群组的提醒
*/
@PostMapping("/groups/{groupId}/{scheduleType}")
public ResponseEntity<Map<String, Object>> triggerGroupReminder(
@PathVariable String groupId,
@PathVariable String scheduleType) {
Map<String, Object> 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<Map<String, Object>> reloadTasks() {
Map<String, Object> 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);
}
} }

View File

@ -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<String, ScheduledFuture<?>> 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<String, TaskReminderConfig.Schedule> 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<String, ScheduledFuture<?>> entry : scheduledTasks.entrySet()) {
String taskKey = entry.getKey();
ScheduledFuture<?> future = entry.getValue();
if (future != null && !future.isCancelled()) {
future.cancel(false);
logger.debug("取消定时任务: {}", taskKey);
}
}
scheduledTasks.clear();
}
/**
* 获取当前活跃的任务数量
*/
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<String, TaskReminderConfig.Schedule> entry : group.getSchedules().entrySet()) {
String scheduleType = entry.getKey();
TaskReminderConfig.Schedule schedule = entry.getValue();
String taskKey = group.getId() + "-" + scheduleType;
ScheduledFuture<?> future = scheduledTasks.get(taskKey);
String status = (future != null && !future.isCancelled()) ? "运行中" : "已停止";
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();
}
}

View File

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

View File

@ -28,52 +28,85 @@ public class TaskReminderService {
@Autowired @Autowired
private HolidayUtil holidayUtil; private HolidayUtil holidayUtil;
@Value("${task.reminder.morning.message}") @Autowired
private String morningMessage; private TaskReminderConfig taskReminderConfig;
@Value("${task.reminder.evening.message}")
private String eveningMessage;
/** /**
* 发送早上任务提醒 * 发送指定群组和时间段的任务提醒
*/ */
public void sendMorningReminder() { public void sendReminder(String groupId, String scheduleType) {
if (!shouldSendReminder()) { if (!shouldSendReminder()) {
logger.info("今天是节假日或周末,跳过早上任务提醒"); logger.info("今天是节假日或周末,跳过任务提醒");
return; return;
} }
logger.info("开始发送早上任务提醒"); TaskReminderConfig.Group group = taskReminderConfig.getGroupById(groupId);
if (group == null) {
logger.error("未找到群组配置: {}", groupId);
return;
}
String message = wechatWebhookService.createTaskReminderMessage(morningMessage, "早上提醒"); if (!group.isEnabled()) {
boolean success = wechatWebhookService.sendMarkdownMessage(message); 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) { if (success) {
logger.info("早上任务提醒发送成功"); logger.info("任务提醒发送成功 - 群组: {}, 类型: {}", group.getName(), scheduleType);
} else { } else {
logger.error("早上任务提醒发送失败"); logger.error("任务提醒发送失败 - 群组: {}, 类型: {}", group.getName(), scheduleType);
} }
} }
/** /**
* 发送晚上任务提醒 * 发送所有启用群组的早上提醒
*/ */
public void sendAllMorningReminders() {
List<TaskReminderConfig.Group> enabledGroups = taskReminderConfig.getEnabledGroups();
for (TaskReminderConfig.Group group : enabledGroups) {
if (group.getSchedules().containsKey("morning")) {
sendReminder(group.getId(), "morning");
}
}
}
/**
* 发送所有启用群组的晚上提醒
*/
public void sendAllEveningReminders() {
List<TaskReminderConfig.Group> 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() { public void sendEveningReminder() {
if (!shouldSendReminder()) { sendAllEveningReminders();
logger.info("今天是节假日或周末,跳过晚上任务提醒");
return;
}
logger.info("开始发送晚上任务提醒");
String message = wechatWebhookService.createTaskReminderMessage(eveningMessage, "下班提醒");
boolean success = wechatWebhookService.sendMarkdownMessage(message);
if (success) {
logger.info("晚上任务提醒发送成功");
} else {
logger.error("晚上任务提醒发送失败");
}
} }
/** /**

View File

@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -192,15 +193,13 @@ public class WechatWebhookService {
message.append("## ").append(systemIcon).append(" ").append(systemName).append("任务状态提醒\n\n"); message.append("## ").append(systemIcon).append(" ").append(systemName).append("任务状态提醒\n\n");
message.append("**⏰ 时间:** ").append(timestamp).append(" (").append(dayOfWeek).append(")\n"); message.append("**⏰ 时间:** ").append(timestamp).append(" (").append(dayOfWeek).append(")\n");
message.append("**📝 提醒类型:** ").append(timeType).append("\n"); message.append("**📝 提醒类型:** ").append(timeType).append("\n\n");
message.append("**👥 群组:** ").append(group.getName()).append("\n\n");
message.append("### 📢 重要提醒\n"); message.append("### 📢 重要提醒\n");
message.append(baseMessage).append("\n\n"); message.append(baseMessage).append("\n\n");
message.append("### 🔗 操作指引\n"); message.append("### 🔗 操作指引\n");
message.append(getTaskSystemInstructions(group.getTaskSystem())); message.append(getTaskSystemInstructions(group.getTaskSystem()));
message.append("---\n"); message.append("---\n");
message.append("💡 **温馨提示:** 及时更新任务状态有助于团队协作和项目管理\n\n"); message.append("💡 **温馨提示:** 及时更新任务状态有助于团队协作和项目管理");
message.append("*Zeodao任务提醒系统 v2.0*");
return message.toString(); return message.toString();
} }