新增待办任务通用领域服务

This commit is contained in:
jsowell
2026-07-16 16:11:38 +08:00
parent e65017ab22
commit 181d4a5b40
9 changed files with 757 additions and 4 deletions

View File

@@ -0,0 +1,44 @@
package com.jsowell.system.constant;
/**
* 待办任务常量。
*
* @author jsowell
*/
public final class TodoTaskConstants {
private TodoTaskConstants() {
}
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_PROCESSING = "PROCESSING";
public static final String STATUS_COMPLETED = "COMPLETED";
public static final String STATUS_CANCELLED = "CANCELLED";
public static final String STATUS_EXPIRED = "EXPIRED";
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String READ_UNREAD = "0";
public static final String READ_READ = "1";
public static final int PRIORITY_NORMAL = 1;
public static final int PRIORITY_IMPORTANT = 2;
public static final int PRIORITY_URGENT = 3;
public static final int DEFAULT_HOME_LIMIT = 3;
public static final int MAX_HOME_LIMIT = 10;
public static boolean isActiveStatus(String status) {
return STATUS_PENDING.equals(status) || STATUS_PROCESSING.equals(status);
}
public static boolean isTerminalStatus(String status) {
return STATUS_COMPLETED.equals(status)
|| STATUS_CANCELLED.equals(status)
|| STATUS_EXPIRED.equals(status);
}
public static boolean isSupportedStatus(String status) {
return STATUS_ACTIVE.equals(status)
|| isActiveStatus(status)
|| isTerminalStatus(status);
}
}

View File

@@ -0,0 +1,47 @@
package com.jsowell.system.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.jsowell.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* 用户待办任务表 sys_todo_task。
*
* @author jsowell
*/
@Data
public class SysTodoTask extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long todoId;
private String taskType;
private String title;
private String summary;
private String content;
private String businessType;
private String businessId;
private String routeName;
private String routeParams;
private Long assigneeUserId;
private String assigneeMerchantId;
private Integer priority;
private String taskStatus;
private String readStatus;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date readTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date dueTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date completedTime;
private Long completedBy;
private String cancelReason;
private String idempotentKey;
private String activeIdempotentKey;
private String delFlag;
}

View File

@@ -0,0 +1,38 @@
package com.jsowell.system.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 创建待办任务命令。
*
* @author jsowell
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TodoTaskCreateCommand implements Serializable {
private static final long serialVersionUID = 1L;
private String taskType;
private String title;
private String summary;
private String content;
private String businessType;
private String businessId;
private String routeName;
private String routeParams;
private Long assigneeUserId;
private String assigneeMerchantId;
private Integer priority;
private Date dueTime;
private String idempotentKey;
private String createBy;
private String remark;
}

View File

@@ -0,0 +1,23 @@
package com.jsowell.system.domain.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 当前用户待办查询条件。
*
* @author jsowell
*/
@Data
public class TodoTaskQuery implements Serializable {
private static final long serialVersionUID = 1L;
/** 由服务端覆盖,不信任请求参数。 */
private Long assigneeUserId;
private String taskStatus;
private String readStatus;
private String taskType;
private String keyword;
private Boolean hideCompleted;
}

View File

