From 57bd7b5d0678d4297a41244a601c02304264ea92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Thu, 30 Jul 2026 15:13:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E6=AD=A5=E9=AA=A4=E7=BB=91=E5=AE=9A=E9=83=A8=E9=97=A8=E4=B8=8E?= =?UTF-8?q?=E5=A4=9A=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增审批对象关联表和存量数据迁移 - 支持用户、角色、部门多选及办理权限匹配 - 完善部门状态与批量管理交互 --- .../controller/system/SysDeptController.java | 202 ++++- .../system/SysDeptControllerTest.java | 247 ++++++ .../entity/ApprovalFlowStepAssignee.java | 11 + .../approval/entity/ApprovalTaskAssignee.java | 11 + .../base/ApprovalFlowStepAssigneeBase.java | 248 ++++++ .../entity/base/ApprovalTaskAssigneeBase.java | 248 ++++++ .../entity/vo/ApprovalAssigneeTargetVo.java | 89 ++ .../entity/vo/ApprovalFlowStepVo.java | 21 + .../approval/entity/vo/ApprovalTaskVo.java | 21 + .../approval/enums/ApprovalAssigneeType.java | 3 +- .../ApprovalFlowStepAssigneeMapper.java | 10 + .../mapper/ApprovalTaskAssigneeMapper.java | 10 + .../service/ApprovalAssigneeService.java | 51 ++ .../impl/ApprovalAssigneeServiceImpl.java | 554 ++++++++++++- .../service/impl/ApprovalFlowServiceImpl.java | 18 +- .../impl/ApprovalInstanceServiceImpl.java | 115 ++- .../impl/ApprovalQueryServiceImpl.java | 81 +- .../impl/ApprovalAssigneeServiceImplTest.java | 248 ++++++ .../ApprovalInstanceMultiAssigneeTest.java | 258 ++++++ ...valMultiAssigneeMigrationContractTest.java | 57 ++ .../V37__mysql_approval_multi_assignee.sql | 127 +++ .../app/src/locales/langs/en-US/approval.json | 12 +- .../app/src/locales/langs/en-US/sysDept.json | 15 +- .../app/src/locales/langs/zh-CN/approval.json | 12 +- .../app/src/locales/langs/zh-CN/sysDept.json | 15 +- .../views/system/approval/ApprovalDetail.vue | 41 +- .../system/approval/ApprovalFlowModal.vue | 764 +++++++++++++----- .../src/views/system/sysDept/SysDeptList.vue | 482 +++++++++-- .../src/views/system/sysDept/SysDeptModal.vue | 88 +- 29 files changed, 3694 insertions(+), 365 deletions(-) create mode 100644 easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java create mode 100644 easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowStepAssignee.java create mode 100644 easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalTaskAssignee.java create mode 100644 easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowStepAssigneeBase.java create mode 100644 easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalTaskAssigneeBase.java create mode 100644 easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalAssigneeTargetVo.java create mode 100644 easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowStepAssigneeMapper.java create mode 100644 easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalTaskAssigneeMapper.java create mode 100644 easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImplTest.java create mode 100644 easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalInstanceMultiAssigneeTest.java create mode 100644 easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalMultiAssigneeMigrationContractTest.java create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V37__mysql_approval_multi_assignee.sql diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysDeptController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysDeptController.java index d008f8b5..6b6aaf61 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysDeptController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysDeptController.java @@ -1,27 +1,38 @@ package tech.easyflow.admin.controller.system; +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.easyflow.approval.entity.ApprovalFlowScope; +import tech.easyflow.approval.entity.ApprovalFlowStepAssignee; +import tech.easyflow.approval.enums.ApprovalAssigneeType; +import tech.easyflow.approval.enums.ApprovalScopeType; +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.common.tree.Tree; - import tech.easyflow.common.web.controller.BaseCurdController; +import tech.easyflow.common.web.jsonbody.JsonBody; 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 tech.easyflow.common.satoken.util.SaTokenUtil; -import com.mybatisflex.core.query.QueryWrapper; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import javax.annotation.Resource; import java.io.Serializable; import java.math.BigInteger; import java.util.Collection; import java.util.Date; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * 部门表 控制层。 @@ -33,18 +44,47 @@ import java.util.List; @RequestMapping("/api/v1/sysDept") public class SysDeptController extends BaseCurdController { - @Resource - private SysAccountService sysAccountService; + private final SysAccountService sysAccountService; + private final ApprovalFlowStepAssigneeMapper approvalFlowStepAssigneeMapper; + private final ApprovalFlowScopeMapper approvalFlowScopeMapper; - public SysDeptController(SysDeptService service) { + /** + * 创建部门管理控制器。 + * + * @param service 部门服务 + * @param sysAccountService 用户服务 + * @param approvalFlowStepAssigneeMapper 审批步骤对象 Mapper + * @param approvalFlowScopeMapper 审批范围 Mapper + */ + public SysDeptController(SysDeptService service, + SysAccountService sysAccountService, + ApprovalFlowStepAssigneeMapper approvalFlowStepAssigneeMapper, + ApprovalFlowScopeMapper approvalFlowScopeMapper) { super(service); + this.sysAccountService = sysAccountService; + this.approvalFlowStepAssigneeMapper = approvalFlowStepAssigneeMapper; + this.approvalFlowScopeMapper = approvalFlowScopeMapper; } + /** + * 获取部门列表默认排序规则。 + * + * @return 默认排序表达式 + */ @Override protected String getDefaultOrderBy() { return "sort_no asc"; } + /** + * 查询部门列表并组装为树形结构。 + * + * @param entity 查询条件 + * @param asTree 是否返回树形结构 + * @param sortKey 排序字段 + * @param sortType 排序方向 + * @return 部门树 + */ @Override @GetMapping("list") public Result> list(SysDept entity, Boolean asTree, String sortKey, String sortType) { @@ -54,9 +94,64 @@ public class SysDeptController extends BaseCurdController changeStatusBatch( + @JsonBody(value = "ids", required = true) List ids, + @JsonBody(value = "status", required = true) Integer status) { + Set uniqueIds = normalizeIds(ids); + if (uniqueIds.isEmpty()) { + return Result.fail("请选择需要操作的部门", null); + } + if (!EnumDataStatus.AVAILABLE.getCode().equals(status) + && !EnumDataStatus.UNAVAILABLE.getCode().equals(status)) { + return Result.fail("部门状态不合法", null); + } + + List records = service.listByIds(uniqueIds); + if (records.size() != uniqueIds.size()) { + return Result.fail("部分部门不存在或已删除,请刷新后重试", null); + } + if (EnumDataStatus.UNAVAILABLE.getCode().equals(status) && containsRootDept(records)) { + return Result.fail("根部门不能禁用", null); + } + if (EnumDataStatus.UNAVAILABLE.getCode().equals(status) + && isUsedByApprovalFlow(uniqueIds)) { + return Result.fail("所选部门已被审批流程使用,不能禁用", null); + } + + LoginAccount loginUser = SaTokenUtil.getLoginAccount(); + SysDept update = new SysDept(); + update.setStatus(status); + update.setModified(new Date()); + update.setModifiedBy(loginUser.getId()); + + QueryWrapper updateWrapper = QueryWrapper.create(); + updateWrapper.in(SysDept::getId, uniqueIds); + int updated = service.getMapper().updateByQuery(update, updateWrapper); + if (updated <= 0) { + return Result.fail("部门状态修改失败", null); + } + return Result.ok(updated); + } + + /** + * {@inheritDoc} + */ @Override protected Result onSaveOrUpdateBefore(SysDept entity, boolean isSave) { LoginAccount loginUser = SaTokenUtil.getLoginAccount(); + if (isSave && entity.getStatus() == null) { + entity.setStatus(EnumDataStatus.AVAILABLE.getCode()); + } BigInteger parentId = entity.getParentId(); if (parentId.equals(BigInteger.ZERO)) { entity.setAncestors(parentId.toString()); @@ -65,7 +160,7 @@ public class SysDeptController extends BaseCurdController ids) { List records = service.listByIds(ids); - for (SysDept dept : records) { - if (Constants.ROOT_DEPT.equals(dept.getDeptCode())) { - return Result.fail(1, "无法删除根部门"); - } + if (records.size() != ids.size()) { + return Result.fail(1, "部分部门不存在或已删除,请刷新后重试"); } - QueryWrapper w = QueryWrapper.create(); - w.in(SysAccount::getDeptId, ids); - long count = sysAccountService.count(w); + if (containsRootDept(records)) { + return Result.fail(1, "无法删除根部门"); + } + + QueryWrapper childQuery = QueryWrapper.create(); + childQuery.in(SysDept::getParentId, ids); + childQuery.notIn(SysDept::getId, ids); + if (service.count(childQuery) > 0) { + return Result.fail(1, "所选部门包含未选中的下级部门,不能删除"); + } + if (isUsedByApprovalFlow(ids)) { + return Result.fail(1, "所选部门已被审批流程使用,请先调整审批配置"); + } + + QueryWrapper accountQuery = QueryWrapper.create(); + accountQuery.in(SysAccount::getDeptId, ids); + long count = sysAccountService.count(accountQuery); if (count > 0) { - return Result.fail(1, "该部门下有员工,不能删除"); + return Result.fail(1, "所选部门下有员工,不能删除"); } return super.onRemoveBefore(ids); } -} \ No newline at end of file + + /** + * 去重并过滤无效部门主键。 + * + * @param ids 原始部门主键集合 + * @return 有效且去重后的主键集合 + */ + private Set normalizeIds(Collection ids) { + Set uniqueIds = new LinkedHashSet<>(); + if (ids == null) { + return uniqueIds; + } + for (BigInteger id : ids) { + if (id != null) { + uniqueIds.add(id); + } + } + return uniqueIds; + } + + /** + * 判断部门集合中是否包含根部门。 + * + * @param records 部门集合 + * @return 包含根部门时返回 {@code true} + */ + private boolean containsRootDept(Collection records) { + return records.stream() + .anyMatch(dept -> Constants.ROOT_DEPT.equals(dept.getDeptCode())); + } + + /** + * 判断部门是否仍被审批步骤或审批范围引用。 + * + * @param ids 部门主键集合 + * @return 存在审批流程引用时返回 {@code true} + */ + private boolean isUsedByApprovalFlow(Collection ids) { + QueryWrapper assigneeQuery = QueryWrapper.create(); + assigneeQuery.eq( + ApprovalFlowStepAssignee::getAssigneeType, + ApprovalAssigneeType.DEPT.getCode()); + assigneeQuery.in(ApprovalFlowStepAssignee::getTargetId, ids); + if (approvalFlowStepAssigneeMapper.selectCountByQuery(assigneeQuery) > 0) { + return true; + } + + QueryWrapper scopeQuery = QueryWrapper.create(); + scopeQuery.eq(ApprovalFlowScope::getScopeType, ApprovalScopeType.DEPT.getCode()); + scopeQuery.in(ApprovalFlowScope::getScopeValue, ids); + return approvalFlowScopeMapper.selectCountByQuery(scopeQuery) > 0; + } +} diff --git a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java new file mode 100644 index 00000000..353f13b6 --- /dev/null +++ b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysDeptControllerTest.java @@ -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 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 saToken = mockStatic(SaTokenUtil.class)) { + saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount); + + Result result = controller.changeStatusBatch(ids, EnumDataStatus.UNAVAILABLE.getCode()); + + assertEquals(result.getErrorCode(), 0); + assertEquals(result.getData(), Integer.valueOf(2)); + } + + ArgumentCaptor 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 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 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 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 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 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 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)); + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowStepAssignee.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowStepAssignee.java new file mode 100644 index 00000000..bac98680 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalFlowStepAssignee.java @@ -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 { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalTaskAssignee.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalTaskAssignee.java new file mode 100644 index 00000000..39403e80 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/ApprovalTaskAssignee.java @@ -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 { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowStepAssigneeBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowStepAssigneeBase.java new file mode 100644 index 00000000..af5ecf12 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalFlowStepAssigneeBase.java @@ -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; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalTaskAssigneeBase.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalTaskAssigneeBase.java new file mode 100644 index 00000000..5c7a6086 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/base/ApprovalTaskAssigneeBase.java @@ -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; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalAssigneeTargetVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalAssigneeTargetVo.java new file mode 100644 index 00000000..8498232d --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalAssigneeTargetVo.java @@ -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; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowStepVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowStepVo.java index 1364e536..800d0fbb 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowStepVo.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalFlowStepVo.java @@ -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 assigneeTargets; + public BigInteger getId() { return id; } @@ -76,4 +79,22 @@ public class ApprovalFlowStepVo { public void setAssigneeTargetName(String assigneeTargetName) { this.assigneeTargetName = assigneeTargetName; } + + /** + * 获取审批对象列表。 + * + * @return 审批对象列表 + */ + public List getAssigneeTargets() { + return assigneeTargets; + } + + /** + * 设置审批对象列表。 + * + * @param assigneeTargets 审批对象列表 + */ + public void setAssigneeTargets(List assigneeTargets) { + this.assigneeTargets = assigneeTargets; + } } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalTaskVo.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalTaskVo.java index 807390e3..18fe7f82 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalTaskVo.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/entity/vo/ApprovalTaskVo.java @@ -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 assigneeTargets; + private BigInteger actedBy; private String actedByName; @@ -129,6 +132,24 @@ public class ApprovalTaskVo { this.assigneeTargetName = assigneeTargetName; } + /** + * 获取审批对象列表。 + * + * @return 审批对象列表 + */ + public List getAssigneeTargets() { + return assigneeTargets; + } + + /** + * 设置审批对象列表。 + * + * @param assigneeTargets 审批对象列表 + */ + public void setAssigneeTargets(List assigneeTargets) { + this.assigneeTargets = assigneeTargets; + } + public BigInteger getActedBy() { return actedBy; } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalAssigneeType.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalAssigneeType.java index 19503444..87f92dd1 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalAssigneeType.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/enums/ApprovalAssigneeType.java @@ -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; diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowStepAssigneeMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowStepAssigneeMapper.java new file mode 100644 index 00000000..3b3c30c9 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalFlowStepAssigneeMapper.java @@ -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 { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalTaskAssigneeMapper.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalTaskAssigneeMapper.java new file mode 100644 index 00000000..d8800d7c --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/mapper/ApprovalTaskAssigneeMapper.java @@ -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 { +} diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalAssigneeService.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalAssigneeService.java index a5dda16d..43fa7bd9 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalAssigneeService.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/ApprovalAssigneeService.java @@ -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> loadStepAssigneeTargets(List steps); + + /** + * 批量加载审批任务的审批对象,关联记录缺失时兼容旧单值字段。 + * + * @param tasks 审批任务 + * @return 任务 ID 到审批对象列表的映射 + */ + Map> loadTaskAssigneeTargets(List tasks); + + /** + * 保存流程步骤审批对象。 + * + * @param stepId 流程步骤 ID + * @param assigneeType 审批对象类型 + * @param targets 审批对象列表 + * @param operatorId 操作人 ID + * @param now 操作时间 + */ + void saveStepAssigneeTargets(BigInteger stepId, String assigneeType, List targets, + BigInteger operatorId, Date now); + + /** + * 删除指定流程步骤的审批对象。 + * + * @param stepIds 流程步骤 ID + */ + void deleteStepAssigneeTargets(List stepIds); + + /** + * 保存任务冻结的审批对象。 + * + * @param taskId 审批任务 ID + * @param assigneeType 审批对象类型 + * @param targets 审批对象列表 + * @param operatorId 操作人 ID + * @param now 操作时间 + */ + void saveTaskAssigneeTargets(BigInteger taskId, String assigneeType, List targets, + BigInteger operatorId, Date now); + /** * 查询可用角色选项。 * diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java index f7ed8869..fd8a7d00 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImpl.java @@ -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 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 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> loadStepAssigneeTargets(List steps) { + if (CollectionUtil.isEmpty(steps)) { + return Map.of(); + } + List stepIds = steps.stream() + .map(ApprovalFlowStep::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List assignees = stepIds.isEmpty() ? List.of() : safeList( + approvalFlowStepAssigneeMapper.selectListByQuery(QueryWrapper.create() + .in(ApprovalFlowStepAssignee::getStepId, stepIds) + .orderBy("created asc, id asc"))); + Map> relationMap = assignees.stream() + .collect(Collectors.groupingBy( + ApprovalFlowStepAssignee::getStepId, + LinkedHashMap::new, + Collectors.toList())); + Map> result = new LinkedHashMap<>(); + for (ApprovalFlowStep step : steps) { + List 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> loadTaskAssigneeTargets(List tasks) { + if (CollectionUtil.isEmpty(tasks)) { + return Map.of(); + } + List taskIds = tasks.stream() + .map(ApprovalTask::getId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + List assignees = taskIds.isEmpty() ? List.of() : safeList( + approvalTaskAssigneeMapper.selectListByQuery(QueryWrapper.create() + .in(ApprovalTaskAssignee::getTaskId, taskIds) + .orderBy("created asc, id asc"))); + Map> relationMap = assignees.stream() + .collect(Collectors.groupingBy( + ApprovalTaskAssignee::getTaskId, + LinkedHashMap::new, + Collectors.toList())); + Map> result = new LinkedHashMap<>(); + for (ApprovalTask task : tasks) { + List 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 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 stepIds) { + if (CollectionUtil.isEmpty(stepIds)) { + return; + } + approvalFlowStepAssigneeMapper.deleteByQuery( + QueryWrapper.create().in(ApprovalFlowStepAssignee::getStepId, stepIds)); + } + + /** + * {@inheritDoc} + */ + @Override + public void saveTaskAssigneeTargets(BigInteger taskId, String assigneeType, + List 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 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 listPendingInstanceIds(BigInteger operatorId, Set roleIds, List instanceIds) { + public Set listPendingInstanceIds(BigInteger operatorId, Set roleIds, + List instanceIds) { if (operatorId == null) { return Set.of(); } - Set 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 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 departmentIds = resolveDepartmentIds(operator == null ? null : operator.getDeptId()); + if (CollectionUtil.isNotEmpty(departmentIds)) { + matchedTaskIds.addAll(loadMatchingRelationTaskIds( + ApprovalAssigneeType.DEPT, departmentIds, operator.getDeptId(), instanceIds)); + } + + Set result = loadPendingTasks(matchedTaskIds, instanceIds).stream() + .map(ApprovalTask::getInstanceId) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + // 旧单值任务仅在没有关联记录时参与兜底,避免旧字段覆盖新关联表的主数据语义。 + List legacyCandidates = loadLegacyPendingCandidates( + operatorId, roleIds, operator == null ? null : operator.getDeptId(), instanceIds); + if (legacyCandidates.isEmpty()) { + return result; + } + Set candidateIds = legacyCandidates.stream() + .map(ApprovalTask::getId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + Set 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 loadMatchingRelationTaskIds(ApprovalAssigneeType type, Set targetIds, + BigInteger currentDeptId, + List 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 loadPendingTasks(Set taskIds, List 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 loadLegacyPendingCandidates(BigInteger operatorId, Set roleIds, + BigInteger deptId, List instanceIds) { + List 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 loadLegacyPendingCandidates(ApprovalAssigneeType type, Set targetIds, + List 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 targets, BigInteger currentDeptId) { + Set 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 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 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 requireTargets(List 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 元素类型 + * @return 非空列表 + */ + private List safeList(List values) { + return values == null ? List.of() : values; + } } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java index 28b7a907..6e239728 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalFlowServiceImpl.java @@ -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 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 loadStepVos(BigInteger flowId) { - return approvalFlowStepMapper.selectListByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId)) - .stream() + List steps = approvalFlowStepMapper.selectListByQuery( + QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId)); + Map> 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 { + Map 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 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> 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 parseAssigneeTargets(Object value) { + if (!(value instanceof List values)) { + return List.of(); + } + List 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 resolveStepTargets(ApprovalFlowStepVo step) { + if (CollectionUtil.isNotEmpty(step.getAssigneeTargets())) { + return step.getAssigneeTargets(); + } + return step.getAssigneeTargetId() == null ? List.of() : List.of(legacyTarget(step)); + } } diff --git a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java index d7816976..0cf2e320 100644 --- a/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java +++ b/easyflow-modules/easyflow-module-approval/src/main/java/tech/easyflow/approval/service/impl/ApprovalQueryServiceImpl.java @@ -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 logs = approvalLogMapper.selectListByQuery( QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId)); Map frozenStepMap = resolveFrozenStepMap(instance); + Map> taskTargetMap = + approvalAssigneeService.loadTaskAssigneeTargets(tasks); Map 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 storedSteps = approvalFlowStepMapper.selectListByQuery( QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, instance.getFlowId())); + Map> 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 parseAssigneeTargets(Object value) { + if (!(value instanceof List values)) { + return List.of(); + } + List 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; + } } diff --git a/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImplTest.java b/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImplTest.java new file mode 100644 index 00000000..d3b8a729 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalAssigneeServiceImplTest.java @@ -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; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalInstanceMultiAssigneeTest.java b/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalInstanceMultiAssigneeTest.java new file mode 100644 index 00000000..fac65bce --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalInstanceMultiAssigneeTest.java @@ -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 saToken = mockStatic(SaTokenUtil.class)) { + saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount); + assertEquals(instanceId, service.submitApproval(request)); + } + + ArgumentCaptor instanceCaptor = ArgumentCaptor.forClass(ApprovalInstance.class); + verify(approvalInstanceMapper).insert(instanceCaptor.capture()); + List> frozenSteps = + (List>) instanceCaptor.getValue().getSnapshotJson().get("steps"); + List> frozenTargets = + (List>) 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 frozenStep(int stepNo, String assigneeType, List> targets) { + Map 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 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; + } +} diff --git a/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalMultiAssigneeMigrationContractTest.java b/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalMultiAssigneeMigrationContractTest.java new file mode 100644 index 00000000..b80753e5 --- /dev/null +++ b/easyflow-modules/easyflow-module-approval/src/test/java/tech/easyflow/approval/service/impl/ApprovalMultiAssigneeMigrationContractTest.java @@ -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 审批多对象迁移"); + } +} diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V37__mysql_approval_multi_assignee.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V37__mysql_approval_multi_assignee.sql new file mode 100644 index 00000000..b8f607c4 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V37__mysql_approval_multi_assignee.sql @@ -0,0 +1,127 @@ +SET NAMES utf8mb4; + +-- 本次迁移依赖“无在途审批”的上线窗口,先阻断再执行表结构变更和历史回填。 +DROP PROCEDURE IF EXISTS `sp_guard_approval_multi_assignee`; +DELIMITER $$ +CREATE PROCEDURE `sp_guard_approval_multi_assignee`() +BEGIN + IF EXISTS ( + SELECT 1 + FROM `tb_approval_instance` + WHERE `status` IN ('PENDING', 'PROCESSING') + LIMIT 1 + ) OR EXISTS ( + SELECT 1 + FROM `tb_approval_task` + WHERE `status` = 'PENDING' + LIMIT 1 + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'approval multi-assignee migration requires no active instances or pending tasks'; + END IF; +END$$ +DELIMITER ; + +CALL `sp_guard_approval_multi_assignee`(); +DROP PROCEDURE IF EXISTS `sp_guard_approval_multi_assignee`; + +CREATE TABLE IF NOT EXISTS `tb_approval_flow_step_assignee` ( + `id` bigint NOT NULL COMMENT '主键', + `step_id` bigint NOT NULL COMMENT '审批步骤ID', + `assignee_type` varchar(16) NOT NULL COMMENT '审批对象类型', + `target_id` bigint NOT NULL COMMENT '审批对象ID', + `target_code` varchar(128) DEFAULT NULL COMMENT '审批对象编码', + `target_name` varchar(128) DEFAULT NULL COMMENT '审批对象名称', + `include_children` tinyint NOT NULL DEFAULT 0 COMMENT '是否包含子部门', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_approval_step_assignee` (`step_id`, `assignee_type`, `target_id`), + KEY `idx_approval_step_assignee_step` (`step_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程步骤对象关联'; + +CREATE TABLE IF NOT EXISTS `tb_approval_task_assignee` ( + `id` bigint NOT NULL COMMENT '主键', + `task_id` bigint NOT NULL COMMENT '审批任务ID', + `assignee_type` varchar(16) NOT NULL COMMENT '审批对象类型', + `target_id` bigint NOT NULL COMMENT '审批对象ID', + `target_code` varchar(128) DEFAULT NULL COMMENT '审批对象编码', + `target_name` varchar(128) DEFAULT NULL COMMENT '审批对象名称', + `include_children` tinyint NOT NULL DEFAULT 0 COMMENT '是否包含子部门', + `created` datetime DEFAULT NULL COMMENT '创建时间', + `created_by` bigint DEFAULT NULL COMMENT '创建者', + `modified` datetime DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_approval_task_assignee` (`task_id`, `assignee_type`, `target_id`), + KEY `idx_approval_task_assignee_target` (`assignee_type`, `target_id`, `task_id`), + KEY `idx_approval_task_assignee_task` (`task_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批任务对象关联'; + +-- 现有流程配置都是单对象,按“一条旧记录对应一条关联记录”完整回填。 +INSERT INTO `tb_approval_flow_step_assignee` ( + `id`, `step_id`, `assignee_type`, `target_id`, `target_code`, `target_name`, + `include_children`, `created`, `created_by`, `modified`, `modified_by` +) +SELECT + step.`id`, step.`id`, step.`assignee_type`, step.`assignee_target_id`, + step.`assignee_target_code`, step.`assignee_target_name`, + 0, step.`created`, step.`created_by`, step.`modified`, step.`modified_by` +FROM `tb_approval_flow_step` step +WHERE step.`assignee_type` IS NOT NULL + AND step.`assignee_target_id` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `tb_approval_flow_step_assignee` relation + WHERE relation.`step_id` = step.`id` + ); + +-- 已完成任务同步回填,保证历史详情继续展示原审批对象。 +INSERT INTO `tb_approval_task_assignee` ( + `id`, `task_id`, `assignee_type`, `target_id`, `target_code`, `target_name`, + `include_children`, `created`, `created_by`, `modified`, `modified_by` +) +SELECT + task.`id`, task.`id`, task.`assignee_type`, task.`assignee_target_id`, + task.`assignee_target_code`, task.`assignee_target_name`, + 0, task.`created`, task.`created_by`, task.`modified`, task.`modified_by` +FROM `tb_approval_task` task +WHERE task.`assignee_type` IS NOT NULL + AND task.`assignee_target_id` IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM `tb_approval_task_assignee` relation + WHERE relation.`task_id` = task.`id` + ); + +DROP PROCEDURE IF EXISTS `sp_verify_approval_multi_assignee`; +DELIMITER $$ +CREATE PROCEDURE `sp_verify_approval_multi_assignee`() +BEGIN + IF EXISTS ( + SELECT 1 + FROM `tb_approval_flow_step` step + LEFT JOIN `tb_approval_flow_step_assignee` relation ON relation.`step_id` = step.`id` + WHERE step.`assignee_type` IS NOT NULL + AND step.`assignee_target_id` IS NOT NULL + AND relation.`id` IS NULL + LIMIT 1 + ) OR EXISTS ( + SELECT 1 + FROM `tb_approval_task` task + LEFT JOIN `tb_approval_task_assignee` relation ON relation.`task_id` = task.`id` + WHERE task.`assignee_type` IS NOT NULL + AND task.`assignee_target_id` IS NOT NULL + AND relation.`id` IS NULL + LIMIT 1 + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'approval multi-assignee backfill verification failed'; + END IF; +END$$ +DELIMITER ; + +CALL `sp_verify_approval_multi_assignee`(); +DROP PROCEDURE IF EXISTS `sp_verify_approval_multi_assignee`; diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/approval.json b/easyflow-ui-admin/app/src/locales/langs/en-US/approval.json index dd742623..105754ad 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/approval.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/approval.json @@ -24,7 +24,8 @@ "revoke": "Revoke", "submit": "Submit Approval", "addScope": "Add Scope", - "addStep": "Add Step" + "addStep": "Add Step", + "removeTarget": "Remove" }, "scope": { "category": "Category", @@ -32,7 +33,8 @@ }, "assignee": { "role": "Role", - "user": "User" + "user": "User", + "dept": "Department" }, "status": { "enabled": "Enabled", @@ -53,7 +55,8 @@ }, "helper": { "scope": "Limit the flow by resource category or applicant department. Leave empty to apply globally.", - "scopeEmpty": "No scope configured. This flow applies globally." + "scopeEmpty": "No scope configured. This flow applies globally.", + "assigneeAny": "Any selected assignee can process the same step." }, "fields": { "flowName": "Flow Name", @@ -77,7 +80,9 @@ "applicantId": "Applicant ID", "submittedAt": "Submitted At", "finishedAt": "Finished At", + "assigneeType": "Assignee Type", "assigneeTarget": "Assignee", + "selectedDeptCount": "Selected departments: {value}", "actedBy": "Acted By", "actedAt": "Acted At", "comment": "Comment", @@ -112,6 +117,7 @@ "message": { "needStep": "At least one step is required", "needStepAssignee": "Each approval step requires an assignee", + "confirmAssigneeTypeChange": "Changing the assignee type clears the current selections. Continue?", "saveSuccess": "Flow saved", "statusUpdated": "Flow status updated", "deleteSuccess": "Flow deleted", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/sysDept.json b/easyflow-ui-admin/app/src/locales/langs/en-US/sysDept.json index 2a2cae66..e5a95c4c 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/sysDept.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/sysDept.json @@ -13,5 +13,18 @@ "modified": "Modified", "modifiedBy": "ModifiedBy", "remark": "Remark", - "isDeleted": "IsDeleted" + "isDeleted": "IsDeleted", + "enable": "Enable", + "disable": "Disable", + "batchEnable": "Enable Selected", + "batchDisable": "Disable Selected", + "batchDelete": "Delete Selected", + "selectedCount": "{count} selected", + "selectRequired": "Select at least one department", + "statusConfirm": "{action} department “{name}”?", + "batchStatusConfirm": "{action} the {count} selected departments?", + "deleteConfirm": "Delete department “{name}”? This action cannot be undone.", + "batchDeleteConfirm": "Delete the {count} selected departments? This action cannot be undone.", + "operationSuccess": "Operation completed", + "operationFailed": "Operation failed. Please try again." } diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json index 7fedcd1e..8499748b 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json @@ -24,7 +24,8 @@ "revoke": "撤回", "submit": "提交审批", "addScope": "新增范围", - "addStep": "新增步骤" + "addStep": "新增步骤", + "removeTarget": "移除" }, "scope": { "category": "分类", @@ -32,7 +33,8 @@ }, "assignee": { "role": "角色", - "user": "用户" + "user": "用户", + "dept": "部门" }, "status": { "enabled": "启用", @@ -53,7 +55,8 @@ }, "helper": { "scope": "按资源分类或申请人部门限定流程命中范围;留空时默认对全部资源生效。", - "scopeEmpty": "未配置范围,当前流程默认全局生效。" + "scopeEmpty": "未配置范围,当前流程默认全局生效。", + "assigneeAny": "同一步骤中,任一选中的审批对象均可处理。" }, "fields": { "flowName": "流程名称", @@ -77,7 +80,9 @@ "applicantId": "申请人ID", "submittedAt": "提交时间", "finishedAt": "完成时间", + "assigneeType": "对象类型", "assigneeTarget": "审批对象", + "selectedDeptCount": "已选 {value} 个部门", "actedBy": "处理人", "actedAt": "处理时间", "comment": "处理意见", @@ -112,6 +117,7 @@ "message": { "needStep": "至少需要一个审批步骤", "needStepAssignee": "每个审批步骤都需要配置审批对象", + "confirmAssigneeTypeChange": "切换审批方式会清空当前已选对象,确认继续吗?", "saveSuccess": "审批流程已保存", "statusUpdated": "流程状态已更新", "deleteSuccess": "流程已删除", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysDept.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysDept.json index 5642394c..ff88e5ec 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysDept.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysDept.json @@ -13,5 +13,18 @@ "modified": "修改时间", "modifiedBy": "修改者", "remark": "备注", - "isDeleted": "删除标识" + "isDeleted": "删除标识", + "enable": "启用", + "disable": "禁用", + "batchEnable": "批量启用", + "batchDisable": "批量禁用", + "batchDelete": "批量删除", + "selectedCount": "已选 {count} 项", + "selectRequired": "请先选择要操作的部门", + "statusConfirm": "确认{action}部门“{name}”吗?", + "batchStatusConfirm": "确认{action}已选中的 {count} 个部门吗?", + "deleteConfirm": "确认删除部门“{name}”吗?删除后不可恢复。", + "batchDeleteConfirm": "确认删除已选中的 {count} 个部门吗?删除后不可恢复。", + "operationSuccess": "操作成功", + "operationFailed": "操作失败,请稍后重试" } diff --git a/easyflow-ui-admin/app/src/views/system/approval/ApprovalDetail.vue b/easyflow-ui-admin/app/src/views/system/approval/ApprovalDetail.vue index 0c20ac60..6ecbc815 100644 --- a/easyflow-ui-admin/app/src/views/system/approval/ApprovalDetail.vue +++ b/easyflow-ui-admin/app/src/views/system/approval/ApprovalDetail.vue @@ -206,13 +206,44 @@ function formatApplicationReason(value?: null | string) { } function formatAssigneeDisplay(row: Record) { - if (!row?.assigneeType || !row?.assigneeTargetName) { + if (!row?.assigneeType) { return '-'; } - if (row.assigneeType === 'ROLE') { - return `${$t('approval.assignee.role')}:${row.assigneeTargetName}`; + const labelMap: Record = { + DEPT: $t('approval.assignee.dept'), + ROLE: $t('approval.assignee.role'), + USER: $t('approval.assignee.user'), + }; + let targets: Array> = []; + if (Array.isArray(row.assigneeTargets) && row.assigneeTargets.length > 0) { + targets = row.assigneeTargets; + } else if (row.assigneeTargetName) { + targets = [ + { + includeChildren: 0, + targetName: row.assigneeTargetName, + }, + ]; } - return `${$t('approval.assignee.user')}:${row.assigneeTargetName}`; + const targetNames = targets + .map((target: Record) => { + const name = String(target?.targetName || '').trim(); + if (!name) { + return ''; + } + if ( + row.assigneeType === 'DEPT' && + (target.includeChildren === 1 || target.includeChildren === true) + ) { + return `${name}(${$t('approval.fields.includeChildren')})`; + } + return name; + }) + .filter(Boolean); + if (targetNames.length === 0) { + return '-'; + } + return `${labelMap[row.assigneeType] || row.assigneeType}:${targetNames.join('、')}`; } function getEventTypeLabel(eventType?: string) { @@ -384,7 +415,7 @@ function formatEventInfo(row: Record) {