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,247 @@
package tech.easyflow.admin.controller.system;
import com.mybatisflex.core.query.QueryWrapper;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.testng.annotations.Test;
import tech.easyflow.approval.mapper.ApprovalFlowScopeMapper;
import tech.easyflow.approval.mapper.ApprovalFlowStepAssigneeMapper;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.mapper.SysDeptMapper;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysDeptService;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
/**
* {@link SysDeptController} 部门状态保存测试。
*/
public class SysDeptControllerTest {
/**
* 验证批量状态修改使用单次更新并写入审计字段。
*/
@Test
public void changeStatusBatchShouldUpdateAllDepartmentsOnce() {
SysDeptService service = mock(SysDeptService.class);
SysDeptMapper mapper = mock(SysDeptMapper.class);
SysDeptController controller = createController(service);
List<BigInteger> ids = List.of(BigInteger.ONE, BigInteger.TWO);
when(service.listByIds(anyCollection()))
.thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1"), buildDept(BigInteger.TWO, "dept_2")));
when(service.getMapper()).thenReturn(mapper);
when(mapper.updateByQuery(any(SysDept.class), any(QueryWrapper.class))).thenReturn(2);
LoginAccount loginAccount = mock(LoginAccount.class);
when(loginAccount.getId()).thenReturn(BigInteger.TEN);
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
Result<Integer> result = controller.changeStatusBatch(ids, EnumDataStatus.UNAVAILABLE.getCode());
assertEquals(result.getErrorCode(), 0);
assertEquals(result.getData(), Integer.valueOf(2));
}
ArgumentCaptor<SysDept> updateCaptor = ArgumentCaptor.forClass(SysDept.class);
verify(mapper).updateByQuery(updateCaptor.capture(), any(QueryWrapper.class));
assertEquals(updateCaptor.getValue().getStatus(), EnumDataStatus.UNAVAILABLE.getCode());
assertEquals(updateCaptor.getValue().getModifiedBy(), BigInteger.TEN);
}
/**
* 验证根部门不能通过批量接口禁用。
*/
@Test
public void changeStatusBatchShouldRejectRootDepartmentDisable() {
SysDeptService service = mock(SysDeptService.class);
SysDeptMapper mapper = mock(SysDeptMapper.class);
SysDeptController controller = createController(service);
SysDept root = buildDept(BigInteger.ONE, Constants.ROOT_DEPT);
when(service.listByIds(anyCollection())).thenReturn(List.of(root));
when(service.getMapper()).thenReturn(mapper);
Result<Integer> result = controller.changeStatusBatch(
List.of(BigInteger.ONE),
EnumDataStatus.UNAVAILABLE.getCode());
assertEquals(result.getErrorCode(), 1);
assertEquals(result.getMessage(), "根部门不能禁用");
verify(mapper, never()).updateByQuery(any(SysDept.class), any(QueryWrapper.class));
}
/**
* 验证批量状态修改拒绝非法状态。
*/
@Test
public void changeStatusBatchShouldRejectInvalidStatus() {
SysDeptService service = mock(SysDeptService.class);
SysDeptController controller = createController(service);
Result<Integer> result = controller.changeStatusBatch(List.of(BigInteger.ONE), 2);
assertEquals(result.getErrorCode(), 1);
assertEquals(result.getMessage(), "部门状态不合法");
verify(service, never()).listByIds(anyCollection());
}
/**
* 验证存在未选中下级部门时禁止批量删除。
*/
@Test
public void removeBatchShouldRejectUnselectedChildDepartment() {
SysDeptService service = mock(SysDeptService.class);
SysDeptController controller = createController(service);
Collection<Serializable> ids = List.of(BigInteger.ONE);
when(service.listByIds(ids)).thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1")));
when(service.count(any(QueryWrapper.class))).thenReturn(1L);
Result<?> result = controller.onRemoveBefore(ids);
assertEquals(result.getErrorCode(), 1);
assertEquals(result.getMessage(), "所选部门包含未选中的下级部门,不能删除");
}
/**
* 验证审批步骤引用的部门不能禁用。
*/
@Test
public void changeStatusBatchShouldRejectApprovalStepReference() {
SysDeptService service = mock(SysDeptService.class);
ApprovalFlowStepAssigneeMapper assigneeMapper = mock(ApprovalFlowStepAssigneeMapper.class);
SysDeptController controller = new SysDeptController(
service,
mock(SysAccountService.class),
assigneeMapper,
mock(ApprovalFlowScopeMapper.class));
when(service.listByIds(anyCollection()))
.thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1")));
when(assigneeMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(1L);
Result<Integer> result = controller.changeStatusBatch(
List.of(BigInteger.ONE),
EnumDataStatus.UNAVAILABLE.getCode());
assertEquals(result.getErrorCode(), 1);
assertEquals(result.getMessage(), "所选部门已被审批流程使用,不能禁用");
}
/**
* 验证审批范围引用的部门不能删除。
*/
@Test
public void removeBatchShouldRejectApprovalScopeReference() {
SysDeptService service = mock(SysDeptService.class);
ApprovalFlowScopeMapper scopeMapper = mock(ApprovalFlowScopeMapper.class);
SysDeptController controller = new SysDeptController(
service,
mock(SysAccountService.class),
mock(ApprovalFlowStepAssigneeMapper.class),
scopeMapper);
Collection<Serializable> ids = List.of(BigInteger.ONE);
when(service.listByIds(ids)).thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1")));
when(service.count(any(QueryWrapper.class))).thenReturn(0L);
when(scopeMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(1L);
Result<?> result = controller.onRemoveBefore(ids);
assertEquals(result.getErrorCode(), 1);
assertEquals(result.getMessage(), "所选部门已被审批流程使用,请先调整审批配置");
}
/**
* 验证新增部门未传状态时默认启用。
*/
@Test
public void saveShouldDefaultMissingStatusToAvailable() {
SysDept entity = buildDept(null);
invokeSaveBefore(entity);
assertEquals(entity.getStatus(), EnumDataStatus.AVAILABLE.getCode());
}
/**
* 验证新增部门显式传入未启用状态时保持原值。
*/
@Test
public void saveShouldPreserveExplicitUnavailableStatus() {
SysDept entity = buildDept(EnumDataStatus.UNAVAILABLE.getCode());
invokeSaveBefore(entity);
assertEquals(entity.getStatus(), EnumDataStatus.UNAVAILABLE.getCode());
}
/**
* 构造待保存的部门。
*
* @param status 部门状态
* @return 部门实体
*/
private SysDept buildDept(Integer status) {
SysDept entity = new SysDept();
entity.setParentId(BigInteger.ZERO);
entity.setStatus(status);
return entity;
}
/**
* 构造指定主键和编码的部门。
*
* @param id 部门主键
* @param deptCode 部门编码
* @return 部门实体
*/
private SysDept buildDept(BigInteger id, String deptCode) {
SysDept entity = new SysDept();
entity.setId(id);
entity.setDeptCode(deptCode);
return entity;
}
/**
* 调用新增前置处理。
*
* @param entity 部门实体
*/
private void invokeSaveBefore(SysDept entity) {
LoginAccount loginAccount = mock(LoginAccount.class);
SysDeptController controller = createController(mock(SysDeptService.class));
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
controller.onSaveOrUpdateBefore(entity, true);
}
}
/**
* 创建带有隔离依赖的部门控制器。
*
* @param service 部门服务
* @return 部门控制器
*/
private SysDeptController createController(SysDeptService service) {
return new SysDeptController(
service,
mock(SysAccountService.class),
mock(ApprovalFlowStepAssigneeMapper.class),
mock(ApprovalFlowScopeMapper.class));
}
}