From b6b052b9df68a2c2eda471a8d426b852e04db739 Mon Sep 17 00:00:00 2001 From: jsowell <123@jsowell.com> Date: Fri, 17 Jul 2026 18:06:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BE=85=E5=8A=9E=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E5=92=8C=E5=8E=86=E5=8F=B2=E5=BD=92=E6=A1=A3=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sql/sys_todo_maintenance_job.sql | 19 +++++++ .../task/TodoTaskMaintenanceTaskTest.java | 36 +++++++++++++ .../TodoTaskMaintenanceServiceImplTest.java | 51 +++++++++++++++++++ .../quartz/task/TodoTaskMaintenanceTask.java | 25 +++++++++ .../domain/dto/TodoTaskMaintenanceResult.java | 20 ++++++++ .../system/mapper/SysTodoTaskMapper.java | 6 +++ .../service/TodoTaskMaintenanceService.java | 10 ++++ .../impl/TodoTaskMaintenanceServiceImpl.java | 38 ++++++++++++++ .../mapper/system/SysTodoTaskMapper.xml | 24 +++++++++ 9 files changed, 229 insertions(+) create mode 100644 docs/sql/sys_todo_maintenance_job.sql create mode 100644 jsowell-admin/src/test/java/com/jsowell/quartz/task/TodoTaskMaintenanceTaskTest.java create mode 100644 jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskMaintenanceServiceImplTest.java create mode 100644 jsowell-quartz/src/main/java/com/jsowell/quartz/task/TodoTaskMaintenanceTask.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskMaintenanceResult.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskMaintenanceService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskMaintenanceServiceImpl.java diff --git a/docs/sql/sys_todo_maintenance_job.sql b/docs/sql/sys_todo_maintenance_job.sql new file mode 100644 index 000000000..86c5f5b1d --- /dev/null +++ b/docs/sql/sys_todo_maintenance_job.sql @@ -0,0 +1,19 @@ +-- 待办任务过期与历史归档定时任务 +-- 默认每天 02:30 执行;归档保留天数通过 todo.retention-days 配置,最少保留 180 天。 + +INSERT INTO sys_job ( + job_name, job_group, invoke_target, cron_expression, + misfire_policy, concurrent, status, create_by, create_time, remark +) +SELECT + '待办过期与历史归档', 'SYSTEM', 'todoTaskMaintenanceTask.maintain()', '0 30 2 * * ?', + '3', '1', '0', 'system', sysdate(), '过期待处理任务并逻辑归档至少180天前的终态任务' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_job + WHERE invoke_target = 'todoTaskMaintenanceTask.maintain()' +); + +SELECT job_id, job_name, invoke_target, cron_expression, status +FROM sys_job +WHERE invoke_target = 'todoTaskMaintenanceTask.maintain()'; diff --git a/jsowell-admin/src/test/java/com/jsowell/quartz/task/TodoTaskMaintenanceTaskTest.java b/jsowell-admin/src/test/java/com/jsowell/quartz/task/TodoTaskMaintenanceTaskTest.java new file mode 100644 index 000000000..ba0bb0624 --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/quartz/task/TodoTaskMaintenanceTaskTest.java @@ -0,0 +1,36 @@ +package com.jsowell.quartz.task; + +import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult; +import com.jsowell.system.service.TodoTaskMaintenanceService; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Date; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TodoTaskMaintenanceTaskTest { + @Test + void maintain_shouldDelegateToMaintenanceService() { + TodoTaskMaintenanceService service = mock(TodoTaskMaintenanceService.class); + TodoTaskMaintenanceTask task = new TodoTaskMaintenanceTask(); + setField(task, "todoTaskMaintenanceService", service); + when(service.maintain()).thenReturn(new TodoTaskMaintenanceResult(1, 2, new Date())); + + task.maintain(); + + verify(service).maintain(); + } + + private static void setField(Object target, String fieldName, Object value) { + try { + Field field = TodoTaskMaintenanceTask.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } +} diff --git a/jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskMaintenanceServiceImplTest.java b/jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskMaintenanceServiceImplTest.java new file mode 100644 index 000000000..9917fdee2 --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskMaintenanceServiceImplTest.java @@ -0,0 +1,51 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult; +import com.jsowell.system.mapper.SysTodoTaskMapper; +import com.jsowell.system.service.impl.TodoTaskMaintenanceServiceImpl; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.lang.reflect.Field; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TodoTaskMaintenanceServiceImplTest { + @Test + void maintain_shouldExpireAndKeepAtLeast180Days() { + SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class); + TodoTaskMaintenanceServiceImpl service = new TodoTaskMaintenanceServiceImpl(); + setField(service, "todoTaskMapper", mapper); + setField(service, "retentionDays", 30); + when(mapper.expireDueTasks(any(Date.class), any(String.class))).thenReturn(2); + when(mapper.archiveTerminalTasks(any(Date.class), any(String.class))).thenReturn(3); + + TodoTaskMaintenanceResult result = service.maintain(); + + assertEquals(2, result.getExpiredCount()); + assertEquals(3, result.getArchivedCount()); + ArgumentCaptor nowCaptor = ArgumentCaptor.forClass(Date.class); + ArgumentCaptor archiveCaptor = ArgumentCaptor.forClass(Date.class); + verify(mapper).expireDueTasks(nowCaptor.capture(), any(String.class)); + verify(mapper).archiveTerminalTasks(archiveCaptor.capture(), any(String.class)); + long retentionDays = (nowCaptor.getValue().getTime() - archiveCaptor.getValue().getTime()) + / (24L * 60 * 60 * 1000); + assertTrue(retentionDays >= 179); + } + + private static void setField(Object target, String fieldName, Object value) { + try { + Field field = TodoTaskMaintenanceServiceImpl.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } +} diff --git a/jsowell-quartz/src/main/java/com/jsowell/quartz/task/TodoTaskMaintenanceTask.java b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/TodoTaskMaintenanceTask.java new file mode 100644 index 000000000..56a75222e --- /dev/null +++ b/jsowell-quartz/src/main/java/com/jsowell/quartz/task/TodoTaskMaintenanceTask.java @@ -0,0 +1,25 @@ +package com.jsowell.quartz.task; + +import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult; +import com.jsowell.system.service.TodoTaskMaintenanceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 待办过期和历史归档定时任务。 + */ +@Component("todoTaskMaintenanceTask") +public class TodoTaskMaintenanceTask { + private static final Logger log = LoggerFactory.getLogger(TodoTaskMaintenanceTask.class); + + @Autowired + private TodoTaskMaintenanceService todoTaskMaintenanceService; + + public void maintain() { + TodoTaskMaintenanceResult result = todoTaskMaintenanceService.maintain(); + log.info("待办维护任务完成,expiredCount={}, archivedCount={}, archiveBefore={}", + result.getExpiredCount(), result.getArchivedCount(), result.getArchiveBefore()); + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskMaintenanceResult.java b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskMaintenanceResult.java new file mode 100644 index 000000000..efb46f3c5 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskMaintenanceResult.java @@ -0,0 +1,20 @@ +package com.jsowell.system.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 待办维护任务执行结果。 + */ +@Data +@AllArgsConstructor +public class TodoTaskMaintenanceResult implements Serializable { + private static final long serialVersionUID = 1L; + + private int expiredCount; + private int archivedCount; + private Date archiveBefore; +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysTodoTaskMapper.java b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysTodoTaskMapper.java index 73833a494..13f0f7cc2 100644 --- a/jsowell-system/src/main/java/com/jsowell/system/mapper/SysTodoTaskMapper.java +++ b/jsowell-system/src/main/java/com/jsowell/system/mapper/SysTodoTaskMapper.java @@ -6,6 +6,7 @@ import com.jsowell.system.domain.dto.TodoTaskQuery; import org.apache.ibatis.annotations.Param; import java.util.List; +import java.util.Date; /** * 待办任务数据层。 @@ -73,4 +74,9 @@ public interface SysTodoTaskMapper { @Param("updateBy") String updateBy); int hideTodo(@Param("todoId") Long todoId, @Param("updateBy") String updateBy); + + int expireDueTasks(@Param("now") Date now, @Param("updateBy") String updateBy); + + int archiveTerminalTasks(@Param("archiveBefore") Date archiveBefore, + @Param("updateBy") String updateBy); } diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskMaintenanceService.java b/jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskMaintenanceService.java new file mode 100644 index 000000000..4754dd811 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskMaintenanceService.java @@ -0,0 +1,10 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult; + +/** + * 待办过期和历史归档维护服务。 + */ +public interface TodoTaskMaintenanceService { + TodoTaskMaintenanceResult maintain(); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskMaintenanceServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskMaintenanceServiceImpl.java new file mode 100644 index 000000000..7dec632f4 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskMaintenanceServiceImpl.java @@ -0,0 +1,38 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.util.DateUtils; +import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult; +import com.jsowell.system.mapper.SysTodoTaskMapper; +import com.jsowell.system.service.TodoTaskMaintenanceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +/** + * 待办过期和历史归档维护服务实现。 + */ +@Service +public class TodoTaskMaintenanceServiceImpl implements TodoTaskMaintenanceService { + private static final int DEFAULT_RETENTION_DAYS = 180; + + @Value("${todo.retention-days:180}") + private Integer retentionDays; + + @Autowired + private SysTodoTaskMapper todoTaskMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public TodoTaskMaintenanceResult maintain() { + Date now = DateUtils.getNowDate(); + int safeRetentionDays = retentionDays == null || retentionDays < DEFAULT_RETENTION_DAYS + ? DEFAULT_RETENTION_DAYS : retentionDays; + int expiredCount = todoTaskMapper.expireDueTasks(now, "todo-maintenance"); + Date archiveBefore = DateUtils.addDays(now, -safeRetentionDays); + int archivedCount = todoTaskMapper.archiveTerminalTasks(archiveBefore, "todo-maintenance"); + return new TodoTaskMaintenanceResult(expiredCount, archivedCount, archiveBefore); + } +} diff --git a/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml index ba58cf2c3..3be8f27fa 100644 --- a/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml +++ b/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml @@ -287,4 +287,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and del_flag = '0' + + update sys_todo_task + set task_status = 'EXPIRED', + completed_time = #{now}, + active_idempotent_key = null, + update_by = #{updateBy}, + update_time = #{now} + where task_status in ('PENDING', 'PROCESSING') + and due_time is not null + and due_time <= #{now} + and del_flag = '0' + + + + update sys_todo_task + set del_flag = '2', + active_idempotent_key = null, + update_by = #{updateBy}, + update_time = sysdate() + where task_status in ('COMPLETED', 'CANCELLED', 'EXPIRED') + and coalesce(completed_time, update_time, create_time) < #{archiveBefore} + and del_flag = '0' + +