@@ -0,0 +1,59 @@
package com.jsowell.system.mapper;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.domain.dto.TodoTaskQuery;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 待办任务数据层。
*
* @author jsowell
*/
public interface SysTodoTaskMapper {
SysTodoTask selectTodoById(@Param("todoId") Long todoId);
SysTodoTask selectTodoByIdForAssignee(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId);
SysTodoTask selectActiveByIdempotentKey(@Param("activeIdempotentKey") String activeIdempotentKey);
List<SysTodoTask> selectTodoList(TodoTaskQuery query);
List<SysTodoTask> selectHomeList(@Param("assigneeUserId") Long assigneeUserId,
@Param("limit") int limit);
int countActive(@Param("assigneeUserId") Long assigneeUserId);
int countUnread(@Param("assigneeUserId") Long assigneeUserId);
int insertTodoTask(SysTodoTask todoTask);
int markTodoRead(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("updateBy") String updateBy);
int markAllRead(@Param("assigneeUserId") Long assigneeUserId,
@Param("updateBy") String updateBy);
int completeTodoForAssignee(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("completedBy") Long completedBy,
@Param("updateBy") String updateBy);
int completeTodoByBusiness(@Param("businessType") String businessType,
@Param("businessId") String businessId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("completedBy") Long completedBy,
@Param("updateBy") String updateBy);
int cancelTodoForAssignee(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("reason") String reason,
@Param("updateBy") String updateBy);
int cancelTodo(@Param("todoId") Long todoId,
@Param("reason") String reason,
@Param("updateBy") String updateBy);
}

View File

@@ -0,0 +1,39 @@
package com.jsowell.system.service;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
import com.jsowell.system.domain.dto.TodoTaskQuery;
import java.util.List;
/**
* 统一待办任务服务。
*
* @author jsowell
*/
public interface TodoTaskService {
SysTodoTask createTask(TodoTaskCreateCommand command);
SysTodoTask getForAssignee(Long todoId, Long assigneeUserId);
List<SysTodoTask> listForAssignee(TodoTaskQuery query, Long assigneeUserId);
List<SysTodoTask> homeList(Long assigneeUserId, Integer limit);
int countActive(Long assigneeUserId);
int countUnread(Long assigneeUserId);
boolean markRead(Long todoId, Long assigneeUserId, String operatorName);
int markAllRead(Long assigneeUserId, String operatorName);
boolean complete(Long todoId, Long assigneeUserId, String operatorName);
int completeByBusiness(String businessType, String businessId, Long assigneeUserId,
Long operatorId, String operatorName);
boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason);
boolean cancel(Long todoId, Long operatorId, String operatorName, String reason);
}

View File

