feat: 支持审批步骤绑定部门与多对象

- 新增审批对象关联表和存量数据迁移

- 支持用户、角色、部门多选及办理权限匹配

- 完善部门状态与批量管理交互
This commit is contained in:
2026-07-30 15:13:11 +08:00
parent 864cea6135
commit 57bd7b5d06
29 changed files with 3694 additions and 365 deletions

View File

@@ -0,0 +1,11 @@
package tech.easyflow.approval.entity;
import com.mybatisflex.annotation.Table;
import tech.easyflow.approval.entity.base.ApprovalFlowStepAssigneeBase;
/**
* 审批流程步骤对象关联实体。
*/
@Table("tb_approval_flow_step_assignee")
public class ApprovalFlowStepAssignee extends ApprovalFlowStepAssigneeBase {
}

View File

@@ -0,0 +1,11 @@
package tech.easyflow.approval.entity;
import com.mybatisflex.annotation.Table;
import tech.easyflow.approval.entity.base.ApprovalTaskAssigneeBase;
/**
* 审批任务对象关联实体。
*/
@Table("tb_approval_task_assignee")
public class ApprovalTaskAssignee extends ApprovalTaskAssigneeBase {
}

View File

@@ -0,0 +1,248 @@
package tech.easyflow.approval.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
/**
* 审批流程步骤对象关联基础字段。
*/
public class ApprovalFlowStepAssigneeBase implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键")
private BigInteger id;
@Column(comment = "审批步骤ID")
private BigInteger stepId;
@Column(comment = "审批对象类型")
private String assigneeType;
@Column(comment = "审批对象ID")
private BigInteger targetId;
@Column(comment = "审批对象编码")
private String targetCode;
@Column(comment = "审批对象名称")
private String targetName;
@Column(comment = "是否包含子部门")
private Integer includeChildren;
@Column(comment = "创建时间")
private Date created;
@Column(comment = "创建者")
private BigInteger createdBy;
@Column(comment = "修改时间")
private Date modified;
@Column(comment = "修改者")
private BigInteger modifiedBy;
/**
* 获取主键。
*
* @return 主键
*/
public BigInteger getId() {
return id;
}
/**
* 设置主键。
*
* @param id 主键
*/
public void setId(BigInteger id) {
this.id = id;
}
/**
* 获取审批步骤 ID。
*
* @return 审批步骤 ID
*/
public BigInteger getStepId() {
return stepId;
}
/**
* 设置审批步骤 ID。
*
* @param stepId 审批步骤 ID
*/
public void setStepId(BigInteger stepId) {
this.stepId = stepId;
}
/**
* 获取审批对象类型。
*
* @return 审批对象类型
*/
public String getAssigneeType() {
return assigneeType;
}
/**
* 设置审批对象类型。
*
* @param assigneeType 审批对象类型
*/
public void setAssigneeType(String assigneeType) {
this.assigneeType = assigneeType;
}
/**
* 获取审批对象 ID。
*
* @return 审批对象 ID
*/
public BigInteger getTargetId() {
return targetId;
}
/**
* 设置审批对象 ID。
*
* @param targetId 审批对象 ID
*/
public void setTargetId(BigInteger targetId) {
this.targetId = targetId;
}
/**
* 获取审批对象编码。
*
* @return 审批对象编码
*/
public String getTargetCode() {
return targetCode;
}
/**
* 设置审批对象编码。
*
* @param targetCode 审批对象编码
*/
public void setTargetCode(String targetCode) {
this.targetCode = targetCode;
}
/**
* 获取审批对象名称。
*
* @return 审批对象名称
*/
public String getTargetName() {
return targetName;
}
/**
* 设置审批对象名称。
*
* @param targetName 审批对象名称
*/
public void setTargetName(String targetName) {
this.targetName = targetName;
}
/**
* 获取是否包含子部门。
*
* @return 1 表示包含0 表示不包含
*/
public Integer getIncludeChildren() {
return includeChildren;
}
/**
* 设置是否包含子部门。
*
* @param includeChildren 1 表示包含0 表示不包含
*/
public void setIncludeChildren(Integer includeChildren) {
this.includeChildren = includeChildren;
}
/**
* 获取创建时间。
*
* @return 创建时间
*/
public Date getCreated() {
return created;
}
/**
* 设置创建时间。
*
* @param created 创建时间
*/
public void setCreated(Date created) {
this.created = created;
}
/**
* 获取创建者。
*
* @return 创建者 ID
*/
public BigInteger getCreatedBy() {
return createdBy;
}
/**
* 设置创建者。
*
* @param createdBy 创建者 ID
*/
public void setCreatedBy(BigInteger createdBy) {
this.createdBy = createdBy;
}
/**
* 获取修改时间。
*
* @return 修改时间
*/
public Date getModified() {
return modified;
}
/**
* 设置修改时间。
*
* @param modified 修改时间
*/
public void setModified(Date modified) {
this.modified = modified;
}
/**
* 获取修改者。
*
* @return 修改者 ID
*/
public BigInteger getModifiedBy() {
return modifiedBy;
}
/**
* 设置修改者。
*
* @param modifiedBy 修改者 ID
*/
public void setModifiedBy(BigInteger modifiedBy) {
this.modifiedBy = modifiedBy;
}
}

View File

@@ -0,0 +1,248 @@
package tech.easyflow.approval.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
/**
* 审批任务对象关联基础字段。
*/
public class ApprovalTaskAssigneeBase implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键")
private BigInteger id;
@Column(comment = "审批任务ID")
private BigInteger taskId;
@Column(comment = "审批对象类型")
private String assigneeType;
@Column(comment = "审批对象ID")
private BigInteger targetId;
@Column(comment = "审批对象编码")
private String targetCode;
@Column(comment = "审批对象名称")
private String targetName;
@Column(comment = "是否包含子部门")
private Integer includeChildren;
@Column(comment = "创建时间")
private Date created;
@Column(comment = "创建者")
private BigInteger createdBy;
@Column(comment = "修改时间")
private Date modified;
@Column(comment = "修改者")
private BigInteger modifiedBy;
/**
* 获取主键。
*
* @return 主键
*/
public BigInteger getId() {
return id;
}
/**
* 设置主键。
*
* @param id 主键
*/
public void setId(BigInteger id) {
this.id = id;
}
/**
* 获取审批任务 ID。
*
* @return 审批任务 ID
*/
public BigInteger getTaskId() {
return taskId;
}
/**
* 设置审批任务 ID。
*
* @param taskId 审批任务 ID
*/
public void setTaskId(BigInteger taskId) {
this.taskId = taskId;
}
/**
* 获取审批对象类型。
*
* @return 审批对象类型
*/
public String getAssigneeType() {
return assigneeType;
}
/**
* 设置审批对象类型。
*
* @param assigneeType 审批对象类型
*/
public void setAssigneeType(String assigneeType) {
this.assigneeType = assigneeType;
}
/**
* 获取审批对象 ID。
*
* @return 审批对象 ID
*/
public BigInteger getTargetId() {
return targetId;
}
/**
* 设置审批对象 ID。
*
* @param targetId 审批对象 ID
*/
public void setTargetId(BigInteger targetId) {
this.targetId = targetId;
}
/**
* 获取审批对象编码。
*
* @return 审批对象编码
*/
public String getTargetCode() {
return targetCode;
}
/**
* 设置审批对象编码。
*
* @param targetCode 审批对象编码
*/
public void setTargetCode(String targetCode) {
this.targetCode = targetCode;
}
/**
* 获取审批对象名称。
*
* @return 审批对象名称
*/
public String getTargetName() {
return targetName;
}
/**
* 设置审批对象名称。
*
* @param targetName 审批对象名称
*/
public void setTargetName(String targetName) {
this.targetName = targetName;
}
/**
* 获取是否包含子部门。
*
* @return 1 表示包含0 表示不包含
*/
public Integer getIncludeChildren() {
return includeChildren;
}
/**
* 设置是否包含子部门。
*
* @param includeChildren 1 表示包含0 表示不包含
*/
public void setIncludeChildren(Integer includeChildren) {
this.includeChildren = includeChildren;
}
/**
* 获取创建时间。
*
* @return 创建时间
*/
public Date getCreated() {
return created;
}
/**
* 设置创建时间。
*
* @param created 创建时间
*/
public void setCreated(Date created) {
this.created = created;
}
/**
* 获取创建者。
*
* @return 创建者 ID
*/
public BigInteger getCreatedBy() {
return createdBy;
}
/**
* 设置创建者。
*
* @param createdBy 创建者 ID
*/
public void setCreatedBy(BigInteger createdBy) {
this.createdBy = createdBy;
}
/**
* 获取修改时间。
*
* @return 修改时间
*/
public Date getModified() {
return modified;
}
/**
* 设置修改时间。
*
* @param modified 修改时间
*/
public void setModified(Date modified) {
this.modified = modified;
}
/**
* 获取修改者。
*
* @return 修改者 ID
*/
public BigInteger getModifiedBy() {
return modifiedBy;
}
/**
* 设置修改者。
*
* @param modifiedBy 修改者 ID
*/
public void setModifiedBy(BigInteger modifiedBy) {
this.modifiedBy = modifiedBy;
}
}

View File

