feat: 增加审批步骤发起人部门限制
- 增加配置期与提审期合法审批账号校验 - 冻结发起人部门并统一待办与办理权限判断 - 增加兼容迁移、前端联动和自动化测试
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* V40 审批发起人部门限制迁移契约测试。
|
||||
*/
|
||||
public class ApprovalApplicantDeptRestrictionMigrationContractTest {
|
||||
|
||||
/**
|
||||
* 验证迁移以关闭开关兼容历史流程,并提供实例与任务冻结字段。
|
||||
*
|
||||
* @throws Exception 迁移文件不可读时抛出
|
||||
*/
|
||||
@Test
|
||||
public void migrationShouldAddBackwardCompatibleRestrictionFields() throws Exception {
|
||||
String sql = migrationSql();
|
||||
|
||||
assertTrue(sql.contains("`restrict_to_applicant_dept` TINYINT NOT NULL DEFAULT 0"));
|
||||
assertTrue(sql.contains("`applicant_dept_id` BIGINT UNSIGNED NULL"));
|
||||
assertTrue(sql.contains("`applicant_dept_name` VARCHAR(128) NULL"));
|
||||
assertTrue(sql.contains("`required_dept_id` BIGINT UNSIGNED NULL"));
|
||||
assertTrue(sql.contains("`idx_approval_task_required_dept_status`"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取工作区中的 V40 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/V40__mysql_approval_applicant_dept_restriction.sql");
|
||||
if (Files.isRegularFile(migration)) {
|
||||
return Files.readString(migration, StandardCharsets.UTF_8);
|
||||
}
|
||||
root = root.getParent();
|
||||
}
|
||||
throw new IllegalStateException("找不到 V40 审批发起人部门限制迁移");
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,12 @@ 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.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysAccountRole;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.service.SysAccountRoleService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
@@ -29,6 +32,7 @@ import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -45,6 +49,9 @@ public class ApprovalAssigneeServiceImplTest {
|
||||
@Mock
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
@Mock
|
||||
private SysAccountRoleService sysAccountRoleService;
|
||||
|
||||
@Mock
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
@@ -199,6 +206,135 @@ public class ApprovalAssigneeServiceImplTest {
|
||||
assertTrue(service.canHandleTask(task, operatorId, Set.of(BigInteger.valueOf(32))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证开启发起人部门限制后不能配置部门审批对象。
|
||||
*/
|
||||
@Test
|
||||
public void normalizeStepAssigneeShouldRejectDepartmentWhenRestricted() {
|
||||
ApprovalFlowStepVo step = new ApprovalFlowStepVo();
|
||||
step.setAssigneeType(ApprovalAssigneeType.DEPT.getCode());
|
||||
step.setRestrictToApplicantDept(1);
|
||||
|
||||
assertThrows(BusinessException.class, () -> service.normalizeStepAssignee(step));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证受限用户没有有效部门时,流程配置校验失败。
|
||||
*/
|
||||
@Test
|
||||
public void validateRestrictedStepCandidatesShouldRejectUserWithoutDepartment() {
|
||||
SysAccount account = account(11, null);
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(account));
|
||||
|
||||
ApprovalFlowStepVo step = restrictedStep(
|
||||
ApprovalAssigneeType.USER, List.of(target(account.getId(), 0)));
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> service.validateRestrictedStepCandidates(List.of(step), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证角色中存在发起人直属部门的有效账号时校验通过。
|
||||
*/
|
||||
@Test
|
||||
public void validateRestrictedStepCandidatesShouldMatchRoleMemberInRequiredDepartment() {
|
||||
BigInteger roleId = BigInteger.valueOf(31);
|
||||
BigInteger accountId = BigInteger.valueOf(11);
|
||||
BigInteger deptId = BigInteger.valueOf(21);
|
||||
SysRole role = role(roleId.longValue(), "reviewer", "审核员");
|
||||
when(sysRoleService.list(any(QueryWrapper.class))).thenReturn(List.of(role));
|
||||
SysAccountRole relation = new SysAccountRole();
|
||||
relation.setRoleId(roleId);
|
||||
relation.setAccountId(accountId);
|
||||
when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(List.of(relation));
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(account(11, deptId)));
|
||||
when(sysDeptService.getById(deptId)).thenReturn(dept(deptId));
|
||||
|
||||
ApprovalFlowStepVo step = restrictedStep(
|
||||
ApprovalAssigneeType.ROLE, List.of(target(roleId, 0)));
|
||||
|
||||
service.validateRestrictedStepCandidates(List.of(step), deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证角色成员与发起人直属部门没有交集时校验失败。
|
||||
*/
|
||||
@Test
|
||||
public void validateRestrictedStepCandidatesShouldRejectEmptyRoleDepartmentIntersection() {
|
||||
BigInteger roleId = BigInteger.valueOf(31);
|
||||
BigInteger deptId = BigInteger.valueOf(21);
|
||||
SysRole role = role(roleId.longValue(), "reviewer", "审核员");
|
||||
when(sysRoleService.list(any(QueryWrapper.class))).thenReturn(List.of(role));
|
||||
SysAccountRole relation = new SysAccountRole();
|
||||
relation.setRoleId(roleId);
|
||||
relation.setAccountId(BigInteger.valueOf(11));
|
||||
when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(List.of(relation));
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
when(sysDeptService.getById(deptId)).thenReturn(dept(deptId));
|
||||
|
||||
ApprovalFlowStepVo step = restrictedStep(
|
||||
ApprovalAssigneeType.ROLE, List.of(target(roleId, 0)));
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> service.validateRestrictedStepCandidates(List.of(step), deptId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务同时命中角色和冻结直属部门时才允许处理。
|
||||
*/
|
||||
@Test
|
||||
public void canHandleTaskShouldRequireFrozenDirectDepartment() {
|
||||
BigInteger operatorId = BigInteger.valueOf(7);
|
||||
BigInteger deptId = BigInteger.valueOf(21);
|
||||
BigInteger roleId = BigInteger.valueOf(31);
|
||||
ApprovalTask task = new ApprovalTask();
|
||||
task.setId(BigInteger.valueOf(101));
|
||||
task.setAssigneeType(ApprovalAssigneeType.ROLE.getCode());
|
||||
task.setRequiredDeptId(deptId);
|
||||
when(approvalTaskAssigneeMapper.selectListByQuery(any(QueryWrapper.class)))
|
||||
.thenReturn(List.of(taskAssignee(task.getId(), roleId.longValue())));
|
||||
when(sysAccountService.getById(operatorId)).thenReturn(account(operatorId.longValue(), deptId));
|
||||
when(sysDeptService.getById(deptId)).thenReturn(dept(deptId));
|
||||
|
||||
assertTrue(service.canHandleTask(task, operatorId, Set.of(roleId)));
|
||||
|
||||
task.setRequiredDeptId(BigInteger.valueOf(22));
|
||||
assertFalse(service.canHandleTask(task, operatorId, Set.of(roleId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证待审批列表会排除不属于当前账号直属部门的受限任务。
|
||||
*/
|
||||
@Test
|
||||
public void listPendingInstanceIdsShouldFilterFrozenDepartment() {
|
||||
BigInteger operatorId = BigInteger.valueOf(7);
|
||||
BigInteger deptId = BigInteger.valueOf(21);
|
||||
BigInteger roleId = BigInteger.valueOf(31);
|
||||
BigInteger matchedTaskId = BigInteger.valueOf(101);
|
||||
BigInteger otherTaskId = BigInteger.valueOf(102);
|
||||
BigInteger matchedInstanceId = BigInteger.valueOf(201);
|
||||
|
||||
when(sysAccountService.getById(operatorId)).thenReturn(account(operatorId.longValue(), deptId));
|
||||
when(sysDeptService.getById(deptId)).thenReturn(dept(deptId));
|
||||
when(sysDeptService.getSelfAndAncestorDeptIds(deptId)).thenReturn(Set.of());
|
||||
when(approvalTaskAssigneeMapper.selectListByQuery(any(QueryWrapper.class)))
|
||||
.thenReturn(
|
||||
List.of(),
|
||||
List.of(
|
||||
taskAssignee(matchedTaskId, roleId.longValue()),
|
||||
taskAssignee(otherTaskId, roleId.longValue())));
|
||||
|
||||
ApprovalTask matchedTask = pendingTask(matchedTaskId, matchedInstanceId, deptId);
|
||||
ApprovalTask otherTask = pendingTask(
|
||||
otherTaskId, BigInteger.valueOf(202), BigInteger.valueOf(22));
|
||||
when(approvalTaskMapper.selectListByQuery(any(QueryWrapper.class)))
|
||||
.thenReturn(List.of(matchedTask, otherTask), List.of(), List.of());
|
||||
|
||||
assertEquals(
|
||||
Set.of(matchedInstanceId),
|
||||
service.listPendingInstanceIds(operatorId, Set.of(roleId), null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造有效角色。
|
||||
*
|
||||
@@ -216,6 +352,70 @@ public class ApprovalAssigneeServiceImplTest {
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造有效账号。
|
||||
*
|
||||
* @param id 账号 ID
|
||||
* @param deptId 部门 ID
|
||||
* @return 账号
|
||||
*/
|
||||
private SysAccount account(long id, BigInteger deptId) {
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(BigInteger.valueOf(id));
|
||||
account.setDeptId(deptId);
|
||||
account.setStatus(EnumDataStatus.AVAILABLE.getCode());
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造有效部门。
|
||||
*
|
||||
* @param deptId 部门 ID
|
||||
* @return 部门
|
||||
*/
|
||||
private SysDept dept(BigInteger deptId) {
|
||||
SysDept dept = new SysDept();
|
||||
dept.setId(deptId);
|
||||
dept.setStatus(EnumDataStatus.AVAILABLE.getCode());
|
||||
return dept;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造受发起人部门限制的步骤。
|
||||
*
|
||||
* @param type 审批对象类型
|
||||
* @param targets 审批对象
|
||||
* @return 步骤
|
||||
*/
|
||||
private ApprovalFlowStepVo restrictedStep(ApprovalAssigneeType type,
|
||||
List<ApprovalAssigneeTargetVo> targets) {
|
||||
ApprovalFlowStepVo step = new ApprovalFlowStepVo();
|
||||
step.setStepNo(1);
|
||||
step.setStepName("审核");
|
||||
step.setAssigneeType(type.getCode());
|
||||
step.setAssigneeTargets(targets);
|
||||
step.setRestrictToApplicantDept(1);
|
||||
return step;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造待审批任务。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
* @param instanceId 实例 ID
|
||||
* @param requiredDeptId 限定部门 ID
|
||||
* @return 待审批任务
|
||||
*/
|
||||
private ApprovalTask pendingTask(BigInteger taskId, BigInteger instanceId, BigInteger requiredDeptId) {
|
||||
ApprovalTask task = new ApprovalTask();
|
||||
task.setId(taskId);
|
||||
task.setInstanceId(instanceId);
|
||||
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
|
||||
task.setAssigneeType(ApprovalAssigneeType.ROLE.getCode());
|
||||
task.setRequiredDeptId(requiredDeptId);
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造审批对象。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package tech.easyflow.approval.service.impl;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import tech.easyflow.approval.entity.ApprovalFlow;
|
||||
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.enums.ApprovalAssigneeType;
|
||||
import tech.easyflow.approval.mapper.ApprovalFlowMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalFlowScopeMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalFlowStepMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
import tech.easyflow.approval.service.ApprovalAssigneeService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* {@link ApprovalFlowServiceImpl} 流程配置校验测试。
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApprovalFlowServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private ApprovalFlowMapper approvalFlowMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalFlowScopeMapper approvalFlowScopeMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalFlowStepMapper approvalFlowStepMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalAssigneeService approvalAssigneeService;
|
||||
|
||||
@InjectMocks
|
||||
private ApprovalFlowServiceImpl service;
|
||||
|
||||
/**
|
||||
* 验证受限步骤在写入流程前完成合法候选账号校验。
|
||||
*/
|
||||
@Test
|
||||
public void saveFlowShouldValidateRestrictedCandidatesBeforeInsert() {
|
||||
ApprovalFlowStepVo step = new ApprovalFlowStepVo();
|
||||
step.setStepName("审核");
|
||||
step.setAssigneeType(ApprovalAssigneeType.USER.getCode());
|
||||
step.setRestrictToApplicantDept(1);
|
||||
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||
target.setTargetId(BigInteger.valueOf(11));
|
||||
step.setAssigneeTargets(List.of(target));
|
||||
|
||||
ApprovalFlowDetailVo request = new ApprovalFlowDetailVo();
|
||||
request.setName("发布审批");
|
||||
request.setResourceType("WORKFLOW");
|
||||
request.setActionType("PUBLISH");
|
||||
request.setPriority(100);
|
||||
request.setStatus("ENABLED");
|
||||
request.setSteps(List.of(step));
|
||||
doAnswer(invocation -> invocation.getArgument(0))
|
||||
.when(approvalAssigneeService)
|
||||
.normalizeStepAssignee(any(ApprovalFlowStepVo.class));
|
||||
doAnswer(invocation -> {
|
||||
((ApprovalFlow) invocation.getArgument(0)).setId(BigInteger.valueOf(301));
|
||||
return 1;
|
||||
}).when(approvalFlowMapper).insert(any(ApprovalFlow.class));
|
||||
|
||||
assertEquals(BigInteger.valueOf(301),
|
||||
service.saveFlow(request, BigInteger.valueOf(7)));
|
||||
|
||||
InOrder order = inOrder(approvalAssigneeService, approvalFlowMapper);
|
||||
order.verify(approvalAssigneeService)
|
||||
.validateRestrictedStepCandidates(any(), isNull());
|
||||
order.verify(approvalFlowMapper).insert(any(ApprovalFlow.class));
|
||||
verify(approvalAssigneeService).saveStepAssigneeTargets(
|
||||
any(), any(), any(), any(), any());
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,14 @@ 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.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
@@ -35,11 +39,14 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
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.doThrow;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -70,6 +77,9 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
@Mock
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
@Mock
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
@InjectMocks
|
||||
private ApprovalInstanceServiceImpl service;
|
||||
|
||||
@@ -81,6 +91,7 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
public void submitApprovalShouldFreezeAllAssigneeTargets() {
|
||||
BigInteger applicantId = BigInteger.valueOf(7);
|
||||
BigInteger tenantId = BigInteger.valueOf(42);
|
||||
BigInteger applicantDeptId = BigInteger.valueOf(43);
|
||||
BigInteger instanceId = BigInteger.valueOf(101);
|
||||
BigInteger taskId = BigInteger.valueOf(201);
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
@@ -92,6 +103,7 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
step.setStepNo(1);
|
||||
step.setStepName("审核");
|
||||
step.setAssigneeType(ApprovalAssigneeType.USER.getCode());
|
||||
step.setRestrictToApplicantDept(1);
|
||||
step.setAssigneeTargets(List.of(
|
||||
target(11, "u11", "用户甲"),
|
||||
target(12, "u12", "用户乙")));
|
||||
@@ -109,7 +121,13 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
SysAccount applicant = new SysAccount();
|
||||
applicant.setId(applicantId);
|
||||
applicant.setTenantId(tenantId);
|
||||
applicant.setDeptId(applicantDeptId);
|
||||
when(sysAccountService.getById(applicantId)).thenReturn(applicant);
|
||||
SysDept applicantDept = new SysDept();
|
||||
applicantDept.setId(applicantDeptId);
|
||||
applicantDept.setDeptName("技术部");
|
||||
applicantDept.setStatus(EnumDataStatus.AVAILABLE.getCode());
|
||||
when(sysDeptService.getById(applicantDeptId)).thenReturn(applicantDept);
|
||||
doAnswer(invocation -> {
|
||||
((ApprovalInstance) invocation.getArgument(0)).setId(instanceId);
|
||||
return 1;
|
||||
@@ -132,6 +150,16 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
(List<Map<String, Object>>) frozenSteps.get(0).get("assigneeTargets");
|
||||
assertEquals(2, frozenTargets.size());
|
||||
assertEquals(BigInteger.valueOf(12), frozenTargets.get(1).get("targetId"));
|
||||
assertEquals(1, frozenSteps.get(0).get("restrictToApplicantDept"));
|
||||
assertEquals(applicantDeptId, instanceCaptor.getValue().getApplicantDeptId());
|
||||
assertEquals("技术部", instanceCaptor.getValue().getApplicantDeptName());
|
||||
assertEquals(applicantDeptId,
|
||||
instanceCaptor.getValue().getSnapshotJson().get("applicantDeptId"));
|
||||
ArgumentCaptor<ApprovalTask> taskCaptor = ArgumentCaptor.forClass(ApprovalTask.class);
|
||||
verify(approvalTaskMapper).insert(taskCaptor.capture());
|
||||
assertEquals(applicantDeptId, taskCaptor.getValue().getRequiredDeptId());
|
||||
verify(approvalAssigneeService).validateRestrictedStepCandidates(
|
||||
eq(List.of(step)), eq(applicantDeptId));
|
||||
verify(approvalAssigneeService).saveTaskAssigneeTargets(
|
||||
eq(taskId),
|
||||
eq(ApprovalAssigneeType.USER.getCode()),
|
||||
@@ -141,6 +169,48 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
verify(approvalLogMapper, org.mockito.Mockito.times(2)).insert(any(ApprovalLog.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证发起人没有直属部门时,会在创建实例前拒绝受限流程提审。
|
||||
*/
|
||||
@Test
|
||||
public void submitApprovalShouldRejectRestrictedFlowWhenApplicantHasNoDepartment() {
|
||||
BigInteger applicantId = BigInteger.valueOf(7);
|
||||
BigInteger tenantId = BigInteger.valueOf(42);
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setApplicantId(applicantId);
|
||||
request.setResourceId(BigInteger.valueOf(501));
|
||||
|
||||
ApprovalFlowStepVo step = new ApprovalFlowStepVo();
|
||||
step.setStepNo(1);
|
||||
step.setStepName("审核");
|
||||
step.setAssigneeType(ApprovalAssigneeType.USER.getCode());
|
||||
step.setAssigneeTargets(List.of(target(11, "u11", "用户甲")));
|
||||
step.setRestrictToApplicantDept(1);
|
||||
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);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||
assertThrows(BusinessException.class, () -> service.submitApproval(request));
|
||||
}
|
||||
|
||||
verify(approvalInstanceMapper, never()).insert(any(ApprovalInstance.class));
|
||||
verify(approvalTaskMapper, never()).insert(any(ApprovalTask.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证进入下一步骤时继续使用实例快照中冻结的全部审批对象。
|
||||
*/
|
||||
@@ -204,6 +274,57 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
assertEquals(Integer.valueOf(2), instance.getCurrentStepNo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证下一受限步骤没有合法候选人时,当前任务保持待审批。
|
||||
*/
|
||||
@Test
|
||||
public void approveShouldKeepCurrentTaskPendingWhenNextRestrictedStepHasNoCandidate() {
|
||||
BigInteger operatorId = BigInteger.valueOf(7);
|
||||
BigInteger tenantId = BigInteger.valueOf(42);
|
||||
BigInteger instanceId = BigInteger.valueOf(101);
|
||||
BigInteger applicantDeptId = BigInteger.valueOf(21);
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setId(instanceId);
|
||||
instance.setTenantId(tenantId);
|
||||
instance.setFlowId(BigInteger.valueOf(301));
|
||||
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
|
||||
instance.setCurrentStepNo(1);
|
||||
instance.setApplicantDeptId(applicantDeptId);
|
||||
instance.setSnapshotJson(Map.of(
|
||||
"steps", List.of(
|
||||
frozenStep(1, ApprovalAssigneeType.USER.getCode(),
|
||||
List.of(frozenTarget(11, 0))),
|
||||
frozenRestrictedStep(2, ApprovalAssigneeType.ROLE.getCode(),
|
||||
List.of(frozenTarget(31, 0))))));
|
||||
|
||||
ApprovalTask currentTask = new ApprovalTask();
|
||||
currentTask.setId(BigInteger.valueOf(201));
|
||||
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());
|
||||
doThrow(new BusinessException("下一步没有候选人"))
|
||||
.when(approvalAssigneeService)
|
||||
.validateRestrictedStepCandidates(any(), eq(applicantDeptId));
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> service.approve(instanceId, "同意", operatorId));
|
||||
|
||||
assertEquals(ApprovalTaskStatus.PENDING.getCode(), currentTask.getStatus());
|
||||
verify(approvalTaskMapper, never()).update(any(ApprovalTask.class));
|
||||
verify(approvalTaskMapper, never()).insert(any(ApprovalTask.class));
|
||||
verify(approvalInstanceMapper, never()).update(any(ApprovalInstance.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造冻结步骤。
|
||||
*
|
||||
@@ -221,6 +342,29 @@ public class ApprovalInstanceMultiAssigneeTest {
|
||||
"assigneeTargetId", primary.get("targetId"),
|
||||
"assigneeTargetCode", "target-" + primary.get("targetId"),
|
||||
"assigneeTargetName", "对象" + primary.get("targetId"),
|
||||
"restrictToApplicantDept", 0,
|
||||
"assigneeTargets", targets);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造受发起人部门限制的冻结步骤。
|
||||
*
|
||||
* @param stepNo 步骤序号
|
||||
* @param assigneeType 审批对象类型
|
||||
* @param targets 审批对象
|
||||
* @return 冻结步骤
|
||||
*/
|
||||
private Map<String, Object> frozenRestrictedStep(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"),
|
||||
"restrictToApplicantDept", 1,
|
||||
"assigneeTargets", targets);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user