发布 v1.10 #5
@@ -1,27 +1,38 @@
|
|||||||
package tech.easyflow.admin.controller.system;
|
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.Constants;
|
||||||
|
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.tree.Tree;
|
import tech.easyflow.common.tree.Tree;
|
||||||
|
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
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.SysAccount;
|
||||||
import tech.easyflow.system.entity.SysDept;
|
import tech.easyflow.system.entity.SysDept;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
import tech.easyflow.system.service.SysDeptService;
|
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.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 部门表 控制层。
|
* 部门表 控制层。
|
||||||
@@ -33,18 +44,47 @@ import java.util.List;
|
|||||||
@RequestMapping("/api/v1/sysDept")
|
@RequestMapping("/api/v1/sysDept")
|
||||||
public class SysDeptController extends BaseCurdController<SysDeptService, SysDept> {
|
public class SysDeptController extends BaseCurdController<SysDeptService, SysDept> {
|
||||||
|
|
||||||
@Resource
|
private final SysAccountService sysAccountService;
|
||||||
private 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);
|
super(service);
|
||||||
|
this.sysAccountService = sysAccountService;
|
||||||
|
this.approvalFlowStepAssigneeMapper = approvalFlowStepAssigneeMapper;
|
||||||
|
this.approvalFlowScopeMapper = approvalFlowScopeMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部门列表默认排序规则。
|
||||||
|
*
|
||||||
|
* @return 默认排序表达式
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected String getDefaultOrderBy() {
|
protected String getDefaultOrderBy() {
|
||||||
return "sort_no asc";
|
return "sort_no asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询部门列表并组装为树形结构。
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否返回树形结构
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方向
|
||||||
|
* @return 部门树
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@GetMapping("list")
|
@GetMapping("list")
|
||||||
public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) {
|
public Result<List<SysDept>> list(SysDept entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
@@ -54,9 +94,64 @@ public class SysDeptController extends BaseCurdController<SysDeptService, SysDep
|
|||||||
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
|
return Result.ok(Tree.tryToTree(sysMenus, "id", "parentId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量修改部门状态。
|
||||||
|
*
|
||||||
|
* @param ids 部门主键集合
|
||||||
|
* @param status 目标状态
|
||||||
|
* @return 实际更新数量
|
||||||
|
*/
|
||||||
|
@PostMapping("changeStatusBatch")
|
||||||
|
@SaCheckPermission("/api/v1/sysDept/save")
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Result<Integer> changeStatusBatch(
|
||||||
|
@JsonBody(value = "ids", required = true) List<BigInteger> ids,
|
||||||
|
@JsonBody(value = "status", required = true) Integer status) {
|
||||||
|
Set<BigInteger> 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<SysDept> 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
|
@Override
|
||||||
protected Result onSaveOrUpdateBefore(SysDept entity, boolean isSave) {
|
protected Result onSaveOrUpdateBefore(SysDept entity, boolean isSave) {
|
||||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||||
|
if (isSave && entity.getStatus() == null) {
|
||||||
|
entity.setStatus(EnumDataStatus.AVAILABLE.getCode());
|
||||||
|
}
|
||||||
BigInteger parentId = entity.getParentId();
|
BigInteger parentId = entity.getParentId();
|
||||||
if (parentId.equals(BigInteger.ZERO)) {
|
if (parentId.equals(BigInteger.ZERO)) {
|
||||||
entity.setAncestors(parentId.toString());
|
entity.setAncestors(parentId.toString());
|
||||||
@@ -73,20 +168,87 @@ public class SysDeptController extends BaseCurdController<SysDeptService, SysDep
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
||||||
List<SysDept> records = service.listByIds(ids);
|
List<SysDept> records = service.listByIds(ids);
|
||||||
for (SysDept dept : records) {
|
if (records.size() != ids.size()) {
|
||||||
if (Constants.ROOT_DEPT.equals(dept.getDeptCode())) {
|
return Result.fail(1, "部分部门不存在或已删除,请刷新后重试");
|
||||||
|
}
|
||||||
|
if (containsRootDept(records)) {
|
||||||
return Result.fail(1, "无法删除根部门");
|
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, "所选部门包含未选中的下级部门,不能删除");
|
||||||
}
|
}
|
||||||
QueryWrapper w = QueryWrapper.create();
|
if (isUsedByApprovalFlow(ids)) {
|
||||||
w.in(SysAccount::getDeptId, ids);
|
return Result.fail(1, "所选部门已被审批流程使用,请先调整审批配置");
|
||||||
long count = sysAccountService.count(w);
|
}
|
||||||
|
|
||||||
|
QueryWrapper accountQuery = QueryWrapper.create();
|
||||||
|
accountQuery.in(SysAccount::getDeptId, ids);
|
||||||
|
long count = sysAccountService.count(accountQuery);
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
return Result.fail(1, "该部门下有员工,不能删除");
|
return Result.fail(1, "所选部门下有员工,不能删除");
|
||||||
}
|
}
|
||||||
return super.onRemoveBefore(ids);
|
return super.onRemoveBefore(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去重并过滤无效部门主键。
|
||||||
|
*
|
||||||
|
* @param ids 原始部门主键集合
|
||||||
|
* @return 有效且去重后的主键集合
|
||||||
|
*/
|
||||||
|
private Set<BigInteger> normalizeIds(Collection<BigInteger> ids) {
|
||||||
|
Set<BigInteger> 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<SysDept> records) {
|
||||||
|
return records.stream()
|
||||||
|
.anyMatch(dept -> Constants.ROOT_DEPT.equals(dept.getDeptCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断部门是否仍被审批步骤或审批范围引用。
|
||||||
|
*
|
||||||
|
* @param ids 部门主键集合
|
||||||
|
* @return 存在审批流程引用时返回 {@code true}
|
||||||
|
*/
|
||||||
|
private boolean isUsedByApprovalFlow(Collection<? extends Serializable> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
import tech.easyflow.approval.mapper.ApprovalFlowScopeMapper;
|
||||||
|
import tech.easyflow.approval.mapper.ApprovalFlowStepAssigneeMapper;
|
||||||
|
import tech.easyflow.common.constant.Constants;
|
||||||
|
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.system.entity.SysDept;
|
||||||
|
import tech.easyflow.system.mapper.SysDeptMapper;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.testng.Assert.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link SysDeptController} 部门状态保存测试。
|
||||||
|
*/
|
||||||
|
public class SysDeptControllerTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证批量状态修改使用单次更新并写入审计字段。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void changeStatusBatchShouldUpdateAllDepartmentsOnce() {
|
||||||
|
SysDeptService service = mock(SysDeptService.class);
|
||||||
|
SysDeptMapper mapper = mock(SysDeptMapper.class);
|
||||||
|
SysDeptController controller = createController(service);
|
||||||
|
List<BigInteger> ids = List.of(BigInteger.ONE, BigInteger.TWO);
|
||||||
|
when(service.listByIds(anyCollection()))
|
||||||
|
.thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1"), buildDept(BigInteger.TWO, "dept_2")));
|
||||||
|
when(service.getMapper()).thenReturn(mapper);
|
||||||
|
when(mapper.updateByQuery(any(SysDept.class), any(QueryWrapper.class))).thenReturn(2);
|
||||||
|
|
||||||
|
LoginAccount loginAccount = mock(LoginAccount.class);
|
||||||
|
when(loginAccount.getId()).thenReturn(BigInteger.TEN);
|
||||||
|
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||||
|
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||||
|
|
||||||
|
Result<Integer> result = controller.changeStatusBatch(ids, EnumDataStatus.UNAVAILABLE.getCode());
|
||||||
|
|
||||||
|
assertEquals(result.getErrorCode(), 0);
|
||||||
|
assertEquals(result.getData(), Integer.valueOf(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentCaptor<SysDept> updateCaptor = ArgumentCaptor.forClass(SysDept.class);
|
||||||
|
verify(mapper).updateByQuery(updateCaptor.capture(), any(QueryWrapper.class));
|
||||||
|
assertEquals(updateCaptor.getValue().getStatus(), EnumDataStatus.UNAVAILABLE.getCode());
|
||||||
|
assertEquals(updateCaptor.getValue().getModifiedBy(), BigInteger.TEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证根部门不能通过批量接口禁用。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void changeStatusBatchShouldRejectRootDepartmentDisable() {
|
||||||
|
SysDeptService service = mock(SysDeptService.class);
|
||||||
|
SysDeptMapper mapper = mock(SysDeptMapper.class);
|
||||||
|
SysDeptController controller = createController(service);
|
||||||
|
SysDept root = buildDept(BigInteger.ONE, Constants.ROOT_DEPT);
|
||||||
|
when(service.listByIds(anyCollection())).thenReturn(List.of(root));
|
||||||
|
when(service.getMapper()).thenReturn(mapper);
|
||||||
|
|
||||||
|
Result<Integer> result = controller.changeStatusBatch(
|
||||||
|
List.of(BigInteger.ONE),
|
||||||
|
EnumDataStatus.UNAVAILABLE.getCode());
|
||||||
|
|
||||||
|
assertEquals(result.getErrorCode(), 1);
|
||||||
|
assertEquals(result.getMessage(), "根部门不能禁用");
|
||||||
|
verify(mapper, never()).updateByQuery(any(SysDept.class), any(QueryWrapper.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证批量状态修改拒绝非法状态。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void changeStatusBatchShouldRejectInvalidStatus() {
|
||||||
|
SysDeptService service = mock(SysDeptService.class);
|
||||||
|
SysDeptController controller = createController(service);
|
||||||
|
|
||||||
|
Result<Integer> result = controller.changeStatusBatch(List.of(BigInteger.ONE), 2);
|
||||||
|
|
||||||
|
assertEquals(result.getErrorCode(), 1);
|
||||||
|
assertEquals(result.getMessage(), "部门状态不合法");
|
||||||
|
verify(service, never()).listByIds(anyCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证存在未选中下级部门时禁止批量删除。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void removeBatchShouldRejectUnselectedChildDepartment() {
|
||||||
|
SysDeptService service = mock(SysDeptService.class);
|
||||||
|
SysDeptController controller = createController(service);
|
||||||
|
Collection<Serializable> ids = List.of(BigInteger.ONE);
|
||||||
|
when(service.listByIds(ids)).thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1")));
|
||||||
|
when(service.count(any(QueryWrapper.class))).thenReturn(1L);
|
||||||
|
|
||||||
|
Result<?> result = controller.onRemoveBefore(ids);
|
||||||
|
|
||||||
|
assertEquals(result.getErrorCode(), 1);
|
||||||
|
assertEquals(result.getMessage(), "所选部门包含未选中的下级部门,不能删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证审批步骤引用的部门不能禁用。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void changeStatusBatchShouldRejectApprovalStepReference() {
|
||||||
|
SysDeptService service = mock(SysDeptService.class);
|
||||||
|
ApprovalFlowStepAssigneeMapper assigneeMapper = mock(ApprovalFlowStepAssigneeMapper.class);
|
||||||
|
SysDeptController controller = new SysDeptController(
|
||||||
|
service,
|
||||||
|
mock(SysAccountService.class),
|
||||||
|
assigneeMapper,
|
||||||
|
mock(ApprovalFlowScopeMapper.class));
|
||||||
|
when(service.listByIds(anyCollection()))
|
||||||
|
.thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1")));
|
||||||
|
when(assigneeMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(1L);
|
||||||
|
|
||||||
|
Result<Integer> result = controller.changeStatusBatch(
|
||||||
|
List.of(BigInteger.ONE),
|
||||||
|
EnumDataStatus.UNAVAILABLE.getCode());
|
||||||
|
|
||||||
|
assertEquals(result.getErrorCode(), 1);
|
||||||
|
assertEquals(result.getMessage(), "所选部门已被审批流程使用,不能禁用");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证审批范围引用的部门不能删除。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void removeBatchShouldRejectApprovalScopeReference() {
|
||||||
|
SysDeptService service = mock(SysDeptService.class);
|
||||||
|
ApprovalFlowScopeMapper scopeMapper = mock(ApprovalFlowScopeMapper.class);
|
||||||
|
SysDeptController controller = new SysDeptController(
|
||||||
|
service,
|
||||||
|
mock(SysAccountService.class),
|
||||||
|
mock(ApprovalFlowStepAssigneeMapper.class),
|
||||||
|
scopeMapper);
|
||||||
|
Collection<Serializable> ids = List.of(BigInteger.ONE);
|
||||||
|
when(service.listByIds(ids)).thenReturn(List.of(buildDept(BigInteger.ONE, "dept_1")));
|
||||||
|
when(service.count(any(QueryWrapper.class))).thenReturn(0L);
|
||||||
|
when(scopeMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(1L);
|
||||||
|
|
||||||
|
Result<?> result = controller.onRemoveBefore(ids);
|
||||||
|
|
||||||
|
assertEquals(result.getErrorCode(), 1);
|
||||||
|
assertEquals(result.getMessage(), "所选部门已被审批流程使用,请先调整审批配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证新增部门未传状态时默认启用。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void saveShouldDefaultMissingStatusToAvailable() {
|
||||||
|
SysDept entity = buildDept(null);
|
||||||
|
|
||||||
|
invokeSaveBefore(entity);
|
||||||
|
|
||||||
|
assertEquals(entity.getStatus(), EnumDataStatus.AVAILABLE.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证新增部门显式传入未启用状态时保持原值。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void saveShouldPreserveExplicitUnavailableStatus() {
|
||||||
|
SysDept entity = buildDept(EnumDataStatus.UNAVAILABLE.getCode());
|
||||||
|
|
||||||
|
invokeSaveBefore(entity);
|
||||||
|
|
||||||
|
assertEquals(entity.getStatus(), EnumDataStatus.UNAVAILABLE.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造待保存的部门。
|
||||||
|
*
|
||||||
|
* @param status 部门状态
|
||||||
|
* @return 部门实体
|
||||||
|
*/
|
||||||
|
private SysDept buildDept(Integer status) {
|
||||||
|
SysDept entity = new SysDept();
|
||||||
|
entity.setParentId(BigInteger.ZERO);
|
||||||
|
entity.setStatus(status);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造指定主键和编码的部门。
|
||||||
|
*
|
||||||
|
* @param id 部门主键
|
||||||
|
* @param deptCode 部门编码
|
||||||
|
* @return 部门实体
|
||||||
|
*/
|
||||||
|
private SysDept buildDept(BigInteger id, String deptCode) {
|
||||||
|
SysDept entity = new SysDept();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setDeptCode(deptCode);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用新增前置处理。
|
||||||
|
*
|
||||||
|
* @param entity 部门实体
|
||||||
|
*/
|
||||||
|
private void invokeSaveBefore(SysDept entity) {
|
||||||
|
LoginAccount loginAccount = mock(LoginAccount.class);
|
||||||
|
SysDeptController controller = createController(mock(SysDeptService.class));
|
||||||
|
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||||
|
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||||
|
controller.onSaveOrUpdateBefore(entity, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建带有隔离依赖的部门控制器。
|
||||||
|
*
|
||||||
|
* @param service 部门服务
|
||||||
|
* @return 部门控制器
|
||||||
|
*/
|
||||||
|
private SysDeptController createController(SysDeptService service) {
|
||||||
|
return new SysDeptController(
|
||||||
|
service,
|
||||||
|
mock(SysAccountService.class),
|
||||||
|
mock(ApprovalFlowStepAssigneeMapper.class),
|
||||||
|
mock(ApprovalFlowScopeMapper.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package tech.easyflow.approval.entity.vo;
|
package tech.easyflow.approval.entity.vo;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批流程步骤项。
|
* 审批流程步骤项。
|
||||||
@@ -21,6 +22,8 @@ public class ApprovalFlowStepVo {
|
|||||||
|
|
||||||
private String assigneeTargetName;
|
private String assigneeTargetName;
|
||||||
|
|
||||||
|
private List<ApprovalAssigneeTargetVo> assigneeTargets;
|
||||||
|
|
||||||
public BigInteger getId() {
|
public BigInteger getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -76,4 +79,22 @@ public class ApprovalFlowStepVo {
|
|||||||
public void setAssigneeTargetName(String assigneeTargetName) {
|
public void setAssigneeTargetName(String assigneeTargetName) {
|
||||||
this.assigneeTargetName = assigneeTargetName;
|
this.assigneeTargetName = assigneeTargetName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批对象列表。
|
||||||
|
*
|
||||||
|
* @return 审批对象列表
|
||||||
|
*/
|
||||||
|
public List<ApprovalAssigneeTargetVo> getAssigneeTargets() {
|
||||||
|
return assigneeTargets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批对象列表。
|
||||||
|
*
|
||||||
|
* @param assigneeTargets 审批对象列表
|
||||||
|
*/
|
||||||
|
public void setAssigneeTargets(List<ApprovalAssigneeTargetVo> assigneeTargets) {
|
||||||
|
this.assigneeTargets = assigneeTargets;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tech.easyflow.approval.entity.vo;
|
|||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批任务视图。
|
* 审批任务视图。
|
||||||
@@ -31,6 +32,8 @@ public class ApprovalTaskVo {
|
|||||||
|
|
||||||
private String assigneeTargetName;
|
private String assigneeTargetName;
|
||||||
|
|
||||||
|
private List<ApprovalAssigneeTargetVo> assigneeTargets;
|
||||||
|
|
||||||
private BigInteger actedBy;
|
private BigInteger actedBy;
|
||||||
|
|
||||||
private String actedByName;
|
private String actedByName;
|
||||||
@@ -129,6 +132,24 @@ public class ApprovalTaskVo {
|
|||||||
this.assigneeTargetName = assigneeTargetName;
|
this.assigneeTargetName = assigneeTargetName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批对象列表。
|
||||||
|
*
|
||||||
|
* @return 审批对象列表
|
||||||
|
*/
|
||||||
|
public List<ApprovalAssigneeTargetVo> getAssigneeTargets() {
|
||||||
|
return assigneeTargets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批对象列表。
|
||||||
|
*
|
||||||
|
* @param assigneeTargets 审批对象列表
|
||||||
|
*/
|
||||||
|
public void setAssigneeTargets(List<ApprovalAssigneeTargetVo> assigneeTargets) {
|
||||||
|
this.assigneeTargets = assigneeTargets;
|
||||||
|
}
|
||||||
|
|
||||||
public BigInteger getActedBy() {
|
public BigInteger getActedBy() {
|
||||||
return actedBy;
|
return actedBy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import tech.easyflow.common.web.exceptions.BusinessException;
|
|||||||
public enum ApprovalAssigneeType {
|
public enum ApprovalAssigneeType {
|
||||||
|
|
||||||
ROLE("ROLE"),
|
ROLE("ROLE"),
|
||||||
USER("USER");
|
USER("USER"),
|
||||||
|
DEPT("DEPT");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.approval.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalFlowStepAssignee;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批流程步骤对象关联 Mapper。
|
||||||
|
*/
|
||||||
|
public interface ApprovalFlowStepAssigneeMapper extends BaseMapper<ApprovalFlowStepAssignee> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.approval.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalTaskAssignee;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批任务对象关联 Mapper。
|
||||||
|
*/
|
||||||
|
public interface ApprovalTaskAssigneeMapper extends BaseMapper<ApprovalTaskAssignee> {
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
package tech.easyflow.approval.service;
|
package tech.easyflow.approval.service;
|
||||||
|
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalFlowStep;
|
||||||
import tech.easyflow.approval.entity.ApprovalTask;
|
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.ApprovalAssigneeOptionVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
|
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +26,53 @@ public interface ApprovalAssigneeService {
|
|||||||
*/
|
*/
|
||||||
ApprovalFlowStepVo normalizeStepAssignee(ApprovalFlowStepVo step);
|
ApprovalFlowStepVo normalizeStepAssignee(ApprovalFlowStepVo step);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量加载流程步骤的审批对象,关联记录缺失时兼容旧单值字段。
|
||||||
|
*
|
||||||
|
* @param steps 流程步骤
|
||||||
|
* @return 步骤 ID 到审批对象列表的映射
|
||||||
|
*/
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadStepAssigneeTargets(List<ApprovalFlowStep> steps);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量加载审批任务的审批对象,关联记录缺失时兼容旧单值字段。
|
||||||
|
*
|
||||||
|
* @param tasks 审批任务
|
||||||
|
* @return 任务 ID 到审批对象列表的映射
|
||||||
|
*/
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadTaskAssigneeTargets(List<ApprovalTask> tasks);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存流程步骤审批对象。
|
||||||
|
*
|
||||||
|
* @param stepId 流程步骤 ID
|
||||||
|
* @param assigneeType 审批对象类型
|
||||||
|
* @param targets 审批对象列表
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param now 操作时间
|
||||||
|
*/
|
||||||
|
void saveStepAssigneeTargets(BigInteger stepId, String assigneeType, List<ApprovalAssigneeTargetVo> targets,
|
||||||
|
BigInteger operatorId, Date now);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定流程步骤的审批对象。
|
||||||
|
*
|
||||||
|
* @param stepIds 流程步骤 ID
|
||||||
|
*/
|
||||||
|
void deleteStepAssigneeTargets(List<BigInteger> stepIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存任务冻结的审批对象。
|
||||||
|
*
|
||||||
|
* @param taskId 审批任务 ID
|
||||||
|
* @param assigneeType 审批对象类型
|
||||||
|
* @param targets 审批对象列表
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param now 操作时间
|
||||||
|
*/
|
||||||
|
void saveTaskAssigneeTargets(BigInteger taskId, String assigneeType, List<ApprovalAssigneeTargetVo> targets,
|
||||||
|
BigInteger operatorId, Date now);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询可用角色选项。
|
* 查询可用角色选项。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,25 +5,39 @@ import com.mybatisflex.core.paginate.Page;
|
|||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
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.ApprovalTask;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalTaskAssignee;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo;
|
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.entity.vo.ApprovalFlowStepVo;
|
||||||
import tech.easyflow.approval.enums.ApprovalAssigneeType;
|
import tech.easyflow.approval.enums.ApprovalAssigneeType;
|
||||||
import tech.easyflow.approval.enums.ApprovalTaskStatus;
|
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.mapper.ApprovalTaskMapper;
|
||||||
import tech.easyflow.approval.service.ApprovalAssigneeService;
|
import tech.easyflow.approval.service.ApprovalAssigneeService;
|
||||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.entity.SysDept;
|
||||||
import tech.easyflow.system.entity.SysRole;
|
import tech.easyflow.system.entity.SysRole;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
import tech.easyflow.system.service.SysRoleService;
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,9 +52,18 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
@Resource
|
@Resource
|
||||||
private SysAccountService sysAccountService;
|
private SysAccountService sysAccountService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysDeptService sysDeptService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ApprovalTaskMapper approvalTaskMapper;
|
private ApprovalTaskMapper approvalTaskMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalFlowStepAssigneeMapper approvalFlowStepAssigneeMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApprovalTaskAssigneeMapper approvalTaskAssigneeMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@@ -50,18 +73,168 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
throw new BusinessException("审批步骤不能为空");
|
throw new BusinessException("审批步骤不能为空");
|
||||||
}
|
}
|
||||||
ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(step.getAssigneeType());
|
ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(step.getAssigneeType());
|
||||||
BigInteger targetId = step.getAssigneeTargetId();
|
List<ApprovalAssigneeTargetVo> requestedTargets = step.getAssigneeTargets();
|
||||||
if (targetId == null) {
|
if (CollectionUtil.isEmpty(requestedTargets) && step.getAssigneeTargetId() != null) {
|
||||||
|
requestedTargets = List.of(legacyTarget(
|
||||||
|
step.getAssigneeTargetId(),
|
||||||
|
step.getAssigneeTargetCode(),
|
||||||
|
step.getAssigneeTargetName()));
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isEmpty(requestedTargets)) {
|
||||||
throw new BusinessException("审批对象不能为空");
|
throw new BusinessException("审批对象不能为空");
|
||||||
}
|
}
|
||||||
ApprovalAssigneeOptionVo option = resolveAssigneeOption(assigneeType, targetId);
|
|
||||||
|
Map<BigInteger, ApprovalAssigneeTargetVo> normalizedTargets = new LinkedHashMap<>();
|
||||||
|
for (ApprovalAssigneeTargetVo target : requestedTargets) {
|
||||||
|
if (target == null || target.getTargetId() == null) {
|
||||||
|
throw new BusinessException("审批对象不能为空");
|
||||||
|
}
|
||||||
|
ApprovalAssigneeOptionVo option = resolveAssigneeOption(assigneeType, target.getTargetId());
|
||||||
|
ApprovalAssigneeTargetVo normalizedTarget = new ApprovalAssigneeTargetVo();
|
||||||
|
normalizedTarget.setTargetId(option.getId());
|
||||||
|
normalizedTarget.setTargetCode(option.getCode());
|
||||||
|
normalizedTarget.setTargetName(option.getName());
|
||||||
|
normalizedTarget.setIncludeChildren(ApprovalAssigneeType.DEPT == assigneeType
|
||||||
|
&& Integer.valueOf(1).equals(target.getIncludeChildren()) ? 1 : 0);
|
||||||
|
normalizedTargets.putIfAbsent(normalizedTarget.getTargetId(), normalizedTarget);
|
||||||
|
}
|
||||||
|
|
||||||
step.setAssigneeType(assigneeType.getCode());
|
step.setAssigneeType(assigneeType.getCode());
|
||||||
step.setAssigneeTargetId(option.getId());
|
step.setAssigneeTargets(new ArrayList<>(normalizedTargets.values()));
|
||||||
step.setAssigneeTargetCode(option.getCode());
|
applyPrimaryTarget(step);
|
||||||
step.setAssigneeTargetName(option.getName());
|
|
||||||
return step;
|
return step;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadStepAssigneeTargets(List<ApprovalFlowStep> steps) {
|
||||||
|
if (CollectionUtil.isEmpty(steps)) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> stepIds = steps.stream()
|
||||||
|
.map(ApprovalFlowStep::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<ApprovalFlowStepAssignee> assignees = stepIds.isEmpty() ? List.of() : safeList(
|
||||||
|
approvalFlowStepAssigneeMapper.selectListByQuery(QueryWrapper.create()
|
||||||
|
.in(ApprovalFlowStepAssignee::getStepId, stepIds)
|
||||||
|
.orderBy("created asc, id asc")));
|
||||||
|
Map<BigInteger, List<ApprovalFlowStepAssignee>> relationMap = assignees.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
ApprovalFlowStepAssignee::getStepId,
|
||||||
|
LinkedHashMap::new,
|
||||||
|
Collectors.toList()));
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> result = new LinkedHashMap<>();
|
||||||
|
for (ApprovalFlowStep step : steps) {
|
||||||
|
List<ApprovalAssigneeTargetVo> targets = relationMap.getOrDefault(step.getId(), List.of()).stream()
|
||||||
|
.filter(item -> Objects.equals(step.getAssigneeType(), item.getAssigneeType()))
|
||||||
|
.map(this::toTarget)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
targets = legacyTargets(step.getAssigneeTargetId(), step.getAssigneeTargetCode(),
|
||||||
|
step.getAssigneeTargetName());
|
||||||
|
}
|
||||||
|
result.put(step.getId(), targets);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<BigInteger, List<ApprovalAssigneeTargetVo>> loadTaskAssigneeTargets(List<ApprovalTask> tasks) {
|
||||||
|
if (CollectionUtil.isEmpty(tasks)) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> taskIds = tasks.stream()
|
||||||
|
.map(ApprovalTask::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<ApprovalTaskAssignee> assignees = taskIds.isEmpty() ? List.of() : safeList(
|
||||||
|
approvalTaskAssigneeMapper.selectListByQuery(QueryWrapper.create()
|
||||||
|
.in(ApprovalTaskAssignee::getTaskId, taskIds)
|
||||||
|
.orderBy("created asc, id asc")));
|
||||||
|
Map<BigInteger, List<ApprovalTaskAssignee>> relationMap = assignees.stream()
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
ApprovalTaskAssignee::getTaskId,
|
||||||
|
LinkedHashMap::new,
|
||||||
|
Collectors.toList()));
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> result = new LinkedHashMap<>();
|
||||||
|
for (ApprovalTask task : tasks) {
|
||||||
|
List<ApprovalAssigneeTargetVo> targets = relationMap.getOrDefault(task.getId(), List.of()).stream()
|
||||||
|
.filter(item -> Objects.equals(task.getAssigneeType(), item.getAssigneeType()))
|
||||||
|
.map(this::toTarget)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
targets = legacyTargets(task.getAssigneeTargetId(), task.getAssigneeTargetCode(),
|
||||||
|
task.getAssigneeTargetName());
|
||||||
|
}
|
||||||
|
result.put(task.getId(), targets);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void saveStepAssigneeTargets(BigInteger stepId, String assigneeType,
|
||||||
|
List<ApprovalAssigneeTargetVo> targets,
|
||||||
|
BigInteger operatorId, Date now) {
|
||||||
|
for (ApprovalAssigneeTargetVo target : requireTargets(targets)) {
|
||||||
|
ApprovalFlowStepAssignee relation = new ApprovalFlowStepAssignee();
|
||||||
|
relation.setStepId(stepId);
|
||||||
|
relation.setAssigneeType(assigneeType);
|
||||||
|
relation.setTargetId(target.getTargetId());
|
||||||
|
relation.setTargetCode(target.getTargetCode());
|
||||||
|
relation.setTargetName(target.getTargetName());
|
||||||
|
relation.setIncludeChildren(normalizeIncludeChildren(assigneeType, target.getIncludeChildren()));
|
||||||
|
relation.setCreated(now);
|
||||||
|
relation.setCreatedBy(operatorId);
|
||||||
|
relation.setModified(now);
|
||||||
|
relation.setModifiedBy(operatorId);
|
||||||
|
approvalFlowStepAssigneeMapper.insert(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void deleteStepAssigneeTargets(List<BigInteger> stepIds) {
|
||||||
|
if (CollectionUtil.isEmpty(stepIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
approvalFlowStepAssigneeMapper.deleteByQuery(
|
||||||
|
QueryWrapper.create().in(ApprovalFlowStepAssignee::getStepId, stepIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void saveTaskAssigneeTargets(BigInteger taskId, String assigneeType,
|
||||||
|
List<ApprovalAssigneeTargetVo> targets,
|
||||||
|
BigInteger operatorId, Date now) {
|
||||||
|
for (ApprovalAssigneeTargetVo target : requireTargets(targets)) {
|
||||||
|
ApprovalTaskAssignee relation = new ApprovalTaskAssignee();
|
||||||
|
relation.setTaskId(taskId);
|
||||||
|
relation.setAssigneeType(assigneeType);
|
||||||
|
relation.setTargetId(target.getTargetId());
|
||||||
|
relation.setTargetCode(target.getTargetCode());
|
||||||
|
relation.setTargetName(target.getTargetName());
|
||||||
|
relation.setIncludeChildren(normalizeIncludeChildren(assigneeType, target.getIncludeChildren()));
|
||||||
|
relation.setCreated(now);
|
||||||
|
relation.setCreatedBy(operatorId);
|
||||||
|
relation.setModified(now);
|
||||||
|
relation.setModifiedBy(operatorId);
|
||||||
|
approvalTaskAssigneeMapper.insert(relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@@ -118,46 +291,210 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(task.getAssigneeType());
|
ApprovalAssigneeType assigneeType = ApprovalAssigneeType.from(task.getAssigneeType());
|
||||||
|
List<ApprovalAssigneeTargetVo> targets = loadTaskAssigneeTargets(List.of(task))
|
||||||
|
.getOrDefault(task.getId(), List.of());
|
||||||
if (ApprovalAssigneeType.USER == assigneeType) {
|
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}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Set<BigInteger> listPendingInstanceIds(BigInteger operatorId, Set<BigInteger> roleIds, List<BigInteger> instanceIds) {
|
public Set<BigInteger> listPendingInstanceIds(BigInteger operatorId, Set<BigInteger> roleIds,
|
||||||
|
List<BigInteger> instanceIds) {
|
||||||
if (operatorId == null) {
|
if (operatorId == null) {
|
||||||
return Set.of();
|
return Set.of();
|
||||||
}
|
}
|
||||||
Set<BigInteger> result = new LinkedHashSet<>();
|
Set<BigInteger> matchedTaskIds = new LinkedHashSet<>();
|
||||||
QueryWrapper userQuery = QueryWrapper.create()
|
matchedTaskIds.addAll(loadMatchingRelationTaskIds(
|
||||||
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode());
|
ApprovalAssigneeType.USER, Set.of(operatorId), null, instanceIds));
|
||||||
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)));
|
|
||||||
if (CollectionUtil.isNotEmpty(roleIds)) {
|
if (CollectionUtil.isNotEmpty(roleIds)) {
|
||||||
QueryWrapper roleQuery = QueryWrapper.create()
|
matchedTaskIds.addAll(loadMatchingRelationTaskIds(
|
||||||
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode())
|
ApprovalAssigneeType.ROLE, roleIds, null, instanceIds));
|
||||||
.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()
|
|
||||||
|
SysAccount operator = sysAccountService.getById(operatorId);
|
||||||
|
Set<BigInteger> departmentIds = resolveDepartmentIds(operator == null ? null : operator.getDeptId());
|
||||||
|
if (CollectionUtil.isNotEmpty(departmentIds)) {
|
||||||
|
matchedTaskIds.addAll(loadMatchingRelationTaskIds(
|
||||||
|
ApprovalAssigneeType.DEPT, departmentIds, operator.getDeptId(), instanceIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<BigInteger> result = loadPendingTasks(matchedTaskIds, instanceIds).stream()
|
||||||
.map(ApprovalTask::getInstanceId)
|
.map(ApprovalTask::getInstanceId)
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new)));
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
|
||||||
|
// 旧单值任务仅在没有关联记录时参与兜底,避免旧字段覆盖新关联表的主数据语义。
|
||||||
|
List<ApprovalTask> legacyCandidates = loadLegacyPendingCandidates(
|
||||||
|
operatorId, roleIds, operator == null ? null : operator.getDeptId(), instanceIds);
|
||||||
|
if (legacyCandidates.isEmpty()) {
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
Set<BigInteger> candidateIds = legacyCandidates.stream()
|
||||||
|
.map(ApprovalTask::getId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Set<BigInteger> relatedTaskIds = candidateIds.isEmpty() ? Set.of() : safeList(
|
||||||
|
approvalTaskAssigneeMapper.selectListByQuery(
|
||||||
|
QueryWrapper.create().in(ApprovalTaskAssignee::getTaskId, candidateIds))).stream()
|
||||||
|
.map(ApprovalTaskAssignee::getTaskId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
legacyCandidates.stream()
|
||||||
|
.filter(task -> !relatedTaskIds.contains(task.getId()))
|
||||||
|
.map(ApprovalTask::getInstanceId)
|
||||||
|
.forEach(result::add);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询能命中当前主体的任务关联 ID。
|
||||||
|
*
|
||||||
|
* @param type 审批对象类型
|
||||||
|
* @param targetIds 候选对象 ID
|
||||||
|
* @param currentDeptId 当前部门 ID,部门审批时用于区分直属命中
|
||||||
|
* @param instanceIds 可选实例过滤
|
||||||
|
* @return 任务 ID 集合
|
||||||
|
*/
|
||||||
|
private Set<BigInteger> loadMatchingRelationTaskIds(ApprovalAssigneeType type, Set<BigInteger> targetIds,
|
||||||
|
BigInteger currentDeptId,
|
||||||
|
List<BigInteger> instanceIds) {
|
||||||
|
if (CollectionUtil.isEmpty(targetIds)) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
QueryWrapper pendingTaskQuery = QueryWrapper.create()
|
||||||
|
.select(ApprovalTask::getId)
|
||||||
|
.from(ApprovalTask.class)
|
||||||
|
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode());
|
||||||
|
if (CollectionUtil.isNotEmpty(instanceIds)) {
|
||||||
|
pendingTaskQuery.in(ApprovalTask::getInstanceId, instanceIds);
|
||||||
|
}
|
||||||
|
return safeList(approvalTaskAssigneeMapper.selectListByQuery(QueryWrapper.create()
|
||||||
|
.eq(ApprovalTaskAssignee::getAssigneeType, type.getCode())
|
||||||
|
.in(ApprovalTaskAssignee::getTargetId, targetIds)
|
||||||
|
.in(ApprovalTaskAssignee::getTaskId, pendingTaskQuery)))
|
||||||
|
.stream()
|
||||||
|
.filter(item -> type != ApprovalAssigneeType.DEPT
|
||||||
|
|| Objects.equals(currentDeptId, item.getTargetId())
|
||||||
|
|| Integer.valueOf(1).equals(item.getIncludeChildren()))
|
||||||
|
.map(ApprovalTaskAssignee::getTaskId)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按任务 ID 批量读取待审批任务。
|
||||||
|
*
|
||||||
|
* @param taskIds 任务 ID
|
||||||
|
* @param instanceIds 可选实例过滤
|
||||||
|
* @return 待审批任务
|
||||||
|
*/
|
||||||
|
private List<ApprovalTask> loadPendingTasks(Set<BigInteger> taskIds, List<BigInteger> instanceIds) {
|
||||||
|
if (CollectionUtil.isEmpty(taskIds)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
QueryWrapper query = QueryWrapper.create()
|
||||||
|
.in(ApprovalTask::getId, taskIds)
|
||||||
|
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode());
|
||||||
|
if (CollectionUtil.isNotEmpty(instanceIds)) {
|
||||||
|
query.in(ApprovalTask::getInstanceId, instanceIds);
|
||||||
|
}
|
||||||
|
return safeList(approvalTaskMapper.selectListByQuery(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询旧单值字段能命中的待审批任务候选。
|
||||||
|
*
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param roleIds 角色 ID
|
||||||
|
* @param deptId 当前部门 ID
|
||||||
|
* @param instanceIds 可选实例过滤
|
||||||
|
* @return 候选任务
|
||||||
|
*/
|
||||||
|
private List<ApprovalTask> loadLegacyPendingCandidates(BigInteger operatorId, Set<BigInteger> roleIds,
|
||||||
|
BigInteger deptId, List<BigInteger> instanceIds) {
|
||||||
|
List<ApprovalTask> result = new ArrayList<>();
|
||||||
|
result.addAll(loadLegacyPendingCandidates(
|
||||||
|
ApprovalAssigneeType.USER, Set.of(operatorId), instanceIds));
|
||||||
|
if (CollectionUtil.isNotEmpty(roleIds)) {
|
||||||
|
result.addAll(loadLegacyPendingCandidates(
|
||||||
|
ApprovalAssigneeType.ROLE, roleIds, instanceIds));
|
||||||
|
}
|
||||||
|
if (deptId != null) {
|
||||||
|
result.addAll(loadLegacyPendingCandidates(
|
||||||
|
ApprovalAssigneeType.DEPT, Set.of(deptId), instanceIds));
|
||||||
|
}
|
||||||
|
return result.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
ApprovalTask::getId,
|
||||||
|
Function.identity(),
|
||||||
|
(left, right) -> left,
|
||||||
|
LinkedHashMap::new))
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询一种旧单值审批对象命中的待审批任务。
|
||||||
|
*
|
||||||
|
* @param type 审批对象类型
|
||||||
|
* @param targetIds 对象 ID
|
||||||
|
* @param instanceIds 可选实例过滤
|
||||||
|
* @return 候选任务
|
||||||
|
*/
|
||||||
|
private List<ApprovalTask> loadLegacyPendingCandidates(ApprovalAssigneeType type, Set<BigInteger> targetIds,
|
||||||
|
List<BigInteger> instanceIds) {
|
||||||
|
QueryWrapper query = QueryWrapper.create()
|
||||||
|
.eq(ApprovalTask::getStatus, ApprovalTaskStatus.PENDING.getCode())
|
||||||
|
.eq(ApprovalTask::getAssigneeType, type.getCode())
|
||||||
|
.in(ApprovalTask::getAssigneeTargetId, targetIds);
|
||||||
|
if (CollectionUtil.isNotEmpty(instanceIds)) {
|
||||||
|
query.in(ApprovalTask::getInstanceId, instanceIds);
|
||||||
|
}
|
||||||
|
return safeList(approvalTaskMapper.selectListByQuery(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前部门是否命中任一部门审批对象。
|
||||||
|
*
|
||||||
|
* @param targets 部门审批对象
|
||||||
|
* @param currentDeptId 当前部门 ID
|
||||||
|
* @return 是否命中
|
||||||
|
*/
|
||||||
|
private boolean matchesDepartment(List<ApprovalAssigneeTargetVo> targets, BigInteger currentDeptId) {
|
||||||
|
Set<BigInteger> departmentIds = resolveDepartmentIds(currentDeptId);
|
||||||
|
if (departmentIds.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return targets.stream().anyMatch(target ->
|
||||||
|
Objects.equals(currentDeptId, target.getTargetId())
|
||||||
|
|| Integer.valueOf(1).equals(target.getIncludeChildren())
|
||||||
|
&& departmentIds.contains(target.getTargetId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前部门及其祖先部门。
|
||||||
|
*
|
||||||
|
* @param currentDeptId 当前部门 ID
|
||||||
|
* @return 部门 ID 集合
|
||||||
|
*/
|
||||||
|
private Set<BigInteger> resolveDepartmentIds(BigInteger currentDeptId) {
|
||||||
|
return currentDeptId == null ? Set.of() : sysDeptService.getSelfAndAncestorDeptIds(currentDeptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析并校验单个审批对象。
|
||||||
|
*
|
||||||
|
* @param assigneeType 审批对象类型
|
||||||
|
* @param targetId 审批对象 ID
|
||||||
|
* @return 规范化选项
|
||||||
|
*/
|
||||||
private ApprovalAssigneeOptionVo resolveAssigneeOption(ApprovalAssigneeType assigneeType, BigInteger targetId) {
|
private ApprovalAssigneeOptionVo resolveAssigneeOption(ApprovalAssigneeType assigneeType, BigInteger targetId) {
|
||||||
if (ApprovalAssigneeType.ROLE == assigneeType) {
|
if (ApprovalAssigneeType.ROLE == assigneeType) {
|
||||||
SysRole role = sysRoleService.getById(targetId);
|
SysRole role = sysRoleService.getById(targetId);
|
||||||
@@ -166,6 +503,13 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
}
|
}
|
||||||
return toRoleOption(role);
|
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);
|
SysAccount account = sysAccountService.getById(targetId);
|
||||||
if (account == null || !EnumDataStatus.AVAILABLE.getCode().equals(account.getStatus())) {
|
if (account == null || !EnumDataStatus.AVAILABLE.getCode().equals(account.getStatus())) {
|
||||||
throw new BusinessException("审批用户不存在或未启用");
|
throw new BusinessException("审批用户不存在或未启用");
|
||||||
@@ -173,6 +517,12 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
return toAccountOption(account);
|
return toAccountOption(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将角色转换为审批选项。
|
||||||
|
*
|
||||||
|
* @param role 角色
|
||||||
|
* @return 审批选项
|
||||||
|
*/
|
||||||
private ApprovalAssigneeOptionVo toRoleOption(SysRole role) {
|
private ApprovalAssigneeOptionVo toRoleOption(SysRole role) {
|
||||||
ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo();
|
ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo();
|
||||||
option.setId(role.getId());
|
option.setId(role.getId());
|
||||||
@@ -181,6 +531,12 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将账号转换为审批选项。
|
||||||
|
*
|
||||||
|
* @param account 账号
|
||||||
|
* @return 审批选项
|
||||||
|
*/
|
||||||
private ApprovalAssigneeOptionVo toAccountOption(SysAccount account) {
|
private ApprovalAssigneeOptionVo toAccountOption(SysAccount account) {
|
||||||
ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo();
|
ApprovalAssigneeOptionVo option = new ApprovalAssigneeOptionVo();
|
||||||
option.setId(account.getId());
|
option.setId(account.getId());
|
||||||
@@ -189,6 +545,26 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
return option;
|
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) {
|
private String resolveAccountDisplayName(SysAccount account) {
|
||||||
if (StringUtils.hasText(account.getNickname())) {
|
if (StringUtils.hasText(account.getNickname())) {
|
||||||
return account.getNickname().trim();
|
return account.getNickname().trim();
|
||||||
@@ -198,4 +574,122 @@ public class ApprovalAssigneeServiceImpl implements ApprovalAssigneeService {
|
|||||||
}
|
}
|
||||||
return String.valueOf(account.getId());
|
return String.valueOf(account.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将步骤的第一个审批对象同步到旧单值字段。
|
||||||
|
*
|
||||||
|
* @param step 步骤
|
||||||
|
*/
|
||||||
|
private void applyPrimaryTarget(ApprovalFlowStepVo step) {
|
||||||
|
ApprovalAssigneeTargetVo primary = step.getAssigneeTargets().get(0);
|
||||||
|
step.setAssigneeTargetId(primary.getTargetId());
|
||||||
|
step.setAssigneeTargetCode(primary.getTargetCode());
|
||||||
|
step.setAssigneeTargetName(primary.getTargetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造旧单值审批对象。
|
||||||
|
*
|
||||||
|
* @param targetId 对象 ID
|
||||||
|
* @param targetCode 对象编码
|
||||||
|
* @param targetName 对象名称
|
||||||
|
* @return 审批对象
|
||||||
|
*/
|
||||||
|
private ApprovalAssigneeTargetVo legacyTarget(BigInteger targetId, String targetCode, String targetName) {
|
||||||
|
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||||
|
target.setTargetId(targetId);
|
||||||
|
target.setTargetCode(targetCode);
|
||||||
|
target.setTargetName(targetName);
|
||||||
|
target.setIncludeChildren(0);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造旧单值审批对象列表。
|
||||||
|
*
|
||||||
|
* @param targetId 对象 ID
|
||||||
|
* @param targetCode 对象编码
|
||||||
|
* @param targetName 对象名称
|
||||||
|
* @return 审批对象列表
|
||||||
|
*/
|
||||||
|
private List<ApprovalAssigneeTargetVo> legacyTargets(BigInteger targetId, String targetCode, String targetName) {
|
||||||
|
return targetId == null ? List.of() : List.of(legacyTarget(targetId, targetCode, targetName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将步骤关联实体转换为审批对象。
|
||||||
|
*
|
||||||
|
* @param relation 步骤关联
|
||||||
|
* @return 审批对象
|
||||||
|
*/
|
||||||
|
private ApprovalAssigneeTargetVo toTarget(ApprovalFlowStepAssignee relation) {
|
||||||
|
return target(relation.getTargetId(), relation.getTargetCode(), relation.getTargetName(),
|
||||||
|
relation.getIncludeChildren());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将任务关联实体转换为审批对象。
|
||||||
|
*
|
||||||
|
* @param relation 任务关联
|
||||||
|
* @return 审批对象
|
||||||
|
*/
|
||||||
|
private ApprovalAssigneeTargetVo toTarget(ApprovalTaskAssignee relation) {
|
||||||
|
return target(relation.getTargetId(), relation.getTargetCode(), relation.getTargetName(),
|
||||||
|
relation.getIncludeChildren());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造审批对象。
|
||||||
|
*
|
||||||
|
* @param targetId 对象 ID
|
||||||
|
* @param targetCode 对象编码
|
||||||
|
* @param targetName 对象名称
|
||||||
|
* @param includeChildren 是否包含子部门
|
||||||
|
* @return 审批对象
|
||||||
|
*/
|
||||||
|
private ApprovalAssigneeTargetVo target(BigInteger targetId, String targetCode, String targetName,
|
||||||
|
Integer includeChildren) {
|
||||||
|
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||||
|
target.setTargetId(targetId);
|
||||||
|
target.setTargetCode(targetCode);
|
||||||
|
target.setTargetName(targetName);
|
||||||
|
target.setIncludeChildren(Integer.valueOf(1).equals(includeChildren) ? 1 : 0);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验审批对象列表。
|
||||||
|
*
|
||||||
|
* @param targets 审批对象列表
|
||||||
|
* @return 非空审批对象列表
|
||||||
|
*/
|
||||||
|
private List<ApprovalAssigneeTargetVo> requireTargets(List<ApprovalAssigneeTargetVo> targets) {
|
||||||
|
if (CollectionUtil.isEmpty(targets)) {
|
||||||
|
throw new BusinessException("审批对象不能为空");
|
||||||
|
}
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化包含子部门标记。
|
||||||
|
*
|
||||||
|
* @param assigneeType 审批对象类型
|
||||||
|
* @param includeChildren 原标记
|
||||||
|
* @return 规范化标记
|
||||||
|
*/
|
||||||
|
private int normalizeIncludeChildren(String assigneeType, Integer includeChildren) {
|
||||||
|
return ApprovalAssigneeType.DEPT.getCode().equals(assigneeType)
|
||||||
|
&& Integer.valueOf(1).equals(includeChildren) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将可能为空的 Mapper 查询结果转换为空列表。
|
||||||
|
*
|
||||||
|
* @param values 查询结果
|
||||||
|
* @param <T> 元素类型
|
||||||
|
* @return 非空列表
|
||||||
|
*/
|
||||||
|
private <T> List<T> safeList(List<T> values) {
|
||||||
|
return values == null ? List.of() : values;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import tech.easyflow.approval.entity.ApprovalFlow;
|
|||||||
import tech.easyflow.approval.entity.ApprovalFlowScope;
|
import tech.easyflow.approval.entity.ApprovalFlowScope;
|
||||||
import tech.easyflow.approval.entity.ApprovalFlowStep;
|
import tech.easyflow.approval.entity.ApprovalFlowStep;
|
||||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
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.ApprovalFlowDetailVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo;
|
import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalFlowScopeVo;
|
import tech.easyflow.approval.entity.vo.ApprovalFlowScopeVo;
|
||||||
@@ -293,6 +294,7 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
|
|||||||
normalized.setStepName(item.getStepName().trim());
|
normalized.setStepName(item.getStepName().trim());
|
||||||
normalized.setAssigneeType(item.getAssigneeType());
|
normalized.setAssigneeType(item.getAssigneeType());
|
||||||
normalized.setAssigneeTargetId(item.getAssigneeTargetId());
|
normalized.setAssigneeTargetId(item.getAssigneeTargetId());
|
||||||
|
normalized.setAssigneeTargets(item.getAssigneeTargets());
|
||||||
approvalAssigneeService.normalizeStepAssignee(normalized);
|
approvalAssigneeService.normalizeStepAssignee(normalized);
|
||||||
result.add(normalized);
|
result.add(normalized);
|
||||||
}
|
}
|
||||||
@@ -304,6 +306,12 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
|
|||||||
|
|
||||||
private void clearChildren(BigInteger flowId) {
|
private void clearChildren(BigInteger flowId) {
|
||||||
approvalFlowScopeMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowScope::getFlowId, flowId));
|
approvalFlowScopeMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowScope::getFlowId, flowId));
|
||||||
|
List<BigInteger> stepIds = approvalFlowStepMapper.selectListByQuery(
|
||||||
|
QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId))
|
||||||
|
.stream()
|
||||||
|
.map(ApprovalFlowStep::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
approvalAssigneeService.deleteStepAssigneeTargets(stepIds);
|
||||||
approvalFlowStepMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId));
|
approvalFlowStepMapper.deleteByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +342,8 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
|
|||||||
step.setModified(now);
|
step.setModified(now);
|
||||||
step.setModifiedBy(operatorId);
|
step.setModifiedBy(operatorId);
|
||||||
approvalFlowStepMapper.insert(step);
|
approvalFlowStepMapper.insert(step);
|
||||||
|
approvalAssigneeService.saveStepAssigneeTargets(
|
||||||
|
step.getId(), stepVo.getAssigneeType(), stepVo.getAssigneeTargets(), operatorId, now);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,8 +378,11 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<ApprovalFlowStepVo> loadStepVos(BigInteger flowId) {
|
private List<ApprovalFlowStepVo> loadStepVos(BigInteger flowId) {
|
||||||
return approvalFlowStepMapper.selectListByQuery(QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId))
|
List<ApprovalFlowStep> steps = approvalFlowStepMapper.selectListByQuery(
|
||||||
.stream()
|
QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, flowId));
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> targetMap =
|
||||||
|
approvalAssigneeService.loadStepAssigneeTargets(steps);
|
||||||
|
return steps.stream()
|
||||||
.sorted(Comparator.comparing(ApprovalFlowStep::getStepNo))
|
.sorted(Comparator.comparing(ApprovalFlowStep::getStepNo))
|
||||||
.map(item -> {
|
.map(item -> {
|
||||||
ApprovalFlowStepVo stepVo = new ApprovalFlowStepVo();
|
ApprovalFlowStepVo stepVo = new ApprovalFlowStepVo();
|
||||||
@@ -380,6 +393,7 @@ public class ApprovalFlowServiceImpl extends ServiceImpl<ApprovalFlowMapper, App
|
|||||||
stepVo.setAssigneeTargetId(item.getAssigneeTargetId());
|
stepVo.setAssigneeTargetId(item.getAssigneeTargetId());
|
||||||
stepVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
stepVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
||||||
stepVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
stepVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
||||||
|
stepVo.setAssigneeTargets(targetMap.getOrDefault(item.getId(), List.of()));
|
||||||
return stepVo;
|
return stepVo;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import tech.easyflow.approval.entity.ApprovalInstance;
|
|||||||
import tech.easyflow.approval.entity.ApprovalFlowStep;
|
import tech.easyflow.approval.entity.ApprovalFlowStep;
|
||||||
import tech.easyflow.approval.entity.ApprovalLog;
|
import tech.easyflow.approval.entity.ApprovalLog;
|
||||||
import tech.easyflow.approval.entity.ApprovalTask;
|
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.ApprovalFlowDetailVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
|
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
@@ -271,6 +272,14 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
|
|||||||
map.put("assigneeTargetId", item.getAssigneeTargetId());
|
map.put("assigneeTargetId", item.getAssigneeTargetId());
|
||||||
map.put("assigneeTargetCode", item.getAssigneeTargetCode());
|
map.put("assigneeTargetCode", item.getAssigneeTargetCode());
|
||||||
map.put("assigneeTargetName", item.getAssigneeTargetName());
|
map.put("assigneeTargetName", item.getAssigneeTargetName());
|
||||||
|
map.put("assigneeTargets", resolveStepTargets(item).stream().map(target -> {
|
||||||
|
Map<String, Object> targetMap = new LinkedHashMap<>();
|
||||||
|
targetMap.put("targetId", target.getTargetId());
|
||||||
|
targetMap.put("targetCode", target.getTargetCode());
|
||||||
|
targetMap.put("targetName", target.getTargetName());
|
||||||
|
targetMap.put("includeChildren", target.getIncludeChildren());
|
||||||
|
return targetMap;
|
||||||
|
}).collect(Collectors.toList()));
|
||||||
return map;
|
return map;
|
||||||
}).collect(Collectors.toList()));
|
}).collect(Collectors.toList()));
|
||||||
return snapshot;
|
return snapshot;
|
||||||
@@ -315,6 +324,10 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
|
|||||||
if (assigneeTargetName != null) {
|
if (assigneeTargetName != null) {
|
||||||
stepVo.setAssigneeTargetName(String.valueOf(assigneeTargetName));
|
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);
|
result.add(stepVo);
|
||||||
}
|
}
|
||||||
mergeStepAssigneeFromFlow(instance.getFlowId(), result);
|
mergeStepAssigneeFromFlow(instance.getFlowId(), result);
|
||||||
@@ -326,22 +339,30 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createTask(BigInteger instanceId, ApprovalFlowStepVo step, BigInteger operatorId, Date now) {
|
private void createTask(BigInteger instanceId, ApprovalFlowStepVo step, BigInteger operatorId, Date now) {
|
||||||
|
List<ApprovalAssigneeTargetVo> targets = resolveStepTargets(step);
|
||||||
|
if (CollectionUtil.isEmpty(targets)) {
|
||||||
|
throw new BusinessException("审批步骤缺少审批对象");
|
||||||
|
}
|
||||||
|
step.setAssigneeTargets(targets);
|
||||||
|
ApprovalAssigneeTargetVo primaryTarget = targets.get(0);
|
||||||
ApprovalTask task = new ApprovalTask();
|
ApprovalTask task = new ApprovalTask();
|
||||||
task.setInstanceId(instanceId);
|
task.setInstanceId(instanceId);
|
||||||
task.setStepNo(step.getStepNo());
|
task.setStepNo(step.getStepNo());
|
||||||
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
|
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
|
||||||
task.setAssigneeRoleCode(ApprovalAssigneeType.ROLE.getCode().equals(step.getAssigneeType())
|
task.setAssigneeRoleCode(ApprovalAssigneeType.ROLE.getCode().equals(step.getAssigneeType())
|
||||||
? step.getAssigneeTargetCode()
|
? primaryTarget.getTargetCode()
|
||||||
: null);
|
: null);
|
||||||
task.setAssigneeType(step.getAssigneeType());
|
task.setAssigneeType(step.getAssigneeType());
|
||||||
task.setAssigneeTargetId(step.getAssigneeTargetId());
|
task.setAssigneeTargetId(primaryTarget.getTargetId());
|
||||||
task.setAssigneeTargetCode(step.getAssigneeTargetCode());
|
task.setAssigneeTargetCode(primaryTarget.getTargetCode());
|
||||||
task.setAssigneeTargetName(step.getAssigneeTargetName());
|
task.setAssigneeTargetName(primaryTarget.getTargetName());
|
||||||
task.setCreated(now);
|
task.setCreated(now);
|
||||||
task.setCreatedBy(operatorId);
|
task.setCreatedBy(operatorId);
|
||||||
task.setModified(now);
|
task.setModified(now);
|
||||||
task.setModifiedBy(operatorId);
|
task.setModifiedBy(operatorId);
|
||||||
approvalTaskMapper.insert(task);
|
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) {
|
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,
|
(left, right) -> left,
|
||||||
LinkedHashMap::new
|
LinkedHashMap::new
|
||||||
));
|
));
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> targetMap =
|
||||||
|
approvalAssigneeService.loadStepAssigneeTargets(storedSteps);
|
||||||
for (ApprovalFlowStepVo step : steps) {
|
for (ApprovalFlowStepVo step : steps) {
|
||||||
if (step.getAssigneeTargetId() != null && step.getAssigneeType() != null) {
|
if (CollectionUtil.isNotEmpty(step.getAssigneeTargets())
|
||||||
|
&& step.getAssigneeTargetId() != null
|
||||||
|
&& step.getAssigneeType() != null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ApprovalFlowStep storedStep = storedMap.get(step.getStepNo());
|
ApprovalFlowStep storedStep = storedMap.get(step.getStepNo());
|
||||||
@@ -492,6 +517,7 @@ public class ApprovalInstanceServiceImpl implements ApprovalInstanceService {
|
|||||||
step.setAssigneeTargetId(storedStep.getAssigneeTargetId());
|
step.setAssigneeTargetId(storedStep.getAssigneeTargetId());
|
||||||
step.setAssigneeTargetCode(storedStep.getAssigneeTargetCode());
|
step.setAssigneeTargetCode(storedStep.getAssigneeTargetCode());
|
||||||
step.setAssigneeTargetName(storedStep.getAssigneeTargetName());
|
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("assigneeTargetId", step.getAssigneeTargetId());
|
||||||
payload.put("assigneeTargetCode", step.getAssigneeTargetCode());
|
payload.put("assigneeTargetCode", step.getAssigneeTargetCode());
|
||||||
payload.put("assigneeTargetName", step.getAssigneeTargetName());
|
payload.put("assigneeTargetName", step.getAssigneeTargetName());
|
||||||
|
payload.put("assigneeTargets", resolveStepTargets(step));
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从冻结快照中解析审批对象列表。
|
||||||
|
*
|
||||||
|
* @param value 快照字段
|
||||||
|
* @return 审批对象列表
|
||||||
|
*/
|
||||||
|
private List<ApprovalAssigneeTargetVo> parseAssigneeTargets(Object value) {
|
||||||
|
if (!(value instanceof List<?> values)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<ApprovalAssigneeTargetVo> result = new ArrayList<>();
|
||||||
|
for (Object item : values) {
|
||||||
|
if (!(item instanceof Map<?, ?> map)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigInteger targetId = parseTargetId(map.get("targetId"));
|
||||||
|
if (targetId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||||
|
target.setTargetId(targetId);
|
||||||
|
target.setTargetCode(map.get("targetCode") == null ? null : String.valueOf(map.get("targetCode")));
|
||||||
|
target.setTargetName(map.get("targetName") == null ? null : String.valueOf(map.get("targetName")));
|
||||||
|
Object includeChildren = map.get("includeChildren");
|
||||||
|
target.setIncludeChildren(includeChildren instanceof Number number && number.intValue() == 1
|
||||||
|
|| Boolean.TRUE.equals(includeChildren) ? 1 : 0);
|
||||||
|
result.add(target);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析快照中的对象 ID。
|
||||||
|
*
|
||||||
|
* @param value 快照值
|
||||||
|
* @return 对象 ID
|
||||||
|
*/
|
||||||
|
private BigInteger parseTargetId(Object value) {
|
||||||
|
if (value instanceof BigInteger bigInteger) {
|
||||||
|
return bigInteger;
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return new BigInteger(number.toString());
|
||||||
|
}
|
||||||
|
if (value instanceof String string && !string.isBlank()) {
|
||||||
|
return new BigInteger(string);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将旧单值步骤转换为单元素审批对象。
|
||||||
|
*
|
||||||
|
* @param step 旧步骤
|
||||||
|
* @return 审批对象
|
||||||
|
*/
|
||||||
|
private ApprovalAssigneeTargetVo legacyTarget(ApprovalFlowStepVo step) {
|
||||||
|
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||||
|
target.setTargetId(step.getAssigneeTargetId());
|
||||||
|
target.setTargetCode(step.getAssigneeTargetCode());
|
||||||
|
target.setTargetName(step.getAssigneeTargetName());
|
||||||
|
target.setIncludeChildren(0);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取步骤审批对象,兼容旧单值步骤。
|
||||||
|
*
|
||||||
|
* @param step 审批步骤
|
||||||
|
* @return 审批对象列表
|
||||||
|
*/
|
||||||
|
private List<ApprovalAssigneeTargetVo> resolveStepTargets(ApprovalFlowStepVo step) {
|
||||||
|
if (CollectionUtil.isNotEmpty(step.getAssigneeTargets())) {
|
||||||
|
return step.getAssigneeTargets();
|
||||||
|
}
|
||||||
|
return step.getAssigneeTargetId() == null ? List.of() : List.of(legacyTarget(step));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import tech.easyflow.approval.entity.ApprovalFlowStep;
|
|||||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||||
import tech.easyflow.approval.entity.ApprovalLog;
|
import tech.easyflow.approval.entity.ApprovalLog;
|
||||||
import tech.easyflow.approval.entity.ApprovalTask;
|
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.ApprovalInstanceDetailVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo;
|
import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
|
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
|
||||||
@@ -174,6 +175,8 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
|
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
|
||||||
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));
|
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));
|
||||||
Map<Integer, ApprovalFlowStepVo> frozenStepMap = resolveFrozenStepMap(instance);
|
Map<Integer, ApprovalFlowStepVo> frozenStepMap = resolveFrozenStepMap(instance);
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> taskTargetMap =
|
||||||
|
approvalAssigneeService.loadTaskAssigneeTargets(tasks);
|
||||||
Map<BigInteger, SysAccount> accountMap = loadAccountMap(instance, tasks, logs, account.getTenantId());
|
Map<BigInteger, SysAccount> accountMap = loadAccountMap(instance, tasks, logs, account.getTenantId());
|
||||||
detail.setApplicantName(resolveAccountName(accountMap.get(instance.getApplicantId())));
|
detail.setApplicantName(resolveAccountName(accountMap.get(instance.getApplicantId())));
|
||||||
detail.setApplicantAccount(resolveAccountLoginName(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.setAssigneeTargetId(item.getAssigneeTargetId());
|
||||||
taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
||||||
taskVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
taskVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
||||||
|
taskVo.setAssigneeTargets(taskTargetMap.getOrDefault(item.getId(), List.of()));
|
||||||
taskVo.setActedBy(item.getActedBy());
|
taskVo.setActedBy(item.getActedBy());
|
||||||
taskVo.setActedByName(resolveAccountName(accountMap.get(item.getActedBy())));
|
taskVo.setActedByName(resolveAccountName(accountMap.get(item.getActedBy())));
|
||||||
taskVo.setActedAt(item.getActedAt());
|
taskVo.setActedAt(item.getActedAt());
|
||||||
@@ -466,14 +470,21 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
if (assigneeTargetName != null) {
|
if (assigneeTargetName != null) {
|
||||||
stepVo.setAssigneeTargetName(String.valueOf(assigneeTargetName));
|
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);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
List<ApprovalFlowStep> storedSteps = approvalFlowStepMapper.selectListByQuery(
|
List<ApprovalFlowStep> storedSteps = approvalFlowStepMapper.selectListByQuery(
|
||||||
QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, instance.getFlowId()));
|
QueryWrapper.create().eq(ApprovalFlowStep::getFlowId, instance.getFlowId()));
|
||||||
|
Map<BigInteger, List<ApprovalAssigneeTargetVo>> targetMap =
|
||||||
|
approvalAssigneeService.loadStepAssigneeTargets(storedSteps);
|
||||||
for (ApprovalFlowStep step : storedSteps) {
|
for (ApprovalFlowStep step : storedSteps) {
|
||||||
ApprovalFlowStepVo stepVo = result.computeIfAbsent(step.getStepNo(), key -> {
|
ApprovalFlowStepVo stepVo = result.computeIfAbsent(step.getStepNo(), key -> {
|
||||||
ApprovalFlowStepVo value = new ApprovalFlowStepVo();
|
ApprovalFlowStepVo value = new ApprovalFlowStepVo();
|
||||||
@@ -495,7 +506,75 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
if (!StringUtils.hasText(stepVo.getAssigneeTargetName())) {
|
if (!StringUtils.hasText(stepVo.getAssigneeTargetName())) {
|
||||||
stepVo.setAssigneeTargetName(step.getAssigneeTargetName());
|
stepVo.setAssigneeTargetName(step.getAssigneeTargetName());
|
||||||
}
|
}
|
||||||
|
if (CollectionUtil.isEmpty(stepVo.getAssigneeTargets())) {
|
||||||
|
stepVo.setAssigneeTargets(targetMap.getOrDefault(step.getId(), List.of()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从冻结快照中解析审批对象列表。
|
||||||
|
*
|
||||||
|
* @param value 快照字段
|
||||||
|
* @return 审批对象列表
|
||||||
|
*/
|
||||||
|
private List<ApprovalAssigneeTargetVo> parseAssigneeTargets(Object value) {
|
||||||
|
if (!(value instanceof List<?> values)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<ApprovalAssigneeTargetVo> result = new ArrayList<>();
|
||||||
|
for (Object item : values) {
|
||||||
|
if (!(item instanceof Map<?, ?> map)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigInteger targetId = parseTargetId(map.get("targetId"));
|
||||||
|
if (targetId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||||
|
target.setTargetId(targetId);
|
||||||
|
target.setTargetCode(map.get("targetCode") == null ? null : String.valueOf(map.get("targetCode")));
|
||||||
|
target.setTargetName(map.get("targetName") == null ? null : String.valueOf(map.get("targetName")));
|
||||||
|
Object includeChildren = map.get("includeChildren");
|
||||||
|
target.setIncludeChildren(includeChildren instanceof Number number && number.intValue() == 1
|
||||||
|
|| Boolean.TRUE.equals(includeChildren) ? 1 : 0);
|
||||||
|
result.add(target);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析快照中的对象 ID。
|
||||||
|
*
|
||||||
|
* @param value 快照值
|
||||||
|
* @return 对象 ID
|
||||||
|
*/
|
||||||
|
private BigInteger parseTargetId(Object value) {
|
||||||
|
if (value instanceof BigInteger bigInteger) {
|
||||||
|
return bigInteger;
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return new BigInteger(number.toString());
|
||||||
|
}
|
||||||
|
if (value instanceof String string && StringUtils.hasText(string)) {
|
||||||
|
return new BigInteger(string);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将旧单值步骤转换为单元素审批对象。
|
||||||
|
*
|
||||||
|
* @param step 旧步骤
|
||||||
|
* @return 审批对象
|
||||||
|
*/
|
||||||
|
private ApprovalAssigneeTargetVo legacyTarget(ApprovalFlowStepVo step) {
|
||||||
|
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||||
|
target.setTargetId(step.getAssigneeTargetId());
|
||||||
|
target.setTargetCode(step.getAssigneeTargetCode());
|
||||||
|
target.setTargetName(step.getAssigneeTargetName());
|
||||||
|
target.setIncludeChildren(0);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package tech.easyflow.approval.service.impl;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalLog;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalTask;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalAssigneeType;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalTaskStatus;
|
||||||
|
import tech.easyflow.approval.mapper.ApprovalFlowStepMapper;
|
||||||
|
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||||
|
import tech.easyflow.approval.mapper.ApprovalLogMapper;
|
||||||
|
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
|
||||||
|
import tech.easyflow.approval.service.ApprovalAssigneeService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalMatchService;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ApprovalInstanceServiceImpl} 多审批对象冻结测试。
|
||||||
|
*/
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class ApprovalInstanceMultiAssigneeTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApprovalMatchService approvalMatchService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApprovalInstanceMapper approvalInstanceMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApprovalTaskMapper approvalTaskMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApprovalLogMapper approvalLogMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApprovalFlowStepMapper approvalFlowStepMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ApprovalAssigneeService approvalAssigneeService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SysAccountService sysAccountService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ApprovalInstanceServiceImpl service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证提审时会把全部审批对象写入实例快照和任务关联。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void submitApprovalShouldFreezeAllAssigneeTargets() {
|
||||||
|
BigInteger applicantId = BigInteger.valueOf(7);
|
||||||
|
BigInteger tenantId = BigInteger.valueOf(42);
|
||||||
|
BigInteger instanceId = BigInteger.valueOf(101);
|
||||||
|
BigInteger taskId = BigInteger.valueOf(201);
|
||||||
|
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||||
|
request.setApplicantId(applicantId);
|
||||||
|
request.setResourceId(BigInteger.valueOf(501));
|
||||||
|
request.setSummary("多用户审批");
|
||||||
|
|
||||||
|
ApprovalFlowStepVo step = new ApprovalFlowStepVo();
|
||||||
|
step.setStepNo(1);
|
||||||
|
step.setStepName("审核");
|
||||||
|
step.setAssigneeType(ApprovalAssigneeType.USER.getCode());
|
||||||
|
step.setAssigneeTargets(List.of(
|
||||||
|
target(11, "u11", "用户甲"),
|
||||||
|
target(12, "u12", "用户乙")));
|
||||||
|
ApprovalFlowDetailVo flow = new ApprovalFlowDetailVo();
|
||||||
|
flow.setId(BigInteger.valueOf(301));
|
||||||
|
flow.setVersion(3);
|
||||||
|
flow.setResourceType("WORKFLOW");
|
||||||
|
flow.setActionType("PUBLISH");
|
||||||
|
flow.setSteps(List.of(step));
|
||||||
|
when(approvalMatchService.matchFlow(request)).thenReturn(flow);
|
||||||
|
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(applicantId);
|
||||||
|
loginAccount.setTenantId(tenantId);
|
||||||
|
SysAccount applicant = new SysAccount();
|
||||||
|
applicant.setId(applicantId);
|
||||||
|
applicant.setTenantId(tenantId);
|
||||||
|
when(sysAccountService.getById(applicantId)).thenReturn(applicant);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
((ApprovalInstance) invocation.getArgument(0)).setId(instanceId);
|
||||||
|
return 1;
|
||||||
|
}).when(approvalInstanceMapper).insert(any(ApprovalInstance.class));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
((ApprovalTask) invocation.getArgument(0)).setId(taskId);
|
||||||
|
return 1;
|
||||||
|
}).when(approvalTaskMapper).insert(any(ApprovalTask.class));
|
||||||
|
|
||||||
|
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||||
|
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||||
|
assertEquals(instanceId, service.submitApproval(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentCaptor<ApprovalInstance> instanceCaptor = ArgumentCaptor.forClass(ApprovalInstance.class);
|
||||||
|
verify(approvalInstanceMapper).insert(instanceCaptor.capture());
|
||||||
|
List<Map<String, Object>> frozenSteps =
|
||||||
|
(List<Map<String, Object>>) instanceCaptor.getValue().getSnapshotJson().get("steps");
|
||||||
|
List<Map<String, Object>> frozenTargets =
|
||||||
|
(List<Map<String, Object>>) frozenSteps.get(0).get("assigneeTargets");
|
||||||
|
assertEquals(2, frozenTargets.size());
|
||||||
|
assertEquals(BigInteger.valueOf(12), frozenTargets.get(1).get("targetId"));
|
||||||
|
verify(approvalAssigneeService).saveTaskAssigneeTargets(
|
||||||
|
eq(taskId),
|
||||||
|
eq(ApprovalAssigneeType.USER.getCode()),
|
||||||
|
argThat(targets -> targets.size() == 2),
|
||||||
|
eq(applicantId),
|
||||||
|
any(Date.class));
|
||||||
|
verify(approvalLogMapper, org.mockito.Mockito.times(2)).insert(any(ApprovalLog.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证进入下一步骤时继续使用实例快照中冻结的全部审批对象。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void approveShouldCreateNextTaskFromFrozenAssigneeTargets() {
|
||||||
|
BigInteger operatorId = BigInteger.valueOf(7);
|
||||||
|
BigInteger tenantId = BigInteger.valueOf(42);
|
||||||
|
BigInteger instanceId = BigInteger.valueOf(101);
|
||||||
|
BigInteger currentTaskId = BigInteger.valueOf(201);
|
||||||
|
BigInteger nextTaskId = BigInteger.valueOf(202);
|
||||||
|
BigInteger firstDeptId = BigInteger.valueOf(31);
|
||||||
|
BigInteger secondDeptId = BigInteger.valueOf(32);
|
||||||
|
|
||||||
|
ApprovalInstance instance = new ApprovalInstance();
|
||||||
|
instance.setId(instanceId);
|
||||||
|
instance.setTenantId(tenantId);
|
||||||
|
instance.setFlowId(BigInteger.valueOf(301));
|
||||||
|
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
|
||||||
|
instance.setCurrentStepNo(1);
|
||||||
|
instance.setSnapshotJson(Map.of(
|
||||||
|
"steps", List.of(
|
||||||
|
frozenStep(1, ApprovalAssigneeType.USER.getCode(),
|
||||||
|
List.of(frozenTarget(11, 0))),
|
||||||
|
frozenStep(2, ApprovalAssigneeType.DEPT.getCode(), List.of(
|
||||||
|
frozenTarget(firstDeptId.longValue(), 0),
|
||||||
|
frozenTarget(secondDeptId.longValue(), 1))))));
|
||||||
|
|
||||||
|
ApprovalTask currentTask = new ApprovalTask();
|
||||||
|
currentTask.setId(currentTaskId);
|
||||||
|
currentTask.setInstanceId(instanceId);
|
||||||
|
currentTask.setStepNo(1);
|
||||||
|
currentTask.setStatus(ApprovalTaskStatus.PENDING.getCode());
|
||||||
|
currentTask.setAssigneeType(ApprovalAssigneeType.USER.getCode());
|
||||||
|
|
||||||
|
SysAccount operator = new SysAccount();
|
||||||
|
operator.setId(operatorId);
|
||||||
|
operator.setTenantId(tenantId);
|
||||||
|
when(sysAccountService.getById(operatorId)).thenReturn(operator);
|
||||||
|
when(approvalInstanceMapper.selectOneByQuery(any())).thenReturn(instance);
|
||||||
|
when(approvalTaskMapper.selectOneByQuery(any())).thenReturn(currentTask);
|
||||||
|
when(approvalAssigneeService.getAvailableRoleIds(operatorId)).thenReturn(Set.of());
|
||||||
|
when(approvalAssigneeService.canHandleTask(currentTask, operatorId, Set.of())).thenReturn(true);
|
||||||
|
when(approvalFlowStepMapper.selectListByQuery(any())).thenReturn(List.of());
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
((ApprovalTask) invocation.getArgument(0)).setId(nextTaskId);
|
||||||
|
return 1;
|
||||||
|
}).when(approvalTaskMapper).insert(any(ApprovalTask.class));
|
||||||
|
|
||||||
|
service.approve(instanceId, "同意", operatorId);
|
||||||
|
|
||||||
|
verify(approvalAssigneeService).saveTaskAssigneeTargets(
|
||||||
|
eq(nextTaskId),
|
||||||
|
eq(ApprovalAssigneeType.DEPT.getCode()),
|
||||||
|
argThat(targets -> targets.size() == 2
|
||||||
|
&& firstDeptId.equals(targets.get(0).getTargetId())
|
||||||
|
&& secondDeptId.equals(targets.get(1).getTargetId())
|
||||||
|
&& Integer.valueOf(1).equals(targets.get(1).getIncludeChildren())),
|
||||||
|
eq(operatorId),
|
||||||
|
any(Date.class));
|
||||||
|
assertEquals(ApprovalInstanceStatus.PROCESSING.getCode(), instance.getStatus());
|
||||||
|
assertEquals(Integer.valueOf(2), instance.getCurrentStepNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造冻结步骤。
|
||||||
|
*
|
||||||
|
* @param stepNo 步骤序号
|
||||||
|
* @param assigneeType 审批对象类型
|
||||||
|
* @param targets 审批对象
|
||||||
|
* @return 冻结步骤
|
||||||
|
*/
|
||||||
|
private Map<String, Object> frozenStep(int stepNo, String assigneeType, List<Map<String, Object>> targets) {
|
||||||
|
Map<String, Object> primary = targets.get(0);
|
||||||
|
return Map.of(
|
||||||
|
"stepNo", stepNo,
|
||||||
|
"stepName", "第" + stepNo + "步",
|
||||||
|
"assigneeType", assigneeType,
|
||||||
|
"assigneeTargetId", primary.get("targetId"),
|
||||||
|
"assigneeTargetCode", "target-" + primary.get("targetId"),
|
||||||
|
"assigneeTargetName", "对象" + primary.get("targetId"),
|
||||||
|
"assigneeTargets", targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造冻结审批对象。
|
||||||
|
*
|
||||||
|
* @param id 对象 ID
|
||||||
|
* @param includeChildren 是否包含子部门
|
||||||
|
* @return 冻结审批对象
|
||||||
|
*/
|
||||||
|
private Map<String, Object> frozenTarget(long id, int includeChildren) {
|
||||||
|
return Map.of(
|
||||||
|
"targetId", BigInteger.valueOf(id),
|
||||||
|
"targetCode", "target-" + id,
|
||||||
|
"targetName", "对象" + id,
|
||||||
|
"includeChildren", includeChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造审批对象。
|
||||||
|
*
|
||||||
|
* @param id 用户 ID
|
||||||
|
* @param code 用户编码
|
||||||
|
* @param name 用户名称
|
||||||
|
* @return 审批对象
|
||||||
|
*/
|
||||||
|
private ApprovalAssigneeTargetVo target(long id, String code, String name) {
|
||||||
|
ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
|
||||||
|
target.setTargetId(BigInteger.valueOf(id));
|
||||||
|
target.setTargetCode(code);
|
||||||
|
target.setTargetName(name);
|
||||||
|
target.setIncludeChildren(0);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 审批多对象迁移");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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`;
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
"revoke": "Revoke",
|
"revoke": "Revoke",
|
||||||
"submit": "Submit Approval",
|
"submit": "Submit Approval",
|
||||||
"addScope": "Add Scope",
|
"addScope": "Add Scope",
|
||||||
"addStep": "Add Step"
|
"addStep": "Add Step",
|
||||||
|
"removeTarget": "Remove"
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
@@ -32,7 +33,8 @@
|
|||||||
},
|
},
|
||||||
"assignee": {
|
"assignee": {
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
"user": "User"
|
"user": "User",
|
||||||
|
"dept": "Department"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
@@ -53,7 +55,8 @@
|
|||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"scope": "Limit the flow by resource category or applicant department. Leave empty to apply globally.",
|
"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": {
|
"fields": {
|
||||||
"flowName": "Flow Name",
|
"flowName": "Flow Name",
|
||||||
@@ -77,7 +80,9 @@
|
|||||||
"applicantId": "Applicant ID",
|
"applicantId": "Applicant ID",
|
||||||
"submittedAt": "Submitted At",
|
"submittedAt": "Submitted At",
|
||||||
"finishedAt": "Finished At",
|
"finishedAt": "Finished At",
|
||||||
|
"assigneeType": "Assignee Type",
|
||||||
"assigneeTarget": "Assignee",
|
"assigneeTarget": "Assignee",
|
||||||
|
"selectedDeptCount": "Selected departments: {value}",
|
||||||
"actedBy": "Acted By",
|
"actedBy": "Acted By",
|
||||||
"actedAt": "Acted At",
|
"actedAt": "Acted At",
|
||||||
"comment": "Comment",
|
"comment": "Comment",
|
||||||
@@ -112,6 +117,7 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"needStep": "At least one step is required",
|
"needStep": "At least one step is required",
|
||||||
"needStepAssignee": "Each approval step requires an assignee",
|
"needStepAssignee": "Each approval step requires an assignee",
|
||||||
|
"confirmAssigneeTypeChange": "Changing the assignee type clears the current selections. Continue?",
|
||||||
"saveSuccess": "Flow saved",
|
"saveSuccess": "Flow saved",
|
||||||
"statusUpdated": "Flow status updated",
|
"statusUpdated": "Flow status updated",
|
||||||
"deleteSuccess": "Flow deleted",
|
"deleteSuccess": "Flow deleted",
|
||||||
|
|||||||
@@ -13,5 +13,18 @@
|
|||||||
"modified": "Modified",
|
"modified": "Modified",
|
||||||
"modifiedBy": "ModifiedBy",
|
"modifiedBy": "ModifiedBy",
|
||||||
"remark": "Remark",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"revoke": "撤回",
|
"revoke": "撤回",
|
||||||
"submit": "提交审批",
|
"submit": "提交审批",
|
||||||
"addScope": "新增范围",
|
"addScope": "新增范围",
|
||||||
"addStep": "新增步骤"
|
"addStep": "新增步骤",
|
||||||
|
"removeTarget": "移除"
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"category": "分类",
|
"category": "分类",
|
||||||
@@ -32,7 +33,8 @@
|
|||||||
},
|
},
|
||||||
"assignee": {
|
"assignee": {
|
||||||
"role": "角色",
|
"role": "角色",
|
||||||
"user": "用户"
|
"user": "用户",
|
||||||
|
"dept": "部门"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"enabled": "启用",
|
"enabled": "启用",
|
||||||
@@ -53,7 +55,8 @@
|
|||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"scope": "按资源分类或申请人部门限定流程命中范围;留空时默认对全部资源生效。",
|
"scope": "按资源分类或申请人部门限定流程命中范围;留空时默认对全部资源生效。",
|
||||||
"scopeEmpty": "未配置范围,当前流程默认全局生效。"
|
"scopeEmpty": "未配置范围,当前流程默认全局生效。",
|
||||||
|
"assigneeAny": "同一步骤中,任一选中的审批对象均可处理。"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"flowName": "流程名称",
|
"flowName": "流程名称",
|
||||||
@@ -77,7 +80,9 @@
|
|||||||
"applicantId": "申请人ID",
|
"applicantId": "申请人ID",
|
||||||
"submittedAt": "提交时间",
|
"submittedAt": "提交时间",
|
||||||
"finishedAt": "完成时间",
|
"finishedAt": "完成时间",
|
||||||
|
"assigneeType": "对象类型",
|
||||||
"assigneeTarget": "审批对象",
|
"assigneeTarget": "审批对象",
|
||||||
|
"selectedDeptCount": "已选 {value} 个部门",
|
||||||
"actedBy": "处理人",
|
"actedBy": "处理人",
|
||||||
"actedAt": "处理时间",
|
"actedAt": "处理时间",
|
||||||
"comment": "处理意见",
|
"comment": "处理意见",
|
||||||
@@ -112,6 +117,7 @@
|
|||||||
"message": {
|
"message": {
|
||||||
"needStep": "至少需要一个审批步骤",
|
"needStep": "至少需要一个审批步骤",
|
||||||
"needStepAssignee": "每个审批步骤都需要配置审批对象",
|
"needStepAssignee": "每个审批步骤都需要配置审批对象",
|
||||||
|
"confirmAssigneeTypeChange": "切换审批方式会清空当前已选对象,确认继续吗?",
|
||||||
"saveSuccess": "审批流程已保存",
|
"saveSuccess": "审批流程已保存",
|
||||||
"statusUpdated": "流程状态已更新",
|
"statusUpdated": "流程状态已更新",
|
||||||
"deleteSuccess": "流程已删除",
|
"deleteSuccess": "流程已删除",
|
||||||
|
|||||||
@@ -13,5 +13,18 @@
|
|||||||
"modified": "修改时间",
|
"modified": "修改时间",
|
||||||
"modifiedBy": "修改者",
|
"modifiedBy": "修改者",
|
||||||
"remark": "备注",
|
"remark": "备注",
|
||||||
"isDeleted": "删除标识"
|
"isDeleted": "删除标识",
|
||||||
|
"enable": "启用",
|
||||||
|
"disable": "禁用",
|
||||||
|
"batchEnable": "批量启用",
|
||||||
|
"batchDisable": "批量禁用",
|
||||||
|
"batchDelete": "批量删除",
|
||||||
|
"selectedCount": "已选 {count} 项",
|
||||||
|
"selectRequired": "请先选择要操作的部门",
|
||||||
|
"statusConfirm": "确认{action}部门“{name}”吗?",
|
||||||
|
"batchStatusConfirm": "确认{action}已选中的 {count} 个部门吗?",
|
||||||
|
"deleteConfirm": "确认删除部门“{name}”吗?删除后不可恢复。",
|
||||||
|
"batchDeleteConfirm": "确认删除已选中的 {count} 个部门吗?删除后不可恢复。",
|
||||||
|
"operationSuccess": "操作成功",
|
||||||
|
"operationFailed": "操作失败,请稍后重试"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,13 +206,44 @@ function formatApplicationReason(value?: null | string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatAssigneeDisplay(row: Record<string, any>) {
|
function formatAssigneeDisplay(row: Record<string, any>) {
|
||||||
if (!row?.assigneeType || !row?.assigneeTargetName) {
|
if (!row?.assigneeType) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
if (row.assigneeType === 'ROLE') {
|
const labelMap: Record<string, string> = {
|
||||||
return `${$t('approval.assignee.role')}:${row.assigneeTargetName}`;
|
DEPT: $t('approval.assignee.dept'),
|
||||||
|
ROLE: $t('approval.assignee.role'),
|
||||||
|
USER: $t('approval.assignee.user'),
|
||||||
|
};
|
||||||
|
let targets: Array<Record<string, any>> = [];
|
||||||
|
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<string, any>) => {
|
||||||
|
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) {
|
function getEventTypeLabel(eventType?: string) {
|
||||||
@@ -384,7 +415,7 @@ function formatEventInfo(row: Record<string, any>) {
|
|||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
:label="$t('approval.fields.assigneeTarget')"
|
:label="$t('approval.fields.assigneeTarget')"
|
||||||
width="220"
|
min-width="260"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatAssigneeDisplay(row) }}
|
{{ formatAssigneeDisplay(row) }}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ElInput,
|
ElInput,
|
||||||
ElInputNumber,
|
ElInputNumber,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
|
ElMessageBox,
|
||||||
ElOption,
|
ElOption,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSwitch,
|
ElSwitch,
|
||||||
@@ -32,7 +33,7 @@ defineExpose({
|
|||||||
|
|
||||||
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
||||||
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
|
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
|
||||||
type AssigneeType = 'ROLE' | 'USER';
|
type AssigneeType = 'DEPT' | 'ROLE' | 'USER';
|
||||||
type ScopeType = 'CATEGORY' | 'DEPT';
|
type ScopeType = 'CATEGORY' | 'DEPT';
|
||||||
type FlowStatus = 'DISABLED' | 'ENABLED';
|
type FlowStatus = 'DISABLED' | 'ENABLED';
|
||||||
|
|
||||||
@@ -49,14 +50,21 @@ interface ScopeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface StepItem {
|
interface StepItem {
|
||||||
assigneeTargetCode?: string;
|
assigneeTargetIds: Array<number | string>;
|
||||||
assigneeTargetId?: number | string;
|
assigneeTargets: AssigneeTarget[];
|
||||||
assigneeTargetName?: string;
|
|
||||||
assigneeType: AssigneeType;
|
assigneeType: AssigneeType;
|
||||||
|
includeChildrenByTarget: Record<string, boolean>;
|
||||||
id?: number | string;
|
id?: number | string;
|
||||||
stepName: string;
|
stepName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AssigneeTarget {
|
||||||
|
includeChildren: boolean;
|
||||||
|
targetCode?: string;
|
||||||
|
targetId: number | string;
|
||||||
|
targetName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface FlowFormModel {
|
interface FlowFormModel {
|
||||||
actionType: ActionType;
|
actionType: ActionType;
|
||||||
id?: number | string;
|
id?: number | string;
|
||||||
@@ -94,14 +102,16 @@ const SCOPE_TYPE_OPTIONS = [
|
|||||||
const ASSIGNEE_TYPE_OPTIONS: Array<{ label: string; value: AssigneeType }> = [
|
const ASSIGNEE_TYPE_OPTIONS: Array<{ label: string; value: AssigneeType }> = [
|
||||||
{ label: $t('approval.assignee.role'), value: 'ROLE' },
|
{ label: $t('approval.assignee.role'), value: 'ROLE' },
|
||||||
{ label: $t('approval.assignee.user'), value: 'USER' },
|
{ label: $t('approval.assignee.user'), value: 'USER' },
|
||||||
|
{ label: $t('approval.assignee.dept'), value: 'DEPT' },
|
||||||
];
|
];
|
||||||
|
const ENABLED_DATA_STATUS = 1;
|
||||||
|
const ASSIGNEE_VISIBLE_TAG_COUNT = 2;
|
||||||
|
|
||||||
const saveForm = ref<FormInstance>();
|
const saveForm = ref<FormInstance>();
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const isAdd = ref(true);
|
const isAdd = ref(true);
|
||||||
const btnLoading = ref(false);
|
const btnLoading = ref(false);
|
||||||
const categoryLoaded = ref(false);
|
const categoryLoaded = ref(false);
|
||||||
const deptLoaded = ref(false);
|
|
||||||
const roleLoaded = ref(false);
|
const roleLoaded = ref(false);
|
||||||
const accountLoading = ref(false);
|
const accountLoading = ref(false);
|
||||||
const deptTreeOptions = ref<any[]>([]);
|
const deptTreeOptions = ref<any[]>([]);
|
||||||
@@ -112,6 +122,19 @@ const categoryOptions = ref<Record<Exclude<ResourceType, ''>, SelectOption[]>>({
|
|||||||
KNOWLEDGE: [],
|
KNOWLEDGE: [],
|
||||||
WORKFLOW: [],
|
WORKFLOW: [],
|
||||||
});
|
});
|
||||||
|
const deptOptionMap = computed(() => {
|
||||||
|
const result = new Map<string, string>();
|
||||||
|
const visit = (items: any[]) => {
|
||||||
|
for (const item of items) {
|
||||||
|
result.set(String(item.id), item.deptName || String(item.id));
|
||||||
|
if (Array.isArray(item.children)) {
|
||||||
|
visit(item.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(deptTreeOptions.value);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
const formModel = ref<FlowFormModel>(buildDefaultForm());
|
const formModel = ref<FlowFormModel>(buildDefaultForm());
|
||||||
|
|
||||||
@@ -158,7 +181,17 @@ function buildDefaultForm(): FlowFormModel {
|
|||||||
resourceType: 'BOT',
|
resourceType: 'BOT',
|
||||||
scopes: [],
|
scopes: [],
|
||||||
status: 'ENABLED',
|
status: 'ENABLED',
|
||||||
steps: [{ assigneeType: 'ROLE', stepName: '' }],
|
steps: [buildDefaultStep()],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDefaultStep(): StepItem {
|
||||||
|
return {
|
||||||
|
assigneeTargetIds: [],
|
||||||
|
assigneeTargets: [],
|
||||||
|
assigneeType: 'ROLE',
|
||||||
|
includeChildrenByTarget: {},
|
||||||
|
stepName: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,17 +233,20 @@ async function openDialog(row: any = {}) {
|
|||||||
scopeValue: item.scopeValue,
|
scopeValue: item.scopeValue,
|
||||||
})),
|
})),
|
||||||
status: res.data?.status || 'ENABLED',
|
status: res.data?.status || 'ENABLED',
|
||||||
steps: (res.data?.steps || []).map((item: any) => ({
|
steps: (res.data?.steps || []).map((item: any) => {
|
||||||
assigneeTargetCode: item.assigneeTargetCode,
|
const targets = normalizeAssigneeTargets(item);
|
||||||
assigneeTargetId: item.assigneeTargetId,
|
return {
|
||||||
assigneeTargetName: item.assigneeTargetName,
|
assigneeTargetIds: targets.map((target) => target.targetId),
|
||||||
|
assigneeTargets: targets,
|
||||||
assigneeType: item.assigneeType || 'ROLE',
|
assigneeType: item.assigneeType || 'ROLE',
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
includeChildrenByTarget: buildIncludeChildrenByTarget(targets),
|
||||||
stepName: item.stepName,
|
stepName: item.stepName,
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
if (formModel.value.steps.length === 0) {
|
if (formModel.value.steps.length === 0) {
|
||||||
formModel.value.steps = [{ assigneeType: 'ROLE', stepName: '' }];
|
formModel.value.steps = [buildDefaultStep()];
|
||||||
}
|
}
|
||||||
for (const step of formModel.value.steps) {
|
for (const step of formModel.value.steps) {
|
||||||
registerAccountOption(step);
|
registerAccountOption(step);
|
||||||
@@ -249,18 +285,15 @@ async function ensureCategoryOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureDeptOptions() {
|
async function ensureDeptOptions() {
|
||||||
if (deptLoaded.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await api.get('/api/v1/sysDept/list', {
|
const res = await api.get('/api/v1/sysDept/list', {
|
||||||
params: {
|
params: {
|
||||||
asTree: true,
|
asTree: true,
|
||||||
sortKey: 'sortNo',
|
sortKey: 'sortNo',
|
||||||
sortType: 'asc',
|
sortType: 'asc',
|
||||||
|
status: ENABLED_DATA_STATUS,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
deptTreeOptions.value = Array.isArray(res.data) ? res.data : [];
|
deptTreeOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||||
deptLoaded.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureRoleOptions() {
|
async function ensureRoleOptions() {
|
||||||
@@ -310,6 +343,41 @@ function normalizeSelectOptions(data: any[] = []) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAssigneeTargets(item: any): AssigneeTarget[] {
|
||||||
|
const targets = Array.isArray(item?.assigneeTargets)
|
||||||
|
? item.assigneeTargets
|
||||||
|
: [];
|
||||||
|
if (targets.length > 0) {
|
||||||
|
return targets
|
||||||
|
.filter((target: any) => target?.targetId)
|
||||||
|
.map((target: any) => ({
|
||||||
|
includeChildren: target.includeChildren === 1,
|
||||||
|
targetCode: target.targetCode,
|
||||||
|
targetId: target.targetId,
|
||||||
|
targetName: target.targetName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (!item?.assigneeTargetId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
includeChildren: false,
|
||||||
|
targetCode: item.assigneeTargetCode,
|
||||||
|
targetId: item.assigneeTargetId,
|
||||||
|
targetName: item.assigneeTargetName,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIncludeChildrenByTarget(targets: AssigneeTarget[]) {
|
||||||
|
const result: Record<string, boolean> = {};
|
||||||
|
for (const target of targets) {
|
||||||
|
result[String(target.targetId)] = target.includeChildren;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function addScope() {
|
function addScope() {
|
||||||
formModel.value.scopes.push({
|
formModel.value.scopes.push({
|
||||||
includeChildren: false,
|
includeChildren: false,
|
||||||
@@ -323,7 +391,7 @@ function removeScope(index: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addStep() {
|
function addStep() {
|
||||||
formModel.value.steps.push({ assigneeType: 'ROLE', stepName: '' });
|
formModel.value.steps.push(buildDefaultStep());
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeStep(index: number) {
|
function removeStep(index: number) {
|
||||||
@@ -341,51 +409,110 @@ function getCategoryScopeOptions() {
|
|||||||
return categoryOptions.value[resourceType];
|
return categoryOptions.value[resourceType];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAssigneeTypeChange(step: StepItem) {
|
async function handleAssigneeTypeChange(
|
||||||
step.assigneeTargetId = undefined;
|
step: StepItem,
|
||||||
step.assigneeTargetCode = '';
|
assigneeType: AssigneeType,
|
||||||
step.assigneeTargetName = '';
|
) {
|
||||||
if (step.assigneeType === 'USER') {
|
if (step.assigneeType === assigneeType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (step.assigneeTargetIds.length > 0) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
$t('approval.message.confirmAssigneeTypeChange'),
|
||||||
|
$t('approval.fields.assigneeTarget'),
|
||||||
|
{
|
||||||
|
cancelButtonText: $t('button.cancel'),
|
||||||
|
confirmButtonText: $t('button.confirm'),
|
||||||
|
type: 'warning',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error === 'cancel' || error === 'close') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step.assigneeType = assigneeType;
|
||||||
|
step.assigneeTargetIds = [];
|
||||||
|
step.assigneeTargets = [];
|
||||||
|
step.includeChildrenByTarget = {};
|
||||||
|
if (assigneeType === 'USER') {
|
||||||
void searchAccountOptions('');
|
void searchAccountOptions('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAssigneeTargetChange(step: StepItem) {
|
function handleAssigneeTargetChange(step: StepItem) {
|
||||||
const options =
|
const selectedIds = new Set(step.assigneeTargetIds.map(String));
|
||||||
step.assigneeType === 'ROLE' ? roleOptions.value : accountOptions.value;
|
step.assigneeTargets = step.assigneeTargets.filter((target) =>
|
||||||
const selected = options.find(
|
selectedIds.has(String(target.targetId)),
|
||||||
(item) => String(item.id) === String(step.assigneeTargetId),
|
|
||||||
);
|
);
|
||||||
step.assigneeTargetName = selected?.label || '';
|
for (const targetId of Object.keys(step.includeChildrenByTarget)) {
|
||||||
|
if (!selectedIds.has(targetId)) {
|
||||||
|
delete step.includeChildrenByTarget[targetId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const targetId of step.assigneeTargetIds) {
|
||||||
|
const targetKey = String(targetId);
|
||||||
|
if (!(targetKey in step.includeChildrenByTarget)) {
|
||||||
|
step.includeChildrenByTarget[targetKey] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAssigneeTarget(step: StepItem, targetId: number | string) {
|
||||||
|
const targetKey = String(targetId);
|
||||||
|
step.assigneeTargetIds = step.assigneeTargetIds.filter(
|
||||||
|
(item) => String(item) !== targetKey,
|
||||||
|
);
|
||||||
|
handleAssigneeTargetChange(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeptTargetName(targetId: number | string) {
|
||||||
|
return deptOptionMap.value.get(String(targetId)) || String(targetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAccountOption(step?: StepItem) {
|
function registerAccountOption(step?: StepItem) {
|
||||||
if (!step?.assigneeTargetId || !step.assigneeTargetName) {
|
if (!step || step.assigneeType !== 'USER') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
for (const target of step.assigneeTargets) {
|
||||||
if (
|
if (
|
||||||
|
!target.targetName ||
|
||||||
accountOptions.value.some(
|
accountOptions.value.some(
|
||||||
(item) => String(item.id) === String(step.assigneeTargetId),
|
(item) => String(item.id) === String(target.targetId),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
accountOptions.value.push({
|
accountOptions.value.push({
|
||||||
id: step.assigneeTargetId,
|
id: target.targetId,
|
||||||
label: step.assigneeTargetName,
|
label: target.targetName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
saveForm.value?.validate(async (valid) => {
|
if (btnLoading.value) {
|
||||||
if (!valid) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
btnLoading.value = true;
|
btnLoading.value = true;
|
||||||
try {
|
try {
|
||||||
|
const valid = await saveForm.value?.validate().catch(() => false);
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const steps = formModel.value.steps
|
const steps = formModel.value.steps
|
||||||
.map((step, index) => ({
|
.map((step, index) => ({
|
||||||
assigneeTargetId: step.assigneeTargetId,
|
assigneeTargets: step.assigneeTargetIds.map((targetId) => ({
|
||||||
|
includeChildren:
|
||||||
|
step.assigneeType === 'DEPT' &&
|
||||||
|
step.includeChildrenByTarget[String(targetId)]
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
targetId,
|
||||||
|
})),
|
||||||
assigneeType: step.assigneeType,
|
assigneeType: step.assigneeType,
|
||||||
id: step.id,
|
id: step.id,
|
||||||
stepName: step.stepName?.trim(),
|
stepName: step.stepName?.trim(),
|
||||||
@@ -394,12 +521,14 @@ async function save() {
|
|||||||
.filter((step) => step.stepName);
|
.filter((step) => step.stepName);
|
||||||
if (steps.length === 0) {
|
if (steps.length === 0) {
|
||||||
ElMessage.warning($t('approval.message.needStep'));
|
ElMessage.warning($t('approval.message.needStep'));
|
||||||
btnLoading.value = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (steps.some((step) => !step.assigneeType || !step.assigneeTargetId)) {
|
if (
|
||||||
|
steps.some(
|
||||||
|
(step) => !step.assigneeType || step.assigneeTargets.length === 0,
|
||||||
|
)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('approval.message.needStepAssignee'));
|
ElMessage.warning($t('approval.message.needStepAssignee'));
|
||||||
btnLoading.value = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +558,6 @@ async function save() {
|
|||||||
: '/api/v1/approvalFlow/update';
|
: '/api/v1/approvalFlow/update';
|
||||||
const res = await api.post(url, payload);
|
const res = await api.post(url, payload);
|
||||||
if (res.errorCode !== 0) {
|
if (res.errorCode !== 0) {
|
||||||
btnLoading.value = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ElMessage.success($t('approval.message.saveSuccess'));
|
ElMessage.success($t('approval.message.saveSuccess'));
|
||||||
@@ -438,7 +566,6 @@ async function save() {
|
|||||||
} finally {
|
} finally {
|
||||||
btnLoading.value = false;
|
btnLoading.value = false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
@@ -600,17 +727,36 @@ function closeDialog() {
|
|||||||
<div
|
<div
|
||||||
v-for="(step, index) in formModel.steps"
|
v-for="(step, index) in formModel.steps"
|
||||||
:key="`step-${index}`"
|
:key="`step-${index}`"
|
||||||
class="grid grid-cols-1 gap-3 rounded-2xl border border-[hsl(var(--divider-faint)/0.66)] bg-[hsl(var(--surface-panel)/0.72)] p-4 md:grid-cols-[72px,minmax(0,1.1fr),148px,minmax(0,1fr),88px]"
|
class="approval-flow-modal__step-card"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex items-center text-sm font-medium text-[hsl(var(--text-muted))]"
|
|
||||||
>
|
>
|
||||||
|
<div class="approval-flow-modal__step-header">
|
||||||
|
<span class="approval-flow-modal__step-index">
|
||||||
{{ $t('approval.fields.stepNo', { value: index + 1 }) }}
|
{{ $t('approval.fields.stepNo', { value: index + 1 }) }}
|
||||||
|
</span>
|
||||||
|
<ElButton
|
||||||
|
text
|
||||||
|
type="danger"
|
||||||
|
:disabled="formModel.steps.length <= 1"
|
||||||
|
@click="removeStep(index)"
|
||||||
|
>
|
||||||
|
{{ $t('button.delete') }}
|
||||||
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="approval-flow-modal__step-grid">
|
||||||
|
<div class="approval-flow-modal__step-field">
|
||||||
|
<span class="approval-flow-modal__step-field-label">
|
||||||
|
{{ $t('approval.fields.stepName') }}
|
||||||
|
</span>
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model.trim="step.stepName"
|
v-model.trim="step.stepName"
|
||||||
:placeholder="$t('approval.placeholder.stepName')"
|
:placeholder="$t('approval.placeholder.stepName')"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="approval-flow-modal__step-field">
|
||||||
|
<span class="approval-flow-modal__step-field-label">
|
||||||
|
{{ $t('approval.fields.assigneeType') }}
|
||||||
|
</span>
|
||||||
<div class="approval-flow-modal__assignee-type">
|
<div class="approval-flow-modal__assignee-type">
|
||||||
<button
|
<button
|
||||||
v-for="item in ASSIGNEE_TYPE_OPTIONS"
|
v-for="item in ASSIGNEE_TYPE_OPTIONS"
|
||||||
@@ -621,20 +767,44 @@ function closeDialog() {
|
|||||||
'is-active': step.assigneeType === item.value,
|
'is-active': step.assigneeType === item.value,
|
||||||
}"
|
}"
|
||||||
:aria-pressed="step.assigneeType === item.value"
|
:aria-pressed="step.assigneeType === item.value"
|
||||||
@click="
|
@click="handleAssigneeTypeChange(step, item.value)"
|
||||||
step.assigneeType !== item.value &&
|
|
||||||
((step.assigneeType = item.value),
|
|
||||||
handleAssigneeTypeChange(step))
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="approval-flow-modal__step-field approval-flow-modal__target-field"
|
||||||
|
>
|
||||||
|
<div class="approval-flow-modal__target-heading">
|
||||||
|
<span class="approval-flow-modal__step-field-label">
|
||||||
|
{{ $t('approval.fields.assigneeTarget') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
step.assigneeType === 'DEPT' &&
|
||||||
|
step.assigneeTargetIds.length > 0
|
||||||
|
"
|
||||||
|
class="approval-flow-modal__target-count"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t('approval.fields.selectedDeptCount', {
|
||||||
|
value: step.assigneeTargetIds.length,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-if="step.assigneeType === 'ROLE'"
|
v-if="step.assigneeType === 'ROLE'"
|
||||||
v-model="step.assigneeTargetId"
|
v-model="step.assigneeTargetIds"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
|
multiple
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
|
:max-collapse-tags="ASSIGNEE_VISIBLE_TAG_COUNT"
|
||||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||||
@change="handleAssigneeTargetChange(step)"
|
@change="handleAssigneeTargetChange(step)"
|
||||||
>
|
>
|
||||||
@@ -645,18 +815,25 @@ function closeDialog() {
|
|||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
|
|
||||||
<ElSelect
|
<ElSelect
|
||||||
v-else
|
v-else-if="step.assigneeType === 'USER'"
|
||||||
v-model="step.assigneeTargetId"
|
v-model="step.assigneeTargetIds"
|
||||||
filterable
|
filterable
|
||||||
clearable
|
clearable
|
||||||
|
multiple
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
|
:max-collapse-tags="ASSIGNEE_VISIBLE_TAG_COUNT"
|
||||||
remote
|
remote
|
||||||
reserve-keyword
|
reserve-keyword
|
||||||
:loading="accountLoading"
|
:loading="accountLoading"
|
||||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||||
:remote-method="searchAccountOptions"
|
:remote-method="searchAccountOptions"
|
||||||
@change="handleAssigneeTargetChange(step)"
|
@change="handleAssigneeTargetChange(step)"
|
||||||
@visible-change="(visible) => visible && searchAccountOptions('')"
|
@visible-change="
|
||||||
|
(visible) => visible && searchAccountOptions('')
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<ElOption
|
<ElOption
|
||||||
v-for="item in accountOptions"
|
v-for="item in accountOptions"
|
||||||
@@ -665,19 +842,70 @@ function closeDialog() {
|
|||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
<ElButton
|
|
||||||
text
|
<div v-else class="approval-flow-modal__dept-target">
|
||||||
type="danger"
|
<ElTreeSelect
|
||||||
:disabled="formModel.steps.length <= 1"
|
v-model="step.assigneeTargetIds"
|
||||||
@click="removeStep(index)"
|
clearable
|
||||||
|
multiple
|
||||||
|
collapse-tags
|
||||||
|
collapse-tags-tooltip
|
||||||
|
:max-collapse-tags="ASSIGNEE_VISIBLE_TAG_COUNT"
|
||||||
|
check-strictly
|
||||||
|
:data="deptTreeOptions"
|
||||||
|
:props="{
|
||||||
|
label: 'deptName',
|
||||||
|
children: 'children',
|
||||||
|
value: 'id',
|
||||||
|
}"
|
||||||
|
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||||
|
@change="handleAssigneeTargetChange(step)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="step.assigneeTargetIds.length > 0"
|
||||||
|
class="approval-flow-modal__dept-target-list"
|
||||||
>
|
>
|
||||||
{{ $t('button.delete') }}
|
<div
|
||||||
|
v-for="targetId in step.assigneeTargetIds"
|
||||||
|
:key="String(targetId)"
|
||||||
|
class="approval-flow-modal__dept-target-option"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="approval-flow-modal__dept-target-name"
|
||||||
|
:title="getDeptTargetName(targetId)"
|
||||||
|
>
|
||||||
|
{{ getDeptTargetName(targetId) }}
|
||||||
|
</span>
|
||||||
|
<div class="approval-flow-modal__dept-target-actions">
|
||||||
|
<label>
|
||||||
|
{{ $t('approval.fields.includeChildren') }}
|
||||||
|
<ElSwitch
|
||||||
|
v-model="
|
||||||
|
step.includeChildrenByTarget[String(targetId)]
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="removeAssigneeTarget(step, targetId)"
|
||||||
|
>
|
||||||
|
{{ $t('approval.action.removeTarget') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ElButton text type="primary" class="w-fit" @click="addStep">
|
<ElButton text type="primary" class="w-fit" @click="addStep">
|
||||||
{{ $t('approval.action.addStep') }}
|
{{ $t('approval.action.addStep') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<p class="approval-flow-modal__assignee-helper">
|
||||||
|
{{ $t('approval.helper.assigneeAny') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</EasyFlowFormModal>
|
</EasyFlowFormModal>
|
||||||
@@ -692,16 +920,16 @@ function closeDialog() {
|
|||||||
|
|
||||||
.approval-flow-modal__section-meta {
|
.approval-flow-modal__section-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__section-meta p {
|
.approval-flow-modal__section-meta p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: hsl(var(--text-muted));
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__scope-list {
|
.approval-flow-modal__scope-list {
|
||||||
@@ -712,8 +940,8 @@ function closeDialog() {
|
|||||||
|
|
||||||
.approval-flow-modal__scope-empty {
|
.approval-flow-modal__scope-empty {
|
||||||
padding: 4px 0 2px;
|
padding: 4px 0 2px;
|
||||||
color: hsl(var(--text-muted));
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__scope-row {
|
.approval-flow-modal__scope-row {
|
||||||
@@ -724,7 +952,7 @@ function closeDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__scope-row + .approval-flow-modal__scope-row {
|
.approval-flow-modal__scope-row + .approval-flow-modal__scope-row {
|
||||||
border-top: 1px solid hsl(var(--divider-faint) / 0.72);
|
border-top: 1px solid hsl(var(--divider-faint) / 72%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__scope-control {
|
.approval-flow-modal__scope-control {
|
||||||
@@ -733,40 +961,90 @@ function closeDialog() {
|
|||||||
|
|
||||||
.approval-flow-modal__scope-switch {
|
.approval-flow-modal__scope-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: hsl(var(--text-muted));
|
align-items: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__step-card {
|
||||||
|
padding: var(--space-4);
|
||||||
|
background: hsl(var(--surface-panel) / 78%);
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 72%);
|
||||||
|
border-radius: var(--radius-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__step-header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 32px;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__step-index {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
background: hsl(var(--primary) / 9%);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__step-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: var(--space-4);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__step-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__step-field-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__assignee-type {
|
.approval-flow-modal__assignee-type {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
width: fit-content;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border: 1px solid hsl(var(--divider-faint) / 0.72);
|
background: hsl(var(--surface-contrast-soft) / 82%);
|
||||||
border-radius: 14px;
|
border: 1px solid hsl(var(--divider-faint) / 72%);
|
||||||
background: hsl(var(--surface-contrast-soft) / 0.82);
|
border-radius: var(--radius-toolbar);
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__assignee-type-item {
|
.approval-flow-modal__assignee-type-item {
|
||||||
min-width: 64px;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 14px;
|
padding: 0 var(--space-3);
|
||||||
border: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: transparent;
|
|
||||||
color: hsl(var(--text-muted));
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-control);
|
||||||
transition:
|
transition:
|
||||||
background-color 0.18s ease,
|
background-color 0.18s ease,
|
||||||
color 0.18s ease,
|
color 0.18s ease,
|
||||||
box-shadow 0.18s ease;
|
box-shadow 0.18s ease;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__assignee-type-item:hover {
|
.approval-flow-modal__assignee-type-item:hover {
|
||||||
@@ -774,14 +1052,86 @@ function closeDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__assignee-type-item:focus-visible {
|
.approval-flow-modal__assignee-type-item:focus-visible {
|
||||||
outline: 2px solid hsl(var(--primary) / 0.28);
|
outline: 2px solid hsl(var(--primary) / 28%);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-flow-modal__assignee-type-item.is-active {
|
.approval-flow-modal__assignee-type-item.is-active {
|
||||||
color: hsl(var(--primary));
|
color: hsl(var(--primary));
|
||||||
background: hsl(var(--primary) / 0.12);
|
background: hsl(var(--primary) / 12%);
|
||||||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.14);
|
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 14%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__target-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__target-count {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target-list {
|
||||||
|
max-height: 176px;
|
||||||
|
padding: 0 var(--space-2);
|
||||||
|
overflow-y: auto;
|
||||||
|
background: hsl(var(--surface-subtle));
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 72%);
|
||||||
|
border-radius: var(--radius-toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target-option {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target-option
|
||||||
|
+ .approval-flow-modal__dept-target-option {
|
||||||
|
border-top: 1px solid hsl(var(--divider-faint) / 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target-name {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: none;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target-actions label {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__assignee-helper {
|
||||||
|
margin: -4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -789,5 +1139,35 @@ function closeDialog() {
|
|||||||
grid-template-columns: 148px minmax(0, 1fr) 128px 72px;
|
grid-template-columns: 148px minmax(0, 1fr) 128px 72px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__step-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(216px, 0.8fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__target-field {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.approval-flow-modal__step-grid {
|
||||||
|
grid-template-columns:
|
||||||
|
minmax(180px, 0.85fr) minmax(216px, 0.72fr)
|
||||||
|
minmax(280px, 1.35fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__target-field {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.approval-flow-modal__dept-target-option {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-flow-modal__dept-target-actions {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { Delete, MoreFilled, Plus } from '@element-plus/icons-vue';
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElDropdown,
|
|
||||||
ElDropdownItem,
|
|
||||||
ElDropdownMenu,
|
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
ElIcon,
|
ElIcon,
|
||||||
@@ -20,89 +17,243 @@ import {
|
|||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
|
|
||||||
import SysDeptModal from './SysDeptModal.vue';
|
import SysDeptModal from './SysDeptModal.vue';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getTree();
|
void getTree();
|
||||||
initDict();
|
initDict();
|
||||||
});
|
});
|
||||||
|
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
|
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||||
const treeData = ref([]);
|
const treeData = ref([]);
|
||||||
|
const selectedRows = ref<any[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const batchActionType = ref<'' | 'delete' | 'disable' | 'enable'>('');
|
||||||
|
const rowDeleteLoadingId = ref('');
|
||||||
|
const rowStatusLoadingId = ref('');
|
||||||
const saveDialog = ref();
|
const saveDialog = ref();
|
||||||
const formInline = ref({
|
const formInline = ref({
|
||||||
deptName: '',
|
deptName: '',
|
||||||
|
status: '',
|
||||||
});
|
});
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
|
const selectedCount = computed(() => selectedRows.value.length);
|
||||||
|
const batchActionLoading = computed(() => batchActionType.value !== '');
|
||||||
|
const rowActionLoading = computed(
|
||||||
|
() => rowDeleteLoadingId.value !== '' || rowStatusLoadingId.value !== '',
|
||||||
|
);
|
||||||
|
|
||||||
function initDict() {
|
function initDict() {
|
||||||
dictStore.fetchDictionary('dataStatus');
|
dictStore.fetchDictionary('dataStatus');
|
||||||
}
|
}
|
||||||
function search(formEl: FormInstance | undefined) {
|
function search(formEl: FormInstance | undefined) {
|
||||||
formEl?.validate((valid) => {
|
formEl?.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
getTree();
|
void getTree();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function reset(formEl: FormInstance | undefined) {
|
function reset(formEl: FormInstance | undefined) {
|
||||||
formEl?.resetFields();
|
formEl?.resetFields();
|
||||||
getTree();
|
void getTree();
|
||||||
}
|
}
|
||||||
function showDialog(row: any) {
|
function showDialog(row: any) {
|
||||||
saveDialog.value.openDialog({ ...row });
|
saveDialog.value.openDialog({ ...row });
|
||||||
}
|
}
|
||||||
function remove(row: any) {
|
|
||||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
function isRootDept(row: any) {
|
||||||
|
return row?.deptCode === 'root_dept';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelectable(row: any) {
|
||||||
|
return !isRootDept(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelectionChange(rows: any[]) {
|
||||||
|
selectedRows.value = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedRows.value = [];
|
||||||
|
tableRef.value?.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedIds() {
|
||||||
|
return selectedRows.value
|
||||||
|
.map((row) => row?.id)
|
||||||
|
.filter((id) => id !== null && id !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: any) {
|
||||||
|
return error?.message || $t('sysDept.operationFailed');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmAction(message: string) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(message, $t('message.noticeTitle'), {
|
||||||
confirmButtonText: $t('message.ok'),
|
confirmButtonText: $t('message.ok'),
|
||||||
cancelButtonText: $t('message.cancel'),
|
cancelButtonText: $t('message.cancel'),
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
beforeClose: (action, instance, done) => {
|
|
||||||
if (action === 'confirm') {
|
|
||||||
instance.confirmButtonLoading = true;
|
|
||||||
api
|
|
||||||
.post('/api/v1/sysDept/remove', { id: row.id })
|
|
||||||
.then((res) => {
|
|
||||||
instance.confirmButtonLoading = false;
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
ElMessage.success(res.message);
|
|
||||||
reset(formRef.value);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
instance.confirmButtonLoading = false;
|
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeStatus(row: any) {
|
||||||
|
const status = row.status === 1 ? 0 : 1;
|
||||||
|
const action = status === 1 ? $t('sysDept.enable') : $t('sysDept.disable');
|
||||||
|
const confirmed = await confirmAction(
|
||||||
|
$t('sysDept.statusConfirm', {
|
||||||
|
action,
|
||||||
|
name: row.deptName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowStatusLoadingId.value = String(row.id);
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/v1/sysDept/changeStatusBatch', {
|
||||||
|
ids: [row.id],
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
if (res.errorCode !== 0) {
|
||||||
|
ElMessage.error(res.message || $t('sysDept.operationFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.success($t('sysDept.operationSuccess'));
|
||||||
|
await getTree();
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(getErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
rowStatusLoadingId.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeSelectedStatus(status: 0 | 1) {
|
||||||
|
const ids = getSelectedIds();
|
||||||
|
if (ids.length === 0) {
|
||||||
|
ElMessage.warning($t('sysDept.selectRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const action = status === 1 ? $t('sysDept.enable') : $t('sysDept.disable');
|
||||||
|
const confirmed = await confirmAction(
|
||||||
|
$t('sysDept.batchStatusConfirm', {
|
||||||
|
action,
|
||||||
|
count: ids.length,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
batchActionType.value = status === 1 ? 'enable' : 'disable';
|
||||||
|
try {
|
||||||
|
const res = await api.post('/api/v1/sysDept/changeStatusBatch', {
|
||||||
|
ids,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
if (res.errorCode !== 0) {
|
||||||
|
ElMessage.error(res.message || $t('sysDept.operationFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.success($t('sysDept.operationSuccess'));
|
||||||
|
await getTree();
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(getErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
batchActionType.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDelete(
|
||||||
|
url: string,
|
||||||
|
payload: Record<string, any>,
|
||||||
|
confirmMessage: string,
|
||||||
|
isBatch: boolean,
|
||||||
|
) {
|
||||||
|
const confirmed = await confirmAction(confirmMessage);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBatch) {
|
||||||
|
batchActionType.value = 'delete';
|
||||||
} else {
|
} else {
|
||||||
done();
|
rowDeleteLoadingId.value = String(payload.id);
|
||||||
}
|
}
|
||||||
},
|
try {
|
||||||
}).catch(() => {});
|
const res = await api.post(url, payload);
|
||||||
|
if (res.errorCode !== 0) {
|
||||||
|
ElMessage.error(res.message || $t('sysDept.operationFailed'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
function getTree() {
|
ElMessage.success($t('sysDept.operationSuccess'));
|
||||||
|
await getTree();
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(getErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
if (isBatch) {
|
||||||
|
batchActionType.value = '';
|
||||||
|
} else {
|
||||||
|
rowDeleteLoadingId.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(row: any) {
|
||||||
|
void submitDelete(
|
||||||
|
'/api/v1/sysDept/remove',
|
||||||
|
{ id: row.id },
|
||||||
|
$t('sysDept.deleteConfirm', { name: row.deptName }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelected() {
|
||||||
|
const ids = getSelectedIds();
|
||||||
|
if (ids.length === 0) {
|
||||||
|
ElMessage.warning($t('sysDept.selectRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void submitDelete(
|
||||||
|
'/api/v1/sysDept/removeBatch',
|
||||||
|
{ ids },
|
||||||
|
$t('sysDept.batchDeleteConfirm', { count: ids.length }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTree() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
api
|
try {
|
||||||
.get('/api/v1/sysDept/list', {
|
const res = await api.get('/api/v1/sysDept/list', {
|
||||||
params: {
|
params: {
|
||||||
asTree: true,
|
asTree: true,
|
||||||
...formInline.value,
|
...formInline.value,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
treeData.value = res.data;
|
|
||||||
});
|
});
|
||||||
|
treeData.value = res.data || [];
|
||||||
|
clearSelection();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-1.5 p-6">
|
<div class="flex h-full flex-col gap-1.5 p-6">
|
||||||
<SysDeptModal ref="saveDialog" @reload="reset" />
|
<SysDeptModal ref="saveDialog" @reload="reset" />
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<ElForm ref="formRef" :inline="true" :model="formInline">
|
<ElForm ref="formRef" :inline="true" :model="formInline" class="min-w-0">
|
||||||
<ElFormItem prop="deptName" class="!mr-3">
|
<ElFormItem prop="deptName" class="!mr-3">
|
||||||
<ElInput
|
<ElInput
|
||||||
class="search-input"
|
class="search-input"
|
||||||
@@ -110,6 +261,14 @@ function getTree() {
|
|||||||
:placeholder="$t('sysDept.deptName')"
|
:placeholder="$t('sysDept.deptName')"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="status" class="!mr-3">
|
||||||
|
<DictSelect
|
||||||
|
class="status-select"
|
||||||
|
v-model="formInline.status"
|
||||||
|
dict-code="dataStatus"
|
||||||
|
:placeholder="$t('sysDept.status')"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
<ElFormItem>
|
<ElFormItem>
|
||||||
<ElButton @click="search(formRef)" type="primary">
|
<ElButton @click="search(formRef)" type="primary">
|
||||||
{{ $t('button.query') }}
|
{{ $t('button.query') }}
|
||||||
@@ -119,9 +278,42 @@ function getTree() {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
<div class="handle-div">
|
<div class="department-actions">
|
||||||
|
<div v-if="selectedCount > 0" class="department-batch-actions">
|
||||||
<ElButton
|
<ElButton
|
||||||
v-access:code="'/api/v1/sysDept/save'"
|
v-access:code="'/api/v1/sysDept/save'"
|
||||||
|
class="department-batch-action is-enable"
|
||||||
|
:disabled="batchActionLoading || rowActionLoading"
|
||||||
|
:loading="batchActionType === 'enable'"
|
||||||
|
text
|
||||||
|
@click="changeSelectedStatus(1)"
|
||||||
|
>
|
||||||
|
{{ $t('sysDept.batchEnable') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-access:code="'/api/v1/sysDept/save'"
|
||||||
|
class="department-batch-action is-disable"
|
||||||
|
:disabled="batchActionLoading || rowActionLoading"
|
||||||
|
:loading="batchActionType === 'disable'"
|
||||||
|
text
|
||||||
|
@click="changeSelectedStatus(0)"
|
||||||
|
>
|
||||||
|
{{ $t('sysDept.batchDisable') }}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-access:code="'/api/v1/sysDept/remove'"
|
||||||
|
class="department-batch-action is-danger"
|
||||||
|
:disabled="batchActionLoading || rowActionLoading"
|
||||||
|
:loading="batchActionType === 'delete'"
|
||||||
|
text
|
||||||
|
@click="removeSelected"
|
||||||
|
>
|
||||||
|
{{ $t('sysDept.batchDelete') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElButton
|
||||||
|
v-access:code="'/api/v1/sysDept/save'"
|
||||||
|
class="department-add-action"
|
||||||
@click="showDialog({})"
|
@click="showDialog({})"
|
||||||
type="primary"
|
type="primary"
|
||||||
>
|
>
|
||||||
@@ -134,7 +326,16 @@ function getTree() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
<div class="bg-background border-border flex-1 rounded-lg border p-5">
|
||||||
<ElTable :data="treeData" row-key="id" v-loading="loading" border>
|
<ElTable
|
||||||
|
ref="tableRef"
|
||||||
|
:data="treeData"
|
||||||
|
:tree-props="{ children: 'children', checkStrictly: true }"
|
||||||
|
row-key="id"
|
||||||
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<ElTableColumn type="selection" width="48" :selectable="isSelectable" />
|
||||||
<ElTableColumn prop="deptName" :label="$t('sysDept.deptName')">
|
<ElTableColumn prop="deptName" :label="$t('sysDept.deptName')">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.deptName }}
|
{{ row.deptName }}
|
||||||
@@ -150,6 +351,11 @@ function getTree() {
|
|||||||
{{ row.sortNo }}
|
{{ row.sortNo }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
|
<ElTableColumn prop="status" :label="$t('sysDept.status')" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ dictStore.getDictLabel('dataStatus', row.status) }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="created" :label="$t('sysDept.created')">
|
<ElTableColumn prop="created" :label="$t('sysDept.created')">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ row.created }}
|
{{ row.created }}
|
||||||
@@ -160,26 +366,61 @@ function getTree() {
|
|||||||
{{ row.remark }}
|
{{ row.remark }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn :label="$t('common.handle')" width="90" align="right">
|
<ElTableColumn
|
||||||
|
:label="$t('common.handle')"
|
||||||
|
width="224"
|
||||||
|
align="right"
|
||||||
|
fixed="right"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="flex items-center gap-3">
|
<div class="department-row-actions">
|
||||||
<ElButton link type="primary" @click="showDialog(row)">
|
<ElButton
|
||||||
|
v-access:code="'/api/v1/sysDept/save'"
|
||||||
|
:disabled="
|
||||||
|
batchActionLoading ||
|
||||||
|
rowDeleteLoadingId !== '' ||
|
||||||
|
rowStatusLoadingId !== '' ||
|
||||||
|
(isRootDept(row) && row.status === 1)
|
||||||
|
"
|
||||||
|
:loading="rowStatusLoadingId === String(row.id)"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="changeStatus(row)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
row.status === 1
|
||||||
|
? $t('sysDept.disable')
|
||||||
|
: $t('sysDept.enable')
|
||||||
|
}}
|
||||||
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
|
v-access:code="'/api/v1/sysDept/save'"
|
||||||
|
:disabled="
|
||||||
|
batchActionLoading ||
|
||||||
|
rowDeleteLoadingId !== '' ||
|
||||||
|
rowStatusLoadingId !== ''
|
||||||
|
"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="showDialog(row)"
|
||||||
|
>
|
||||||
{{ $t('button.edit') }}
|
{{ $t('button.edit') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<ElButton
|
||||||
<ElDropdown>
|
v-access:code="'/api/v1/sysDept/remove'"
|
||||||
<ElButton link :icon="MoreFilled" />
|
:disabled="
|
||||||
|
batchActionLoading ||
|
||||||
<template #dropdown>
|
rowStatusLoadingId !== '' ||
|
||||||
<ElDropdownMenu>
|
rowDeleteLoadingId !== '' ||
|
||||||
<ElDropdownItem @click="remove(row)">
|
isRootDept(row)
|
||||||
<ElButton link :icon="Delete" type="danger">
|
"
|
||||||
|
:loading="rowDeleteLoadingId === String(row.id)"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="remove(row)"
|
||||||
|
>
|
||||||
{{ $t('button.delete') }}
|
{{ $t('button.delete') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</ElDropdownItem>
|
|
||||||
</ElDropdownMenu>
|
|
||||||
</template>
|
|
||||||
</ElDropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
@@ -193,4 +434,121 @@ function getTree() {
|
|||||||
width: 300px;
|
width: 300px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-select {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 3px;
|
||||||
|
background: hsl(var(--toolbar-bg));
|
||||||
|
border: 1px solid hsl(var(--toolbar-border));
|
||||||
|
border-radius: var(--radius-toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-actions :deep(.department-add-action) {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions :deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions :deep(.department-batch-action) {
|
||||||
|
height: 30px;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
transition:
|
||||||
|
color var(--motion-duration-base) var(--motion-ease-standard),
|
||||||
|
background-color var(--motion-duration-base) var(--motion-ease-standard),
|
||||||
|
box-shadow var(--motion-duration-base) var(--motion-ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions :deep(.department-batch-action.is-enable) {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions :deep(.department-batch-action.is-disable) {
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions :deep(.department-batch-action.is-danger) {
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions
|
||||||
|
:deep(.department-batch-action.is-enable:not(.is-disabled):hover),
|
||||||
|
.department-batch-actions
|
||||||
|
:deep(.department-batch-action.is-enable:not(.is-disabled):focus-visible) {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
background: hsl(var(--primary) / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions
|
||||||
|
:deep(.department-batch-action.is-disable:not(.is-disabled):hover),
|
||||||
|
.department-batch-actions
|
||||||
|
:deep(.department-batch-action.is-disable:not(.is-disabled):focus-visible) {
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
background: hsl(var(--surface-contrast-soft));
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions
|
||||||
|
:deep(.department-batch-action.is-danger:not(.is-disabled):hover),
|
||||||
|
.department-batch-actions
|
||||||
|
:deep(.department-batch-action.is-danger:not(.is-disabled):focus-visible) {
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
background: hsl(var(--destructive) / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions
|
||||||
|
:deep(.department-batch-action:not(.is-disabled):focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px hsl(var(--primary) / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-row-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.department-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.department-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-batch-actions :deep(.department-batch-action) {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
@@ -12,24 +12,15 @@ import DictSelect from '#/components/dict/DictSelect.vue';
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
const emit = defineEmits(['reload']);
|
||||||
// vue
|
|
||||||
onMounted(() => {});
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDialog,
|
openDialog,
|
||||||
});
|
});
|
||||||
const saveForm = ref<FormInstance>();
|
const saveForm = ref<FormInstance>();
|
||||||
// variables
|
// variables
|
||||||
|
const ENABLED_STATUS = 1;
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const isAdd = ref(true);
|
const isAdd = ref(true);
|
||||||
const entity = ref<any>({
|
const entity = ref<any>(buildDefaultEntity());
|
||||||
parentId: '',
|
|
||||||
ancestors: '',
|
|
||||||
deptName: '',
|
|
||||||
deptCode: '',
|
|
||||||
sortNo: '',
|
|
||||||
status: '',
|
|
||||||
remark: '',
|
|
||||||
});
|
|
||||||
const btnLoading = ref(false);
|
const btnLoading = ref(false);
|
||||||
const rules = ref({
|
const rules = ref({
|
||||||
parentId: [
|
parentId: [
|
||||||
@@ -43,40 +34,52 @@ const rules = ref({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
// functions
|
// functions
|
||||||
function openDialog(row: any) {
|
function buildDefaultEntity() {
|
||||||
if (row.id) {
|
return {
|
||||||
isAdd.value = false;
|
ancestors: '',
|
||||||
|
deptCode: '',
|
||||||
|
deptName: '',
|
||||||
|
parentId: '',
|
||||||
|
remark: '',
|
||||||
|
sortNo: '',
|
||||||
|
status: ENABLED_STATUS,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
entity.value = row;
|
function openDialog(row: any = {}) {
|
||||||
|
isAdd.value = !row.id;
|
||||||
|
entity.value = {
|
||||||
|
...buildDefaultEntity(),
|
||||||
|
...row,
|
||||||
|
};
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
function save() {
|
async function save() {
|
||||||
saveForm.value?.validate((valid) => {
|
if (btnLoading.value) {
|
||||||
if (valid) {
|
return;
|
||||||
|
}
|
||||||
btnLoading.value = true;
|
btnLoading.value = true;
|
||||||
api
|
try {
|
||||||
.post(
|
const valid = await saveForm.value?.validate().catch(() => false);
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await api.post(
|
||||||
isAdd.value ? 'api/v1/sysDept/save' : 'api/v1/sysDept/update',
|
isAdd.value ? 'api/v1/sysDept/save' : 'api/v1/sysDept/update',
|
||||||
entity.value,
|
entity.value,
|
||||||
)
|
);
|
||||||
.then((res) => {
|
|
||||||
btnLoading.value = false;
|
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message);
|
ElMessage.success(res.message);
|
||||||
emit('reload');
|
emit('reload');
|
||||||
closeDialog();
|
closeDialog();
|
||||||
}
|
}
|
||||||
})
|
} finally {
|
||||||
.catch(() => {
|
|
||||||
btnLoading.value = false;
|
btnLoading.value = false;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
saveForm.value?.resetFields();
|
saveForm.value?.resetFields();
|
||||||
isAdd.value = true;
|
isAdd.value = true;
|
||||||
entity.value = {};
|
entity.value = buildDefaultEntity();
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -120,6 +123,13 @@ function closeDialog() {
|
|||||||
<ElFormItem prop="sortNo" :label="$t('sysDept.sortNo')">
|
<ElFormItem prop="sortNo" :label="$t('sysDept.sortNo')">
|
||||||
<ElInput v-model.trim="entity.sortNo" />
|
<ElInput v-model.trim="entity.sortNo" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="status" :label="$t('sysDept.status')">
|
||||||
|
<DictSelect
|
||||||
|
v-model="entity.status"
|
||||||
|
:clearable="false"
|
||||||
|
dict-code="dataStatus"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
<ElFormItem prop="remark" :label="$t('sysDept.remark')">
|
<ElFormItem prop="remark" :label="$t('sysDept.remark')">
|
||||||
<ElInput v-model.trim="entity.remark" />
|
<ElInput v-model.trim="entity.remark" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user