@@ -0,0 +1,89 @@
package tech.easyflow.approval.entity.vo;
import java.math.BigInteger;
/**
* 审批对象目标。
*/
public class ApprovalAssigneeTargetVo {
private BigInteger targetId;
private String targetCode;
private String targetName;
private Integer includeChildren;
/**
* 获取审批对象 ID。
*
* @return 审批对象 ID
*/
public BigInteger getTargetId() {
return targetId;
}
/**
* 设置审批对象 ID。
*
* @param targetId 审批对象 ID
*/
public void setTargetId(BigInteger targetId) {
this.targetId = targetId;
}
/**
* 获取审批对象编码。
*
* @return 审批对象编码
*/
public String getTargetCode() {
return targetCode;
}
/**
* 设置审批对象编码。
*
* @param targetCode 审批对象编码
*/
public void setTargetCode(String targetCode) {
this.targetCode = targetCode;
}
/**
* 获取审批对象名称。
*
* @return 审批对象名称
*/
public String getTargetName() {
return targetName;
}
/**
* 设置审批对象名称。
*
* @param targetName 审批对象名称
*/
public void setTargetName(String targetName) {
this.targetName = targetName;
}
/**
* 获取是否包含子部门。
*
* @return 1 表示包含0 表示不包含
*/
public Integer getIncludeChildren() {
return includeChildren;
}
/**
* 设置是否包含子部门。
*
* @param includeChildren 1 表示包含0 表示不包含
*/
public void setIncludeChildren(Integer includeChildren) {
this.includeChildren = includeChildren;
}
}

View File

@@ -1,6 +1,7 @@
package tech.easyflow.approval.entity.vo;
import java.math.BigInteger;
import java.util.List;
/**
* 审批流程步骤项。
@@ -21,6 +22,8 @@ public class ApprovalFlowStepVo {
private String assigneeTargetName;
private List<ApprovalAssigneeTargetVo> assigneeTargets;
public BigInteger getId() {
return id;
}
@@ -76,4 +79,22 @@ public class ApprovalFlowStepVo {
public void setAssigneeTargetName(String assigneeTargetName) {
this.assigneeTargetName = assigneeTargetName;
}
/**
* 获取审批对象列表。
*
* @return 审批对象列表
*/
public List<ApprovalAssigneeTargetVo> getAssigneeTargets() {
return assigneeTargets;
}
/**
* 设置审批对象列表。
*
* @param assigneeTargets 审批对象列表
*/
public void setAssigneeTargets(List<ApprovalAssigneeTargetVo> assigneeTargets) {
this.assigneeTargets = assigneeTargets;
}
}

View File

@@ -2,6 +2,7 @@ package tech.easyflow.approval.entity.vo;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
/**
* 审批任务视图。
@@ -31,6 +32,8 @@ public class ApprovalTaskVo {
private String assigneeTargetName;
private List<ApprovalAssigneeTargetVo> assigneeTargets;
private BigInteger actedBy;
private String actedByName;
@@ -129,6 +132,24 @@ public class ApprovalTaskVo {
this.assigneeTargetName = assigneeTargetName;
}
/**
* 获取审批对象列表。
*
* @return 审批对象列表
*/
public List<ApprovalAssigneeTargetVo> getAssigneeTargets() {
return assigneeTargets;
}
/**
* 设置审批对象列表。
*
* @param assigneeTargets 审批对象列表
*/
public void setAssigneeTargets(List<ApprovalAssigneeTargetVo> assigneeTargets) {
this.assigneeTargets = assigneeTargets;
}
public BigInteger getActedBy() {
return actedBy;
}

View File

