feat(dept): 科室管理——flw_dept/flw_dept_member 组+成员模型取代单用户映射行(议题3,拍板D1选B)
build / build-push (push) Successful in 54s

- deptKey=本地自增 id;source_group_id 幂等(一键拉取=初始化/刷新语义,不重复建组);不做存量迁移
- 四处读源全部切新表:AssigneeResolver(宽容口径:deptKey 不存在/停用返回空,图示/轨迹不炸)、
  enabledDeptKeys 发布校验白名单、DeptLabelSupport 唯一出口(图示科室视图+轨迹富化)、编辑器 deptList 聚合
- /admin/dept/* 全套端点(CRUD/成员/拉取预览导入);旧 /map/* 映射 CRUD 端点删除
  (保留 /map/lookupUser 选人检索与 /user/teacherTree);flw_assignee_map 实体/表保留不删
- IT 种子切源(FlowTestSupport tag→deptKey 注册表,用例主体零改动);全量 191 绿;E2E 22/22
  (local-debug/four-topics-e-e2e.py,含切源后果验证与生产盘点 SQL)
This commit is contained in:
chenxing
2026-10-10 19:47:58 +08:00
parent 4293c9628a
commit a06b610d9d
18 changed files with 865 additions and 159 deletions
@@ -51,6 +51,7 @@ public class FlowAdminController {
private final com.yunhai.flowcenter.service.DiagramImageService imageService; private final com.yunhai.flowcenter.service.DiagramImageService imageService;
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper; private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
private final com.yunhai.flowcenter.service.FlowAppAdminService appAdminService; private final com.yunhai.flowcenter.service.FlowAppAdminService appAdminService;
private final com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService;
// ==================== 流程定义 ==================== // ==================== 流程定义 ====================
@@ -161,35 +162,91 @@ public class FlowAdminController {
.body(imageService.renderPng(diagramService.build(def, null, mode))); .body(imageService.renderPng(diagramService.build(def, null, mode)));
} }
// ==================== 办理人映射 ==================== // ==================== 科室管理(2026-10-10 四议题·议题3,取代 /map/* 映射 CRUD) ====================
@GetMapping("/map/list") /** 科室列表(编辑器 ctx.depts 同结构:deptKey=id 字符串 + 启用成员数) */
public List<FlwAssigneeMap> mapList(BoaUser user, @GetMapping("/dept/list")
@RequestParam(value = "deptKey", required = false) String deptKey) { public List<com.yunhai.flowcenter.service.FlowDeptAdminService.DeptVo> deptList(BoaUser user) {
return mapAdminService.listByDept(adminGuard.requireAdmin(user, null), deptKey); return deptAdminService.list(adminGuard.requireAdmin(user, null));
} }
@PostMapping("/map/save") @PostMapping("/dept/create")
public FlwAssigneeMap mapSave(BoaUser user, @RequestBody Map<String, Object> body) { public com.yunhai.flowcenter.entity.FlwDept deptCreate(BoaUser user, @RequestBody Map<String, Object> body) {
return deptAdminService.create(adminGuard.requireAdmin(user, null), (String) body.get("deptName"));
}
@PostMapping("/dept/rename")
public com.yunhai.flowcenter.entity.FlwDept deptRename(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null); Long schId = adminGuard.requireAdmin(user, null);
return mapAdminService.save(schId, return deptAdminService.rename(schId, longVal(body.get("id")), (String) body.get("deptName"));
(String) body.get("deptKey"), (String) body.get("deptName"),
body.get("userId") == null ? null : Integer.valueOf(String.valueOf(body.get("userId"))),
(String) body.get("userName"));
} }
@PostMapping("/map/disable/{id}") /** 停用/启用科室(停用=解析不出办理人、发布校验拦新引用;成员不动) */
public void mapDisable(BoaUser user, @PathVariable("id") Long id) { @PostMapping("/dept/status")
mapAdminService.disable(adminGuard.requireAdmin(user, null), id); public void deptStatus(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
deptAdminService.setStatus(schId, longVal(body.get("id")), boolVal(body.get("enabled")));
} }
/** 启用映射(停用行的恢复入口;DEPT_KEY 解析只取启用行) */ /** 删除科室(级联删成员;被流程引用的 deptKey 将解析失败——发布校验/办理时会拦) */
@PostMapping("/map/enable/{id}") @PostMapping("/dept/delete")
public FlwAssigneeMap mapEnable(BoaUser user, @PathVariable("id") Long id) { public void deptDelete(BoaUser user, @RequestBody Map<String, Object> body) {
return mapAdminService.enable(adminGuard.requireAdmin(user, null), id); Long schId = adminGuard.requireAdmin(user, null);
deptAdminService.delete(schId, longVal(body.get("id")));
} }
/** 成员检索(UserBase /oapi/user/byId) */ @GetMapping("/dept/members")
public List<com.yunhai.flowcenter.entity.FlwDeptMember> deptMembers(BoaUser user,
@RequestParam("deptId") Long deptId) {
return deptAdminService.members(adminGuard.requireAdmin(user, null), deptId);
}
/** 批量加成员(已存在跳过;姓名经 UserBase 补全),返回新增数 */
@PostMapping("/dept/memberAdd")
public int deptMemberAdd(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
List<Integer> userIds = new java.util.ArrayList<>();
Object raw = body.get("userIds");
if (raw instanceof List) {
for (Object o : (List<?>) raw) {
if (o != null) {
userIds.add(Integer.valueOf(String.valueOf(o)));
}
}
}
return deptAdminService.memberAdd(schId, longVal(body.get("deptId")), userIds);
}
@PostMapping("/dept/memberRemove")
public void deptMemberRemove(BoaUser user, @RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
deptAdminService.memberRemove(schId, longVal(body.get("memberId")));
}
/**
* 一键拉取教师组·预览:学校子树内教师组容器 → 科室+组内成员(UserBase 组 id 即将落
* flw_dept.source_group_id)。导入前先展示给管理员确认,不落库。
*/
@GetMapping("/dept/groupPreview")
public List<com.yunhai.flowcenter.service.FlowMapAdminService.GroupDeptVo> groupPreview(BoaUser user) {
return mapAdminService.previewGroupDepts(adminGuard.requireAdmin(user, null));
}
/** 一键拉取教师组·导入:upsert flw_dept(source_group_id 幂等,重复拉取=刷新组名与成员集) */
@PostMapping("/dept/importGroups")
public com.yunhai.flowcenter.service.FlowDeptAdminService.GroupImportResultVo importGroups(BoaUser user) {
return deptAdminService.importGroups(adminGuard.requireAdmin(user, null));
}
private static Long longVal(Object o) {
return o == null ? null : Long.valueOf(String.valueOf(o));
}
private static boolean boolVal(Object o) {
return o == null ? false : Boolean.parseBoolean(String.valueOf(o));
}
/** 成员检索(UserBase /oapi/user/byId;编辑器选人/姓名补全共用,非映射 CRUD,保留) */
@GetMapping("/map/lookupUser") @GetMapping("/map/lookupUser")
public com.yunhai.platform.utils.model.BoaUser lookupUser(BoaUser user, public com.yunhai.platform.utils.model.BoaUser lookupUser(BoaUser user,
@RequestParam("userId") Integer userId) { @RequestParam("userId") Integer userId) {
@@ -197,21 +254,6 @@ public class FlowAdminController {
return mapAdminService.lookupUser(userId); return mapAdminService.lookupUser(userId);
} }
/**
* 一键拉取教师组·预览:学校子树内教师组容器 → 科室+组内成员(deptKey=UserBase 组 id)。
* 导入前先展示给管理员确认,不落库。
*/
@GetMapping("/map/groupPreview")
public List<com.yunhai.flowcenter.service.FlowMapAdminService.GroupDeptVo> groupPreview(BoaUser user) {
return mapAdminService.previewGroupDepts(adminGuard.requireAdmin(user, null));
}
/** 一键拉取教师组·导入:批量落映射(已存在且启用中的 sch+dept+user 跳过),返回计数 */
@PostMapping("/map/importGroups")
public com.yunhai.flowcenter.service.FlowMapAdminService.GroupImportResultVo importGroups(BoaUser user) {
return mapAdminService.importGroups(adminGuard.requireAdmin(user, null));
}
// ==================== 选人组件数据源 ==================== // ==================== 选人组件数据源 ====================
/** 选人组件树:学校组树(组→组内用户叶子),管理端「选择用户」弹窗共用 */ /** 选人组件树:学校组树(组→组内用户叶子),管理端「选择用户」弹窗共用 */
@@ -1,7 +1,7 @@
package com.yunhai.flowcenter.engine; package com.yunhai.flowcenter.engine;
import com.yunhai.flowcenter.entity.FlwAssigneeMap; import com.yunhai.flowcenter.entity.FlwDeptMember;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.flowcenter.service.FlowDeptAdminService;
import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.openapi.base.OpenApiUserClient; import com.yunhai.platform.openapi.base.OpenApiUserClient;
import com.yunhai.platform.utils.exception.ErrorCode; import com.yunhai.platform.utils.exception.ErrorCode;
@@ -14,15 +14,15 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* 办理人解析(概览 §六):DEPT_KEY 查显式映射表(零跨服务依赖);USERS 定义直配, * 办理人解析(概览 §六):DEPT_KEY 查科室管理(flw_dept+flw_dept_member,2026-10-10 切源,
* 姓名经 UserBase /oapi/user/byId 补全(失败降级空名不阻塞)。SUBMITTER_DEPT/FORM_FIELD 二期。 * 零跨服务依赖);USERS 定义直配,姓名经 UserBase /oapi/user/byId 补全(失败降级空名不阻塞)。
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class AssigneeResolver { public class AssigneeResolver {
private final FlwAssigneeMapRepository mapRepository; private final FlowDeptAdminService deptAdminService;
private final OpenApiUserClient userClient; private final OpenApiUserClient userClient;
public List<AssigneeRef> resolve(Long schId, FlowDefModel.Node node) { public List<AssigneeRef> resolve(Long schId, FlowDefModel.Node node) {
@@ -32,8 +32,7 @@ public class AssigneeResolver {
} }
List<AssigneeRef> refs = new ArrayList<>(); List<AssigneeRef> refs = new ArrayList<>();
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) { if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
List<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, a.deptKey); for (FlwDeptMember m : deptAdminService.resolveEnabled(schId, a.deptKey)) {
for (FlwAssigneeMap m : maps) {
refs.add(new AssigneeRef(m.getUserId(), m.getUserName())); refs.add(new AssigneeRef(m.getUserId(), m.getUserName()));
} }
} else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) { } else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) {
@@ -47,14 +46,14 @@ public class AssigneeResolver {
} }
if (refs.isEmpty()) { if (refs.isEmpty()) {
throw new BizException(ErrorCode.PARAM_ERROR, throw new BizException(ErrorCode.PARAM_ERROR,
"节点[" + node.id + "]无可用办理人(映射可能已被停用)"); "节点[" + node.id + "]无可用办理人(科室可能已被停用或成员为空)");
} }
return refs; return refs;
} }
/** /**
* 抄送接收人解析(NOTIFY 专用,宽容口径):结构不合法照抛(发布期校验本应拦住,属脏定义), * 抄送接收人解析(NOTIFY 专用,宽容口径):结构不合法照抛(发布期校验本应拦住,属脏定义),
* 但解析不出人(映射被停用/名单为空)只返回空列表由调用方记日志——抄送是知会不是审批, * 但解析不出人(科室被停用/名单为空)只返回空列表由调用方记日志——抄送是知会不是审批,
* 绝不能因抄送对象缺失卡住流程推进。与 {@link #resolve} 的强口径相反。 * 绝不能因抄送对象缺失卡住流程推进。与 {@link #resolve} 的强口径相反。
*/ */
public List<AssigneeRef> resolveTolerant(Long schId, FlowDefModel.Node node) { public List<AssigneeRef> resolveTolerant(Long schId, FlowDefModel.Node node) {
@@ -64,7 +63,7 @@ public class AssigneeResolver {
} }
List<AssigneeRef> refs = new ArrayList<>(); List<AssigneeRef> refs = new ArrayList<>();
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) { if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
for (FlwAssigneeMap m : mapRepository.findEnabled(schId, a.deptKey)) { for (FlwDeptMember m : deptAdminService.resolveEnabled(schId, a.deptKey)) {
refs.add(new AssigneeRef(m.getUserId(), m.getUserName())); refs.add(new AssigneeRef(m.getUserId(), m.getUserName()));
} }
} else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) { } else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) {
@@ -0,0 +1,44 @@
package com.yunhai.flowcenter.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
/**
* 科室(2026-10-10 四议题·科室管理):"组+成员"模型取代单用户映射行(flw_assignee_map)。
* <b>deptKey = 本表自增主键 id 的字符串形态</b>(拍板 D1:本地自增,不做存量迁移——旧映射的
* deptKey=UserBase 组 id,切源后解析失败属拍板接受的预期,需在科室管理重建后重新配置流程)。
* source_group_id 存 UserBase 组 id 防重复拉取重复建组(uk 对 NULL 不生效,手工建组天然多行)。
*/
@Getter
@Setter
@Entity
@Table(name = "flw_dept",
uniqueConstraints = @UniqueConstraint(name = "uk_flw_dept_sch_srcgroup", columnNames = {"sch_id", "source_group_id"}),
indexes = @Index(name = "idx_flw_dept_sch_status", columnList = "sch_id, status"))
public class FlwDept extends BaseEntity {
/** 租户键 */
@Column(name = "sch_id", nullable = false)
private Long schId;
/** 科室名 */
@Column(name = "dept_name", nullable = false, length = 128)
private String deptName;
/** 0=启用 / 1=停用(沿用 flw_assignee_map 的 status 语义;停用科室解析不出办理人) */
@Column(name = "status", nullable = false)
private Integer status = 0;
/**
* 来源 UserBase 组 id(一键拉取时落,防重复建组);手工建组为 NULL(uk 对 NULL 不生效,
* MySQL 允许多 NULL——手工多科室天然支持)
*/
@Column(name = "source_group_id")
private Integer sourceGroupId;
}
@@ -0,0 +1,38 @@
package com.yunhai.flowcenter.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
/**
* 科室成员(2026-10-10 四议题·科室管理):user_name 是拉取/添加时快照(与旧映射行同款读模型——
* 解析零跨服务调用)。status 预留停用语义(管理面只做物理增删,引擎只认 0)。
*/
@Getter
@Setter
@Entity
@Table(name = "flw_dept_member",
uniqueConstraints = @UniqueConstraint(name = "uk_flw_dept_member", columnNames = {"dept_id", "user_id"}),
indexes = @Index(name = "idx_flw_dept_member_dept", columnList = "dept_id, status"))
public class FlwDeptMember extends BaseEntity {
/** 所属科室(flw_dept.id) */
@Column(name = "dept_id", nullable = false)
private Long deptId;
@Column(name = "user_id", nullable = false)
private Integer userId;
/** 姓名快照(拉取/添加时落;解析与轨迹展示用) */
@Column(name = "user_name", nullable = false, length = 128)
private String userName;
/** 0=启用 / 1=停用 */
@Column(name = "status", nullable = false)
private Integer status = 0;
}
@@ -0,0 +1,20 @@
package com.yunhai.flowcenter.repository;
import com.yunhai.flowcenter.entity.FlwDeptMember;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface FlwDeptMemberRepository extends JpaRepository<FlwDeptMember, Long> {
List<FlwDeptMember> findByDeptIdOrderByIdAsc(Long deptId);
List<FlwDeptMember> findByDeptIdAndStatusOrderByIdAsc(Long deptId, Integer status);
Optional<FlwDeptMember> findByDeptIdAndUserId(Long deptId, Integer userId);
void deleteByDeptId(Long deptId);
long countByDeptIdAndStatus(Long deptId, Integer status);
}
@@ -0,0 +1,19 @@
package com.yunhai.flowcenter.repository;
import com.yunhai.flowcenter.entity.FlwDept;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface FlwDeptRepository extends JpaRepository<FlwDept, Long> {
List<FlwDept> findBySchIdOrderByIdAsc(Long schId);
List<FlwDept> findBySchIdAndStatusOrderByIdAsc(Long schId, Integer status);
Optional<FlwDept> findBySchIdAndId(Long schId, Long id);
/** 一键拉取幂等:同源 UserBase 组只建一行 */
Optional<FlwDept> findBySchIdAndSourceGroupId(Long schId, Integer sourceGroupId);
}
@@ -1,13 +1,9 @@
package com.yunhai.flowcenter.service; package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
import java.util.List;
/** /**
* 科室口径文案的唯一出口(2026-09-18):DEPT_KEY 节点的"科室名"取值只在 {@link #mappedDeptName} * 科室口径文案的唯一出口(2026-09-18 建,2026-10-10 切源 flw_dept):DEPT_KEY 节点的"科室名"
* 里实现一次——流程图(FlowDiagramService 的科室视图)与流程轨迹(FlowQueryService 的科室口径) * 取值只在这里实现一次——流程图(FlowDiagramService 的科室视图)与流程轨迹(FlowQueryService
* 共用,避免两边对同一份映射表给出不同名字。 * 的科室口径)共用,避免两边对同一份科室表给出不同名字。
*/ */
final class DeptLabelSupport { final class DeptLabelSupport {
@@ -15,18 +11,14 @@ final class DeptLabelSupport {
} }
/** /**
* 映射行里的科室名:取第一个非空 deptName(一个 deptKey 的表现通常一致;科室名可空, * 科室名(flw_dept.dept_name):科室不存在/已删返回 null,由调用方决定回落链
* 空则返回 null 由调用方决定回落链)。 * (科室不存在≠科室停用——停用科室仍有名字,轨迹/图示照常显示)。
*/ */
static String mappedDeptName(List<FlwAssigneeMap> maps) { static String deptName(FlowDeptAdminService deptAdminService, Long schId, String deptKey) {
if (maps == null) { try {
return deptAdminService.deptName(schId, deptKey);
} catch (Exception e) {
return null; return null;
} }
for (FlwAssigneeMap m : maps) {
if (m.getDeptName() != null && !m.getDeptName().trim().isEmpty()) {
return m.getDeptName();
}
}
return null;
} }
} }
@@ -33,10 +33,10 @@ import java.util.stream.Collectors;
public class FlowDefAdminService { public class FlowDefAdminService {
private final FlwDefRepository defRepository; private final FlwDefRepository defRepository;
private final FlwAssigneeMapRepository mapRepository;
private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository; private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository; private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository;
private final com.yunhai.flowcenter.repository.FlwAppRepository appRepository; private final com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
private final FlowDeptAdminService deptAdminService;
private final FlowEngine flowEngine; private final FlowEngine flowEngine;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -242,8 +242,8 @@ public class FlowDefAdminService {
} }
private Set<String> enabledDeptKeys(Long schId) { private Set<String> enabledDeptKeys(Long schId) {
return mapRepository.findEnabledBySchId(schId).stream() // 2026-10-10 科室管理切源:发布校验白名单改查 flw_dept/flw_dept_member(拍板 D1)
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet()); return deptAdminService.enabledDeptKeys(schId);
} }
/** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */ /** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */
@@ -0,0 +1,274 @@
package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.entity.FlwDept;
import com.yunhai.flowcenter.entity.FlwDeptMember;
import com.yunhai.flowcenter.repository.FlwDeptMemberRepository;
import com.yunhai.flowcenter.repository.FlwDeptRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* 科室管理(2026-10-10 四议题·议题3,拍板 D1 选 B):"科室组+成员"模型取代单用户映射行。
* deptKey=本地自增主键;不做存量迁移(旧 flw_assignee_map 的 deptKey=UserBase 组 id,切源后
* 解析失败属预期,需在本科室管理重建后重新配置流程定义)。一键拉取保留为初始化语义
* (source_group_id 幂等防重复建组,重复拉取=刷新组名与成员集)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class FlowDeptAdminService {
private final FlwDeptRepository deptRepository;
private final FlwDeptMemberRepository memberRepository;
/** 复用旧映射服务的组树拉取(previewGroupDepts 口径:教师组容器子组=科室+成员)与用户检索 */
private final FlowMapAdminService mapAdminService;
// ==================== 读模型(编辑器/校验共用) ====================
/** 科室列表(编辑器 ctx.depts 结构:deptKey=id 字符串;count=启用成员数,停用科室恒 0) */
public List<DeptVo> list(Long schId) {
List<DeptVo> out = new ArrayList<>();
for (FlwDept dept : deptRepository.findBySchIdOrderByIdAsc(schId)) {
DeptVo vo = new DeptVo();
vo.setDeptKey(String.valueOf(dept.getId()));
vo.setDeptName(dept.getDeptName());
vo.setStatus(dept.getStatus());
vo.setCount(dept.getStatus() == 0
? (int) memberRepository.countByDeptIdAndStatus(dept.getId(), 0) : 0);
out.add(vo);
}
return out;
}
/** 发布校验白名单:启用且有 ≥1 启用成员的科室 deptKey 全集(替代旧 enabledDeptKeys) */
public Set<String> enabledDeptKeys(Long schId) {
Set<String> keys = new HashSet<>();
for (FlwDept dept : deptRepository.findBySchIdAndStatusOrderByIdAsc(schId, 0)) {
if (memberRepository.countByDeptIdAndStatus(dept.getId(), 0) > 0) {
keys.add(String.valueOf(dept.getId()));
}
}
return keys;
}
/** 办理人解析(AssigneeResolver/图示/轨迹共用,宽容口径):deptKey 不存在或科室停用返回
* 空列表(旧 findEnabled 同语义——示意图/轨迹渲染遇已删科室不炸;强口径由调用方判空抛) */
public List<FlwDeptMember> resolveEnabled(Long schId, String deptKey) {
FlwDept dept = findDept(schId, deptKey);
if (dept == null || dept.getStatus() != 0) {
return new ArrayList<>();
}
return memberRepository.findByDeptIdAndStatusOrderByIdAsc(dept.getId(), 0);
}
/** 科室名(DeptLabelSupport 唯一出口:流程图科室视图 + 轨迹富化) */
public String deptName(Long schId, String deptKey) {
FlwDept dept = findDept(schId, deptKey);
return dept == null ? null : dept.getDeptName();
}
// ==================== 科室 CRUD ====================
@Transactional
public FlwDept create(Long schId, String deptName) {
String name = requireName(deptName);
FlwDept dept = new FlwDept();
dept.setSchId(schId);
dept.setDeptName(name);
dept.setStatus(0);
return deptRepository.save(dept);
}
@Transactional
public FlwDept rename(Long schId, Long id, String deptName) {
FlwDept dept = requireDeptById(schId, id);
dept.setDeptName(requireName(deptName));
return deptRepository.save(dept);
}
/** 停用=科室级(成员不动,恢复即回);停用科室解析不出办理人、发布校验拦新引用 */
@Transactional
public FlwDept setStatus(Long schId, Long id, boolean enabled) {
FlwDept dept = requireDeptById(schId, id);
dept.setStatus(enabled ? 0 : 1);
return deptRepository.save(dept);
}
/** 删除科室(级联物理删成员;被流程定义引用的 deptKey 将解析失败——发布校验/办理时会拦) */
@Transactional
public void delete(Long schId, Long id) {
FlwDept dept = requireDeptById(schId, id);
memberRepository.deleteByDeptId(dept.getId());
deptRepository.delete(dept);
}
// ==================== 成员维护 ====================
public List<FlwDeptMember> members(Long schId, Long deptId) {
requireDeptById(schId, deptId);
return memberRepository.findByDeptIdOrderByIdAsc(deptId);
}
/** 批量加成员(姓名经 UserBase 补全,失败降级 #id;已存在跳过) */
@Transactional
public int memberAdd(Long schId, Long deptId, List<Integer> userIds) {
FlwDept dept = requireDeptById(schId, deptId);
int added = 0;
for (Integer userId : userIds == null ? new ArrayList<Integer>() : userIds) {
if (userId == null || memberRepository.findByDeptIdAndUserId(dept.getId(), userId).isPresent()) {
continue;
}
FlwDeptMember m = new FlwDeptMember();
m.setDeptId(dept.getId());
m.setUserId(userId);
m.setUserName(safeName(userId));
m.setStatus(0);
memberRepository.save(m);
added++;
}
return added;
}
@Transactional
public void memberRemove(Long schId, Long memberId) {
FlwDeptMember m = memberRepository.findById(memberId)
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "成员行不存在:" + memberId));
requireDeptById(schId, m.getDeptId());
memberRepository.delete(m);
}
// ==================== 一键拉取(初始化语义) ====================
/**
* 拉取导入:按 {@link FlowMapAdminService#previewGroupDepts} 口径 upsert——
* source_group_id 命中则刷新组名+成员集(删除不在拉取集内的旧成员),未命中新建+灌成员。
*/
@Transactional
public GroupImportResultVo importGroups(Long schId) {
List<FlowMapAdminService.GroupDeptVo> depts = mapAdminService.previewGroupDepts(schId);
GroupImportResultVo result = new GroupImportResultVo();
for (FlowMapAdminService.GroupDeptVo dept : depts) {
Integer sourceGroupId = Integer.valueOf(dept.getDeptKey()); // 预览口径:deptKey=UserBase 组 id
FlwDept row = deptRepository.findBySchIdAndSourceGroupId(schId, sourceGroupId).orElse(null);
if (row == null) {
row = new FlwDept();
row.setSchId(schId);
row.setSourceGroupId(sourceGroupId);
row.setStatus(0);
result.setCreated(result.getCreated() + 1);
} else {
result.setRefreshed(result.getRefreshed() + 1);
}
row.setDeptName(dept.getDeptName());
row = deptRepository.save(row);
// 成员集全量对齐(uk 幂等 + 删不在集内的旧成员)
Set<Integer> pulled = new HashSet<>();
for (FlowMapAdminService.MemberVo m : dept.getUsers()) {
if (m.getUserId() == null) {
continue;
}
pulled.add(m.getUserId());
Optional<FlwDeptMember> existing = memberRepository.findByDeptIdAndUserId(row.getId(), m.getUserId());
if (existing.isPresent()) {
FlwDeptMember em = existing.get();
em.setUserName(m.getUserName() == null ? "" : m.getUserName());
em.setStatus(0);
memberRepository.save(em);
} else {
FlwDeptMember nm = new FlwDeptMember();
nm.setDeptId(row.getId());
nm.setUserId(m.getUserId());
nm.setUserName(m.getUserName() == null ? "" : m.getUserName());
nm.setStatus(0);
memberRepository.save(nm);
result.setUsers(result.getUsers() + 1);
}
}
for (FlwDeptMember old : memberRepository.findByDeptIdOrderByIdAsc(row.getId())) {
if (!pulled.contains(old.getUserId())) {
memberRepository.delete(old);
}
}
}
log.info("[flow-center] 科室一键拉取:sch={} 拉取 {} 科室(新建 {} 刷新 {}),新增成员 {}",
schId, depts.size(), result.getCreated(), result.getRefreshed(), result.getUsers());
return result;
}
// ==================== 内部 ====================
private FlwDept requireDeptById(Long schId, Long id) {
FlwDept dept = deptRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "科室不存在:" + id));
if (!dept.getSchId().equals(schId)) {
throw new BizException(ErrorCode.FORBIDDEN, "科室不属于该学校");
}
return dept;
}
private FlwDept requireDept(Long schId, String deptKey) {
FlwDept dept = findDept(schId, deptKey);
if (dept == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "科室不存在:deptKey=" + deptKey);
}
return dept;
}
private FlwDept findDept(Long schId, String deptKey) {
if (deptKey == null || deptKey.trim().isEmpty()) {
return null;
}
try {
return deptRepository.findBySchIdAndId(schId, Long.valueOf(deptKey.trim())).orElse(null);
} catch (NumberFormatException e) {
return null;
}
}
private static String requireName(String deptName) {
if (deptName == null || deptName.trim().isEmpty()) {
throw new BizException(ErrorCode.PARAM_ERROR, "科室名称不能为空");
}
return deptName.trim();
}
private String safeName(Integer userId) {
try {
return mapAdminService.lookupUser(userId).getName();
} catch (Exception e) {
return "#" + userId;
}
}
// ==================== VO ====================
/** 编辑器/管理页科室行(deptKey=自增 id 字符串,与 DeptOption 前端结构对齐) */
@Getter
@Setter
public static class DeptVo {
private String deptKey;
private String deptName;
private Integer status;
private int count;
}
@Getter
@Setter
public static class GroupImportResultVo {
private int created;
private int refreshed;
private int users;
}
}
@@ -2,7 +2,7 @@ package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.engine.FlowDefModel; import com.yunhai.flowcenter.engine.FlowDefModel;
import com.yunhai.flowcenter.engine.FlowEngine; import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwAssigneeMap; import com.yunhai.flowcenter.entity.FlwDeptMember;
import com.yunhai.flowcenter.entity.FlwCc; import com.yunhai.flowcenter.entity.FlwCc;
import com.yunhai.flowcenter.entity.FlwDef; import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance; import com.yunhai.flowcenter.entity.FlwInstance;
@@ -10,7 +10,6 @@ import com.yunhai.flowcenter.entity.FlwRouteLog;
import com.yunhai.flowcenter.entity.FlwTask; import com.yunhai.flowcenter.entity.FlwTask;
import com.yunhai.flowcenter.enums.FlwEnums; import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.model.FlowModels; import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwCcRepository; import com.yunhai.flowcenter.repository.FlwCcRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository; import com.yunhai.flowcenter.repository.FlwInstanceRepository;
@@ -53,7 +52,7 @@ public class FlowDiagramService {
private static final String USERS_HINT_DEPT_VIEW = "指定办理人"; private static final String USERS_HINT_DEPT_VIEW = "指定办理人";
private final FlwDefRepository defRepository; private final FlwDefRepository defRepository;
private final FlwAssigneeMapRepository mapRepository; private final FlowDeptAdminService deptAdminService;
private final FlwInstanceRepository instanceRepository; private final FlwInstanceRepository instanceRepository;
private final FlwTaskRepository taskRepository; private final FlwTaskRepository taskRepository;
private final FlwCcRepository ccRepository; private final FlwCcRepository ccRepository;
@@ -222,9 +221,9 @@ public class FlowDiagramService {
vo.setAssigneeType(r.type); vo.setAssigneeType(r.type);
vo.setDeptKey(r.deptKey); vo.setDeptKey(r.deptKey);
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(r.type)) { if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(r.type)) {
List<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, r.deptKey); List<FlwDeptMember> members = deptAdminService.resolveEnabled(schId, r.deptKey);
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + maps.size() + "人" vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + members.size() + "人"
: "抄送 " + namesWithSuffix(memberNames(maps), new ArrayList<>())); : "抄送 " + namesWithSuffix(memberNames(members), new ArrayList<>()));
} else if (FlowDefModel.ASSIGNEE_USERS.equals(r.type)) { } else if (FlowDefModel.ASSIGNEE_USERS.equals(r.type)) {
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送指定人员" vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送指定人员"
: "抄送 " + namesWithSuffix(userNames(r.userIds), new ArrayList<>())); : "抄送 " + namesWithSuffix(userNames(r.userIds), new ArrayList<>()));
@@ -243,14 +242,14 @@ public class FlowDiagramService {
vo.setAssigneeType(a.type); vo.setAssigneeType(a.type);
vo.setDeptKey(a.deptKey); vo.setDeptKey(a.deptKey);
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) { if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
List<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, a.deptKey); List<FlwDeptMember> members = deptAdminService.resolveEnabled(schId, a.deptKey);
if (MODE_USER.equals(mode)) { if (MODE_USER.equals(mode)) {
vo.setSubLabel(namesWithSuffix(memberNames(maps), suffixes(n))); vo.setSubLabel(namesWithSuffix(memberNames(members), suffixes(n)));
} else { } else {
// dept 视图与 auto 的 DEPT_KEY 自适应同文案:科室名 + 启用映射人数 // dept 视图与 auto 的 DEPT_KEY 自适应同文案:科室名 + 启用成员人数
vo.setLabel(deptLabel(n, a, maps)); vo.setLabel(deptLabel(n, a, schId));
List<String> parts = new ArrayList<>(); List<String> parts = new ArrayList<>();
parts.add("科室 · " + maps.size() + "人"); parts.add("科室 · " + members.size() + "人");
parts.addAll(suffixes(n)); parts.addAll(suffixes(n));
vo.setSubLabel(String.join(" · ", parts)); vo.setSubLabel(String.join(" · ", parts));
} }
@@ -264,14 +263,14 @@ public class FlowDiagramService {
return vo; return vo;
} }
/** 科室视图 label 兜底链:有映射行取 deptName(全空→deptKey);无映射行→节点名(再退 deptKey) */ /** 科室视图 label 兜底链:科室存在取 dept_name;科室不存在→节点名(再退 deptKey) */
private String deptLabel(FlowDefModel.Node n, FlowDefModel.Assignee a, List<FlwAssigneeMap> maps) { private String deptLabel(FlowDefModel.Node n, FlowDefModel.Assignee a, Long schId) {
// 科室名取值与流程轨迹科室口径共用(DeptLabelSupport),只保留本视图特有的兜底链 // 科室名取值与流程轨迹科室口径共用(DeptLabelSupport),只保留本视图特有的兜底链
String deptName = DeptLabelSupport.mappedDeptName(maps); String deptName = DeptLabelSupport.deptName(deptAdminService, schId, a.deptKey);
if (deptName != null) { if (deptName != null) {
return deptName; return deptName;
} }
return maps.isEmpty() ? firstNonBlank(n.name, a.deptKey) : firstNonBlank(a.deptKey, n.name); return firstNonBlank(n.name, a.deptKey);
} }
/** 人员名单(顿号连接)+ 会签/时限后缀(" · "分隔);名单与后缀均空则返回 null */ /** 人员名单(顿号连接)+ 会签/时限后缀(" · "分隔);名单与后缀均空则返回 null */
@@ -308,9 +307,9 @@ public class FlowDiagramService {
return parts; return parts;
} }
private List<String> memberNames(List<FlwAssigneeMap> maps) { private List<String> memberNames(List<FlwDeptMember> members) {
List<String> names = new ArrayList<>(); List<String> names = new ArrayList<>();
for (FlwAssigneeMap m : maps) { for (FlwDeptMember m : members) {
names.add(blankToId(m.getUserName(), m.getUserId())); names.add(blankToId(m.getUserName(), m.getUserId()));
} }
return names; return names;
@@ -11,7 +11,6 @@ import com.yunhai.flowcenter.entity.FlwRouteLog;
import com.yunhai.flowcenter.entity.FlwTask; import com.yunhai.flowcenter.entity.FlwTask;
import com.yunhai.flowcenter.enums.FlwEnums; import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.model.FlowModels; import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwBranchRepository; import com.yunhai.flowcenter.repository.FlwBranchRepository;
import com.yunhai.flowcenter.repository.FlwCcRepository; import com.yunhai.flowcenter.repository.FlwCcRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwDefRepository;
@@ -47,7 +46,7 @@ public class FlowQueryService {
private final FlwCcRepository ccRepository; private final FlwCcRepository ccRepository;
private final FlwRouteLogRepository routeLogRepository; private final FlwRouteLogRepository routeLogRepository;
private final FlwDefRepository defRepository; private final FlwDefRepository defRepository;
private final FlwAssigneeMapRepository mapRepository; private final FlowDeptAdminService deptAdminService;
private final FlowEngine flowEngine; private final FlowEngine flowEngine;
public BasePageResult<FlowModels.InstanceItem> pageMy(Long schId, Integer submitterId, public BasePageResult<FlowModels.InstanceItem> pageMy(Long schId, Integer submitterId,
@@ -476,7 +475,7 @@ public class FlowQueryService {
} }
row.put("deptKey", assignee.deptKey); row.put("deptKey", assignee.deptKey);
String deptName = deptNames.computeIfAbsent(assignee.deptKey, String deptName = deptNames.computeIfAbsent(assignee.deptKey,
k -> DeptLabelSupport.mappedDeptName(mapRepository.findEnabled(schId, k))); k -> DeptLabelSupport.deptName(deptAdminService, schId, k));
if (deptName != null) { if (deptName != null) {
row.put("deptName", deptName); row.put("deptName", deptName);
} }
@@ -0,0 +1,28 @@
-- 2026-10-10 四议题·科室管理:科室组+成员模型(取代 flw_assignee_map 单用户映射行)
-- ddl-auto=update 会自动建表;本脚本为生产备案。注意:deptKey=flw_dept.id 自增(拍板 D1 本地自增,
-- 不做存量迁移——旧映射 deptKey=UserBase 组 id 的流程定义需在科室管理重建后重新配置)。
CREATE TABLE IF NOT EXISTS `flw_dept` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`sch_id` BIGINT NOT NULL COMMENT '租户键',
`dept_name` VARCHAR(128) NOT NULL COMMENT '科室名',
`status` INT NOT NULL DEFAULT 0 COMMENT '0=启用 / 1=停用',
`source_group_id` INT NULL COMMENT '来源 UserBase 组 id(一键拉取幂等;手工建组 NULL,uk 对 NULL 不生效天然多行)',
`create_time` DATETIME(6) NULL,
`update_time` DATETIME(6) NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_flw_dept_sch_srcgroup` (`sch_id`, `source_group_id`),
KEY `idx_flw_dept_sch_status` (`sch_id`, `status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '科室(应用设置-科室管理)';
CREATE TABLE IF NOT EXISTS `flw_dept_member` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`dept_id` BIGINT NOT NULL COMMENT '所属科室 flw_dept.id',
`user_id` INT NOT NULL,
`user_name` VARCHAR(128) NOT NULL COMMENT '姓名快照(拉取/添加时落)',
`status` INT NOT NULL DEFAULT 0 COMMENT '0=启用 / 1=停用',
`create_time` DATETIME(6) NULL,
`update_time` DATETIME(6) NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_flw_dept_member` (`dept_id`, `user_id`),
KEY `idx_flw_dept_member_dept` (`dept_id`, `status`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '科室成员';
@@ -228,7 +228,7 @@ class FlowEngineIT extends FlowTestSupport {
assertEquals(2, trace.get(3).get("round")); assertEquals(2, trace.get(3).get("round"));
// 科室口径富化(DEPT_KEY 节点):deptName 取映射里的科室名,前端"按科室"据此换掉人名 // 科室口径富化(DEPT_KEY 节点):deptName 取映射里的科室名,前端"按科室"据此换掉人名
assertEquals("DEPT_KEY", trace.get(3).get("assigneeType")); assertEquals("DEPT_KEY", trace.get(3).get("assigneeType"));
assertEquals("ybb", trace.get(3).get("deptKey")); assertEquals(dk("ybb"), trace.get(3).get("deptKey"));
assertEquals("ybb科", trace.get(3).get("deptName")); assertEquals("ybb科", trace.get(3).get("deptName"));
// ② 已办去重:同一办件一行,结果是最后一次(同意),次数=2 // ② 已办去重:同一办件一行,结果是最后一次(同意),次数=2
@@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.FlowEngine; import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwDef; import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.enums.FlwEnums; import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwRouterRepository; import com.yunhai.flowcenter.repository.FlwRouterRepository;
import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.apistd.BizException;
@@ -45,10 +44,10 @@ class FlowDefAdminServiceSaveTest {
defRepository = mock(FlwDefRepository.class); defRepository = mock(FlwDefRepository.class);
appRepository = mock(com.yunhai.flowcenter.repository.FlwAppRepository.class); appRepository = mock(com.yunhai.flowcenter.repository.FlwAppRepository.class);
service = new FlowDefAdminService(defRepository, service = new FlowDefAdminService(defRepository,
mock(FlwAssigneeMapRepository.class),
mock(FlwRouterRepository.class), mock(FlwRouterRepository.class),
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class), mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
appRepository, appRepository,
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class),
mock(FlowEngine.class), mock(FlowEngine.class),
new ObjectMapper()); new ObjectMapper());
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.FlowEngine; import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwDef; import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.repository.FlwAppRepository; import com.yunhai.flowcenter.repository.FlwAppRepository;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwRouterRepository; import com.yunhai.flowcenter.repository.FlwRouterRepository;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -38,10 +37,10 @@ class FlowDefAdminServiceUpdateMetaTest {
defRepository = mock(FlwDefRepository.class); defRepository = mock(FlwDefRepository.class);
appRepository = mock(FlwAppRepository.class); appRepository = mock(FlwAppRepository.class);
service = new FlowDefAdminService(defRepository, service = new FlowDefAdminService(defRepository,
mock(FlwAssigneeMapRepository.class),
mock(FlwRouterRepository.class), mock(FlwRouterRepository.class),
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class), mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
appRepository, appRepository,
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class),
mock(FlowEngine.class), mock(FlowEngine.class),
new ObjectMapper()); new ObjectMapper());
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
@@ -0,0 +1,209 @@
package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.entity.FlwDept;
import com.yunhai.flowcenter.entity.FlwDeptMember;
import com.yunhai.flowcenter.repository.FlwDeptMemberRepository;
import com.yunhai.flowcenter.repository.FlwDeptRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 科室管理单测(2026-10-10 四议题·议题3,拍板 D1):CRUD 守卫、enabledDeptKeys 白名单口径、
* 解析宽容口径(deptKey 不存在/科室停用返回空,示意图/轨迹不炸)、一键拉取幂等
* (source_group_id 命中刷新组名+成员集对齐,删不在集内旧成员)。
*/
class FlowDeptAdminServiceTest {
private static final Long SCH = 990001L;
private FlwDeptRepository deptRepository;
private FlwDeptMemberRepository memberRepository;
private FlowMapAdminService mapAdminService;
private FlowDeptAdminService service;
@BeforeEach
void setUp() {
deptRepository = mock(FlwDeptRepository.class);
memberRepository = mock(FlwDeptMemberRepository.class);
mapAdminService = mock(FlowMapAdminService.class);
service = new FlowDeptAdminService(deptRepository, memberRepository, mapAdminService);
when(deptRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(memberRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
}
private FlwDept dept(long id, String name, int status, Integer sourceGroupId) {
FlwDept d = new FlwDept();
d.setId(id);
d.setSchId(SCH);
d.setDeptName(name);
d.setStatus(status);
d.setSourceGroupId(sourceGroupId);
return d;
}
private FlwDeptMember member(long deptId, int userId, String name) {
FlwDeptMember m = new FlwDeptMember();
m.setDeptId(deptId);
m.setUserId(userId);
m.setUserName(name);
m.setStatus(0);
return m;
}
private FlowMapAdminService.GroupDeptVo groupVo(int groupId, String name, FlowMapAdminService.MemberVo... users) {
FlowMapAdminService.GroupDeptVo vo = new FlowMapAdminService.GroupDeptVo();
vo.setDeptKey(String.valueOf(groupId));
vo.setDeptName(name);
vo.setUsers(Arrays.asList(users));
return vo;
}
private FlowMapAdminService.MemberVo userVo(int userId, String name) {
return new FlowMapAdminService.MemberVo(userId, name);
}
@Test
void list_启用户口count_停用科室恒0() {
FlwDept on = dept(1L, "人事处", 0, null);
FlwDept off = dept(2L, "教务处", 1, null);
when(deptRepository.findBySchIdOrderByIdAsc(SCH)).thenReturn(Arrays.asList(on, off));
when(memberRepository.countByDeptIdAndStatus(1L, 0)).thenReturn(3L);
when(memberRepository.countByDeptIdAndStatus(2L, 0)).thenReturn(5L);
List<FlowDeptAdminService.DeptVo> rows = service.list(SCH);
assertEquals(2, rows.size());
assertEquals("1", rows.get(0).getDeptKey());
assertEquals(3, rows.get(0).getCount());
assertEquals(0, rows.get(1).getCount(), "停用科室 count 恒 0(编辑器提示'无启用人员')");
}
@Test
void enabledDeptKeys_仅含有启用成员的启用科室() {
FlwDept with = dept(1L, "人事处", 0, null);
FlwDept empty = dept(2L, "教务处", 0, null);
FlwDept off = dept(3L, "保卫处", 1, null);
when(deptRepository.findBySchIdAndStatusOrderByIdAsc(SCH, 0)).thenReturn(Arrays.asList(with, empty));
when(memberRepository.countByDeptIdAndStatus(1L, 0)).thenReturn(2L);
when(memberRepository.countByDeptIdAndStatus(2L, 0)).thenReturn(0L);
assertEquals(Collections.singleton("1"), service.enabledDeptKeys(SCH));
}
@Test
void resolveEnabled_宽容口径() {
// deptKey 不存在(旧映射 deptKey=UserBase 组 id 切源后即此态)→ 空,不抛
when(deptRepository.findBySchIdAndId(SCH, 123L)).thenReturn(Optional.empty());
assertTrue(service.resolveEnabled(SCH, "123").isEmpty());
// 非数字 deptKey → 空,不抛(脏定义防御)
assertTrue(service.resolveEnabled(SCH, "abc").isEmpty());
// 科室停用 → 空
when(deptRepository.findBySchIdAndId(SCH, 2L)).thenReturn(Optional.of(dept(2L, "教务处", 1, null)));
assertTrue(service.resolveEnabled(SCH, "2").isEmpty());
// 启用科室 → 启用成员
FlwDept on = dept(1L, "人事处", 0, null);
when(deptRepository.findBySchIdAndId(SCH, 1L)).thenReturn(Optional.of(on));
when(memberRepository.findByDeptIdAndStatusOrderByIdAsc(1L, 0))
.thenReturn(Arrays.asList(member(1L, 11, "张三"), member(1L, 12, "李四")));
assertEquals(2, service.resolveEnabled(SCH, "1").size());
}
@Test
void deptName_切源唯一出口() {
when(deptRepository.findBySchIdAndId(SCH, 1L)).thenReturn(Optional.of(dept(1L, "人事处", 0, null)));
assertEquals("人事处", service.deptName(SCH, "1"));
when(deptRepository.findBySchIdAndId(SCH, 9L)).thenReturn(Optional.empty());
assertNull(service.deptName(SCH, "9"));
}
@Test
void create_name必填() {
assertEquals("新科室", service.create(SCH, " 新科室 ").getDeptName());
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> service.create(SCH, " "));
}
@Test
void delete_级联删成员_跨校拒() {
FlwDept d = dept(1L, "人事处", 0, null);
when(deptRepository.findById(1L)).thenReturn(Optional.of(d));
service.delete(SCH, 1L);
verify(memberRepository).deleteByDeptId(1L);
verify(deptRepository).delete(d);
FlwDept other = dept(2L, "x", 0, null);
other.setSchId(2L);
when(deptRepository.findById(2L)).thenReturn(Optional.of(other));
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> service.delete(SCH, 2L));
}
@Test
void memberAdd_跳过已存在_失败降级井号() {
FlwDept d = dept(1L, "人事处", 0, null);
when(deptRepository.findById(1L)).thenReturn(Optional.of(d));
when(memberRepository.findByDeptIdAndUserId(1L, 11)).thenReturn(Optional.of(member(1L, 11, "张三")));
when(mapAdminService.lookupUser(12)).thenThrow(new RuntimeException("userbase down"));
int added = service.memberAdd(SCH, 1L, Arrays.asList(11, 12));
assertEquals(1, added);
ArgumentCaptor<FlwDeptMember> cap = ArgumentCaptor.forClass(FlwDeptMember.class);
verify(memberRepository, times(1)).save(cap.capture());
assertEquals(12, cap.getValue().getUserId());
assertEquals("#12", cap.getValue().getUserName());
}
@Test
void importGroups_首拉新建_重复拉取刷新并对齐成员集() {
when(mapAdminService.previewGroupDepts(SCH)).thenReturn(Arrays.asList(
groupVo(7001, "人事处", userVo(11, "张三"), userVo(12, "李四"))));
// 首拉:无既有行 → 新建 + 灌成员
FlwDept created = dept(101L, "人事处", 0, 7001);
when(deptRepository.findBySchIdAndSourceGroupId(SCH, 7001)).thenReturn(Optional.empty());
when(deptRepository.save(any())).thenReturn(created);
when(memberRepository.findByDeptIdOrderByIdAsc(101L)).thenReturn(Collections.emptyList());
FlowDeptAdminService.GroupImportResultVo r1 = service.importGroups(SCH);
assertEquals(1, r1.getCreated());
assertEquals(2, r1.getUsers());
verify(memberRepository, times(2)).save(any(FlwDeptMember.class));
// 重复拉取:命中 source_group_id → 刷新组名;成员集对齐(11 留、12 删、13 增)
FlwDept existing = dept(101L, "人事处旧名", 0, 7001);
when(deptRepository.findBySchIdAndSourceGroupId(SCH, 7001)).thenReturn(Optional.of(existing));
when(mapAdminService.previewGroupDepts(SCH)).thenReturn(Arrays.asList(
groupVo(7001, "人事处新名", userVo(11, "张三"), userVo(13, "王五"))));
FlwDeptMember old12 = member(101L, 12, "李四");
FlwDeptMember old11 = member(101L, 11, "张三");
when(memberRepository.findByDeptIdOrderByIdAsc(101L)).thenReturn(Arrays.asList(old11, old12));
when(memberRepository.findByDeptIdAndUserId(eq(101L), anyInt())).thenReturn(Optional.empty());
when(memberRepository.findByDeptIdAndUserId(101L, 11)).thenReturn(Optional.of(old11));
FlowDeptAdminService.GroupImportResultVo r2 = service.importGroups(SCH);
assertEquals(1, r2.getRefreshed());
assertEquals(1, r2.getUsers());
assertEquals("人事处新名", existing.getDeptName());
verify(memberRepository).delete(old12);
verify(memberRepository, never()).delete(old11);
}
}
@@ -3,7 +3,7 @@ package com.yunhai.flowcenter.service;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.controller.api.FlowAdminController; import com.yunhai.flowcenter.controller.api.FlowAdminController;
import com.yunhai.flowcenter.controller.api.FlowUserController; import com.yunhai.flowcenter.controller.api.FlowUserController;
import com.yunhai.flowcenter.entity.FlwAssigneeMap; import com.yunhai.flowcenter.entity.FlwDeptMember;
import com.yunhai.flowcenter.entity.FlwDef; import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance; import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.entity.FlwTask; import com.yunhai.flowcenter.entity.FlwTask;
@@ -11,7 +11,6 @@ import com.yunhai.flowcenter.engine.AssigneeResolver;
import com.yunhai.flowcenter.engine.FlowEngine; import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.enums.FlwEnums; import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.model.FlowModels; import com.yunhai.flowcenter.model.FlowModels;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwBranchRepository; import com.yunhai.flowcenter.repository.FlwBranchRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository; import com.yunhai.flowcenter.repository.FlwInstanceRepository;
@@ -85,7 +84,7 @@ class FlowDiagramServiceTest {
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n2\"},{\"from\":\"n2\",\"to\":\"end\"}]}"; + "\"edges\":[{\"from\":\"start\",\"to\":\"n2\"},{\"from\":\"n2\",\"to\":\"end\"}]}";
private FlwDefRepository defRepository; private FlwDefRepository defRepository;
private FlwAssigneeMapRepository mapRepository; private com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService;
private FlwTaskRepository taskRepository; private FlwTaskRepository taskRepository;
private com.yunhai.flowcenter.repository.FlwCcRepository ccRepository; private com.yunhai.flowcenter.repository.FlwCcRepository ccRepository;
private OpenApiUserClient userClient; private OpenApiUserClient userClient;
@@ -94,7 +93,7 @@ class FlowDiagramServiceTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
defRepository = mock(FlwDefRepository.class); defRepository = mock(FlwDefRepository.class);
mapRepository = mock(FlwAssigneeMapRepository.class); deptAdminService = mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class);
taskRepository = mock(FlwTaskRepository.class); taskRepository = mock(FlwTaskRepository.class);
ccRepository = mock(com.yunhai.flowcenter.repository.FlwCcRepository.class); ccRepository = mock(com.yunhai.flowcenter.repository.FlwCcRepository.class);
userClient = mock(OpenApiUserClient.class); userClient = mock(OpenApiUserClient.class);
@@ -107,7 +106,7 @@ class FlowDiagramServiceTest {
taskRepository, mock(FlwBranchRepository.class), ccRepository, taskRepository, mock(FlwBranchRepository.class), ccRepository,
mock(AssigneeResolver.class), mock(com.yunhai.flowcenter.engine.ConditionRouteService.class), mock(AssigneeResolver.class), mock(com.yunhai.flowcenter.engine.ConditionRouteService.class),
mock(ApplicationEventPublisher.class)); mock(ApplicationEventPublisher.class));
service = new FlowDiagramService(defRepository, mapRepository, service = new FlowDiagramService(defRepository, deptAdminService,
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class), mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
taskRepository, ccRepository, taskRepository, ccRepository,
mock(com.yunhai.flowcenter.repository.FlwRouteLogRepository.class), mock(com.yunhai.flowcenter.repository.FlwRouteLogRepository.class),
@@ -127,11 +126,9 @@ class FlowDiagramServiceTest {
return def; return def;
} }
private FlwAssigneeMap mapRow(String deptName, int userId, String userName) { private FlwDeptMember memberRow(int userId, String userName) {
FlwAssigneeMap m = new FlwAssigneeMap(); FlwDeptMember m = new FlwDeptMember();
m.setSchId(SCH); m.setDeptId(7L);
m.setDeptKey("rs");
m.setDeptName(deptName);
m.setUserId(userId); m.setUserId(userId);
m.setUserName(userName); m.setUserName(userName);
m.setStatus(0); m.setStatus(0);
@@ -153,8 +150,9 @@ class FlowDiagramServiceTest {
@Test @Test
void c1_deptView_deptKeyNode_labelDeptName_subLabelCountAndDeadline() { void c1_deptView_deptKeyNode_labelDeptName_subLabelCountAndDeadline() {
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四"))); when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, "李四")));
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept"); FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
FlowModels.NodeVo n1 = node(vo, "n1"); FlowModels.NodeVo n1 = node(vo, "n1");
assertEquals("人事处", n1.getLabel()); assertEquals("人事处", n1.getLabel());
@@ -170,30 +168,32 @@ class FlowDiagramServiceTest {
@Test @Test
void c1_deptView_fallbackChain_deptKeyThenNodeName() { void c1_deptView_fallbackChain_deptKeyThenNodeName() {
// 映射行全无名 → deptKey 兜底 // 科室已被删(deptName=null)→ 节点名兜底(成员解析空,人数照实)
when(mapRepository.findEnabled(SCH, "rs")) when(deptAdminService.resolveEnabled(SCH, "rs"))
.thenReturn(Collections.singletonList(mapRow("", 11, "张三"))); .thenReturn(Collections.singletonList(memberRow(11, "张三")));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1"); FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
assertEquals("rs", n1.getLabel());
assertEquals("科室 · 1人 · 时限48h", n1.getSubLabel());
// 无映射行 → 节点名兜底(人数照实为 0)
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Collections.emptyList());
n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
assertEquals("人事处审批", n1.getLabel()); assertEquals("人事处审批", n1.getLabel());
assertEquals("科室 · 1人 · 时限48h", n1.getSubLabel());
// 科室存在但成员为空 → 科室名照常,人数 0
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Collections.emptyList());
n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
assertEquals("人事处", n1.getLabel());
assertEquals("科室 · 0人 · 时限48h", n1.getSubLabel()); assertEquals("科室 · 0人 · 时限48h", n1.getSubLabel());
} }
@Test @Test
void c1_anyCountersign_appendsSuffix() { void c1_anyCountersign_appendsSuffix() {
when(mapRepository.findEnabled(SCH, "rs")) when(deptAdminService.resolveEnabled(SCH, "rs"))
.thenReturn(Collections.singletonList(mapRow("人事处", 11, "张三"))); .thenReturn(Collections.singletonList(memberRow(11, "张三")));
FlowModels.NodeVo n1 = node(service.build(def(ANY_DEF_JSON), null, "dept"), "n1"); FlowModels.NodeVo n1 = node(service.build(def(ANY_DEF_JSON), null, "dept"), "n1");
assertEquals("科室 · 1人 · 任一人通过", n1.getSubLabel()); assertEquals("科室 · 1人 · 任一人通过", n1.getSubLabel());
} }
@Test @Test
void c1_fixedLabels_andEdgesCarried() { void c1_fixedLabels_andEdgesCarried() {
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList()); when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept"); FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
assertEquals("开始", node(vo, "start").getLabel()); assertEquals("开始", node(vo, "start").getLabel());
assertEquals("并行分发", node(vo, "sp1").getLabel()); assertEquals("并行分发", node(vo, "sp1").getLabel());
@@ -215,8 +215,9 @@ class FlowDiagramServiceTest {
@Test @Test
void c1_autoView_deptKeyNode_rendersDeptStyle() { void c1_autoView_deptKeyNode_rendersDeptStyle() {
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四"))); when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, "李四")));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "auto"), "n1"); FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "auto"), "n1");
assertEquals("人事处", n1.getLabel()); assertEquals("人事处", n1.getLabel());
assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel()); assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel());
@@ -226,8 +227,9 @@ class FlowDiagramServiceTest {
/** 2026-09-18:mode 缺省改为人员视图(原 auto 自适应)——老深链显式带 auto 仍按自适应出图 */ /** 2026-09-18:mode 缺省改为人员视图(原 auto 自适应)——老深链显式带 auto 仍按自适应出图 */
@Test @Test
void c1_defaultMode_isUserView() { void c1_defaultMode_isUserView() {
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四"))); when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, "李四")));
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, null); FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, null);
assertEquals("user", vo.getMode()); assertEquals("user", vo.getMode());
FlowModels.NodeVo n1 = node(vo, "n1"); FlowModels.NodeVo n1 = node(vo, "n1");
@@ -288,8 +290,9 @@ class FlowDiagramServiceTest {
@Test @Test
void c3_userView_deptKeyNode_listsMemberNames_blankNameDegradesToId() { void c3_userView_deptKeyNode_listsMemberNames_blankNameDegradesToId() {
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, null))); when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
memberRow(11, "张三"), memberRow(12, null)));
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "user"), "n1"); FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "user"), "n1");
// 人员视图:label=节点名,subLabel=映射成员名(空名降级 #id)+ 会签/时限后缀 // 人员视图:label=节点名,subLabel=映射成员名(空名降级 #id)+ 会签/时限后缀
assertEquals("人事处审批", n1.getLabel()); assertEquals("人事处审批", n1.getLabel());
@@ -304,7 +307,8 @@ class FlowDiagramServiceTest {
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc( when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
SCH, "g1", FlwEnums.DefStatus.PUBLISHED.getCode())) SCH, "g1", FlwEnums.DefStatus.PUBLISHED.getCode()))
.thenReturn(Optional.of(def)); .thenReturn(Optional.of(def));
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList()); when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.byGuid(SCH, "g1", null); FlowModels.DiagramVo vo = service.byGuid(SCH, "g1", null);
assertEquals("教师入职", vo.getName()); assertEquals("教师入职", vo.getName());
assertEquals(Integer.valueOf(2), vo.getVersion()); assertEquals(Integer.valueOf(2), vo.getVersion());
@@ -349,7 +353,8 @@ class FlowDiagramServiceTest {
instance.setSchId(SCH); instance.setSchId(SCH);
instance.setDefId(12L); instance.setDefId(12L);
when(defRepository.findById(12L)).thenReturn(Optional.of(def(DEPT_DEF_JSON))); when(defRepository.findById(12L)).thenReturn(Optional.of(def(DEPT_DEF_JSON)));
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList()); when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.byInstance(instance, "dept"); FlowModels.DiagramVo vo = service.byInstance(instance, "dept");
assertEquals(Long.valueOf(100L), vo.getInstanceId()); assertEquals(Long.valueOf(100L), vo.getInstanceId());
assertEquals(Long.valueOf(12L), vo.getDefId()); assertEquals(Long.valueOf(12L), vo.getDefId());
@@ -360,7 +365,8 @@ class FlowDiagramServiceTest {
void c6_build_draftDef_ok() { void c6_build_draftDef_ok() {
FlwDef draft = def(DEPT_DEF_JSON); FlwDef draft = def(DEPT_DEF_JSON);
draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode()); draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList()); when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
FlowModels.DiagramVo vo = service.build(draft, null, "auto"); FlowModels.DiagramVo vo = service.build(draft, null, "auto");
assertEquals(Integer.valueOf(0), vo.getStatus()); assertEquals(Integer.valueOf(0), vo.getStatus());
assertEquals(5, vo.getNodes().size()); assertEquals(5, vo.getNodes().size());
@@ -484,7 +490,8 @@ class FlowDiagramServiceTest {
mock(FlwInstanceRepository.class), diagramService, mock(FlwInstanceRepository.class), diagramService,
mock(com.yunhai.flowcenter.service.DiagramImageService.class), mock(com.yunhai.flowcenter.service.DiagramImageService.class),
new ObjectMapper(), new ObjectMapper(),
mock(com.yunhai.flowcenter.service.FlowAppAdminService.class)); mock(com.yunhai.flowcenter.service.FlowAppAdminService.class),
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class));
} }
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) { private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
@@ -1,27 +1,34 @@
package com.yunhai.flowcenter.support; package com.yunhai.flowcenter.support;
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
import com.yunhai.flowcenter.entity.FlwCallbackLog; import com.yunhai.flowcenter.entity.FlwCallbackLog;
import com.yunhai.flowcenter.entity.FlwCallbackReg; import com.yunhai.flowcenter.entity.FlwCallbackReg;
import com.yunhai.flowcenter.entity.FlwDef; import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwDept;
import com.yunhai.flowcenter.entity.FlwDeptMember;
import com.yunhai.flowcenter.entity.FlwRouteLog; import com.yunhai.flowcenter.entity.FlwRouteLog;
import com.yunhai.flowcenter.entity.FlwRouter; import com.yunhai.flowcenter.entity.FlwRouter;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwCallbackLogRepository; import com.yunhai.flowcenter.repository.FlwCallbackLogRepository;
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository; import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
import com.yunhai.flowcenter.repository.FlwCcRepository; import com.yunhai.flowcenter.repository.FlwCcRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwDeptMemberRepository;
import com.yunhai.flowcenter.repository.FlwDeptRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository; import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwRouteLogRepository; import com.yunhai.flowcenter.repository.FlwRouteLogRepository;
import com.yunhai.flowcenter.repository.FlwRouterRepository; import com.yunhai.flowcenter.repository.FlwRouterRepository;
import com.yunhai.flowcenter.repository.FlwTaskRepository; import com.yunhai.flowcenter.repository.FlwTaskRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* IT 公共基类:专用测试库 bsch_flowcenter_test,每例清表(引擎测试不能开测试事务—— * IT 公共基类:专用测试库 bsch_flowcenter_test,每例清表(引擎测试不能开测试事务——
* AFTER_COMMIT 推进监听器依赖真实提交);提供定义落库/发布、映射注册的快捷方法。 * AFTER_COMMIT 推进监听器依赖真实提交);提供定义落库/发布、科室注册的快捷方法。
* 2026-10-10 科室切源:DEPT_KEY 解析读 flw_dept/flw_dept_member,deptKey=科室自增 id——
* 用例仍以 tag("ybb"/"ka")书写,经 {@link #mapDept} 注册 tag→deptKey,def helper 输出
* JSON 时替换(dk()),用例主体零改动。
*/ */
public abstract class FlowTestSupport { public abstract class FlowTestSupport {
@@ -34,7 +41,9 @@ public abstract class FlowTestSupport {
@Autowired @Autowired
protected FlwTaskRepository taskRepository; protected FlwTaskRepository taskRepository;
@Autowired @Autowired
protected FlwAssigneeMapRepository mapRepository; protected FlwDeptRepository deptRepository;
@Autowired
protected FlwDeptMemberRepository deptMemberRepository;
@Autowired @Autowired
protected FlwCallbackRegRepository regRepository; protected FlwCallbackRegRepository regRepository;
@Autowired @Autowired
@@ -46,6 +55,14 @@ public abstract class FlowTestSupport {
@Autowired @Autowired
protected FlwRouteLogRepository routeLogRepository; protected FlwRouteLogRepository routeLogRepository;
/** tag → 实际 deptKey(flw_dept.id 字符串);mapDept 注册,def helper 消费 */
private final Map<String, String> deptKeyByTag = new HashMap<>();
/** tag 对应的实际 deptKey(未注册的 tag 原样返回——脏定义防御用例依赖此行为) */
protected String dk(String tag) {
return deptKeyByTag.getOrDefault(tag, tag);
}
protected void cleanDb() { protected void cleanDb() {
logRepository.deleteAll(); logRepository.deleteAll();
regRepository.deleteAll(); regRepository.deleteAll();
@@ -55,17 +72,38 @@ public abstract class FlowTestSupport {
taskRepository.deleteAll(); taskRepository.deleteAll();
instanceRepository.deleteAll(); instanceRepository.deleteAll();
defRepository.deleteAll(); defRepository.deleteAll();
mapRepository.deleteAll(); deptMemberRepository.deleteAll();
deptRepository.deleteAll();
deptKeyByTag.clear();
} }
protected FlwAssigneeMap mapDept(String deptKey, Integer userId, String userName) { /**
FlwAssigneeMap map = new FlwAssigneeMap(); * 注册科室成员(tag 复用同一科室行;未注册过的 tag 新建科室并登记 deptKey)。
map.setSchId(SCH); * 返回科室名(deptName=tag+"科",轨迹/图示断言用)。
map.setDeptKey(deptKey); */
map.setDeptName(deptKey + "科"); protected String mapDept(String tag, Integer userId, String userName) {
map.setUserId(userId); FlwDept dept = deptKeyByTag.containsKey(tag) ? reloadDept(tag) : createDept(tag);
map.setUserName(userName); deptKeyByTag.put(tag, String.valueOf(dept.getId()));
return mapRepository.save(map); FlwDeptMember member = new FlwDeptMember();
member.setDeptId(dept.getId());
member.setUserId(userId);
member.setUserName(userName);
member.setStatus(0);
deptMemberRepository.save(member);
return dept.getDeptName();
}
private FlwDept reloadDept(String tag) {
return deptRepository.findById(Long.valueOf(deptKeyByTag.get(tag)))
.orElseThrow(() -> new IllegalStateException("科室行丢失:" + tag));
}
private FlwDept createDept(String tag) {
FlwDept dept = new FlwDept();
dept.setSchId(SCH);
dept.setDeptName(tag + "科");
dept.setStatus(0);
return deptRepository.save(dept);
} }
protected FlwDef saveAndPublishDef(String code, String name, String contentJson) { protected FlwDef saveAndPublishDef(String code, String name, String contentJson) {
@@ -88,7 +126,7 @@ public abstract class FlowTestSupport {
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"" + nodeName + "\"," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"" + nodeName + "\","
+ "\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptKey + "\"},\"countersign\":\"ALL\"}," + "\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptKey) + "\"},\"countersign\":\"ALL\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}]," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
@@ -102,8 +140,8 @@ public abstract class FlowTestSupport {
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"}," + "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
+ "{\"id\":\"t_a\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"t_a\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"t_b\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + "{\"id\":\"t_b\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
@@ -121,7 +159,7 @@ public abstract class FlowTestSupport {
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送校领导\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301,302]}}," + "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送校领导\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301,302]}},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"cc1\"},{\"from\":\"cc1\",\"to\":\"n1\"}," + "\"edges\":[{\"from\":\"start\",\"to\":\"cc1\"},{\"from\":\"cc1\",\"to\":\"n1\"},"
@@ -136,7 +174,7 @@ public abstract class FlowTestSupport {
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"}," + "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301]}}," + "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301]}},"
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
@@ -158,8 +196,8 @@ public abstract class FlowTestSupport {
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"}," + "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"乙线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"乙线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"cond1\"}," + "\"edges\":[{\"from\":\"start\",\"to\":\"cond1\"},"
@@ -178,9 +216,9 @@ public abstract class FlowTestSupport {
+ "\"code\":\"" + codeOf() + "\",\"name\":\"主线条件测试流程\",\"start\":\"start\"," + "\"code\":\"" + codeOf() + "\",\"name\":\"主线条件测试流程\",\"start\":\"start\","
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"n0\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}}," + "{\"id\":\"n0\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
+ "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"}," + "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n0\"}," + "\"edges\":[{\"from\":\"start\",\"to\":\"n0\"},"
@@ -213,8 +251,8 @@ public abstract class FlowTestSupport {
+ "\"code\":\"" + codeOf() + "\",\"name\":\"多出口测试流程\",\"start\":\"start\"," + "\"code\":\"" + codeOf() + "\",\"name\":\"多出口测试流程\",\"start\":\"start\","
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"}," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},"
@@ -230,7 +268,7 @@ public abstract class FlowTestSupport {
+ "\"code\":\"" + codeOf() + "\",\"name\":\"夡签测试流程\",\"start\":\"start\"," + "\"code\":\"" + codeOf() + "\",\"name\":\"夡签测试流程\",\"start\":\"start\","
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"任一人审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptKey + "\"},\"countersign\":\"ANY\"}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"任一人审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptKey) + "\"},\"countersign\":\"ANY\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}]," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
@@ -248,13 +286,13 @@ public abstract class FlowTestSupport {
+ "\"code\":\"" + codeOf() + "\",\"name\":\"嵌套并行测试流程\",\"start\":\"start\"," + "\"code\":\"" + codeOf() + "\",\"name\":\"嵌套并行测试流程\",\"start\":\"start\","
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
+ "{\"id\":\"s1\",\"type\":\"PARALLEL_SPLIT\"}," + "{\"id\":\"s1\",\"type\":\"PARALLEL_SPLIT\"},"
+ "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
+ "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"}," + "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"},"
+ "{\"id\":\"tc\",\"type\":\"APPROVAL\",\"name\":\"丙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptC + "\"}}," + "{\"id\":\"tc\",\"type\":\"APPROVAL\",\"name\":\"丙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptC) + "\"}},"
+ "{\"id\":\"td\",\"type\":\"APPROVAL\",\"name\":\"丁科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptD + "\"}}," + "{\"id\":\"td\",\"type\":\"APPROVAL\",\"name\":\"丁科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptD) + "\"}},"
+ "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
@@ -277,10 +315,10 @@ public abstract class FlowTestSupport {
+ "\"code\":\"" + codeOf() + "\",\"name\":\"出口并行测试流程\",\"start\":\"start\"," + "\"code\":\"" + codeOf() + "\",\"name\":\"出口并行测试流程\",\"start\":\"start\","
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"分流审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"分流审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
+ "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"}," + "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"},"
+ "{\"id\":\"p1\",\"type\":\"APPROVAL\",\"name\":\"并行甲\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"p1\",\"type\":\"APPROVAL\",\"name\":\"并行甲\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"p2\",\"type\":\"APPROVAL\",\"name\":\"并行乙\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + "{\"id\":\"p2\",\"type\":\"APPROVAL\",\"name\":\"并行乙\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
+ "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
@@ -299,12 +337,12 @@ public abstract class FlowTestSupport {
+ "\"code\":\"" + codeOf() + "\",\"name\":\"主线并行尾验流程\",\"start\":\"start\"," + "\"code\":\"" + codeOf() + "\",\"name\":\"主线并行尾验流程\",\"start\":\"start\","
+ "\"nodes\":[" + "\"nodes\":["
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}}," + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"}," + "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
+ "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ka\"}}," + "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk("ka") + "\"}},"
+ "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"kb\"}}," + "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk("kb") + "\"}},"
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
+ "{\"id\":\"n3\",\"type\":\"APPROVAL\",\"name\":\"出口复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," + "{\"id\":\"n3\",\"type\":\"APPROVAL\",\"name\":\"出口复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
+ "{\"id\":\"end\",\"type\":\"END\"}" + "{\"id\":\"end\",\"type\":\"END\"}"
+ "]," + "],"
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"split1\"}," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"split1\"},"