Files
gxtyzx-java/src/main/java/com/gxwebsoft/approval/service/ApprovalTaskService.java
T

552 lines
22 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.gxwebsoft.approval.service;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.gxwebsoft.approval.constant.ApprovalConstants;
import com.gxwebsoft.approval.entity.ApprovalInstance;
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.support.ApprovalApprover;
import com.gxwebsoft.approval.support.ApprovalNode;
import com.gxwebsoft.approval.vo.ApprovalAssignVO;
import com.gxwebsoft.approval.vo.ApprovalUserVO;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.system.entity.User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* 审批任务动作:通过、驳回、撤回与节点推进。
*
* <p>跨库约束:审批人解析必须走 {@link ApprovalDirectoryService}(core 专用连接),
* 在事务外完成;写 master 的部分放进 TransactionTemplate。任务处理使用
* WHERE status='PENDING' 的条件更新,抢先后到者收到「任务已处理」。</p>
*/
@Service
public class ApprovalTaskService {
private static final String RESOLVER_REPORTER_ORG_LEADER = "reporterOrgLeader";
private static final String RESOLVER_ACCEPT_ORG_LEADER = "acceptOrgLeader";
private static final String RESOLVER_ARCHIVIST = "archivist";
private static final String RESOLVER_REPORTER = "reporter";
private static final String RESOLVER_REPORTER_AND_DISPATCH_TARGET = "reporterAndDispatchTarget";
private static final String RESOLVER_ACCEPT_ORG_MEMBERS = "acceptOrgMembers";
private static final int MAX_OPINION_LENGTH = 500;
@Resource
private ApprovalInstanceMapper instanceMapper;
@Resource
private ApprovalTaskMapper taskMapper;
@Resource
private ApprovalEngineService engineService;
@Resource
private ApprovalDirectoryService directoryService;
@Resource
private PlatformTransactionManager transactionManager;
/**
* 通过:APPROVE 节点或签,处理后取消同节点同轮其余待办并推进下一节点。
*/
public Result approve(Long taskId, String opinion, User user) {
ApprovalTask task = requireTask(taskId, user);
if (!ApprovalConstants.NODE_APPROVE.equals(task.getNodeType())) {
throw new BusinessException("当前任务不支持通过");
}
String normalized = normalizeOpinion(opinion);
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
List<ApprovalApprover> approvers =
next == null ? Collections.emptyList() : resolveApprovers(instance, next);
String warning = inTransaction(() -> doApprove(taskId, task, normalized, user, next, approvers));
return new Result(instance.getId(), warning);
}
/**
* 存档确认:CONFIRM 任务只能确认(无意见),同节点同轮全部完成后实例进入审批完成,
* 并激活归档节点;归档节点缺配时实例停在审批中并提示管理员补配置。
*/
public Result confirm(Long taskId, User user) {
ApprovalTask task = requireTask(taskId, user);
if (!ApprovalConstants.NODE_CONFIRM.equals(task.getNodeType())) {
throw new BusinessException("当前任务不支持存档确认");
}
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
ensureNotArchived(instance);
ApprovalNode next = engineService.nextNode(instance, task.getNodeKey());
List<ApprovalApprover> approvers =
next == null ? Collections.emptyList() : resolveApprovers(instance, next);
String warning = inTransaction(() -> doConfirm(taskId, task, user, next, approvers));
return new Result(instance.getId(), warning);
}
/**
* 归档:ARCHIVE 任务或签,处理后取消同节点其余待办,实例进入只读终态。
*/
public Result archive(Long taskId, User user) {
ApprovalTask task = requireTask(taskId, user);
if (!ApprovalConstants.NODE_ARCHIVE.equals(task.getNodeType())) {
throw new BusinessException("当前任务不支持归档");
}
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
ensureNotArchived(instance);
inTransaction(() -> doArchive(taskId, task, user));
return new Result(instance.getId(), null);
}
/**
* 驳回:意见必填,同轮待办全部取消,实例回到报送人。
*/
public Result reject(Long taskId, String opinion, User user) {
ApprovalTask task = requireTask(taskId, user);
boolean rejectable = ApprovalConstants.NODE_APPROVE.equals(task.getNodeType())
|| ApprovalConstants.NODE_DISPATCH.equals(task.getNodeType());
if (!rejectable) {
throw new BusinessException("当前任务不支持驳回");
}
String required = requireOpinion(opinion);
inTransaction(() -> doReject(taskId, task, required, user));
return new Result(task.getInstanceId(), null);
}
/**
* 撤回:仅报送人,且本轮没有任何任务被处理。
*/
public Result withdraw(Long instanceId, User user) {
ApprovalInstance instance = instanceMapper.selectById(instanceId);
if (instance == null) {
throw new BusinessException("单据不存在");
}
if (user == null || !instance.getReporterId().equals(user.getUserId())) {
throw new BusinessException("无权操作该单据");
}
if (ApprovalConstants.STATUS_ARCHIVED.equals(instance.getStatus())) {
throw new BusinessException("单据已归档,不能修改");
}
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
throw new BusinessException("当前状态不可撤回");
}
ensureNoHandledTask(instance);
inTransaction(() -> doWithdraw(instanceId, user));
return new Result(instanceId, null);
}
/**
* 改派:管理员把待处理任务换给同租户其他启用用户,原因必填并写 ASSIGN 记录。
*
* <p>改派不改变任务状态,只替换处理人;操作人不必是任务当前处理人。</p>
*/
public ApprovalAssignVO assign(Long taskId, ApprovalTaskAssignParam param, User user) {
if (param == null || param.getTargetUserId() == null) {
throw new BusinessException("请选择改派对象");
}
String reason = requireAssignReason(param.getReason());
ApprovalTask task = requireAssignableTask(taskId);
// 事务外解析目标用户:启用、未删除且属于当前租户(@DS("server") 查询显式带租户)
List<ApprovalUserVO> targets =
directoryService.listActiveUsersByIds(Collections.singletonList(param.getTargetUserId()));
if (targets.isEmpty()) {
throw new BusinessException("改派对象不存在或已停用");
}
ApprovalUserVO target = targets.get(0);
String targetName = displayName(target);
return inTransaction(() -> doAssign(taskId, task, target.getUserId(), targetName, reason, user));
}
public Integer todoCount(User user) {
return taskMapper.countPendingInstances(user.getUserId());
}
private String doApprove(Long taskId, ApprovalTask task, String opinion, User user,
ApprovalNode next, List<ApprovalApprover> approvers) {
// 先锁实例串行化同单据的处理,再做任务条件更新,避免并发或签互相等待行锁
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureTaskHandleable(task, locked);
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, opinion) == 0) {
throw new BusinessException("任务已处理");
}
engineService.cancelPendingTasks(locked.getId(), locked.getRound(), taskId);
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_APPROVE, opinion);
if (next == null) {
return null;
}
int created = engineService.moveToNode(locked, next, locked.getRound(), approvers);
return created == 0 ? "当前节点缺少审批人,请联系管理员补配置" : null;
}
private String doConfirm(Long taskId, ApprovalTask task, User user,
ApprovalNode next, List<ApprovalApprover> approvers) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureNotArchived(locked);
ensureTaskHandleable(task, locked);
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, null) == 0) {
throw new BusinessException("任务已处理");
}
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_CONFIRM, null);
Integer remaining = taskMapper.selectCount(new LambdaQueryWrapper<ApprovalTask>()
.eq(ApprovalTask::getInstanceId, locked.getId())
.eq(ApprovalTask::getRound, locked.getRound())
.eq(ApprovalTask::getNodeKey, task.getNodeKey())
.eq(ApprovalTask::getStatus, ApprovalConstants.TASK_PENDING));
if (remaining != null && remaining > 0) {
return null;
}
if (next == null) {
return "当前节点缺少审批人,请联系管理员补配置";
}
int created = engineService.moveToNode(locked, next, locked.getRound(), approvers);
if (created == 0) {
return "当前节点缺少审批人,请联系管理员补配置";
}
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
.eq("id", locked.getId())
.eq("status", ApprovalConstants.STATUS_PENDING)
.set("status", ApprovalConstants.STATUS_APPROVED)
.set("approve_time", LocalDateTime.now()));
if (rows == 0) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
return null;
}
private void doArchive(Long taskId, ApprovalTask task, User user) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureNotArchived(locked);
if (!ApprovalConstants.STATUS_APPROVED.equals(locked.getStatus())) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
ensureTaskCurrent(task, locked);
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, null) == 0) {
throw new BusinessException("任务已处理");
}
engineService.cancelPendingTasks(locked.getId(), locked.getRound(), taskId);
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_ARCHIVE, null);
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
.eq("id", locked.getId())
.eq("status", ApprovalConstants.STATUS_APPROVED)
.set("status", ApprovalConstants.STATUS_ARCHIVED)
.set("archive_time", LocalDateTime.now())
.set("archivist_id", user.getUserId())
.set("archivist_name", displayName(user)));
if (rows == 0) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
}
private void doReject(Long taskId, ApprovalTask task, String opinion, User user) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureTaskHandleable(task, locked);
if (updatePendingTask(taskId, ApprovalConstants.TASK_DONE, opinion) == 0) {
throw new BusinessException("任务已处理");
}
Integer round = locked.getRound();
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_REJECT, opinion);
engineService.cancelPendingTasks(locked.getId(), round, null);
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
.eq("id", locked.getId())
.eq("status", ApprovalConstants.STATUS_PENDING)
.set("status", ApprovalConstants.STATUS_REJECTED)
.set("round", round + 1));
if (rows == 0) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
}
private void doWithdraw(Long instanceId, User user) {
ApprovalInstance locked = engineService.lockInstance(instanceId);
if (!ApprovalConstants.STATUS_PENDING.equals(locked.getStatus())) {
throw new BusinessException("当前状态不可撤回");
}
ensureNoHandledTask(locked);
Integer round = locked.getRound();
engineService.addRecord(locked, null, locked.getCurrentNodeKey(), user,
ApprovalConstants.ACTION_WITHDRAW, null);
engineService.cancelPendingTasks(locked.getId(), round, null);
int rows = instanceMapper.update(null, new UpdateWrapper<ApprovalInstance>()
.eq("id", instanceId)
.eq("status", ApprovalConstants.STATUS_PENDING)
.set("status", ApprovalConstants.STATUS_WITHDRAWN)
.set("round", round + 1));
if (rows == 0) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
}
/**
* 事务内改派:锁实例后条件更新处理人,并追加 ASSIGN 记录。
*/
private ApprovalAssignVO doAssign(Long taskId, ApprovalTask task, Integer targetUserId,
String targetName, String reason, User user) {
ApprovalInstance locked = engineService.lockInstance(task.getInstanceId());
ensureTaskHandleable(task, locked);
int rows = taskMapper.update(null, new UpdateWrapper<ApprovalTask>()
.eq("id", taskId)
.eq("status", ApprovalConstants.TASK_PENDING)
.set("assignee_id", targetUserId)
.set("assignee_name", targetName));
if (rows == 0) {
throw new BusinessException("任务已处理");
}
engineService.addRecord(locked, taskId, task.getNodeKey(), user,
ApprovalConstants.ACTION_ASSIGN, reason);
ApprovalAssignVO vo = new ApprovalAssignVO();
vo.setInstanceId(locked.getId());
vo.setTaskId(taskId);
vo.setOriginalAssigneeId(task.getAssigneeId());
vo.setOriginalAssigneeName(task.getAssigneeName());
vo.setAssigneeId(targetUserId);
vo.setAssigneeName(targetName);
vo.setReason(reason);
return vo;
}
/**
* 改派前的基本校验:任务存在、待处理且单据仍在审批中。
*/
private ApprovalTask requireAssignableTask(Long taskId) {
if (taskId == null) {
throw new BusinessException("任务不存在");
}
ApprovalTask task = taskMapper.selectById(taskId);
if (task == null) {
throw new BusinessException("任务不存在");
}
if (!ApprovalConstants.TASK_PENDING.equals(task.getStatus())) {
throw new BusinessException("任务已处理");
}
ApprovalInstance instance = instanceMapper.selectById(task.getInstanceId());
if (instance == null) {
throw new BusinessException("单据不存在");
}
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
throw new BusinessException("单据不在审批中,无法改派");
}
return task;
}
/**
* 任务必须属于实例当前轮次、当前节点,否则视为已被处理或已失效。
*/
private void ensureTaskHandleable(ApprovalTask task, ApprovalInstance instance) {
if (!ApprovalConstants.STATUS_PENDING.equals(instance.getStatus())) {
throw new BusinessException("单据状态已变化,请刷新后重试");
}
ensureTaskCurrent(task, instance);
}
private void ensureTaskCurrent(ApprovalTask task, ApprovalInstance instance) {
if (!task.getRound().equals(instance.getRound())
|| !task.getNodeKey().equals(instance.getCurrentNodeKey())) {
throw new BusinessException("任务已处理");
}
}
private void ensureNotArchived(ApprovalInstance instance) {
if (ApprovalConstants.STATUS_ARCHIVED.equals(instance.getStatus())) {
throw new BusinessException("单据已归档,不能修改");
}
}
private void ensureNoHandledTask(ApprovalInstance instance) {
Integer handled = taskMapper.selectCount(new LambdaQueryWrapper<ApprovalTask>()
.eq(ApprovalTask::getInstanceId, instance.getId())
.eq(ApprovalTask::getRound, instance.getRound())
.eq(ApprovalTask::getStatus, ApprovalConstants.TASK_DONE));
if (handled != null && handled > 0) {
throw new BusinessException("已有审批人处理,无法撤回");
}
}
private int updatePendingTask(Long taskId, String status, String opinion) {
UpdateWrapper<ApprovalTask> wrapper = new UpdateWrapper<ApprovalTask>()
.eq("id", taskId)
.eq("status", ApprovalConstants.TASK_PENDING)
.set("status", status)
.set("handle_time", LocalDateTime.now());
if (opinion != null) {
wrapper.set("opinion", opinion);
}
return taskMapper.update(null, wrapper);
}
private ApprovalTask requireTask(Long taskId, User user) {
if (taskId == null) {
throw new BusinessException("任务不存在");
}
ApprovalTask task = taskMapper.selectById(taskId);
if (task == null) {
throw new BusinessException("任务不存在");
}
if (user == null || !user.getUserId().equals(task.getAssigneeId())) {
throw new BusinessException("无权处理该任务");
}
return task;
}
/**
* 事务外按节点解析器解析审批人;缺配时返回空列表,由调用方提示并停留当前节点。
*/
private List<ApprovalApprover> resolveApprovers(ApprovalInstance instance, ApprovalNode node) {
String resolver = node.getResolver();
if (StrUtil.isBlank(resolver)) {
return Collections.emptyList();
}
switch (resolver) {
case RESOLVER_REPORTER_ORG_LEADER:
return roleApprovers(ApprovalConstants.departmentLeaderRoleCode(instance.getReportOrgId()));
case RESOLVER_ACCEPT_ORG_LEADER:
return roleApprovers(ApprovalConstants.departmentLeaderRoleCode(instance.getAcceptOrgId()));
case RESOLVER_ARCHIVIST:
return roleApprovers(ApprovalConstants.ROLE_ARCHIVIST);
case RESOLVER_REPORTER:
return userApprovers(Collections.singletonList(instance.getReporterId()));
case RESOLVER_REPORTER_AND_DISPATCH_TARGET:
return userApprovers(confirmCandidateIds(instance));
case RESOLVER_ACCEPT_ORG_MEMBERS:
return organizationApprovers(instance.getAcceptOrgId());
default:
return Collections.emptyList();
}
}
private List<Integer> confirmCandidateIds(ApprovalInstance instance) {
List<Integer> ids = new ArrayList<>();
if (instance.getReporterId() != null) {
ids.add(instance.getReporterId());
}
if (instance.getDispatchTargetId() != null && !ids.contains(instance.getDispatchTargetId())) {
ids.add(instance.getDispatchTargetId());
}
return ids;
}
private List<ApprovalApprover> roleApprovers(String roleCode) {
if (StrUtil.isBlank(roleCode)) {
return Collections.emptyList();
}
return toApprovers(directoryService.listRoleMembers(roleCode));
}
private List<ApprovalApprover> userApprovers(List<Integer> userIds) {
return toApprovers(directoryService.listActiveUsersByIds(userIds));
}
private List<ApprovalApprover> organizationApprovers(Integer organizationId) {
return toApprovers(directoryService.listOrganizationMembers(organizationId));
}
private List<ApprovalApprover> toApprovers(List<ApprovalUserVO> users) {
return users.stream()
.map(user -> new ApprovalApprover(user.getUserId(), displayName(user)))
.collect(Collectors.toList());
}
private String displayName(ApprovalUserVO user) {
if (StrUtil.isNotBlank(user.getRealName())) {
return user.getRealName();
}
if (StrUtil.isNotBlank(user.getNickname())) {
return user.getNickname();
}
return user.getUsername();
}
private String displayName(User user) {
if (StrUtil.isNotBlank(user.getRealName())) {
return user.getRealName();
}
if (StrUtil.isNotBlank(user.getNickname())) {
return user.getNickname();
}
return user.getUsername();
}
private String normalizeOpinion(String opinion) {
if (opinion == null) {
return null;
}
String trimmed = opinion.trim();
if (trimmed.isEmpty()) {
return null;
}
if (trimmed.length() > MAX_OPINION_LENGTH) {
throw new BusinessException("意见不能超过500字");
}
return trimmed;
}
private String requireOpinion(String opinion) {
String normalized = normalizeOpinion(opinion);
if (normalized == null) {
throw new BusinessException("请填写驳回意见");
}
return normalized;
}
private String requireAssignReason(String reason) {
String normalized = normalizeOpinion(reason);
if (normalized == null) {
throw new BusinessException("请填写改派原因");
}
return normalized;
}
private <T> T inTransaction(Supplier<T> action) {
return new TransactionTemplate(transactionManager).execute(status -> action.get());
}
private void inTransaction(Runnable action) {
new TransactionTemplate(transactionManager).execute(status -> {
action.run();
return null;
});
}
/**
* 动作结果:实例id + 需要透出的节点缺配提示。
*/
public static class Result {
private final Long instanceId;
private final String warning;
public Result(Long instanceId, String warning) {
this.instanceId = instanceId;
this.warning = warning;
}
public Long getInstanceId() {
return instanceId;
}
public String getWarning() {
return warning;
}
}
}