@@ -9,7 +9,8 @@ import tech.easyflow.common.web.exceptions.BusinessException;
public enum ApprovalAssigneeType {
ROLE("ROLE"),
USER("USER");
USER("USER"),
DEPT("DEPT");
private final String code;

View File

@@ -0,0 +1,10 @@
package tech.easyflow.approval.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.approval.entity.ApprovalFlowStepAssignee;
/**
* 审批流程步骤对象关联 Mapper。
*/
public interface ApprovalFlowStepAssigneeMapper extends BaseMapper<ApprovalFlowStepAssignee> {
}

View File

@@ -0,0 +1,10 @@
package tech.easyflow.approval.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.easyflow.approval.entity.ApprovalTaskAssignee;
/**
* 审批任务对象关联 Mapper。
*/
public interface ApprovalTaskAssigneeMapper extends BaseMapper<ApprovalTaskAssignee> {
}

View File

@@ -1,12 +1,16 @@
package tech.easyflow.approval.service;
import com.mybatisflex.core.paginate.Page;
import tech.easyflow.approval.entity.ApprovalFlowStep;
import tech.easyflow.approval.entity.ApprovalTask;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@@ -22,6 +26,53 @@ public interface ApprovalAssigneeService {
*/
ApprovalFlowStepVo normalizeStepAssignee(ApprovalFlowStepVo step);
/**
* 批量加载流程步骤的审批对象,关联记录缺失时兼容旧单值字段。
*
* @param steps 流程步骤
* @return 步骤 ID 到审批对象列表的映射
*/
Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadStepAssigneeTargets(List<ApprovalFlowStep> steps);
/**
* 批量加载审批任务的审批对象,关联记录缺失时兼容旧单值字段。
*
* @param tasks 审批任务
* @return 任务 ID 到审批对象列表的映射
*/
Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadTaskAssigneeTargets(List<ApprovalTask> tasks);
/**
* 保存流程步骤审批对象。
*
* @param stepId 流程步骤 ID
* @param assigneeType 审批对象类型
* @param targets 审批对象列表
* @param operatorId 操作人 ID
* @param now 操作时间
*/
void saveStepAssigneeTargets(BigInteger stepId, String assigneeType, List<ApprovalAssigneeTargetVo> targets,
BigInteger operatorId, Date now);
/**
* 删除指定流程步骤的审批对象。
*
* @param stepIds 流程步骤 ID
*/
void deleteStepAssigneeTargets(List<BigInteger> stepIds);
/**
* 保存任务冻结的审批对象。
*
* @param taskId 审批任务 ID
* @param assigneeType 审批对象类型
* @param targets 审批对象列表
* @param operatorId 操作人 ID
* @param now 操作时间
*/
void saveTaskAssigneeTargets(BigInteger taskId, String assigneeType, List<ApprovalAssigneeTargetVo> targets,
BigInteger operatorId, Date now);
/**
* 查询可用角色选项。
*

View File

@@ -5,25 +5,39 @@ import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.approval.entity.ApprovalFlowStep;
import tech.easyflow.approval.entity.ApprovalFlowStepAssignee;
import tech.easyflow.approval.entity.ApprovalTask;
import tech.easyflow.approval.entity.ApprovalTaskAssignee;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
import tech.easyflow.approval.enums.ApprovalAssigneeType;
import tech.easyflow.approval.enums.ApprovalTaskStatus;
import tech.easyflow.approval.mapper.ApprovalFlowStepAssigneeMapper;
import tech.easyflow.approval.mapper.ApprovalTaskAssigneeMapper;
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
import tech.easyflow.approval.service.ApprovalAssigneeService;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysDeptService;
import tech.easyflow.system.service.SysRoleService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -38,9 +52,18 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
@Resource
private SysAccountService sysAccountService;
@Resource
private SysDeptService sysDeptService;
@Resource
private ApprovalTaskMapper approvalTaskMapper;
@Resource
private ApprovalFlowStepAssigneeMapper approvalFlowStepAssigneeMapper;
@Resource
private ApprovalTaskAssigneeMapper approvalTaskAssigneeMapper;
/**
* {@inheritDoc}
*/
@@ -50,18 +73,168 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
throw new BusinessException("审批步骤不能为空");
}
ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(step.getAssigneeType());
BigInteger targetId = step.getAssigneeTargetId();
if (targetId == null) {
List<ApprovalAssigneeTargetVo> requestedTargets = step.getAssigneeTargets();
if (CollectionUtil.isEmpty(requestedTargets) && step.getAssigneeTargetId() != null) {
requestedTargets = List.of(legacyTarget(
step.getAssigneeTargetId(),
step.getAssigneeTargetCode(),
step.getAssigneeTargetName()));
}
if (CollectionUtil.isEmpty(requestedTargets)) {
throw new BusinessException("审批对象不能为空");
}
ApprovalAssigneeOptionVo option = resolveAssigneeOption(assigneeType, targetId);
Map<BigInteger, ApprovalAssigneeTargetVo> normalizedTargets = new LinkedHashMap<>();
for (ApprovalAssigneeTargetVo target : requestedTargets) {
if (target == null || target.getTargetId() == null) {
throw new BusinessException("审批对象不能为空");
}
ApprovalAssigneeOptionVo option = resolveAssigneeOption(assigneeType, target.getTargetId());
ApprovalAssigneeTargetVo normalizedTarget = new ApprovalAssigneeTargetVo();
normalizedTarget.setTargetId(option.getId());
normalizedTarget.setTargetCode(option.getCode());
normalizedTarget.setTargetName(option.getName());
normalizedTarget.setIncludeChildren(ApprovalAssigneeType.DEPT == assigneeType
&& Integer.valueOf(1).equals(target.getIncludeChildren()) ? 1 : 0);
normalizedTargets.putIfAbsent(normalizedTarget.getTargetId(), normalizedTarget);
}
step.setAssigneeType(assigneeType.getCode());
step.setAssigneeTargetId(option.getId());
step.setAssigneeTargetCode(option.getCode());
step.setAssigneeTargetName(option.getName());
step.setAssigneeTargets(new ArrayList<>(normalizedTargets.values()));
applyPrimaryTarget(step);
return step;
}
/**
* {@inheritDoc}
*/
@Override
public Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadStepAssigneeTargets(List<ApprovalFlowStep> steps) {
if (CollectionUtil.isEmpty(steps)) {
return Map.of();
}
List<BigInteger> stepIds = steps.stream()
.map(ApprovalFlowStep::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
List<ApprovalFlowStepAssignee> assignees = stepIds.isEmpty() ? List.of() : safeList(
approvalFlowStepAssigneeMapper.selectListByQuery(QueryWrapper.create()
.in(ApprovalFlowStepAssignee::getStepId, stepIds)
.orderBy("created asc, id asc")));
Map<BigInteger, List<ApprovalFlowStepAssignee>> relationMap = assignees.stream()
.collect(Collectors.groupingBy(
ApprovalFlowStepAssignee::getStepId,
LinkedHashMap::new,
Collectors.toList()));
Map<BigInteger, List<ApprovalAssigneeTargetVo>> result = new LinkedHashMap<>();
for (ApprovalFlowStep step : steps) {
List<ApprovalAssigneeTargetVo> targets = relationMap.getOrDefault(step.getId(), List.of()).stream()
.filter(item -> Objects.equals(step.getAssigneeType(), item.getAssigneeType()))
.map(this::toTarget)
.collect(Collectors.toList());
if (targets.isEmpty()) {
targets = legacyTargets(step.getAssigneeTargetId(), step.getAssigneeTargetCode(),
step.getAssigneeTargetName());
}
result.put(step.getId(), targets);
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadTaskAssigneeTargets(List<ApprovalTask> tasks) {
if (CollectionUtil.isEmpty(tasks)) {
return Map.of();
}
List<BigInteger> taskIds = tasks.stream()
.map(ApprovalTask::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
List<ApprovalTaskAssignee> assignees = taskIds.isEmpty() ? List.of() : safeList(
approvalTaskAssigneeMapper.selectListByQuery(QueryWrapper.create()
.in(ApprovalTaskAssignee::getTaskId, taskIds)
.orderBy("created asc, id asc")));
Map<BigInteger, List<ApprovalTaskAssignee>> relationMap = assignees.stream()
.collect(Collectors.groupingBy(
ApprovalTaskAssignee::getTaskId,
LinkedHashMap::new,
Collectors.toList()));
Map<BigInteger, List<ApprovalAssigneeTargetVo>> result = new LinkedHashMap<>();
for (ApprovalTask task : tasks) {
List<ApprovalAssigneeTargetVo> targets = relationMap.getOrDefault(task.getId(), List.of()).stream()
.filter(item -> Objects.equals(task.getAssigneeType(), item.getAssigneeType()))
.map(this::toTarget)
.collect(Collectors.toList());
if (targets.isEmpty()) {
targets = legacyTargets(task.getAssigneeTargetId(), task.getAssigneeTargetCode(),
task.getAssigneeTargetName());
}
result.put(task.getId(), targets);
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public void saveStepAssigneeTargets(BigInteger stepId, String assigneeType,
List<ApprovalAssigneeTargetVo> targets,
BigInteger operatorId, Date now) {
for (ApprovalAssigneeTargetVo target : requireTargets(targets)) {
ApprovalFlowStepAssignee relation = new ApprovalFlowStepAssignee();
relation.setStepId(stepId);
relation.setAssigneeType(assigneeType);
relation.setTargetId(target.getTargetId());
relation.setTargetCode(target.getTargetCode());
relation.setTargetName(target.getTargetName());
relation.setIncludeChildren(normalizeIncludeChildren(assigneeType, target.getIncludeChildren()));
relation.setCreated(now);
relation.setCreatedBy(operatorId);
relation.setModified(now);
relation.setModifiedBy(operatorId);
approvalFlowStepAssigneeMapper.insert(relation);
}
}
/**
* {@inheritDoc}
*/
@Override
public void deleteStepAssigneeTargets(List<BigInteger> stepIds) {
if (CollectionUtil.isEmpty(stepIds)) {
return;
}
approvalFlowStepAssigneeMapper.deleteByQuery(
QueryWrapper.create().in(ApprovalFlowStepAssignee::getStepId, stepIds));
}
/**
* {@inheritDoc}
*/
@Override
public void saveTaskAssigneeTargets(BigInteger taskId, String assigneeType,
List<ApprovalAssigneeTargetVo> targets,
BigInteger operatorId, Date now) {
for (ApprovalAssigneeTargetVo target : requireTargets(targets)) {
ApprovalTaskAssignee relation = new ApprovalTaskAssignee();
relation.setTaskId(taskId);
relation.setAssigneeType(assigneeType);
relation.setTargetId(target.getTargetId());
relation.setTargetCode(target.getTargetCode());
relation.setTargetName(target.getTargetName());
relation.setIncludeChildren(normalizeIncludeChildren(assigneeType, target.getIncludeChildren()));
relation.setCreated(now);
relation.setCreatedBy(operatorId);
relation.setModified(now);
relation.setModifiedBy(operatorId);
approvalTaskAssigneeMapper.insert(relation);
}
}
/**
* {@inheritDoc}
*/
@@ -118,46 +291,210 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
return false;
}
ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(task.getAssigneeType());
List<ApprovalAssigneeTargetVo> targets = loadTaskAssigneeTargets(List.of(task))
.getOrDefault(task.getId(), List.of());
if (ApprovalAssigneeType.USER == assigneeType) {
return operatorId.equals(task.getAssigneeTargetId());
return targets.stream().anyMatch(target -> operatorId.equals(target.getTargetId()));
}
return roleIds != null && roleIds.contains(task.getAssigneeTargetId());
if (ApprovalAssigneeType.ROLE == assigneeType) {
return roleIds != null && targets.stream().anyMatch(target -> roleIds.contains(target.getTargetId()));
}
SysAccount operator = sysAccountService.getById(operatorId);
return operator != null && matchesDepartment(targets, operator.getDeptId());
}
/**
* {@inheritDoc}
*/
@Override
public Set<BigInteger> listPendingInstanceIds(BigInteger operatorId, Set<BigInteger> roleIds, List<BigInteger> instanceIds) {
public Set<BigInteger> listPendingInstanceIds(BigInteger operatorId, Set<BigInteger> roleIds,
List<BigInteger> instanceIds) {
if (operatorId == null) {
return Set.of();
}
Set<BigInteger> result = new LinkedHashSet<>();
QueryWrapper userQuery = QueryWrapper.create()
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode());
if (CollectionUtil.isNotEmpty(instanceIds)) {
userQuery.in(ApprovalTask::getInstanceId, instanceIds);
}
userQuery.eq(ApprovalTask::getAssigneeType, ApprovalAssigneeType.USER.getCode())
.eq(ApprovalTask::getAssigneeTargetId, operatorId);
result.addAll(approvalTaskMapper.selectListByQuery(userQuery).stream()
.map(ApprovalTask::getInstanceId)
.collect(Collectors.toCollection(LinkedHashSet::new)));
Set<BigInteger> matchedTaskIds = new LinkedHashSet<>();
matchedTaskIds.addAll(loadMatchingRelationTaskIds(
ApprovalAssigneeType.USER, Set.of(operatorId), null, instanceIds));
if (CollectionUtil.isNotEmpty(roleIds)) {
QueryWrapper roleQuery = QueryWrapper.create()
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode())
.eq(ApprovalTask::getAssigneeType, ApprovalAssigneeType.ROLE.getCode())
.in(ApprovalTask::getAssigneeTargetId, roleIds);
if (CollectionUtil.isNotEmpty(instanceIds)) {
roleQuery.in(ApprovalTask::getInstanceId, instanceIds);
}
result.addAll(approvalTaskMapper.selectListByQuery(roleQuery).stream()
.map(ApprovalTask::getInstanceId)
.collect(Collectors.toCollection(LinkedHashSet::new)));
matchedTaskIds.addAll(loadMatchingRelationTaskIds(
ApprovalAssigneeType.ROLE, roleIds, null, instanceIds));
}
SysAccount operator = sysAccountService.getById(operatorId);
Set<BigInteger> departmentIds = resolveDepartmentIds(operator == null ? null : operator.getDeptId());
if (CollectionUtil.isNotEmpty(departmentIds)) {
matchedTaskIds.addAll(loadMatchingRelationTaskIds(
ApprovalAssigneeType.DEPT, departmentIds, operator.getDeptId(), instanceIds));
}
Set<BigInteger> result = loadPendingTasks(matchedTaskIds, instanceIds).stream()
.map(ApprovalTask::getInstanceId)
.collect(Collectors.toCollection(LinkedHashSet::new));
// 旧单值任务仅在没有关联记录时参与兜底,避免旧字段覆盖新关联表的主数据语义。
List<ApprovalTask> legacyCandidates = loadLegacyPendingCandidates(
operatorId, roleIds, operator == null ? null : operator.getDeptId(), instanceIds);
if (legacyCandidates.isEmpty()) {
return result;
}
Set<BigInteger> candidateIds = legacyCandidates.stream()
.map(ApprovalTask::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<BigInteger> relatedTaskIds = candidateIds.isEmpty() ? Set.of() : safeList(
approvalTaskAssigneeMapper.selectListByQuery(
QueryWrapper.create().in(ApprovalTaskAssignee::getTaskId, candidateIds))).stream()
.map(ApprovalTaskAssignee::getTaskId)
.collect(Collectors.toSet());
legacyCandidates.stream()
.filter(task -> !relatedTaskIds.contains(task.getId()))
.map(ApprovalTask::getInstanceId)
.forEach(result::add);
return result;
}
/**
* 查询能命中当前主体的任务关联 ID。
*
* @param type 审批对象类型
* @param targetIds 候选对象 ID
* @param currentDeptId 当前部门 ID部门审批时用于区分直属命中
* @param instanceIds 可选实例过滤
* @return 任务 ID 集合
*/
private Set<BigInteger> loadMatchingRelationTaskIds(ApprovalAssigneeType type, Set<BigInteger> targetIds,
BigInteger currentDeptId,
List<BigInteger> instanceIds) {
if (CollectionUtil.isEmpty(targetIds)) {
return Set.of();
}
QueryWrapper pendingTaskQuery = QueryWrapper.create()
.select(ApprovalTask::getId)
.from(ApprovalTask.class)
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode());
if (CollectionUtil.isNotEmpty(instanceIds)) {
pendingTaskQuery.in(ApprovalTask::getInstanceId, instanceIds);
}
return safeList(approvalTaskAssigneeMapper.selectListByQuery(QueryWrapper.create()
.eq(ApprovalTaskAssignee::getAssigneeType, type.getCode())
.in(ApprovalTaskAssignee::getTargetId, targetIds)
.in(ApprovalTaskAssignee::getTaskId, pendingTaskQuery)))
.stream()
.filter(item -> type != ApprovalAssigneeType.DEPT
|| Objects.equals(currentDeptId, item.getTargetId())
|| Integer.valueOf(1).equals(item.getIncludeChildren()))
.map(ApprovalTaskAssignee::getTaskId)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* 按任务 ID 批量读取待审批任务。
*
* @param taskIds 任务 ID
* @param instanceIds 可选实例过滤
* @return 待审批任务
*/
private List<ApprovalTask> loadPendingTasks(Set<BigInteger> taskIds, List<BigInteger> instanceIds) {
if (CollectionUtil.isEmpty(taskIds)) {
return List.of();
}
QueryWrapper query = QueryWrapper.create()
.in(ApprovalTask::getId, taskIds)
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode());
if (CollectionUtil.isNotEmpty(instanceIds)) {
query.in(ApprovalTask::getInstanceId, instanceIds);
}
return safeList(approvalTaskMapper.selectListByQuery(query));
}
/**
* 查询旧单值字段能命中的待审批任务候选。
*
* @param operatorId 操作人 ID
* @param roleIds 角色 ID
* @param deptId 当前部门 ID
* @param instanceIds 可选实例过滤
* @return 候选任务
*/
private List<ApprovalTask> loadLegacyPendingCandidates(BigInteger operatorId, Set<BigInteger> roleIds,
BigInteger deptId, List<BigInteger> instanceIds) {
List<ApprovalTask> result = new ArrayList<>();
result.addAll(loadLegacyPendingCandidates(
ApprovalAssigneeType.USER, Set.of(operatorId), instanceIds));
if (CollectionUtil.isNotEmpty(roleIds)) {
result.addAll(loadLegacyPendingCandidates(
ApprovalAssigneeType.ROLE, roleIds, instanceIds));
}
if (deptId != null) {
result.addAll(loadLegacyPendingCandidates(
ApprovalAssigneeType.DEPT, Set.of(deptId), instanceIds));
}
return result.stream()
.collect(Collectors.toMap(
ApprovalTask::getId,
Function.identity(),
(left, right) -> left,
LinkedHashMap::new))
.values()
.stream()
.collect(Collectors.toList());
}
/**
* 查询一种旧单值审批对象命中的待审批任务。
*
* @param type 审批对象类型
* @param targetIds 对象 ID
* @param instanceIds 可选实例过滤
* @return 候选任务
*/
private List<ApprovalTask> loadLegacyPendingCandidates(ApprovalAssigneeType type, Set<BigInteger> targetIds,
List<BigInteger> instanceIds) {
QueryWrapper query = QueryWrapper.create()
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode())
.eq(ApprovalTask::getAssigneeType, type.getCode())
.in(ApprovalTask::getAssigneeTargetId, targetIds);
if (CollectionUtil.isNotEmpty(instanceIds)) {
query.in(ApprovalTask::getInstanceId, instanceIds);
}
return safeList(approvalTaskMapper.selectListByQuery(query));
}
/**
* 判断当前部门是否命中任一部门审批对象。
*
* @param targets 部门审批对象
* @param currentDeptId 当前部门 ID
* @return 是否命中
*/
private boolean matchesDepartment(List<ApprovalAssigneeTargetVo> targets, BigInteger currentDeptId) {
Set<BigInteger> departmentIds = resolveDepartmentIds(currentDeptId);
if (departmentIds.isEmpty()) {
return false;
}
return targets.stream().anyMatch(target ->
Objects.equals(currentDeptId, target.getTargetId())
|| Integer.valueOf(1).equals(target.getIncludeChildren())
&& departmentIds.contains(target.getTargetId()));
}
/**
* 获取当前部门及其祖先部门。
*
* @param currentDeptId 当前部门 ID
* @return 部门 ID 集合
*/
private Set<BigInteger> resolveDepartmentIds(BigInteger currentDeptId) {
return currentDeptId == null ? Set.of() : sysDeptService.getSelfAndAncestorDeptIds(currentDeptId);
}
/**
* 解析并校验单个审批对象。
*
* @param assigneeType 审批对象类型
* @param targetId 审批对象 ID
* @return 规范化选项
*/
private ApprovalAssigneeOptionVo resolveAssigneeOption(ApprovalAssigneeType assigneeType, BigInteger targetId) {
if (ApprovalAssigneeType.ROLE == assigneeType) {
SysRole role = sysRoleService.getById(targetId);
@@ -166,6 +503,13 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
}
return toRoleOption(role);
}
if (ApprovalAssigneeType.DEPT == assigneeType) {
SysDept dept = sysDeptService.getById(targetId);
if (dept == null || !EnumDataStatus.AVAILABLE.getCode().equals(dept.getStatus())) {
throw new BusinessException("审批部门不存在或未启用");
}
return toDeptOption(dept);
}
SysAccount account = sysAccountService.getById(targetId);
if (account == null || !EnumDataStatus.AVAILABLE.getCode().equals(account.getStatus())) {
throw new BusinessException("审批用户不存在或未启用");
@@ -173,6 +517,12 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
return toAccountOption(account);
}
/**
* 将角色转换为审批选项。
*
* @param role 角色
* @return 审批选项
*/
private ApprovalAssigneeOptionVo toRoleOption(SysRole role) {
ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo();
option.setId(role.getId());
@@ -181,6 +531,12 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
return option;
}
/**
* 将账号转换为审批选项。
*
* @param account 账号
* @return 审批选项
*/
private ApprovalAssigneeOptionVo toAccountOption(SysAccount account) {
ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo();
option.setId(account.getId());
@@ -189,6 +545,26 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
return option;
}
/**
* 将部门转换为审批选项。
*
* @param dept 部门
* @return 审批选项
*/
private ApprovalAssigneeOptionVo toDeptOption(SysDept dept) {
ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo();
option.setId(dept.getId());
option.setCode(dept.getDeptCode());
option.setName(dept.getDeptName());
return option;
}
/**
* 解析账号展示名称。
*
* @param account 账号
* @return 展示名称
*/
private String resolveAccountDisplayName(SysAccount account) {
if (StringUtils.hasText(account.getNickname())) {
return account.getNickname().trim();
@@ -198,4 +574,122 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
}
return String.valueOf(account.getId());
}
/**
* 将步骤的第一个审批对象同步到旧单值字段。
*
* @param step 步骤
*/
private void applyPrimaryTarget(ApprovalFlowStepVo step) {
ApprovalAssigneeTargetVo primary = step.getAssigneeTargets().get(0);
step.setAssigneeTargetId(primary.getTargetId());
step.setAssigneeTargetCode(primary.getTargetCode());
step.setAssigneeTargetName(primary.getTargetName());
}
/**
* 构造旧单值审批对象。
*
* @param targetId 对象 ID
* @param targetCode 对象编码
* @param targetName 对象名称
* @return 审批对象
*/
private ApprovalAssigneeTargetVo legacyTarget(BigInteger targetId, String targetCode, String targetName) {
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(targetId);
target.setTargetCode(targetCode);
target.setTargetName(targetName);
target.setIncludeChildren(0);
return target;
}
/**
* 构造旧单值审批对象列表。
*
* @param targetId 对象 ID
* @param targetCode 对象编码
* @param targetName 对象名称
* @return 审批对象列表
*/
private List<ApprovalAssigneeTargetVo> legacyTargets(BigInteger targetId, String targetCode, String targetName) {
return targetId == null ? List.of() : List.of(legacyTarget(targetId, targetCode, targetName));
}
/**
* 将步骤关联实体转换为审批对象。
*
* @param relation 步骤关联
* @return 审批对象
*/
private ApprovalAssigneeTargetVo toTarget(ApprovalFlowStepAssignee relation) {
return target(relation.getTargetId(), relation.getTargetCode(), relation.getTargetName(),
relation.getIncludeChildren());
}
/**
* 将任务关联实体转换为审批对象。
*
* @param relation 任务关联
* @return 审批对象
*/
private ApprovalAssigneeTargetVo toTarget(ApprovalTaskAssignee relation) {
return target(relation.getTargetId(), relation.getTargetCode(), relation.getTargetName(),
relation.getIncludeChildren());
}
/**
* 构造审批对象。
*
* @param targetId 对象 ID
* @param targetCode 对象编码
* @param targetName 对象名称
* @param includeChildren 是否包含子部门
* @return 审批对象
*/
private ApprovalAssigneeTargetVo target(BigInteger targetId, String targetCode, String targetName,
Integer includeChildren) {
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(targetId);
target.setTargetCode(targetCode);
target.setTargetName(targetName);
target.setIncludeChildren(Integer.valueOf(1).equals(includeChildren) ? 1 : 0);
return target;
}
/**
* 校验审批对象列表。
*
* @param targets 审批对象列表
* @return 非空审批对象列表
*/
private List<ApprovalAssigneeTargetVo> requireTargets(List<ApprovalAssigneeTargetVo> targets) {
if (CollectionUtil.isEmpty(targets)) {
throw new BusinessException("审批对象不能为空");
}
return targets;
}
/**
* 规范化包含子部门标记。
*
* @param assigneeType 审批对象类型
* @param includeChildren 原标记
* @return 规范化标记
*/
private int normalizeIncludeChildren(String assigneeType, Integer includeChildren) {
return ApprovalAssigneeType.DEPT.getCode().equals(assigneeType)
&& Integer.valueOf(1).equals(includeChildren) ? 1 : 0;
}
/**
* 将可能为空的 Mapper 查询结果转换为空列表。
*
* @param values 查询结果
* @param <T> 元素类型
* @return 非空列表
*/
private <T> List<T> safeList(List<T> values) {
return values == null ? List.of() : values;
}
}

View File

@@ -12,6 +12,7 @@ import tech.easyflow.approval.entity.ApprovalFlow;
import tech.easyflow.approval.entity.ApprovalFlowScope;
import tech.easyflow.approval.entity.ApprovalFlowStep;
import tech.easyflow.approval.entity.ApprovalInstance;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowScopeVo;
@@ -293,6 +294,7 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
normalized.setStepName(item.getStepName().trim());
normalized.setAssigneeType(item.getAssigneeType());
normalized.setAssigneeTargetId(item.getAssigneeTargetId());
normalized.setAssigneeTargets(item.getAssigneeTargets());
approvalAssigneeService.normalizeStepAssignee(normalized);
result.add(normalized);
}
@@ -304,6 +306,12 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
private void clearChildren(BigInteger flowId) {
approvalFlowScopeMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowScope::getFlowId, flowId));
List<BigInteger> stepIds = approvalFlowStepMapper.selectListByQuery(
QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId))
.stream()
.map(ApprovalFlowStep::getId)
.collect(Collectors.toList());
approvalAssigneeService.deleteStepAssigneeTargets(stepIds);
approvalFlowStepMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId));
}
@@ -334,6 +342,8 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
step.setModified(now);
step.setModifiedBy(operatorId);
approvalFlowStepMapper.insert(step);
approvalAssigneeService.saveStepAssigneeTargets(
step.getId(), stepVo.getAssigneeType(), stepVo.getAssigneeTargets(), operatorId, now);
}
}
@@ -368,8 +378,11 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
}
private List<ApprovalFlowStepVo> loadStepVos(BigInteger flowId) {
return approvalFlowStepMapper.selectListByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId))
.stream()
List<ApprovalFlowStep> steps = approvalFlowStepMapper.selectListByQuery(
QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId));
Map<BigInteger, List<ApprovalAssigneeTargetVo>> targetMap =
approvalAssigneeService.loadStepAssigneeTargets(steps);
return steps.stream()
.sorted(Comparator.comparing(ApprovalFlowStep::getStepNo))
.map(item -> {
ApprovalFlowStepVo stepVo = new ApprovalFlowStepVo();
@@ -380,6 +393,7 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
stepVo.setAssigneeTargetId(item.getAssigneeTargetId());
stepVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
stepVo.setAssigneeTargetName(item.getAssigneeTargetName());
stepVo.setAssigneeTargets(targetMap.getOrDefault(item.getId(), List.of()));
return stepVo;
})
.collect(Collectors.toList());

