feat(approval): 派单候选池、派单动作与双方存档 (#24)

This commit is contained in:
2026-10-30 21:42:04 +08:00
parent d395abb6f0
commit 92a982054b
7 changed files with 685 additions and 3 deletions
@@ -68,6 +68,9 @@ public final class ApprovalConstants {
// 角色编码
public static final String ROLE_ARCHIVIST = "approval_archivist";
public static final String ROLE_SUPER_ADMIN = "superAdmin";
/** 普通消费用户角色:无部门且仅有这些角色时不算派单候选。 */
public static final String ROLE_USER = "user";
public static final String ROLE_GUEST = "guest";
// 附件规则
public static final long MAX_ATTACHMENT_SIZE = 20L * 1024 * 1024;
@@ -2,12 +2,14 @@ package com.gxwebsoft.approval.controller;
import com.gxwebsoft.approval.param.ApprovalTaskActionParam;
import com.gxwebsoft.approval.param.ApprovalTaskAssignParam;
import com.gxwebsoft.approval.param.ApprovalTaskDispatchParam;
import com.gxwebsoft.approval.param.ApprovalTaskListParam;
import com.gxwebsoft.approval.service.ApprovalInstanceService;
import com.gxwebsoft.approval.service.ApprovalTaskService;
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
import com.gxwebsoft.approval.vo.ApprovalInstanceVO;
import com.gxwebsoft.approval.vo.ApprovalItemVO;
import com.gxwebsoft.approval.vo.ApprovalUserVO;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
@@ -24,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* 审批待办与任务动作接口(员工端与 PC 共用)。
@@ -108,6 +111,25 @@ public class ApprovalTaskController extends BaseController {
return success("撤回成功", instanceService.detail(result.getInstanceId(), user));
}
@PreAuthorize("hasAuthority('approval:instance:approve')")
@ApiOperation("派单候选池")
@GetMapping("/task/{id}/candidates")
public ApiResult<List<ApprovalUserVO>> candidates(@PathVariable("id") Long id) {
return success(taskService.candidates(id, getLoginUser()));
}
@PreAuthorize("hasAuthority('approval:instance:approve')")
@OperationLog(value = "派单审批待办", module = "审批管理")
@ApiOperation("派单")
@PostMapping("/task/{id}/dispatch")
public ApiResult<ApprovalInstanceVO> dispatch(@PathVariable("id") Long id,
@RequestBody(required = false) ApprovalTaskDispatchParam param) {
User user = getLoginUser();
ApprovalTaskService.Result result = taskService.dispatch(id, param, user);
return success(result.getWarning() == null ? "派单成功" : result.getWarning(),
instanceService.detail(result.getInstanceId(), user));
}
@PreAuthorize("hasAuthority('approval:instance:assign')")
@OperationLog(value = "改派审批待办", module = "审批管理")
@ApiOperation("改派")
@@ -0,0 +1,17 @@
package com.gxwebsoft.approval.param;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 派单参数。
*/
@Data
public class ApprovalTaskDispatchParam implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("被派单人用户id;必须来自候选池")
private Integer targetUserId;
}
@@ -2,6 +2,7 @@ package com.gxwebsoft.approval.service;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.gxwebsoft.approval.constant.ApprovalConstants;
import com.gxwebsoft.approval.support.ApprovalTenantContext;
import com.gxwebsoft.approval.vo.ApprovalOrganizationVO;
import com.gxwebsoft.approval.vo.ApprovalUserVO;
@@ -176,6 +177,33 @@ public class ApprovalDirectoryService {
return jdbcTemplate.query(sql, USER_MAPPER, args.toArray());
}
/**
* 派单候选池:优先受理部门启用成员;部门无成员时回退为当前租户的非普通消费用户。
*
* <p>回退排除「未归属组织且角色仅为 user/guest 或无角色」的账号;
* 部门有成员时不混入回退候选。</p>
*/
public List<ApprovalUserVO> listDispatchCandidates(Integer acceptOrganizationId) {
List<ApprovalUserVO> members = listOrganizationMembers(acceptOrganizationId);
if (!members.isEmpty()) {
return members;
}
Integer tenantId = ApprovalTenantContext.currentTenantId();
return jdbcTemplate.query(
"SELECT u.user_id, u.username, u.nickname, u.real_name, u.organization_id, o.organization_name "
+ "FROM sys_user u "
+ "LEFT JOIN sys_organization o ON o.organization_id = u.organization_id AND o.tenant_id = ? "
+ "WHERE u.tenant_id = ? AND u.status = 0 AND u.deleted = 0 "
+ "AND NOT ((u.organization_id IS NULL OR u.organization_id = 0) AND NOT EXISTS ("
+ "SELECT 1 FROM sys_user_role ur "
+ "JOIN sys_role r ON r.role_id = ur.role_id AND r.tenant_id = ? "
+ "WHERE ur.user_id = u.user_id AND ur.tenant_id = ? AND r.deleted = 0 "
+ "AND r.role_code NOT IN (?, ?))) "
+ "ORDER BY u.user_id",
USER_MAPPER, tenantId, tenantId, tenantId, tenantId,
ApprovalConstants.ROLE_USER, ApprovalConstants.ROLE_GUEST);
}
/** 是否持有任一角色(用于 allVisible 判断)。 */
public boolean hasAnyRole(Integer userId, String... roleCodes) {
if (userId == null || roleCodes == null || roleCodes.length == 0) {
@@ -566,10 +566,10 @@ public class ApprovalInstanceService {
permissions.setCanWithdraw(done == null || done == 0);
}
if (myTask != null) {
boolean approveNode = ApprovalConstants.NODE_APPROVE.equals(myTask.getNodeType())
|| ApprovalConstants.NODE_DISPATCH.equals(myTask.getNodeType());
boolean approveNode = ApprovalConstants.NODE_APPROVE.equals(myTask.getNodeType());
boolean rejectableNode = approveNode || ApprovalConstants.NODE_DISPATCH.equals(myTask.getNodeType());
permissions.setCanApprove(approveNode);
permissions.setCanReject(approveNode);
permissions.setCanReject(rejectableNode);
permissions.setCanDispatch(ApprovalConstants.NODE_DISPATCH.equals(myTask.getNodeType()));
permissions.setCanConfirm(ApprovalConstants.NODE_CONFIRM.equals(myTask.getNodeType()));
permissions.setCanArchive(ApprovalConstants.NODE_ARCHIVE.equals(myTask.getNodeType()));
@@ -9,6 +9,7 @@ import com.gxwebsoft.approval.entity.ApprovalTask;
import com.gxwebsoft.approval.mapper.ApprovalInstanceMapper;
import com.gxwebsoft.approval.mapper.ApprovalTaskMapper;
import com.gxwebsoft.approval.param.ApprovalTaskAssignParam;
import com.gxwebsoft.approval.param.ApprovalTaskDispatchParam;
import com.gxwebsoft.approval.support.ApprovalApprover;
import com.gxwebsoft.approval.support.ApprovalNode;
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
@@ -175,6 +176,43 @@ public class ApprovalTaskService {
return inTransaction(() -> doAssign(taskId, task, target.getUserId(), targetName, reason, user));
}
/**
* 派单候选池:仅派单节点、本人待办的任务可查询;优先受理部门启用成员,
* 部门无成员时回退为租户非普通消费用户。
*/
public List<ApprovalUserVO> candidates(Long taskId, User user) {
ApprovalTask task = requireDispatchTask(taskId, user);
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
ensureTaskHandleable(task, instance);
return directoryService.listDispatchCandidates(instance.getAcceptOrgId());
}
/**
* 派单:选定候选池中的 1 人即完成派单节点,激活存档节点并为报送人与被派单人生成两条
* 存档任务。目标校验与审批人解析在事务外完成(core 库),写 master 的部分在事务内。
*/
public Result dispatch(Long taskId, ApprovalTaskDispatchParam param, User user) {
if (param == null || param.getTargetUserId() == null) {
throw new BusinessException("请选择派单对象");
}
ApprovalTask task = requireDispatchTask(taskId, user);
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
ensureNotArchived(instance);
ApprovalUserVO target = requireDispatchTarget(instance, param.getTargetUserId());
String targetName = displayName(target);
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
List<ApprovalApprover> approvers = dispatchNextApprovers(instance, next, target);
String warning = inTransaction(() ->
doDispatch(taskId, task, target, targetName, user, next, approvers));
return new Result(instance.getId(), warning);
}
public Integer todoCount(User user) {
return taskMapper.countPendingInstances(user.getUserId());
}
@@ -326,6 +364,94 @@ public class ApprovalTaskService {
return vo;
}
/**
* 事务内派单:锁实例后条件更新任务、快照派单对象、写派单记录并激活存档节点。
*/
private String doDispatch(Long taskId, ApprovalTask task, ApprovalUserVO target, String targetName,
User user, ApprovalNode next, List<ApprovalApprover> approvers) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureNotArchived(locked);
ensureTaskHandleable(task, locked);
if (updateDispatchTask(taskId, target.getUserId(), targetName) == 0) {
throw new BusinessException("任务已处理");
}
engineService.cancelPendingTasks(locked.getId(), locked.getRound(), taskId);
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
.eq("id", locked.getId())
.eq("status", ApprovalConstants.STATUS_PENDING)
.set("dispatch_target_id", target.getUserId())
.set("dispatch_target_name", targetName));
if (rows == 0) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
locked.setDispatchTargetId(target.getUserId());
locked.setDispatchTargetName(targetName);
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_DISPATCH, null);
if (next == null) {
return "当前节点缺少审批人,请联系管理员补配置";
}
int created = engineService.moveToNode(locked, next, locked.getRound(), approvers);
return created == 0 ? "当前节点缺少审批人,请联系管理员补配置" : null;
}
/**
* 派单前校验:任务存在、属于本人、是派单节点且仍待处理。
*/
private ApprovalTask requireDispatchTask(Long taskId, User user) {
ApprovalTask task = requireTask(taskId, user);
if (!ApprovalConstants.NODE_DISPATCH.equals(task.getNodeType())) {
throw new BusinessException("当前任务不支持派单");
}
return task;
}
/**
* 派单目标必须来自候选池(受理部门启用成员,或部门无成员时的租户回退候选)。
*/
private ApprovalUserVO requireDispatchTarget(ApprovalInstance instance, Integer targetUserId) {
for (ApprovalUserVO candidate : directoryService.listDispatchCandidates(instance.getAcceptOrgId())) {
if (targetUserId.equals(candidate.getUserId())) {
return candidate;
}
}
throw new BusinessException("派单对象不在候选范围内");
}
/**
* 存档节点审批人:报送人 + 本次派单对象(实例快照尚未写入,需显式解析)。
*/
private List<ApprovalApprover> dispatchNextApprovers(ApprovalInstance instance, ApprovalNode next,
ApprovalUserVO target) {
if (next == null) {
return Collections.emptyList();
}
if (!RESOLVER_REPORTER_AND_DISPATCH_TARGET.equals(next.getResolver())) {
return resolveApprovers(instance, next);
}
List<Integer> userIds = new ArrayList<>();
if (instance.getReporterId() != null) {
userIds.add(instance.getReporterId());
}
if (!userIds.contains(target.getUserId())) {
userIds.add(target.getUserId());
}
return userApprovers(userIds);
}
/**
* 派单任务条件更新:完成并快照派单对象。
*/
private int updateDispatchTask(Long taskId, Integer targetUserId, String targetName) {
return taskMapper.update(null, new UpdateWrapper<ApprovalTask>()
.eq("id", taskId)
.eq("status", ApprovalConstants.TASK_PENDING)
.set("status", ApprovalConstants.TASK_DONE)
.set("handle_time", LocalDateTime.now())
.set("dispatch_target_id", targetUserId)
.set("dispatch_target_name", targetName));
}
/**
* 改派前的基本校验:任务存在、待处理且单据仍在审批中。
*/