新增待办管理端异常处理能力

This commit is contained in:
jsowell
2026-07-17 18:00:02 +08:00
parent 27ed7e717b
commit 558949f4e1
11 changed files with 753 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@@ -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<SysTodoTask> selectTodoList(TodoTaskQuery query);
List<SysTodoTask> selectAdminTodoList(TodoTaskAdminQuery query);
List<SysTodoTask> 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);
}

View File

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

View File

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

View File

@@ -93,6 +93,40 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by priority desc, create_time desc, todo_id desc
</select>
<select id="selectAdminTodoList" parameterType="com.jsowell.system.domain.dto.TodoTaskAdminQuery"
resultMap="SysTodoTaskResult">
<include refid="selectTodoVo" />
<where>
del_flag = '0'
<if test="assigneeUserId != null">
and assignee_user_id = #{assigneeUserId}
</if>
<if test="assigneeMerchantId != null and assigneeMerchantId != ''">
and assignee_merchant_id = #{assigneeMerchantId}
</if>
<if test="taskStatus != null and taskStatus == 'ACTIVE'">
and task_status in ('PENDING', 'PROCESSING')
</if>
<if test="taskStatus != null and taskStatus != '' and taskStatus != 'ACTIVE'">
and task_status = #{taskStatus}
</if>
<if test="readStatus != null and readStatus != ''">
and read_status = #{readStatus}
</if>
<if test="taskType != null and taskType != ''">
and task_type = #{taskType}
</if>
<if test="keyword != null and keyword != ''">
and (title like concat('%', #{keyword}, '%')
or business_id like concat('%', #{keyword}, '%'))
</if>
<if test="hideCompleted != null and hideCompleted">
and task_status != 'COMPLETED'
</if>
</where>
order by priority desc, create_time desc, todo_id desc
</select>
<select id="selectHomeList" resultMap="SysTodoTaskResult">
<include refid="selectTodoVo" />
where assignee_user_id = #{assigneeUserId}
@@ -228,4 +262,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and del_flag = '0'
</update>
<update id="assignTodo">
update sys_todo_task
set assignee_user_id = #{assigneeUserId},
assignee_merchant_id = #{assigneeMerchantId},
read_status = '0',
read_time = null,
active_idempotent_key = #{activeIdempotentKey},
update_by = #{updateBy},
update_time = sysdate()
where todo_id = #{todoId}
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
</update>
<update id="hideTodo">
update sys_todo_task
set active_idempotent_key = null,
del_flag = '2',
update_by = #{updateBy},
update_time = sysdate()
where todo_id = #{todoId}
and task_status in ('COMPLETED', 'CANCELLED', 'EXPIRED')
and del_flag = '0'
</update>
</mapper>