View File

@@ -12,6 +12,7 @@ import tech.easyflow.approval.entity.ApprovalInstance;
import tech.easyflow.approval.entity.ApprovalFlowStep;
import tech.easyflow.approval.entity.ApprovalLog;
import tech.easyflow.approval.entity.ApprovalTask;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
@@ -271,6 +272,14 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
map.put("assigneeTargetId", item.getAssigneeTargetId());
map.put("assigneeTargetCode", item.getAssigneeTargetCode());
map.put("assigneeTargetName", item.getAssigneeTargetName());
map.put("assigneeTargets", resolveStepTargets(item).stream().map(target -> {
Map<String, Object> targetMap = new LinkedHashMap<>();
targetMap.put("targetId", target.getTargetId());
targetMap.put("targetCode", target.getTargetCode());
targetMap.put("targetName", target.getTargetName());
targetMap.put("includeChildren", target.getIncludeChildren());
return targetMap;
}).collect(Collectors.toList()));
return map;
}).collect(Collectors.toList()));
return snapshot;
@@ -315,6 +324,10 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
if (assigneeTargetName != null) {
stepVo.setAssigneeTargetName(String.valueOf(assigneeTargetName));
}
stepVo.setAssigneeTargets(parseAssigneeTargets(stepMap.get("assigneeTargets")));
if (stepVo.getAssigneeTargets().isEmpty() && stepVo.getAssigneeTargetId() != null) {
stepVo.setAssigneeTargets(List.of(legacyTarget(stepVo)));
}
result.add(stepVo);
}
mergeStepAssigneeFromFlow(instance.getFlowId(), result);
@@ -326,22 +339,30 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
}
private void createTask(BigInteger instanceId, ApprovalFlowStepVo step, BigInteger operatorId, Date now) {
List<ApprovalAssigneeTargetVo> targets = resolveStepTargets(step);
if (CollectionUtil.isEmpty(targets)) {
throw new BusinessException("审批步骤缺少审批对象");
}
step.setAssigneeTargets(targets);
ApprovalAssigneeTargetVo primaryTarget = targets.get(0);
ApprovalTask task = new ApprovalTask();
task.setInstanceId(instanceId);
task.setStepNo(step.getStepNo());
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
task.setAssigneeRoleCode(ApprovalAssigneeType.ROLE.getCode().equals(step.getAssigneeType())
? step.getAssigneeTargetCode()
? primaryTarget.getTargetCode()
: null);
task.setAssigneeType(step.getAssigneeType());
task.setAssigneeTargetId(step.getAssigneeTargetId());
task.setAssigneeTargetCode(step.getAssigneeTargetCode());
task.setAssigneeTargetName(step.getAssigneeTargetName());
task.setAssigneeTargetId(primaryTarget.getTargetId());
task.setAssigneeTargetCode(primaryTarget.getTargetCode());
task.setAssigneeTargetName(primaryTarget.getTargetName());
task.setCreated(now);
task.setCreatedBy(operatorId);
task.setModified(now);
task.setModifiedBy(operatorId);
approvalTaskMapper.insert(task);
approvalAssigneeService.saveTaskAssigneeTargets(
task.getId(), step.getAssigneeType(), targets, operatorId, now);
}
private void finishTask(ApprovalTask task, String status, String comment, BigInteger operatorId, Date now) {
@@ -480,8 +501,12 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
(left, right) -> left,
LinkedHashMap::new
));
Map<BigInteger, List<ApprovalAssigneeTargetVo>> targetMap =
approvalAssigneeService.loadStepAssigneeTargets(storedSteps);
for (ApprovalFlowStepVo step : steps) {
if (step.getAssigneeTargetId() != null && step.getAssigneeType() != null) {
if (CollectionUtil.isNotEmpty(step.getAssigneeTargets())
&& step.getAssigneeTargetId() != null
&& step.getAssigneeType() != null) {
continue;
}
ApprovalFlowStep storedStep = storedMap.get(step.getStepNo());
@@ -492,6 +517,7 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
step.setAssigneeTargetId(storedStep.getAssigneeTargetId());
step.setAssigneeTargetCode(storedStep.getAssigneeTargetCode());
step.setAssigneeTargetName(storedStep.getAssigneeTargetName());
step.setAssigneeTargets(targetMap.getOrDefault(storedStep.getId(), List.of()));
}
}
@@ -509,6 +535,85 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
payload.put("assigneeTargetId", step.getAssigneeTargetId());
payload.put("assigneeTargetCode", step.getAssigneeTargetCode());
payload.put("assigneeTargetName", step.getAssigneeTargetName());
payload.put("assigneeTargets", resolveStepTargets(step));
return payload;
}
/**
* 从冻结快照中解析审批对象列表。
*
* @param value 快照字段
* @return 审批对象列表
*/
private List<ApprovalAssigneeTargetVo> parseAssigneeTargets(Object value) {
if (!(value instanceof List<?> values)) {
return List.of();
}
List<ApprovalAssigneeTargetVo> result = new ArrayList<>();
for (Object item : values) {
if (!(item instanceof Map<?, ?> map)) {
continue;
}
BigInteger targetId = parseTargetId(map.get("targetId"));
if (targetId == null) {
continue;
}
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(targetId);
target.setTargetCode(map.get("targetCode") == null ? null : String.valueOf(map.get("targetCode")));
target.setTargetName(map.get("targetName") == null ? null : String.valueOf(map.get("targetName")));
Object includeChildren = map.get("includeChildren");
target.setIncludeChildren(includeChildren instanceof Number number && number.intValue() == 1
|| Boolean.TRUE.equals(includeChildren) ? 1 : 0);
result.add(target);
}
return result;
}
/**
* 解析快照中的对象 ID。
*
* @param value 快照值
* @return 对象 ID
*/
private BigInteger parseTargetId(Object value) {
if (value instanceof BigInteger bigInteger) {
return bigInteger;
}
if (value instanceof Number number) {
return new BigInteger(number.toString());
}
if (value instanceof String string && !string.isBlank()) {
return new BigInteger(string);
}
return null;
}
/**
* 将旧单值步骤转换为单元素审批对象。
*
* @param step 旧步骤
* @return 审批对象
*/
private ApprovalAssigneeTargetVo legacyTarget(ApprovalFlowStepVo step) {
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(step.getAssigneeTargetId());
target.setTargetCode(step.getAssigneeTargetCode());
target.setTargetName(step.getAssigneeTargetName());
target.setIncludeChildren(0);
return target;
}
/**
* 获取步骤审批对象,兼容旧单值步骤。
*
* @param step 审批步骤
* @return 审批对象列表
*/
private List<ApprovalAssigneeTargetVo> resolveStepTargets(ApprovalFlowStepVo step) {
if (CollectionUtil.isNotEmpty(step.getAssigneeTargets())) {
return step.getAssigneeTargets();
}
return step.getAssigneeTargetId() == null ? List.of() : List.of(legacyTarget(step));
}
}

