From 558949f4e14bb9e2486ef1b5effed61df97416ab Mon Sep 17 00:00:00 2001 From: jsowell <123@jsowell.com> Date: Fri, 17 Jul 2026 18:00:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BE=85=E5=8A=9E=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=AB=AF=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E8=83=BD?= =?UTF-8?q?=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/sql/sys_todo_menu.sql | 41 ++++ .../system/SysTodoTaskAdminController.java | 74 ++++++ .../service/TodoTaskAdminServiceImplTest.java | 146 ++++++++++++ .../SysTodoTaskAdminControllerTest.java | 73 ++++++ .../dto/TodoTaskAdminCreateRequest.java | 69 ++++++ .../system/domain/dto/TodoTaskAdminQuery.java | 21 ++ .../domain/dto/TodoTaskAssignRequest.java | 23 ++ .../system/mapper/SysTodoTaskMapper.java | 11 + .../system/service/TodoTaskAdminService.java | 23 ++ .../impl/TodoTaskAdminServiceImpl.java | 213 ++++++++++++++++++ .../mapper/system/SysTodoTaskMapper.xml | 59 +++++ 11 files changed, 753 insertions(+) create mode 100644 jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysTodoTaskAdminController.java create mode 100644 jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskAdminServiceImplTest.java create mode 100644 jsowell-admin/src/test/java/com/jsowell/web/controller/system/SysTodoTaskAdminControllerTest.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminCreateRequest.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminQuery.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAssignRequest.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskAdminService.java create mode 100644 jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskAdminServiceImpl.java diff --git a/docs/sql/sys_todo_menu.sql b/docs/sql/sys_todo_menu.sql index 2754f2fca..a5fd8a1a2 100644 --- a/docs/sql/sys_todo_menu.sql +++ b/docs/sql/sys_todo_menu.sql @@ -134,6 +134,47 @@ WHERE role_info.status = '0' -- INSERT INTO sys_role_menu (role_id, menu_id) VALUES (<业务处理角色ID>, @todoCompleteMenuId); -- INSERT INTO sys_role_menu (role_id, menu_id) VALUES (<任务关闭角色ID>, @todoCancelMenuId); +-- 管理端异常任务处理权限,仅默认授予 role_key = 'admin' 的系统管理员。 +INSERT INTO sys_menu ( + menu_name, parent_id, order_num, path, component, is_frame, is_cache, + menu_type, visible, status, perms, icon, create_by, create_time, remark +) +SELECT permission_info.menu_name, @todoMenuId, permission_info.order_num, '#', '', 1, 0, + 'F', '0', '0', permission_info.perms, '#', 'system', sysdate(), permission_info.remark +FROM ( + SELECT '待办管理查询' AS menu_name, 11 AS order_num, 'system:todo:admin:list' AS perms, '查看全量待办' AS remark + UNION ALL SELECT '待办人工创建', 12, 'system:todo:admin:create', '人工补发异常待办' + UNION ALL SELECT '待办重新分配', 13, 'system:todo:admin:assign', '重新分配有效待办' + UNION ALL SELECT '待办管理取消', 14, 'system:todo:admin:cancel', '管理员关闭异常待办' + UNION ALL SELECT '待办逻辑隐藏', 15, 'system:todo:admin:remove', '隐藏终态待办记录' +) permission_info +WHERE NOT EXISTS ( + SELECT 1 FROM sys_menu existing_menu + WHERE existing_menu.parent_id = @todoMenuId + AND existing_menu.perms = permission_info.perms +); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT role_info.role_id, menu_info.menu_id +FROM sys_role role_info +JOIN sys_menu menu_info + ON menu_info.parent_id = @todoMenuId + AND menu_info.perms IN ( + 'system:todo:admin:list', + 'system:todo:admin:create', + 'system:todo:admin:assign', + 'system:todo:admin:cancel', + 'system:todo:admin:remove' + ) +WHERE role_info.role_key = 'admin' + AND role_info.status = '0' + AND role_info.del_flag = '0' + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu relation + WHERE relation.role_id = role_info.role_id + AND relation.menu_id = menu_info.menu_id + ); + SELECT @todoMenuId AS todo_menu_id, @todoQueryMenuId AS todo_query_menu_id, diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysTodoTaskAdminController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysTodoTaskAdminController.java new file mode 100644 index 000000000..843f853ad --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/system/SysTodoTaskAdminController.java @@ -0,0 +1,74 @@ +package com.jsowell.web.controller.system; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.core.page.TableDataInfo; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest; +import com.jsowell.system.domain.dto.TodoTaskAdminQuery; +import com.jsowell.system.domain.dto.TodoTaskAssignRequest; +import com.jsowell.system.domain.dto.TodoTaskCancelRequest; +import com.jsowell.system.service.TodoTaskAdminService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 待办管理端接口。 + */ +@RestController +@RequestMapping("/system/todo/admin") +public class SysTodoTaskAdminController extends BaseController { + @Autowired + private TodoTaskAdminService todoTaskAdminService; + + @PreAuthorize("@ss.hasPermi('system:todo:admin:list')") + @GetMapping("/list") + public TableDataInfo list(TodoTaskAdminQuery query) { + startPage(); + List list = todoTaskAdminService.list(query); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('system:todo:admin:create')") + @Log(title = "待办管理", businessType = BusinessType.INSERT) + @PostMapping("/create") + public AjaxResult create(@Validated @RequestBody TodoTaskAdminCreateRequest request) { + return AjaxResult.success(todoTaskAdminService.create(request, getUsername())); + } + + @PreAuthorize("@ss.hasPermi('system:todo:admin:assign')") + @Log(title = "待办管理", businessType = BusinessType.UPDATE) + @PutMapping("/{todoId}/assign") + public AjaxResult assign(@PathVariable Long todoId, + @Validated @RequestBody TodoTaskAssignRequest request) { + return toAjax(todoTaskAdminService.assign(todoId, request, getUsername())); + } + + @PreAuthorize("@ss.hasPermi('system:todo:admin:cancel')") + @Log(title = "待办管理", businessType = BusinessType.UPDATE) + @PutMapping("/{todoId}/cancel") + public AjaxResult cancel(@PathVariable Long todoId, + @Validated @RequestBody TodoTaskCancelRequest request) { + return toAjax(todoTaskAdminService.cancel(todoId, getUserId(), getUsername(), request.getReason())); + } + + @PreAuthorize("@ss.hasPermi('system:todo:admin:remove')") + @Log(title = "待办管理", businessType = BusinessType.DELETE) + @DeleteMapping("/{todoId}") + public AjaxResult hide(@PathVariable Long todoId) { + return toAjax(todoTaskAdminService.hide(todoId, getUsername())); + } +} diff --git a/jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskAdminServiceImplTest.java b/jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskAdminServiceImplTest.java new file mode 100644 index 000000000..020bf8bdc --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/system/service/TodoTaskAdminServiceImplTest.java @@ -0,0 +1,146 @@ +package com.jsowell.system.service; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.system.constant.TodoTaskConstants; +import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest; +import com.jsowell.system.domain.dto.TodoTaskAdminQuery; +import com.jsowell.system.domain.dto.TodoTaskAssignRequest; +import com.jsowell.system.domain.dto.TodoTaskCreateCommand; +import com.jsowell.system.mapper.SysTodoTaskMapper; +import com.jsowell.system.mapper.SysUserMapper; +import com.jsowell.system.service.impl.TodoTaskAdminServiceImpl; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.lang.reflect.Field; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +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 TodoTaskAdminServiceImplTest { + @Test + void list_shouldUseAdminQueryWithoutForcingAssignee() { + Dependencies dependencies = new Dependencies(); + TodoTaskAdminServiceImpl service = dependencies.createService(); + TodoTaskAdminQuery query = new TodoTaskAdminQuery(); + query.setTaskStatus(TodoTaskConstants.STATUS_ACTIVE); + when(dependencies.todoTaskMapper.selectAdminTodoList(query)) + .thenReturn(Collections.singletonList(new SysTodoTask())); + + assertEquals(1, service.list(query).size()); + verify(dependencies.todoTaskMapper).selectAdminTodoList(query); + } + + @Test + void create_shouldValidateUserAndGenerateIdempotentKey() { + Dependencies dependencies = new Dependencies(); + TodoTaskAdminServiceImpl service = dependencies.createService(); + when(dependencies.userMapper.selectUserById(8L)).thenReturn(activeUser(8L)); + when(dependencies.todoTaskService.createTask(any(TodoTaskCreateCommand.class))) + .thenReturn(new SysTodoTask()); + TodoTaskAdminCreateRequest request = createRequest(); + + service.create(request, "admin"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TodoTaskCreateCommand.class); + verify(dependencies.todoTaskService).createTask(captor.capture()); + assertEquals("MANUAL_TASK:MANUAL_BUSINESS:B100:8", captor.getValue().getIdempotentKey()); + assertEquals("admin", captor.getValue().getCreateBy()); + } + + @Test + void assign_shouldUpdateActiveTaskAndResetAssignmentKey() { + Dependencies dependencies = new Dependencies(); + TodoTaskAdminServiceImpl service = dependencies.createService(); + when(dependencies.userMapper.selectUserById(9L)).thenReturn(activeUser(9L)); + SysTodoTask task = task(TodoTaskConstants.STATUS_PENDING); + when(dependencies.todoTaskMapper.selectTodoById(20L)).thenReturn(task); + when(dependencies.todoTaskMapper.assignTodo(20L, 9L, "100", + "MANUAL_TASK:MANUAL_BUSINESS:B100:9", "admin")).thenReturn(1); + TodoTaskAssignRequest request = new TodoTaskAssignRequest(); + request.setAssigneeUserId(9L); + request.setAssigneeMerchantId("100"); + + assertTrue(service.assign(20L, request, "admin")); + + verify(dependencies.todoTaskMapper).assignTodo(20L, 9L, "100", + "MANUAL_TASK:MANUAL_BUSINESS:B100:9", "admin"); + } + + @Test + void hide_shouldOnlyAllowTerminalTask() { + Dependencies dependencies = new Dependencies(); + TodoTaskAdminServiceImpl service = dependencies.createService(); + when(dependencies.todoTaskMapper.selectTodoById(20L)) + .thenReturn(task(TodoTaskConstants.STATUS_PENDING)); + + assertThrows(ServiceException.class, () -> service.hide(20L, "admin")); + + when(dependencies.todoTaskMapper.selectTodoById(21L)) + .thenReturn(task(TodoTaskConstants.STATUS_CANCELLED)); + when(dependencies.todoTaskMapper.hideTodo(21L, "admin")).thenReturn(1); + assertTrue(service.hide(21L, "admin")); + } + + private static TodoTaskAdminCreateRequest createRequest() { + TodoTaskAdminCreateRequest request = new TodoTaskAdminCreateRequest(); + request.setTaskType("MANUAL_TASK"); + request.setTitle("人工待办"); + request.setBusinessType("MANUAL_BUSINESS"); + request.setBusinessId("B100"); + request.setRouteName("TodoCenter"); + request.setAssigneeUserId(8L); + return request; + } + + private static SysTodoTask task(String status) { + SysTodoTask task = new SysTodoTask(); + task.setTodoId(20L); + task.setTaskType("MANUAL_TASK"); + task.setBusinessType("MANUAL_BUSINESS"); + task.setBusinessId("B100"); + task.setTaskStatus(status); + return task; + } + + private static SysUser activeUser(Long userId) { + SysUser user = new SysUser(); + user.setUserId(userId); + user.setStatus(Constants.ZERO); + user.setDelFlag(Constants.ZERO); + return user; + } + + private static void setField(Object target, String fieldName, Object value) { + try { + Field field = TodoTaskAdminServiceImpl.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + private static class Dependencies { + private final SysTodoTaskMapper todoTaskMapper = mock(SysTodoTaskMapper.class); + private final SysUserMapper userMapper = mock(SysUserMapper.class); + private final TodoTaskService todoTaskService = mock(TodoTaskService.class); + + private TodoTaskAdminServiceImpl createService() { + TodoTaskAdminServiceImpl service = new TodoTaskAdminServiceImpl(); + setField(service, "todoTaskMapper", todoTaskMapper); + setField(service, "userMapper", userMapper); + setField(service, "todoTaskService", todoTaskService); + return service; + } + } +} diff --git a/jsowell-admin/src/test/java/com/jsowell/web/controller/system/SysTodoTaskAdminControllerTest.java b/jsowell-admin/src/test/java/com/jsowell/web/controller/system/SysTodoTaskAdminControllerTest.java new file mode 100644 index 000000000..ca6fd3781 --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/web/controller/system/SysTodoTaskAdminControllerTest.java @@ -0,0 +1,73 @@ +package com.jsowell.web.controller.system; + +import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest; +import com.jsowell.system.domain.dto.TodoTaskCancelRequest; +import com.jsowell.system.service.TodoTaskAdminService; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class SysTodoTaskAdminControllerTest { + @Test + void create_shouldPassCurrentOperator() { + TodoTaskAdminService service = mock(TodoTaskAdminService.class); + TestController controller = newController(service, 1L, "admin"); + TodoTaskAdminCreateRequest request = new TodoTaskAdminCreateRequest(); + when(service.create(request, "admin")).thenReturn(new SysTodoTask()); + + controller.create(request); + + verify(service).create(request, "admin"); + } + + @Test + void cancel_shouldPassCurrentUserAndOperator() { + TodoTaskAdminService service = mock(TodoTaskAdminService.class); + TestController controller = newController(service, 1L, "admin"); + TodoTaskCancelRequest request = new TodoTaskCancelRequest(); + request.setReason("异常任务关闭"); + when(service.cancel(20L, 1L, "admin", "异常任务关闭")).thenReturn(true); + + controller.cancel(20L, request); + + verify(service).cancel(20L, 1L, "admin", "异常任务关闭"); + } + + private static TestController newController(TodoTaskAdminService service, + Long userId, String username) { + TestController controller = new TestController(userId, username); + try { + Field field = SysTodoTaskAdminController.class.getDeclaredField("todoTaskAdminService"); + field.setAccessible(true); + field.set(controller, service); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + return controller; + } + + private static class TestController extends SysTodoTaskAdminController { + private final Long userId; + private final String username; + + private TestController(Long userId, String username) { + this.userId = userId; + this.username = username; + } + + @Override + public Long getUserId() { + return userId; + } + + @Override + public String getUsername() { + return username; + } + } +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminCreateRequest.java b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminCreateRequest.java new file mode 100644 index 000000000..b7afa4bfa --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminCreateRequest.java @@ -0,0 +1,69 @@ +package com.jsowell.system.domain.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Date; + +/** + * 管理端人工创建待办请求。 + */ +@Data +public class TodoTaskAdminCreateRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @NotBlank(message = "待办类型不能为空") + @Size(max = 64, message = "待办类型不能超过64个字符") + private String taskType; + + @NotBlank(message = "待办标题不能为空") + @Size(max = 200, message = "待办标题不能超过200个字符") + private String title; + + @Size(max = 500, message = "待办摘要不能超过500个字符") + private String summary; + + @Size(max = 20000, message = "待办内容不能超过20000个字符") + private String content; + + @NotBlank(message = "业务类型不能为空") + @Size(max = 64, message = "业务类型不能超过64个字符") + private String businessType; + + @NotBlank(message = "业务主键不能为空") + @Size(max = 64, message = "业务主键不能超过64个字符") + private String businessId; + + @NotBlank(message = "业务路由不能为空") + @Size(max = 100, message = "业务路由不能超过100个字符") + private String routeName; + + @Size(max = 1000, message = "路由参数不能超过1000个字符") + private String routeParams; + + @NotNull(message = "接收用户不能为空") + @Min(value = 1, message = "接收用户不能为空") + private Long assigneeUserId; + + @Size(max = 64, message = "运营商 ID 不能超过64个字符") + private String assigneeMerchantId; + + @Min(value = 1, message = "优先级只能为1、2或3") + @Max(value = 3, message = "优先级只能为1、2或3") + private Integer priority; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date dueTime; + + @Size(max = 200, message = "幂等键不能超过200个字符") + private String idempotentKey; + + @Size(max = 500, message = "备注不能超过500个字符") + private String remark; +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminQuery.java b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminQuery.java new file mode 100644 index 000000000..f3c46f64e --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAdminQuery.java @@ -0,0 +1,21 @@ +package com.jsowell.system.domain.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 管理端待办查询条件。 + */ +@Data +public class TodoTaskAdminQuery implements Serializable { + private static final long serialVersionUID = 1L; + + private Long assigneeUserId; + private String assigneeMerchantId; + private String taskStatus; + private String readStatus; + private String taskType; + private String keyword; + private Boolean hideCompleted; +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAssignRequest.java b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAssignRequest.java new file mode 100644 index 000000000..e09595ded --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/domain/dto/TodoTaskAssignRequest.java @@ -0,0 +1,23 @@ +package com.jsowell.system.domain.dto; + +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * 管理端待办分配请求。 + */ +@Data +public class TodoTaskAssignRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @NotNull(message = "接收用户不能为空") + @Min(value = 1, message = "接收用户不能为空") + private Long assigneeUserId; + + @Size(max = 64, message = "运营商 ID 不能超过64个字符") + private String assigneeMerchantId; +} 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 b72fc1c08..73833a494 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 @@ -1,6 +1,7 @@ package com.jsowell.system.mapper; import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskAdminQuery; import com.jsowell.system.domain.dto.TodoTaskQuery; import org.apache.ibatis.annotations.Param; @@ -21,6 +22,8 @@ public interface SysTodoTaskMapper { List selectTodoList(TodoTaskQuery query); + List selectAdminTodoList(TodoTaskAdminQuery query); + List selectHomeList(@Param("assigneeUserId") Long assigneeUserId, @Param("limit") int limit); @@ -62,4 +65,12 @@ public interface SysTodoTaskMapper { @Param("assigneeUserId") Long assigneeUserId, @Param("reason") String reason, @Param("updateBy") String updateBy); + + int assignTodo(@Param("todoId") Long todoId, + @Param("assigneeUserId") Long assigneeUserId, + @Param("assigneeMerchantId") String assigneeMerchantId, + @Param("activeIdempotentKey") String activeIdempotentKey, + @Param("updateBy") String updateBy); + + int hideTodo(@Param("todoId") Long todoId, @Param("updateBy") String updateBy); } diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskAdminService.java b/jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskAdminService.java new file mode 100644 index 000000000..0497eb0e7 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/TodoTaskAdminService.java @@ -0,0 +1,23 @@ +package com.jsowell.system.service; + +import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest; +import com.jsowell.system.domain.dto.TodoTaskAdminQuery; +import com.jsowell.system.domain.dto.TodoTaskAssignRequest; + +import java.util.List; + +/** + * 待办管理端服务。 + */ +public interface TodoTaskAdminService { + List list(TodoTaskAdminQuery query); + + SysTodoTask create(TodoTaskAdminCreateRequest request, String operatorName); + + boolean assign(Long todoId, TodoTaskAssignRequest request, String operatorName); + + boolean cancel(Long todoId, Long operatorId, String operatorName, String reason); + + boolean hide(Long todoId, String operatorName); +} diff --git a/jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskAdminServiceImpl.java b/jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskAdminServiceImpl.java new file mode 100644 index 000000000..c1d4f9e95 --- /dev/null +++ b/jsowell-system/src/main/java/com/jsowell/system/service/impl/TodoTaskAdminServiceImpl.java @@ -0,0 +1,213 @@ +package com.jsowell.system.service.impl; + +import com.jsowell.common.constant.Constants; +import com.jsowell.common.core.domain.entity.SysUser; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.StringUtils; +import com.jsowell.system.constant.TodoTaskConstants; +import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest; +import com.jsowell.system.domain.dto.TodoTaskAdminQuery; +import com.jsowell.system.domain.dto.TodoTaskAssignRequest; +import com.jsowell.system.domain.dto.TodoTaskCreateCommand; +import com.jsowell.system.mapper.SysTodoTaskMapper; +import com.jsowell.system.mapper.SysUserMapper; +import com.jsowell.system.service.TodoTaskAdminService; +import com.jsowell.system.service.TodoTaskService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +/** + * 待办管理端服务实现。 + */ +@Service +public class TodoTaskAdminServiceImpl implements TodoTaskAdminService { + @Autowired + private SysTodoTaskMapper todoTaskMapper; + + @Autowired + private SysUserMapper userMapper; + + @Autowired + private TodoTaskService todoTaskService; + + @Override + public List list(TodoTaskAdminQuery query) { + TodoTaskAdminQuery safeQuery = query == null ? new TodoTaskAdminQuery() : query; + validateQuery(safeQuery); + return todoTaskMapper.selectAdminTodoList(safeQuery); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public SysTodoTask create(TodoTaskAdminCreateRequest request, String operatorName) { + if (request == null) { + throw new ServiceException("待办创建参数不能为空"); + } + validateActiveUser(request.getAssigneeUserId()); + String idempotentKey = StringUtils.isNotBlank(request.getIdempotentKey()) + ? request.getIdempotentKey().trim() + : buildAssignmentKey(request.getTaskType(), request.getBusinessType(), + request.getBusinessId(), request.getAssigneeUserId()); + return todoTaskService.createTask(TodoTaskCreateCommand.builder() + .taskType(request.getTaskType()) + .title(request.getTitle()) + .summary(request.getSummary()) + .content(request.getContent()) + .businessType(request.getBusinessType()) + .businessId(request.getBusinessId()) + .routeName(request.getRouteName()) + .routeParams(request.getRouteParams()) + .assigneeUserId(request.getAssigneeUserId()) + .assigneeMerchantId(request.getAssigneeMerchantId()) + .priority(request.getPriority()) + .dueTime(request.getDueTime()) + .idempotentKey(idempotentKey) + .createBy(normalizeOperator(operatorName)) + .remark(request.getRemark()) + .build()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean assign(Long todoId, TodoTaskAssignRequest request, String operatorName) { + if (request == null) { + throw new ServiceException("待办分配参数不能为空"); + } + validateTodoId(todoId); + validateActiveUser(request.getAssigneeUserId()); + SysTodoTask todoTask = requireTask(todoId); + if (!TodoTaskConstants.isActiveStatus(todoTask.getTaskStatus())) { + throw new ServiceException("只有待处理任务可以重新分配"); + } + String activeIdempotentKey = buildAssignmentKey(todoTask.getTaskType(), + todoTask.getBusinessType(), todoTask.getBusinessId(), request.getAssigneeUserId()); + String assigneeMerchantId = StringUtils.isBlank(request.getAssigneeMerchantId()) + ? todoTask.getAssigneeMerchantId() + : normalizeOptional(request.getAssigneeMerchantId(), 64); + try { + int rows = todoTaskMapper.assignTodo(todoId, request.getAssigneeUserId(), assigneeMerchantId, + activeIdempotentKey, + normalizeOperator(operatorName)); + if (rows > 0) { + return true; + } + } catch (DuplicateKeyException exception) { + throw new ServiceException("目标用户已存在同一业务的有效待办"); + } + throw new ServiceException("当前待办状态不允许重新分配"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean cancel(Long todoId, Long operatorId, String operatorName, String reason) { + return todoTaskService.cancel(todoId, operatorId, operatorName, reason); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean hide(Long todoId, String operatorName) { + validateTodoId(todoId); + SysTodoTask todoTask = requireTask(todoId); + if (!TodoTaskConstants.isTerminalStatus(todoTask.getTaskStatus())) { + throw new ServiceException("待处理任务需先取消后才能隐藏"); + } + if (todoTaskMapper.hideTodo(todoId, normalizeOperator(operatorName)) > 0) { + return true; + } + throw new ServiceException("当前待办状态不允许隐藏"); + } + + private void validateQuery(TodoTaskAdminQuery query) { + if (query.getAssigneeUserId() != null && query.getAssigneeUserId() <= 0) { + throw new ServiceException("接收用户 ID 不合法"); + } + if (StringUtils.isNotEmpty(query.getTaskStatus()) + && !TodoTaskConstants.isSupportedStatus(query.getTaskStatus())) { + throw new ServiceException("不支持的待办状态筛选条件"); + } + if (StringUtils.isNotEmpty(query.getReadStatus()) + && !TodoTaskConstants.READ_UNREAD.equals(query.getReadStatus()) + && !TodoTaskConstants.READ_READ.equals(query.getReadStatus())) { + throw new ServiceException("不支持的阅读状态筛选条件"); + } + query.setAssigneeMerchantId(normalizeOptional(query.getAssigneeMerchantId(), 64)); + query.setTaskType(normalizeOptional(query.getTaskType(), 64)); + query.setKeyword(normalizeOptional(query.getKeyword(), 100)); + } + + private SysTodoTask requireTask(Long todoId) { + SysTodoTask todoTask = todoTaskMapper.selectTodoById(todoId); + if (todoTask == null) { + throw new ServiceException("待办任务不存在"); + } + return todoTask; + } + + private void validateActiveUser(Long userId) { + if (userId == null || userId <= 0) { + throw new ServiceException("接收用户不能为空"); + } + SysUser user = userMapper.selectUserById(userId); + if (user == null || !Constants.ZERO.equals(user.getStatus()) + || !Constants.ZERO.equals(user.getDelFlag())) { + throw new ServiceException("接收用户不存在或已停用"); + } + } + + private void validateTodoId(Long todoId) { + if (todoId == null || todoId <= 0) { + throw new ServiceException("待办任务 ID 不能为空"); + } + } + + private String buildAssignmentKey(String taskType, String businessType, + String businessId, Long assigneeUserId) { + String source = StringUtils.trim(taskType) + ":" + StringUtils.trim(businessType) + + ":" + StringUtils.trim(businessId) + ":" + assigneeUserId; + if (source.length() <= 200) { + return source; + } + return "ADMIN_ASSIGN:" + sha256(source); + } + + private String sha256(String value) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + StringBuilder result = new StringBuilder(bytes.length * 2); + for (byte item : bytes) { + result.append(String.format("%02x", item & 0xff)); + } + return result.toString(); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("当前运行环境不支持 SHA-256", exception); + } + } + + private String normalizeOptional(String value, int maxLength) { + if (StringUtils.isEmpty(value)) { + return null; + } + String safeValue = value.trim(); + if (safeValue.length() > maxLength) { + throw new ServiceException("查询条件长度不能超过" + maxLength + "个字符"); + } + return safeValue; + } + + private String normalizeOperator(String operatorName) { + String safeOperator = StringUtils.isNotEmpty(operatorName) ? operatorName.trim() : "system"; + if (safeOperator.length() > 64) { + throw new ServiceException("操作人名称不能超过64个字符"); + } + return safeOperator; + } +} diff --git a/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml b/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml index 6d2fbd578..ba58cf2c3 100644 --- a/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml +++ b/jsowell-system/src/main/resources/mapper/system/SysTodoTaskMapper.xml @@ -93,6 +93,40 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by priority desc, create_time desc, todo_id desc + +