@@ -0,0 +1,286 @@
package com.jsowell.system.service.impl;
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.TodoTaskCreateCommand;
import com.jsowell.system.domain.dto.TodoTaskQuery;
import com.jsowell.system.mapper.SysTodoTaskMapper;
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.util.List;
/**
* 统一待办任务服务实现。
*
* @author jsowell
*/
@Service
public class TodoTaskServiceImpl implements TodoTaskService {
private static final String SYSTEM_OPERATOR = "system";
@Autowired
private SysTodoTaskMapper todoTaskMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public SysTodoTask createTask(TodoTaskCreateCommand command) {
if (command == null) {
throw new ServiceException("待办创建参数不能为空");
}
SysTodoTask todoTask = buildTask(command);
SysTodoTask existingTask = todoTaskMapper.selectActiveByIdempotentKey(todoTask.getActiveIdempotentKey());
if (existingTask != null) {
return existingTask;
}
try {
todoTaskMapper.insertTodoTask(todoTask);
return todoTask;
} catch (DuplicateKeyException exception) {
existingTask = todoTaskMapper.selectActiveByIdempotentKey(todoTask.getActiveIdempotentKey());
if (existingTask != null) {
return existingTask;
}
throw exception;
}
}
@Override
public SysTodoTask getForAssignee(Long todoId, Long assigneeUserId) {
validateIdentity(todoId, assigneeUserId);
SysTodoTask todoTask = todoTaskMapper.selectTodoByIdForAssignee(todoId, assigneeUserId);
if (todoTask == null) {
throw new ServiceException("待办任务不存在或无权访问");
}
return todoTask;
}
@Override
public List<SysTodoTask> listForAssignee(TodoTaskQuery query, Long assigneeUserId) {
validateUserId(assigneeUserId);
TodoTaskQuery safeQuery = query == null ? new TodoTaskQuery() : query;
validateQuery(safeQuery);
safeQuery.setAssigneeUserId(assigneeUserId);
return todoTaskMapper.selectTodoList(safeQuery);
}
@Override
public List<SysTodoTask> homeList(Long assigneeUserId, Integer limit) {
validateUserId(assigneeUserId);
int safeLimit = limit == null ? TodoTaskConstants.DEFAULT_HOME_LIMIT : limit;
if (safeLimit < 1) {
safeLimit = TodoTaskConstants.DEFAULT_HOME_LIMIT;
}
if (safeLimit > TodoTaskConstants.MAX_HOME_LIMIT) {
safeLimit = TodoTaskConstants.MAX_HOME_LIMIT;
}
return todoTaskMapper.selectHomeList(assigneeUserId, safeLimit);
}
@Override
public int countActive(Long assigneeUserId) {
validateUserId(assigneeUserId);
return todoTaskMapper.countActive(assigneeUserId);
}
@Override
public int countUnread(Long assigneeUserId) {
validateUserId(assigneeUserId);
return todoTaskMapper.countUnread(assigneeUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean markRead(Long todoId, Long assigneeUserId, String operatorName) {
validateIdentity(todoId, assigneeUserId);
int rows = todoTaskMapper.markTodoRead(todoId, assigneeUserId, normalizeOperator(operatorName, assigneeUserId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
return TodoTaskConstants.READ_READ.equals(todoTask.getReadStatus());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int markAllRead(Long assigneeUserId, String operatorName) {
validateUserId(assigneeUserId);
return todoTaskMapper.markAllRead(assigneeUserId, normalizeOperator(operatorName, assigneeUserId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean complete(Long todoId, Long assigneeUserId, String operatorName) {
validateIdentity(todoId, assigneeUserId);
int rows = todoTaskMapper.completeTodoForAssignee(todoId, assigneeUserId, assigneeUserId,
normalizeOperator(operatorName, assigneeUserId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
if (TodoTaskConstants.STATUS_COMPLETED.equals(todoTask.getTaskStatus())) {
return true;
}
throw new ServiceException("当前待办状态不允许完成");
}
@Override
@Transactional(rollbackFor = Exception.class)
public int completeByBusiness(String businessType, String businessId, Long assigneeUserId,
Long operatorId, String operatorName) {
String safeBusinessType = normalizeRequired(businessType, "业务类型", 64);
String safeBusinessId = normalizeRequired(businessId, "业务主键", 64);
if (assigneeUserId != null) {
validateUserId(assigneeUserId);
}
if (operatorId == null || operatorId <= 0) {
throw new ServiceException("待办完成人不能为空");
}
return todoTaskMapper.completeTodoByBusiness(safeBusinessType, safeBusinessId, assigneeUserId,
operatorId, normalizeOperator(operatorName, operatorId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason) {
validateIdentity(todoId, assigneeUserId);
String safeReason = normalizeRequired(reason, "取消原因", 500);
int rows = todoTaskMapper.cancelTodoForAssignee(todoId, assigneeUserId, safeReason,
normalizeOperator(operatorName, assigneeUserId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
if (TodoTaskConstants.STATUS_CANCELLED.equals(todoTask.getTaskStatus())) {
return true;
}
throw new ServiceException("当前待办状态不允许取消");
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancel(Long todoId, Long operatorId, String operatorName, String reason) {
if (todoId == null || todoId <= 0) {
throw new ServiceException("待办任务 ID 不能为空");
}
if (operatorId == null || operatorId <= 0) {
throw new ServiceException("取消操作人不能为空");
}
String safeReason = normalizeRequired(reason, "取消原因", 500);
int rows = todoTaskMapper.cancelTodo(todoId, safeReason, normalizeOperator(operatorName, operatorId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = todoTaskMapper.selectTodoById(todoId);
if (todoTask == null) {
throw new ServiceException("待办任务不存在");
}
if (TodoTaskConstants.STATUS_CANCELLED.equals(todoTask.getTaskStatus())) {
return true;
}
throw new ServiceException("当前待办状态不允许取消");
}
private SysTodoTask buildTask(TodoTaskCreateCommand command) {
if (command.getAssigneeUserId() == null || command.getAssigneeUserId() <= 0) {
throw new ServiceException("待办接收用户不能为空");
}
Integer priority = command.getPriority() == null
? TodoTaskConstants.PRIORITY_NORMAL : command.getPriority();
if (priority < TodoTaskConstants.PRIORITY_NORMAL || priority > TodoTaskConstants.PRIORITY_URGENT) {
throw new ServiceException("待办优先级只能为 1、2 或 3");
}
SysTodoTask todoTask = new SysTodoTask();
todoTask.setTaskType(normalizeRequired(command.getTaskType(), "待办类型", 64));
todoTask.setTitle(normalizeRequired(command.getTitle(), "待办标题", 200));
todoTask.setSummary(normalizeOptional(command.getSummary(), "待办摘要", 500));
todoTask.setContent(normalizeOptional(command.getContent(), "待办内容", 20000));
todoTask.setBusinessType(normalizeRequired(command.getBusinessType(), "业务类型", 64));
todoTask.setBusinessId(normalizeRequired(command.getBusinessId(), "业务主键", 64));
todoTask.setRouteName(normalizeRequired(command.getRouteName(), "业务路由", 100));
todoTask.setRouteParams(normalizeOptional(command.getRouteParams(), "路由参数", 1000));
todoTask.setAssigneeUserId(command.getAssigneeUserId());
todoTask.setAssigneeMerchantId(normalizeOptional(command.getAssigneeMerchantId(), "运营商 ID", 64));
todoTask.setPriority(priority);
todoTask.setTaskStatus(TodoTaskConstants.STATUS_PENDING);
todoTask.setReadStatus(TodoTaskConstants.READ_UNREAD);
todoTask.setDueTime(command.getDueTime());
todoTask.setIdempotentKey(normalizeRequired(command.getIdempotentKey(), "幂等键", 200));
todoTask.setActiveIdempotentKey(todoTask.getIdempotentKey());
todoTask.setCreateBy(normalizeOperator(command.getCreateBy(), null));
todoTask.setRemark(normalizeOptional(command.getRemark(), "备注", 500));
todoTask.setDelFlag("0");
return todoTask;
}
private void validateQuery(TodoTaskQuery query) {
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.setTaskType(normalizeOptional(query.getTaskType(), "待办类型", 64));
query.setKeyword(normalizeOptional(query.getKeyword(), "关键字", 100));
}
private void validateIdentity(Long todoId, Long assigneeUserId) {
if (todoId == null || todoId <= 0) {
throw new ServiceException("待办任务 ID 不能为空");
}
validateUserId(assigneeUserId);
}
private void validateUserId(Long userId) {
if (userId == null || userId <= 0) {
throw new ServiceException("待办接收用户不能为空");
}
}
private String normalizeRequired(String value, String fieldName, int maxLength) {
String safeValue = StringUtils.trim(value);
if (StringUtils.isEmpty(safeValue)) {
throw new ServiceException(fieldName + "不能为空");
}
if (safeValue.length() > maxLength) {
throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符");
}
return safeValue;
}
private String normalizeOptional(String value, String fieldName, int maxLength) {
if (StringUtils.isEmpty(value)) {
return null;
}
String safeValue = value.trim();
if (safeValue.length() > maxLength) {
throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符");
}
return safeValue;
}
private String normalizeOperator(String operatorName, Long operatorId) {
String safeOperator = StringUtils.isNotEmpty(operatorName)
? operatorName.trim()
: (operatorId == null ? SYSTEM_OPERATOR : String.valueOf(operatorId));
if (safeOperator.length() > 64) {
throw new ServiceException("操作人名称不能超过64个字符");
}
return safeOperator;
}
}