View File

@@ -12,6 +12,7 @@ import tech.easyflow.approval.entity.ApprovalFlowStep;
import tech.easyflow.approval.entity.ApprovalInstance;
import tech.easyflow.approval.entity.ApprovalLog;
import tech.easyflow.approval.entity.ApprovalTask;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalInstanceDetailVo;
import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
@@ -174,6 +175,8 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));
Map<Integer, ApprovalFlowStepVo> frozenStepMap = resolveFrozenStepMap(instance);
Map<BigInteger, List<ApprovalAssigneeTargetVo>> taskTargetMap =
approvalAssigneeService.loadTaskAssigneeTargets(tasks);
Map<BigInteger, SysAccount> accountMap = loadAccountMap(instance, tasks, logs, account.getTenantId());
detail.setApplicantName(resolveAccountName(accountMap.get(instance.getApplicantId())));
detail.setApplicantAccount(resolveAccountLoginName(accountMap.get(instance.getApplicantId())));
@@ -192,6 +195,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
taskVo.setAssigneeTargetId(item.getAssigneeTargetId());
taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
taskVo.setAssigneeTargetName(item.getAssigneeTargetName());
taskVo.setAssigneeTargets(taskTargetMap.getOrDefault(item.getId(), List.of()));
taskVo.setActedBy(item.getActedBy());
taskVo.setActedByName(resolveAccountName(accountMap.get(item.getActedBy())));
taskVo.setActedAt(item.getActedAt());
@@ -466,14 +470,21 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
if (assigneeTargetName != null) {
stepVo.setAssigneeTargetName(String.valueOf(assigneeTargetName));
}
stepVo.setAssigneeTargets(parseAssigneeTargets(map.get("assigneeTargets")));
if (stepVo.getAssigneeTargets().isEmpty() && stepVo.getAssigneeTargetId() != null) {
stepVo.setAssigneeTargets(List.of(legacyTarget(stepVo)));
}
result.put(stepVo.getStepNo(), stepVo);
}
}
if (!result.isEmpty() && result.values().stream().allMatch(item -> item.getAssigneeType() != null && item.getAssigneeTargetId() != null)) {
if (!result.isEmpty() && result.values().stream().allMatch(item ->
item.getAssigneeType() != null && CollectionUtil.isNotEmpty(item.getAssigneeTargets()))) {
return result;
}
List<ApprovalFlowStep> storedSteps = approvalFlowStepMapper.selectListByQuery(
QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, instance.getFlowId()));
Map<BigInteger, List<ApprovalAssigneeTargetVo>> targetMap =
approvalAssigneeService.loadStepAssigneeTargets(storedSteps);
for (ApprovalFlowStep step : storedSteps) {
ApprovalFlowStepVo stepVo = result.computeIfAbsent(step.getStepNo(), key -> {
ApprovalFlowStepVo value = new ApprovalFlowStepVo();
@@ -495,7 +506,75 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
if (!StringUtils.hasText(stepVo.getAssigneeTargetName())) {
stepVo.setAssigneeTargetName(step.getAssigneeTargetName());
}
if (CollectionUtil.isEmpty(stepVo.getAssigneeTargets())) {
stepVo.setAssigneeTargets(targetMap.getOrDefault(step.getId(), List.of()));
}
}
return result;
}
/**
* 从冻结快照中解析审批对象列表。
*
* @param value 快照字段
* @return 审批对象列表
*/
private List<ApprovalAssigneeTargetVo> parseAssigneeTargets(Object value) {
if (!(value instanceof List<?> values)) {
return List.of();
}
List<ApprovalAssigneeTargetVo> result = new ArrayList<>();
for (Object item : values) {
if (!(item instanceof Map<?, ?> map)) {
continue;
}
BigInteger targetId = parseTargetId(map.get("targetId"));
if (targetId == null) {
continue;
}
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(targetId);
target.setTargetCode(map.get("targetCode") == null ? null : String.valueOf(map.get("targetCode")));
target.setTargetName(map.get("targetName") == null ? null : String.valueOf(map.get("targetName")));
Object includeChildren = map.get("includeChildren");
target.setIncludeChildren(includeChildren instanceof Number number && number.intValue() == 1
|| Boolean.TRUE.equals(includeChildren) ? 1 : 0);
result.add(target);
}
return result;
}
/**
* 解析快照中的对象 ID。
*
* @param value 快照值
* @return 对象 ID
*/
private BigInteger parseTargetId(Object value) {
if (value instanceof BigInteger bigInteger) {
return bigInteger;
}
if (value instanceof Number number) {
return new BigInteger(number.toString());
}
if (value instanceof String string && StringUtils.hasText(string)) {
return new BigInteger(string);
}
return null;
}
/**
* 将旧单值步骤转换为单元素审批对象。
*
* @param step 旧步骤
* @return 审批对象
*/
private ApprovalAssigneeTargetVo legacyTarget(ApprovalFlowStepVo step) {
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(step.getAssigneeTargetId());
target.setTargetCode(step.getAssigneeTargetCode());
target.setTargetName(step.getAssigneeTargetName());
target.setIncludeChildren(0);
return target;
}
}

View File

@@ -0,0 +1,248 @@
package tech.easyflow.approval.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import tech.easyflow.approval.entity.ApprovalTask;
import tech.easyflow.approval.entity.ApprovalTaskAssignee;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
import tech.easyflow.approval.enums.ApprovalAssigneeType;
import tech.easyflow.approval.enums.ApprovalTaskStatus;
import tech.easyflow.approval.mapper.ApprovalFlowStepAssigneeMapper;
import tech.easyflow.approval.mapper.ApprovalTaskAssigneeMapper;
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysDeptService;
import tech.easyflow.system.service.SysRoleService;
import java.math.BigInteger;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
/**
* {@link ApprovalAssigneeServiceImpl} 多审批对象语义测试。
*/
@RunWith(MockitoJUnitRunner.class)
public class ApprovalAssigneeServiceImplTest {
@Mock
private SysRoleService sysRoleService;
@Mock
private SysAccountService sysAccountService;
@Mock
private SysDeptService sysDeptService;
@Mock
private ApprovalTaskMapper approvalTaskMapper;
@Mock
private ApprovalFlowStepAssigneeMapper approvalFlowStepAssigneeMapper;
@Mock
private ApprovalTaskAssigneeMapper approvalTaskAssigneeMapper;
@InjectMocks
private ApprovalAssigneeServiceImpl service;
/**
* 验证同一步骤支持多个角色,并按 ID 去重、由服务端补齐名称。
*/
@Test
public void normalizeStepAssigneeShouldResolveAndDeduplicateMultipleRoles() {
SysRole reviewer = role(11, "reviewer", "审核员");
SysRole owner = role(12, "owner", "负责人");
when(sysRoleService.getById(reviewer.getId())).thenReturn(reviewer);
when(sysRoleService.getById(owner.getId())).thenReturn(owner);
ApprovalFlowStepVo step = new ApprovalFlowStepVo();
step.setAssigneeType(ApprovalAssigneeType.ROLE.getCode());
step.setAssigneeTargets(List.of(
target(reviewer.getId(), 0),
target(owner.getId(), 0),
target(reviewer.getId(), 0)));
ApprovalFlowStepVo normalized = service.normalizeStepAssignee(step);
assertEquals(2, normalized.getAssigneeTargets().size());
assertEquals(reviewer.getId(), normalized.getAssigneeTargetId());
assertEquals("审核员", normalized.getAssigneeTargetName());
assertEquals("负责人", normalized.getAssigneeTargets().get(1).getTargetName());
}
/**
* 验证部门审批可通过“包含子部门”命中下级部门成员。
*/
@Test
public void canHandleTaskShouldMatchDescendantDepartment() {
BigInteger operatorId = BigInteger.valueOf(7);
BigInteger parentDeptId = BigInteger.valueOf(21);
BigInteger currentDeptId = BigInteger.valueOf(22);
ApprovalTask task = new ApprovalTask();
task.setId(BigInteger.valueOf(101));
task.setAssigneeType(ApprovalAssigneeType.DEPT.getCode());
ApprovalTaskAssignee relation = new ApprovalTaskAssignee();
relation.setTaskId(task.getId());
relation.setAssigneeType(ApprovalAssigneeType.DEPT.getCode());
relation.setTargetId(parentDeptId);
relation.setIncludeChildren(1);
when(approvalTaskAssigneeMapper.selectListByQuery(any(QueryWrapper.class)))
.thenReturn(List.of(relation));
SysAccount operator = new SysAccount();
operator.setId(operatorId);
operator.setDeptId(currentDeptId);
when(sysAccountService.getById(operatorId)).thenReturn(operator);
when(sysDeptService.getSelfAndAncestorDeptIds(currentDeptId))
.thenReturn(Set.of(parentDeptId, currentDeptId));
assertTrue(service.canHandleTask(task, operatorId, Set.of()));
}
/**
* 验证未启用“包含子部门”时,下级部门成员不能处理父部门任务。
*/
@Test
public void canHandleTaskShouldRejectDescendantDepartmentWhenFlagDisabled() {
BigInteger operatorId = BigInteger.valueOf(7);
BigInteger parentDeptId = BigInteger.valueOf(21);
BigInteger currentDeptId = BigInteger.valueOf(22);
ApprovalTask task = new ApprovalTask();
task.setId(BigInteger.valueOf(101));
task.setAssigneeType(ApprovalAssigneeType.DEPT.getCode());
ApprovalTaskAssignee relation = new ApprovalTaskAssignee();
relation.setTaskId(task.getId());
relation.setAssigneeType(ApprovalAssigneeType.DEPT.getCode());
relation.setTargetId(parentDeptId);
relation.setIncludeChildren(0);
when(approvalTaskAssigneeMapper.selectListByQuery(any(QueryWrapper.class)))
.thenReturn(List.of(relation));
SysAccount operator = new SysAccount();
operator.setId(operatorId);
operator.setDeptId(currentDeptId);
when(sysAccountService.getById(operatorId)).thenReturn(operator);
when(sysDeptService.getSelfAndAncestorDeptIds(currentDeptId))
.thenReturn(Set.of(parentDeptId, currentDeptId));
assertFalse(service.canHandleTask(task, operatorId, Set.of()));
}
/**
* 验证包含子部门的部门任务会出现在下级部门成员的待审批列表。
*/
@Test
public void listPendingInstanceIdsShouldMatchDescendantDepartment() {
BigInteger operatorId = BigInteger.valueOf(7);
BigInteger parentDeptId = BigInteger.valueOf(21);
BigInteger currentDeptId = BigInteger.valueOf(22);
BigInteger taskId = BigInteger.valueOf(101);
BigInteger instanceId = BigInteger.valueOf(201);
SysAccount operator = new SysAccount();
operator.setId(operatorId);
operator.setDeptId(currentDeptId);
when(sysAccountService.getById(operatorId)).thenReturn(operator);
when(sysDeptService.getSelfAndAncestorDeptIds(currentDeptId))
.thenReturn(Set.of(parentDeptId, currentDeptId));
ApprovalTaskAssignee relation = new ApprovalTaskAssignee();
relation.setTaskId(taskId);
relation.setAssigneeType(ApprovalAssigneeType.DEPT.getCode());
relation.setTargetId(parentDeptId);
relation.setIncludeChildren(1);
when(approvalTaskAssigneeMapper.selectListByQuery(any(QueryWrapper.class)))
.thenReturn(List.of(), List.of(relation));
ApprovalTask task = new ApprovalTask();
task.setId(taskId);
task.setInstanceId(instanceId);
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
when(approvalTaskMapper.selectListByQuery(any(QueryWrapper.class)))
.thenReturn(List.of(task), List.of(), List.of());
assertEquals(
Set.of(instanceId),
service.listPendingInstanceIds(operatorId, Set.of(), null));
}
/**
* 验证多个角色审批对象采用 OR 语义,命中任一角色即可处理。
*/
@Test
public void canHandleTaskShouldMatchAnyRoleTarget() {
BigInteger operatorId = BigInteger.valueOf(7);
ApprovalTask task = new ApprovalTask();
task.setId(BigInteger.valueOf(101));
task.setAssigneeType(ApprovalAssigneeType.ROLE.getCode());
ApprovalTaskAssignee first = taskAssignee(task.getId(), 31);
ApprovalTaskAssignee second = taskAssignee(task.getId(), 32);
when(approvalTaskAssigneeMapper.selectListByQuery(any(QueryWrapper.class)))
.thenReturn(List.of(first, second));
assertTrue(service.canHandleTask(task, operatorId, Set.of(BigInteger.valueOf(32))));
}
/**
* 构造有效角色。
*
* @param id 角色 ID
* @param code 角色编码
* @param name 角色名称
* @return 角色
*/
private SysRole role(long id, String code, String name) {
SysRole role = new SysRole();
role.setId(BigInteger.valueOf(id));
role.setRoleKey(code);
role.setRoleName(name);
role.setStatus(EnumDataStatus.AVAILABLE.getCode());
return role;
}
/**
* 构造审批对象。
*
* @param targetId 对象 ID
* @param includeChildren 是否包含子部门
* @return 审批对象
*/
private ApprovalAssigneeTargetVo target(BigInteger targetId, int includeChildren) {
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(targetId);
target.setIncludeChildren(includeChildren);
return target;
}
/**
* 构造角色任务关联。
*
* @param taskId 任务 ID
* @param targetId 角色 ID
* @return 任务关联
*/
private ApprovalTaskAssignee taskAssignee(BigInteger taskId, long targetId) {
ApprovalTaskAssignee relation = new ApprovalTaskAssignee();
relation.setTaskId(taskId);
relation.setAssigneeType(ApprovalAssigneeType.ROLE.getCode());
relation.setTargetId(BigInteger.valueOf(targetId));
relation.setIncludeChildren(0);
return relation;
}
}

View File

@@ -0,0 +1,258 @@
package tech.easyflow.approval.service.impl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnitRunner;
import tech.easyflow.approval.entity.ApprovalInstance;
import tech.easyflow.approval.entity.ApprovalLog;
import tech.easyflow.approval.entity.ApprovalTask;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
import tech.easyflow.approval.enums.ApprovalAssigneeType;
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
import tech.easyflow.approval.enums.ApprovalTaskStatus;
import tech.easyflow.approval.mapper.ApprovalFlowStepMapper;
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
import tech.easyflow.approval.mapper.ApprovalLogMapper;
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
import tech.easyflow.approval.service.ApprovalAssigneeService;
import tech.easyflow.approval.service.ApprovalMatchService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.service.SysAccountService;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link ApprovalInstanceServiceImpl} 多审批对象冻结测试。
*/
@RunWith(MockitoJUnitRunner.class)
public class ApprovalInstanceMultiAssigneeTest {
@Mock
private ApprovalMatchService approvalMatchService;
@Mock
private ApprovalInstanceMapper approvalInstanceMapper;
@Mock
private ApprovalTaskMapper approvalTaskMapper;
@Mock
private ApprovalLogMapper approvalLogMapper;
@Mock
private ApprovalFlowStepMapper approvalFlowStepMapper;
@Mock
private ApprovalAssigneeService approvalAssigneeService;
@Mock
private SysAccountService sysAccountService;
@InjectMocks
private ApprovalInstanceServiceImpl service;
/**
* 验证提审时会把全部审批对象写入实例快照和任务关联。
*/
@Test
@SuppressWarnings("unchecked")
public void submitApprovalShouldFreezeAllAssigneeTargets() {
BigInteger applicantId = BigInteger.valueOf(7);
BigInteger tenantId = BigInteger.valueOf(42);
BigInteger instanceId = BigInteger.valueOf(101);
BigInteger taskId = BigInteger.valueOf(201);
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
request.setApplicantId(applicantId);
request.setResourceId(BigInteger.valueOf(501));
request.setSummary("多用户审批");
ApprovalFlowStepVo step = new ApprovalFlowStepVo();
step.setStepNo(1);
step.setStepName("审核");
step.setAssigneeType(ApprovalAssigneeType.USER.getCode());
step.setAssigneeTargets(List.of(
target(11, "u11", "用户甲"),
target(12, "u12", "用户乙")));
ApprovalFlowDetailVo flow = new ApprovalFlowDetailVo();
flow.setId(BigInteger.valueOf(301));
flow.setVersion(3);
flow.setResourceType("WORKFLOW");
flow.setActionType("PUBLISH");
flow.setSteps(List.of(step));
when(approvalMatchService.matchFlow(request)).thenReturn(flow);
LoginAccount loginAccount = new LoginAccount();
loginAccount.setId(applicantId);
loginAccount.setTenantId(tenantId);
SysAccount applicant = new SysAccount();
applicant.setId(applicantId);
applicant.setTenantId(tenantId);
when(sysAccountService.getById(applicantId)).thenReturn(applicant);
doAnswer(invocation -> {
((ApprovalInstance) invocation.getArgument(0)).setId(instanceId);
return 1;
}).when(approvalInstanceMapper).insert(any(ApprovalInstance.class));
doAnswer(invocation -> {
((ApprovalTask) invocation.getArgument(0)).setId(taskId);
return 1;
}).when(approvalTaskMapper).insert(any(ApprovalTask.class));
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
assertEquals(instanceId, service.submitApproval(request));
}
ArgumentCaptor<ApprovalInstance> instanceCaptor = ArgumentCaptor.forClass(ApprovalInstance.class);
verify(approvalInstanceMapper).insert(instanceCaptor.capture());
List<Map<String, Object>> frozenSteps =
(List<Map<String, Object>>) instanceCaptor.getValue().getSnapshotJson().get("steps");
List<Map<String, Object>> frozenTargets =
(List<Map<String, Object>>) frozenSteps.get(0).get("assigneeTargets");
assertEquals(2, frozenTargets.size());
assertEquals(BigInteger.valueOf(12), frozenTargets.get(1).get("targetId"));
verify(approvalAssigneeService).saveTaskAssigneeTargets(
eq(taskId),
eq(ApprovalAssigneeType.USER.getCode()),
argThat(targets -> targets.size() == 2),
eq(applicantId),
any(Date.class));
verify(approvalLogMapper, org.mockito.Mockito.times(2)).insert(any(ApprovalLog.class));
}
/**
* 验证进入下一步骤时继续使用实例快照中冻结的全部审批对象。
*/
@Test
public void approveShouldCreateNextTaskFromFrozenAssigneeTargets() {
BigInteger operatorId = BigInteger.valueOf(7);
BigInteger tenantId = BigInteger.valueOf(42);
BigInteger instanceId = BigInteger.valueOf(101);
BigInteger currentTaskId = BigInteger.valueOf(201);
BigInteger nextTaskId = BigInteger.valueOf(202);
BigInteger firstDeptId = BigInteger.valueOf(31);
BigInteger secondDeptId = BigInteger.valueOf(32);
ApprovalInstance instance = new ApprovalInstance();
instance.setId(instanceId);
instance.setTenantId(tenantId);
instance.setFlowId(BigInteger.valueOf(301));
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
instance.setCurrentStepNo(1);
instance.setSnapshotJson(Map.of(
"steps", List.of(
frozenStep(1, ApprovalAssigneeType.USER.getCode(),
List.of(frozenTarget(11, 0))),
frozenStep(2, ApprovalAssigneeType.DEPT.getCode(), List.of(
frozenTarget(firstDeptId.longValue(), 0),
frozenTarget(secondDeptId.longValue(), 1))))));
ApprovalTask currentTask = new ApprovalTask();
currentTask.setId(currentTaskId);
currentTask.setInstanceId(instanceId);
currentTask.setStepNo(1);
currentTask.setStatus(ApprovalTaskStatus.PENDING.getCode());
currentTask.setAssigneeType(ApprovalAssigneeType.USER.getCode());
SysAccount operator = new SysAccount();
operator.setId(operatorId);
operator.setTenantId(tenantId);
when(sysAccountService.getById(operatorId)).thenReturn(operator);
when(approvalInstanceMapper.selectOneByQuery(any())).thenReturn(instance);
when(approvalTaskMapper.selectOneByQuery(any())).thenReturn(currentTask);
when(approvalAssigneeService.getAvailableRoleIds(operatorId)).thenReturn(Set.of());
when(approvalAssigneeService.canHandleTask(currentTask, operatorId, Set.of())).thenReturn(true);
when(approvalFlowStepMapper.selectListByQuery(any())).thenReturn(List.of());
doAnswer(invocation -> {
((ApprovalTask) invocation.getArgument(0)).setId(nextTaskId);
return 1;
}).when(approvalTaskMapper).insert(any(ApprovalTask.class));
service.approve(instanceId, "同意", operatorId);
verify(approvalAssigneeService).saveTaskAssigneeTargets(
eq(nextTaskId),
eq(ApprovalAssigneeType.DEPT.getCode()),
argThat(targets -> targets.size() == 2
&& firstDeptId.equals(targets.get(0).getTargetId())
&& secondDeptId.equals(targets.get(1).getTargetId())
&& Integer.valueOf(1).equals(targets.get(1).getIncludeChildren())),
eq(operatorId),
any(Date.class));
assertEquals(ApprovalInstanceStatus.PROCESSING.getCode(), instance.getStatus());
assertEquals(Integer.valueOf(2), instance.getCurrentStepNo());
}
/**
* 构造冻结步骤。
*
* @param stepNo 步骤序号
* @param assigneeType 审批对象类型
* @param targets 审批对象
* @return 冻结步骤
*/
private Map<String, Object> frozenStep(int stepNo, String assigneeType, List<Map<String, Object>> targets) {
Map<String, Object> primary = targets.get(0);
return Map.of(
"stepNo", stepNo,
"stepName", "" + stepNo + "",
"assigneeType", assigneeType,
"assigneeTargetId", primary.get("targetId"),
"assigneeTargetCode", "target-" + primary.get("targetId"),
"assigneeTargetName", "对象" + primary.get("targetId"),
"assigneeTargets", targets);
}
/**
* 构造冻结审批对象。
*
* @param id 对象 ID
* @param includeChildren 是否包含子部门
* @return 冻结审批对象
*/
private Map<String, Object> frozenTarget(long id, int includeChildren) {
return Map.of(
"targetId", BigInteger.valueOf(id),
"targetCode", "target-" + id,
"targetName", "对象" + id,
"includeChildren", includeChildren);
}
/**
* 构造审批对象。
*
* @param id 用户 ID
* @param code 用户编码
* @param name 用户名称
* @return 审批对象
*/
private ApprovalAssigneeTargetVo target(long id, String code, String name) {
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
target.setTargetId(BigInteger.valueOf(id));
target.setTargetCode(code);
target.setTargetName(name);
target.setIncludeChildren(0);
return target;
}
}

View File

@@ -0,0 +1,57 @@
package tech.easyflow.approval.service.impl;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
/**
* V37 审批多对象迁移契约测试。
*/
public class ApprovalMultiAssigneeMigrationContractTest {
/**
* 验证迁移先阻断在途审批,再创建关联表并完整回填配置和历史任务。
*
* @throws Exception 迁移文件不可读时抛出
*/
@Test
public void migrationShouldGuardAndBackfillBothAssigneeRelations() throws Exception {
String sql = migrationSql();
assertTrue(sql.contains("WHERE `status` IN ('PENDING', 'PROCESSING')"));
assertTrue(sql.contains("WHERE `status` = 'PENDING'"));
assertTrue(sql.contains("SIGNAL SQLSTATE '45000'"));
assertTrue(sql.indexOf("CALL `sp_guard_approval_multi_assignee`()")
< sql.indexOf("CREATE TABLE IF NOT EXISTS `tb_approval_flow_step_assignee`"));
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS `tb_approval_task_assignee`"));
assertTrue(sql.contains("UNIQUE KEY `uk_approval_step_assignee`"));
assertTrue(sql.contains("UNIQUE KEY `uk_approval_task_assignee`"));
assertTrue(sql.contains("FROM `tb_approval_flow_step` step"));
assertTrue(sql.contains("FROM `tb_approval_task` task"));
assertTrue(sql.contains("approval multi-assignee backfill verification failed"));
}
/**
* 读取工作区中的 V37 MySQL 迁移。
*
* @return 迁移 SQL
* @throws Exception 迁移文件不存在或不可读时抛出
*/
private String migrationSql() throws Exception {
Path root = Path.of(System.getProperty("maven.multiModuleProjectDirectory",
Path.of(System.getProperty("user.dir")).toAbsolutePath().toString()));
while (root != null) {
Path migration = root.resolve("easyflow-starter/easyflow-starter-all/src/main/resources/"
+ "db/migration/mysql/V37__mysql_approval_multi_assignee.sql");
if (Files.isRegularFile(migration)) {
return Files.readString(migration, StandardCharsets.UTF_8);
}
root = root.getParent();
}
throw new IllegalStateException("找不到 V37 审批多对象迁移");
}
}