diff --git a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java index 296f164..d4f79a0 100644 --- a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java +++ b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java @@ -51,6 +51,7 @@ public class FlowAdminController { private final com.yunhai.flowcenter.service.DiagramImageService imageService; private final com.fasterxml.jackson.databind.ObjectMapper objectMapper; 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))); } - // ==================== 办理人映射 ==================== + // ==================== 科室管理(2026-10-10 四议题·议题3,取代 /map/* 映射 CRUD) ==================== - @GetMapping("/map/list") - public List mapList(BoaUser user, - @RequestParam(value = "deptKey", required = false) String deptKey) { - return mapAdminService.listByDept(adminGuard.requireAdmin(user, null), deptKey); + /** 科室列表(编辑器 ctx.depts 同结构:deptKey=id 字符串 + 启用成员数) */ + @GetMapping("/dept/list") + public List deptList(BoaUser user) { + return deptAdminService.list(adminGuard.requireAdmin(user, null)); } - @PostMapping("/map/save") - public FlwAssigneeMap mapSave(BoaUser user, @RequestBody Map body) { + @PostMapping("/dept/create") + public com.yunhai.flowcenter.entity.FlwDept deptCreate(BoaUser user, @RequestBody Map 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 body) { Long schId = adminGuard.requireAdmin(user, null); - return mapAdminService.save(schId, - (String) body.get("deptKey"), (String) body.get("deptName"), - body.get("userId") == null ? null : Integer.valueOf(String.valueOf(body.get("userId"))), - (String) body.get("userName")); + return deptAdminService.rename(schId, longVal(body.get("id")), (String) body.get("deptName")); } - @PostMapping("/map/disable/{id}") - public void mapDisable(BoaUser user, @PathVariable("id") Long id) { - mapAdminService.disable(adminGuard.requireAdmin(user, null), id); + /** 停用/启用科室(停用=解析不出办理人、发布校验拦新引用;成员不动) */ + @PostMapping("/dept/status") + public void deptStatus(BoaUser user, @RequestBody Map body) { + Long schId = adminGuard.requireAdmin(user, null); + deptAdminService.setStatus(schId, longVal(body.get("id")), boolVal(body.get("enabled"))); } - /** 启用映射(停用行的恢复入口;DEPT_KEY 解析只取启用行) */ - @PostMapping("/map/enable/{id}") - public FlwAssigneeMap mapEnable(BoaUser user, @PathVariable("id") Long id) { - return mapAdminService.enable(adminGuard.requireAdmin(user, null), id); + /** 删除科室(级联删成员;被流程引用的 deptKey 将解析失败——发布校验/办理时会拦) */ + @PostMapping("/dept/delete") + public void deptDelete(BoaUser user, @RequestBody Map body) { + Long schId = adminGuard.requireAdmin(user, null); + deptAdminService.delete(schId, longVal(body.get("id"))); } - /** 成员检索(UserBase /oapi/user/byId) */ + @GetMapping("/dept/members") + public List 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 body) { + Long schId = adminGuard.requireAdmin(user, null); + List 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 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 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") public com.yunhai.platform.utils.model.BoaUser lookupUser(BoaUser user, @RequestParam("userId") Integer userId) { @@ -197,21 +254,6 @@ public class FlowAdminController { return mapAdminService.lookupUser(userId); } - /** - * 一键拉取教师组·预览:学校子树内教师组容器 → 科室+组内成员(deptKey=UserBase 组 id)。 - * 导入前先展示给管理员确认,不落库。 - */ - @GetMapping("/map/groupPreview") - public List 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)); - } - // ==================== 选人组件数据源 ==================== /** 选人组件树:学校组树(组→组内用户叶子),管理端「选择用户」弹窗共用 */ diff --git a/src/main/java/com/yunhai/flowcenter/engine/AssigneeResolver.java b/src/main/java/com/yunhai/flowcenter/engine/AssigneeResolver.java index d7e810c..09da03b 100644 --- a/src/main/java/com/yunhai/flowcenter/engine/AssigneeResolver.java +++ b/src/main/java/com/yunhai/flowcenter/engine/AssigneeResolver.java @@ -1,7 +1,7 @@ package com.yunhai.flowcenter.engine; -import com.yunhai.flowcenter.entity.FlwAssigneeMap; -import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; +import com.yunhai.flowcenter.entity.FlwDeptMember; +import com.yunhai.flowcenter.service.FlowDeptAdminService; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.openapi.base.OpenApiUserClient; import com.yunhai.platform.utils.exception.ErrorCode; @@ -14,15 +14,15 @@ import java.util.ArrayList; import java.util.List; /** - * 办理人解析(概览 §六):DEPT_KEY 查显式映射表(零跨服务依赖);USERS 定义直配, - * 姓名经 UserBase /oapi/user/byId 补全(失败降级空名不阻塞)。SUBMITTER_DEPT/FORM_FIELD 二期。 + * 办理人解析(概览 §六):DEPT_KEY 查科室管理(flw_dept+flw_dept_member,2026-10-10 切源, + * 零跨服务依赖);USERS 定义直配,姓名经 UserBase /oapi/user/byId 补全(失败降级空名不阻塞)。 */ @Slf4j @Service @RequiredArgsConstructor public class AssigneeResolver { - private final FlwAssigneeMapRepository mapRepository; + private final FlowDeptAdminService deptAdminService; private final OpenApiUserClient userClient; public List resolve(Long schId, FlowDefModel.Node node) { @@ -32,8 +32,7 @@ public class AssigneeResolver { } List refs = new ArrayList<>(); if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) { - List maps = mapRepository.findEnabled(schId, a.deptKey); - for (FlwAssigneeMap m : maps) { + for (FlwDeptMember m : deptAdminService.resolveEnabled(schId, a.deptKey)) { refs.add(new AssigneeRef(m.getUserId(), m.getUserName())); } } else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) { @@ -47,14 +46,14 @@ public class AssigneeResolver { } if (refs.isEmpty()) { throw new BizException(ErrorCode.PARAM_ERROR, - "节点[" + node.id + "]无可用办理人(映射可能已被停用)"); + "节点[" + node.id + "]无可用办理人(科室可能已被停用或成员为空)"); } return refs; } /** * 抄送接收人解析(NOTIFY 专用,宽容口径):结构不合法照抛(发布期校验本应拦住,属脏定义), - * 但解析不出人(映射被停用/名单为空)只返回空列表由调用方记日志——抄送是知会不是审批, + * 但解析不出人(科室被停用/名单为空)只返回空列表由调用方记日志——抄送是知会不是审批, * 绝不能因抄送对象缺失卡住流程推进。与 {@link #resolve} 的强口径相反。 */ public List resolveTolerant(Long schId, FlowDefModel.Node node) { @@ -64,7 +63,7 @@ public class AssigneeResolver { } List refs = new ArrayList<>(); 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())); } } else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) { diff --git a/src/main/java/com/yunhai/flowcenter/entity/FlwDept.java b/src/main/java/com/yunhai/flowcenter/entity/FlwDept.java new file mode 100644 index 0000000..3e2e5af --- /dev/null +++ b/src/main/java/com/yunhai/flowcenter/entity/FlwDept.java @@ -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)。 + * deptKey = 本表自增主键 id 的字符串形态(拍板 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; +} diff --git a/src/main/java/com/yunhai/flowcenter/entity/FlwDeptMember.java b/src/main/java/com/yunhai/flowcenter/entity/FlwDeptMember.java new file mode 100644 index 0000000..5d9fa51 --- /dev/null +++ b/src/main/java/com/yunhai/flowcenter/entity/FlwDeptMember.java @@ -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; +} diff --git a/src/main/java/com/yunhai/flowcenter/repository/FlwDeptMemberRepository.java b/src/main/java/com/yunhai/flowcenter/repository/FlwDeptMemberRepository.java new file mode 100644 index 0000000..c3e5459 --- /dev/null +++ b/src/main/java/com/yunhai/flowcenter/repository/FlwDeptMemberRepository.java @@ -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 { + + List findByDeptIdOrderByIdAsc(Long deptId); + + List findByDeptIdAndStatusOrderByIdAsc(Long deptId, Integer status); + + Optional findByDeptIdAndUserId(Long deptId, Integer userId); + + void deleteByDeptId(Long deptId); + + long countByDeptIdAndStatus(Long deptId, Integer status); +} diff --git a/src/main/java/com/yunhai/flowcenter/repository/FlwDeptRepository.java b/src/main/java/com/yunhai/flowcenter/repository/FlwDeptRepository.java new file mode 100644 index 0000000..24aae53 --- /dev/null +++ b/src/main/java/com/yunhai/flowcenter/repository/FlwDeptRepository.java @@ -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 { + + List findBySchIdOrderByIdAsc(Long schId); + + List findBySchIdAndStatusOrderByIdAsc(Long schId, Integer status); + + Optional findBySchIdAndId(Long schId, Long id); + + /** 一键拉取幂等:同源 UserBase 组只建一行 */ + Optional findBySchIdAndSourceGroupId(Long schId, Integer sourceGroupId); +} diff --git a/src/main/java/com/yunhai/flowcenter/service/DeptLabelSupport.java b/src/main/java/com/yunhai/flowcenter/service/DeptLabelSupport.java index ff2a267..86754ed 100644 --- a/src/main/java/com/yunhai/flowcenter/service/DeptLabelSupport.java +++ b/src/main/java/com/yunhai/flowcenter/service/DeptLabelSupport.java @@ -1,13 +1,9 @@ package com.yunhai.flowcenter.service; -import com.yunhai.flowcenter.entity.FlwAssigneeMap; - -import java.util.List; - /** - * 科室口径文案的唯一出口(2026-09-18):DEPT_KEY 节点的"科室名"取值只在 {@link #mappedDeptName} - * 里实现一次——流程图(FlowDiagramService 的科室视图)与流程轨迹(FlowQueryService 的科室口径) - * 共用,避免两边对同一份映射表给出不同名字。 + * 科室口径文案的唯一出口(2026-09-18 建,2026-10-10 切源 flw_dept):DEPT_KEY 节点的"科室名" + * 取值只在这里实现一次——流程图(FlowDiagramService 的科室视图)与流程轨迹(FlowQueryService + * 的科室口径)共用,避免两边对同一份科室表给出不同名字。 */ final class DeptLabelSupport { @@ -15,18 +11,14 @@ final class DeptLabelSupport { } /** - * 映射行里的科室名:取第一个非空 deptName(一个 deptKey 的表现通常一致;科室名可空, - * 空则返回 null 由调用方决定回落链)。 + * 科室名(flw_dept.dept_name):科室不存在/已删返回 null,由调用方决定回落链 + * (科室不存在≠科室停用——停用科室仍有名字,轨迹/图示照常显示)。 */ - static String mappedDeptName(List maps) { - if (maps == null) { + static String deptName(FlowDeptAdminService deptAdminService, Long schId, String deptKey) { + try { + return deptAdminService.deptName(schId, deptKey); + } catch (Exception e) { return null; } - for (FlwAssigneeMap m : maps) { - if (m.getDeptName() != null && !m.getDeptName().trim().isEmpty()) { - return m.getDeptName(); - } - } - return null; } } diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java index 48cc55a..6258828 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java @@ -33,10 +33,10 @@ import java.util.stream.Collectors; public class FlowDefAdminService { private final FlwDefRepository defRepository; - private final FlwAssigneeMapRepository mapRepository; private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository; private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository; private final com.yunhai.flowcenter.repository.FlwAppRepository appRepository; + private final FlowDeptAdminService deptAdminService; private final FlowEngine flowEngine; private final ObjectMapper objectMapper; @@ -242,8 +242,8 @@ public class FlowDefAdminService { } private Set enabledDeptKeys(Long schId) { - return mapRepository.findEnabledBySchId(schId).stream() - .map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet()); + // 2026-10-10 科室管理切源:发布校验白名单改查 flw_dept/flw_dept_member(拍板 D1) + return deptAdminService.enabledDeptKeys(schId); } /** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */ diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowDeptAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowDeptAdminService.java new file mode 100644 index 0000000..73b42d2 --- /dev/null +++ b/src/main/java/com/yunhai/flowcenter/service/FlowDeptAdminService.java @@ -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 list(Long schId) { + List 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 enabledDeptKeys(Long schId) { + Set 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 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 members(Long schId, Long deptId) { + requireDeptById(schId, deptId); + return memberRepository.findByDeptIdOrderByIdAsc(deptId); + } + + /** 批量加成员(姓名经 UserBase 补全,失败降级 #id;已存在跳过) */ + @Transactional + public int memberAdd(Long schId, Long deptId, List userIds) { + FlwDept dept = requireDeptById(schId, deptId); + int added = 0; + for (Integer userId : userIds == null ? new ArrayList() : 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 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 pulled = new HashSet<>(); + for (FlowMapAdminService.MemberVo m : dept.getUsers()) { + if (m.getUserId() == null) { + continue; + } + pulled.add(m.getUserId()); + Optional 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; + } +} diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowDiagramService.java b/src/main/java/com/yunhai/flowcenter/service/FlowDiagramService.java index f010752..0d958c8 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowDiagramService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowDiagramService.java @@ -2,7 +2,7 @@ package com.yunhai.flowcenter.service; import com.yunhai.flowcenter.engine.FlowDefModel; 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.FlwDef; 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.enums.FlwEnums; import com.yunhai.flowcenter.model.FlowModels; -import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.flowcenter.repository.FlwCcRepository; import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwInstanceRepository; @@ -53,7 +52,7 @@ public class FlowDiagramService { private static final String USERS_HINT_DEPT_VIEW = "指定办理人"; private final FlwDefRepository defRepository; - private final FlwAssigneeMapRepository mapRepository; + private final FlowDeptAdminService deptAdminService; private final FlwInstanceRepository instanceRepository; private final FlwTaskRepository taskRepository; private final FlwCcRepository ccRepository; @@ -222,9 +221,9 @@ public class FlowDiagramService { vo.setAssigneeType(r.type); vo.setDeptKey(r.deptKey); if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(r.type)) { - List maps = mapRepository.findEnabled(schId, r.deptKey); - vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + maps.size() + "人" - : "抄送 " + namesWithSuffix(memberNames(maps), new ArrayList<>())); + List members = deptAdminService.resolveEnabled(schId, r.deptKey); + vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + members.size() + "人" + : "抄送 " + namesWithSuffix(memberNames(members), new ArrayList<>())); } else if (FlowDefModel.ASSIGNEE_USERS.equals(r.type)) { vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送指定人员" : "抄送 " + namesWithSuffix(userNames(r.userIds), new ArrayList<>())); @@ -243,14 +242,14 @@ public class FlowDiagramService { vo.setAssigneeType(a.type); vo.setDeptKey(a.deptKey); if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) { - List maps = mapRepository.findEnabled(schId, a.deptKey); + List members = deptAdminService.resolveEnabled(schId, a.deptKey); if (MODE_USER.equals(mode)) { - vo.setSubLabel(namesWithSuffix(memberNames(maps), suffixes(n))); + vo.setSubLabel(namesWithSuffix(memberNames(members), suffixes(n))); } else { - // dept 视图与 auto 的 DEPT_KEY 自适应同文案:科室名 + 启用映射人数 - vo.setLabel(deptLabel(n, a, maps)); + // dept 视图与 auto 的 DEPT_KEY 自适应同文案:科室名 + 启用成员人数 + vo.setLabel(deptLabel(n, a, schId)); List parts = new ArrayList<>(); - parts.add("科室 · " + maps.size() + "人"); + parts.add("科室 · " + members.size() + "人"); parts.addAll(suffixes(n)); vo.setSubLabel(String.join(" · ", parts)); } @@ -264,14 +263,14 @@ public class FlowDiagramService { return vo; } - /** 科室视图 label 兜底链:有映射行取 deptName(全空→deptKey);无映射行→节点名(再退 deptKey) */ - private String deptLabel(FlowDefModel.Node n, FlowDefModel.Assignee a, List maps) { + /** 科室视图 label 兜底链:科室存在取 dept_name;科室不存在→节点名(再退 deptKey) */ + private String deptLabel(FlowDefModel.Node n, FlowDefModel.Assignee a, Long schId) { // 科室名取值与流程轨迹科室口径共用(DeptLabelSupport),只保留本视图特有的兜底链 - String deptName = DeptLabelSupport.mappedDeptName(maps); + String deptName = DeptLabelSupport.deptName(deptAdminService, schId, a.deptKey); if (deptName != null) { return deptName; } - return maps.isEmpty() ? firstNonBlank(n.name, a.deptKey) : firstNonBlank(a.deptKey, n.name); + return firstNonBlank(n.name, a.deptKey); } /** 人员名单(顿号连接)+ 会签/时限后缀(" · "分隔);名单与后缀均空则返回 null */ @@ -308,9 +307,9 @@ public class FlowDiagramService { return parts; } - private List memberNames(List maps) { + private List memberNames(List members) { List names = new ArrayList<>(); - for (FlwAssigneeMap m : maps) { + for (FlwDeptMember m : members) { names.add(blankToId(m.getUserName(), m.getUserId())); } return names; diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java b/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java index 0a88214..f939deb 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java @@ -11,7 +11,6 @@ import com.yunhai.flowcenter.entity.FlwRouteLog; import com.yunhai.flowcenter.entity.FlwTask; import com.yunhai.flowcenter.enums.FlwEnums; import com.yunhai.flowcenter.model.FlowModels; -import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.flowcenter.repository.FlwBranchRepository; import com.yunhai.flowcenter.repository.FlwCcRepository; import com.yunhai.flowcenter.repository.FlwDefRepository; @@ -47,7 +46,7 @@ public class FlowQueryService { private final FlwCcRepository ccRepository; private final FlwRouteLogRepository routeLogRepository; private final FlwDefRepository defRepository; - private final FlwAssigneeMapRepository mapRepository; + private final FlowDeptAdminService deptAdminService; private final FlowEngine flowEngine; public BasePageResult pageMy(Long schId, Integer submitterId, @@ -476,7 +475,7 @@ public class FlowQueryService { } row.put("deptKey", assignee.deptKey); String deptName = deptNames.computeIfAbsent(assignee.deptKey, - k -> DeptLabelSupport.mappedDeptName(mapRepository.findEnabled(schId, k))); + k -> DeptLabelSupport.deptName(deptAdminService, schId, k)); if (deptName != null) { row.put("deptName", deptName); } diff --git a/src/main/resources/sql/20261010_create_flw_dept.sql b/src/main/resources/sql/20261010_create_flw_dept.sql new file mode 100644 index 0000000..f6aa607 --- /dev/null +++ b/src/main/resources/sql/20261010_create_flw_dept.sql @@ -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 = '科室成员'; diff --git a/src/test/java/com/yunhai/flowcenter/engine/FlowEngineIT.java b/src/test/java/com/yunhai/flowcenter/engine/FlowEngineIT.java index 82611ff..d6c6420 100644 --- a/src/test/java/com/yunhai/flowcenter/engine/FlowEngineIT.java +++ b/src/test/java/com/yunhai/flowcenter/engine/FlowEngineIT.java @@ -228,7 +228,7 @@ class FlowEngineIT extends FlowTestSupport { assertEquals(2, trace.get(3).get("round")); // 科室口径富化(DEPT_KEY 节点):deptName 取映射里的科室名,前端"按科室"据此换掉人名 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")); // ② 已办去重:同一办件一行,结果是最后一次(同意),次数=2 diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceSaveTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceSaveTest.java index 92e5388..09f9b81 100644 --- a/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceSaveTest.java +++ b/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceSaveTest.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yunhai.flowcenter.engine.FlowEngine; import com.yunhai.flowcenter.entity.FlwDef; import com.yunhai.flowcenter.enums.FlwEnums; -import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwRouterRepository; import com.yunhai.platform.apistd.BizException; @@ -45,10 +44,10 @@ class FlowDefAdminServiceSaveTest { defRepository = mock(FlwDefRepository.class); appRepository = mock(com.yunhai.flowcenter.repository.FlwAppRepository.class); service = new FlowDefAdminService(defRepository, - mock(FlwAssigneeMapRepository.class), mock(FlwRouterRepository.class), mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class), appRepository, + mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class), mock(FlowEngine.class), new ObjectMapper()); when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java index f9eb873..f981ae5 100644 --- a/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java +++ b/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yunhai.flowcenter.engine.FlowEngine; import com.yunhai.flowcenter.entity.FlwDef; import com.yunhai.flowcenter.repository.FlwAppRepository; -import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwRouterRepository; import org.junit.jupiter.api.BeforeEach; @@ -38,10 +37,10 @@ class FlowDefAdminServiceUpdateMetaTest { defRepository = mock(FlwDefRepository.class); appRepository = mock(FlwAppRepository.class); service = new FlowDefAdminService(defRepository, - mock(FlwAssigneeMapRepository.class), mock(FlwRouterRepository.class), mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class), appRepository, + mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class), mock(FlowEngine.class), new ObjectMapper()); when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowDeptAdminServiceTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowDeptAdminServiceTest.java new file mode 100644 index 0000000..692ac30 --- /dev/null +++ b/src/test/java/com/yunhai/flowcenter/service/FlowDeptAdminServiceTest.java @@ -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 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 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); + } +} diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java index 5af96ae..ffb9922 100644 --- a/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java +++ b/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java @@ -3,7 +3,7 @@ package com.yunhai.flowcenter.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.yunhai.flowcenter.controller.api.FlowAdminController; 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.FlwInstance; 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.enums.FlwEnums; import com.yunhai.flowcenter.model.FlowModels; -import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.flowcenter.repository.FlwBranchRepository; import com.yunhai.flowcenter.repository.FlwDefRepository; import com.yunhai.flowcenter.repository.FlwInstanceRepository; @@ -85,7 +84,7 @@ class FlowDiagramServiceTest { + "\"edges\":[{\"from\":\"start\",\"to\":\"n2\"},{\"from\":\"n2\",\"to\":\"end\"}]}"; private FlwDefRepository defRepository; - private FlwAssigneeMapRepository mapRepository; + private com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService; private FlwTaskRepository taskRepository; private com.yunhai.flowcenter.repository.FlwCcRepository ccRepository; private OpenApiUserClient userClient; @@ -94,7 +93,7 @@ class FlowDiagramServiceTest { @BeforeEach void setUp() { defRepository = mock(FlwDefRepository.class); - mapRepository = mock(FlwAssigneeMapRepository.class); + deptAdminService = mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class); taskRepository = mock(FlwTaskRepository.class); ccRepository = mock(com.yunhai.flowcenter.repository.FlwCcRepository.class); userClient = mock(OpenApiUserClient.class); @@ -107,7 +106,7 @@ class FlowDiagramServiceTest { taskRepository, mock(FlwBranchRepository.class), ccRepository, mock(AssigneeResolver.class), mock(com.yunhai.flowcenter.engine.ConditionRouteService.class), mock(ApplicationEventPublisher.class)); - service = new FlowDiagramService(defRepository, mapRepository, + service = new FlowDiagramService(defRepository, deptAdminService, mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class), taskRepository, ccRepository, mock(com.yunhai.flowcenter.repository.FlwRouteLogRepository.class), @@ -127,11 +126,9 @@ class FlowDiagramServiceTest { return def; } - private FlwAssigneeMap mapRow(String deptName, int userId, String userName) { - FlwAssigneeMap m = new FlwAssigneeMap(); - m.setSchId(SCH); - m.setDeptKey("rs"); - m.setDeptName(deptName); + private FlwDeptMember memberRow(int userId, String userName) { + FlwDeptMember m = new FlwDeptMember(); + m.setDeptId(7L); m.setUserId(userId); m.setUserName(userName); m.setStatus(0); @@ -153,8 +150,9 @@ class FlowDiagramServiceTest { @Test void c1_deptView_deptKeyNode_labelDeptName_subLabelCountAndDeadline() { - when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( - mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四"))); + when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处"); + when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList( + memberRow(11, "张三"), memberRow(12, "李四"))); FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept"); FlowModels.NodeVo n1 = node(vo, "n1"); assertEquals("人事处", n1.getLabel()); @@ -170,30 +168,32 @@ class FlowDiagramServiceTest { @Test void c1_deptView_fallbackChain_deptKeyThenNodeName() { - // 映射行全无名 → deptKey 兜底 - when(mapRepository.findEnabled(SCH, "rs")) - .thenReturn(Collections.singletonList(mapRow("", 11, "张三"))); + // 科室已被删(deptName=null)→ 节点名兜底(成员解析空,人数照实) + when(deptAdminService.resolveEnabled(SCH, "rs")) + .thenReturn(Collections.singletonList(memberRow(11, "张三"))); 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("科室 · 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()); } @Test void c1_anyCountersign_appendsSuffix() { - when(mapRepository.findEnabled(SCH, "rs")) - .thenReturn(Collections.singletonList(mapRow("人事处", 11, "张三"))); + when(deptAdminService.resolveEnabled(SCH, "rs")) + .thenReturn(Collections.singletonList(memberRow(11, "张三"))); FlowModels.NodeVo n1 = node(service.build(def(ANY_DEF_JSON), null, "dept"), "n1"); assertEquals("科室 · 1人 · 任一人通过", n1.getSubLabel()); } @Test 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"); assertEquals("开始", node(vo, "start").getLabel()); assertEquals("并行分发", node(vo, "sp1").getLabel()); @@ -215,8 +215,9 @@ class FlowDiagramServiceTest { @Test void c1_autoView_deptKeyNode_rendersDeptStyle() { - when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( - mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四"))); + when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处"); + 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"); assertEquals("人事处", n1.getLabel()); assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel()); @@ -226,8 +227,9 @@ class FlowDiagramServiceTest { /** 2026-09-18:mode 缺省改为人员视图(原 auto 自适应)——老深链显式带 auto 仍按自适应出图 */ @Test void c1_defaultMode_isUserView() { - when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( - mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四"))); + when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处"); + when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList( + memberRow(11, "张三"), memberRow(12, "李四"))); FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, null); assertEquals("user", vo.getMode()); FlowModels.NodeVo n1 = node(vo, "n1"); @@ -288,8 +290,9 @@ class FlowDiagramServiceTest { @Test void c3_userView_deptKeyNode_listsMemberNames_blankNameDegradesToId() { - when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList( - mapRow("人事处", 11, "张三"), mapRow("人事处", 12, null))); + when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处"); + 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"); // 人员视图:label=节点名,subLabel=映射成员名(空名降级 #id)+ 会签/时限后缀 assertEquals("人事处审批", n1.getLabel()); @@ -304,7 +307,8 @@ class FlowDiagramServiceTest { when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc( SCH, "g1", FlwEnums.DefStatus.PUBLISHED.getCode())) .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); assertEquals("教师入职", vo.getName()); assertEquals(Integer.valueOf(2), vo.getVersion()); @@ -349,7 +353,8 @@ class FlowDiagramServiceTest { instance.setSchId(SCH); instance.setDefId(12L); 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"); assertEquals(Long.valueOf(100L), vo.getInstanceId()); assertEquals(Long.valueOf(12L), vo.getDefId()); @@ -360,7 +365,8 @@ class FlowDiagramServiceTest { void c6_build_draftDef_ok() { FlwDef draft = def(DEPT_DEF_JSON); 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"); assertEquals(Integer.valueOf(0), vo.getStatus()); assertEquals(5, vo.getNodes().size()); @@ -484,7 +490,8 @@ class FlowDiagramServiceTest { mock(FlwInstanceRepository.class), diagramService, mock(com.yunhai.flowcenter.service.DiagramImageService.class), 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) { diff --git a/src/test/java/com/yunhai/flowcenter/support/FlowTestSupport.java b/src/test/java/com/yunhai/flowcenter/support/FlowTestSupport.java index 59441f8..f160714 100644 --- a/src/test/java/com/yunhai/flowcenter/support/FlowTestSupport.java +++ b/src/test/java/com/yunhai/flowcenter/support/FlowTestSupport.java @@ -1,27 +1,34 @@ package com.yunhai.flowcenter.support; -import com.yunhai.flowcenter.entity.FlwAssigneeMap; import com.yunhai.flowcenter.entity.FlwCallbackLog; import com.yunhai.flowcenter.entity.FlwCallbackReg; 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.FlwRouter; -import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; import com.yunhai.flowcenter.repository.FlwCallbackLogRepository; import com.yunhai.flowcenter.repository.FlwCallbackRegRepository; import com.yunhai.flowcenter.repository.FlwCcRepository; 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.FlwRouteLogRepository; import com.yunhai.flowcenter.repository.FlwRouterRepository; import com.yunhai.flowcenter.repository.FlwTaskRepository; import org.springframework.beans.factory.annotation.Autowired; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; /** * 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 { @@ -34,7 +41,9 @@ public abstract class FlowTestSupport { @Autowired protected FlwTaskRepository taskRepository; @Autowired - protected FlwAssigneeMapRepository mapRepository; + protected FlwDeptRepository deptRepository; + @Autowired + protected FlwDeptMemberRepository deptMemberRepository; @Autowired protected FlwCallbackRegRepository regRepository; @Autowired @@ -46,6 +55,14 @@ public abstract class FlowTestSupport { @Autowired protected FlwRouteLogRepository routeLogRepository; + /** tag → 实际 deptKey(flw_dept.id 字符串);mapDept 注册,def helper 消费 */ + private final Map deptKeyByTag = new HashMap<>(); + + /** tag 对应的实际 deptKey(未注册的 tag 原样返回——脏定义防御用例依赖此行为) */ + protected String dk(String tag) { + return deptKeyByTag.getOrDefault(tag, tag); + } + protected void cleanDb() { logRepository.deleteAll(); regRepository.deleteAll(); @@ -55,17 +72,38 @@ public abstract class FlowTestSupport { taskRepository.deleteAll(); instanceRepository.deleteAll(); defRepository.deleteAll(); - mapRepository.deleteAll(); + deptMemberRepository.deleteAll(); + deptRepository.deleteAll(); + deptKeyByTag.clear(); } - protected FlwAssigneeMap mapDept(String deptKey, Integer userId, String userName) { - FlwAssigneeMap map = new FlwAssigneeMap(); - map.setSchId(SCH); - map.setDeptKey(deptKey); - map.setDeptName(deptKey + "科"); - map.setUserId(userId); - map.setUserName(userName); - return mapRepository.save(map); + /** + * 注册科室成员(tag 复用同一科室行;未注册过的 tag 新建科室并登记 deptKey)。 + * 返回科室名(deptName=tag+"科",轨迹/图示断言用)。 + */ + protected String mapDept(String tag, Integer userId, String userName) { + FlwDept dept = deptKeyByTag.containsKey(tag) ? reloadDept(tag) : createDept(tag); + deptKeyByTag.put(tag, String.valueOf(dept.getId())); + 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) { @@ -88,7 +126,7 @@ public abstract class FlowTestSupport { + "\"nodes\":[" + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"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\"}" + "]," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}]," @@ -102,8 +140,8 @@ public abstract class FlowTestSupport { + "\"nodes\":[" + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"}," - + "{\"id\":\"t_a\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," - + "{\"id\":\"t_b\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + + "{\"id\":\"t_a\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}}," + + "{\"id\":\"t_b\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}}," + "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"end\",\"type\":\"END\"}" + "]," @@ -121,7 +159,7 @@ public abstract class FlowTestSupport { + "\"nodes\":[" + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"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\"}" + "]," + "\"edges\":[{\"from\":\"start\",\"to\":\"cc1\"},{\"from\":\"cc1\",\"to\":\"n1\"}," @@ -136,7 +174,7 @@ public abstract class FlowTestSupport { + "\"nodes\":[" + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"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\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"end\",\"type\":\"END\"}" @@ -158,8 +196,8 @@ public abstract class FlowTestSupport { + "\"nodes\":[" + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," + "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"}," - + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," - + "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"乙线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}}," + + "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"乙线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}}," + "{\"id\":\"end\",\"type\":\"END\"}" + "]," + "\"edges\":[{\"from\":\"start\",\"to\":\"cond1\"}," @@ -178,9 +216,9 @@ public abstract class FlowTestSupport { + "\"code\":\"" + codeOf() + "\",\"name\":\"主线条件测试流程\",\"start\":\"start\"," + "\"nodes\":[" + "{\"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\":\"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\"}" + "]," + "\"edges\":[{\"from\":\"start\",\"to\":\"n0\"}," @@ -213,8 +251,8 @@ public abstract class FlowTestSupport { + "\"code\":\"" + codeOf() + "\",\"name\":\"多出口测试流程\",\"start\":\"start\"," + "\"nodes\":[" + "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"}," - + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," - + "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + + "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}}," + + "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}}," + "{\"id\":\"end\",\"type\":\"END\"}" + "]," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"}," @@ -230,7 +268,7 @@ public abstract class FlowTestSupport { + "\"code\":\"" + codeOf() + "\",\"name\":\"夡签测试流程\",\"start\":\"start\"," + "\"nodes\":[" + "{\"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\"}" + "]," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}]," @@ -248,13 +286,13 @@ public abstract class FlowTestSupport { + "\"code\":\"" + codeOf() + "\",\"name\":\"嵌套并行测试流程\",\"start\":\"start\"," + "\"nodes\":[" + "{\"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\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," - + "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + + "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}}," + + "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}}," + "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"}," - + "{\"id\":\"tc\",\"type\":\"APPROVAL\",\"name\":\"丙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptC + "\"}}," - + "{\"id\":\"td\",\"type\":\"APPROVAL\",\"name\":\"丁科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptD + "\"}}," + + "{\"id\":\"tc\",\"type\":\"APPROVAL\",\"name\":\"丙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptC) + "\"}}," + + "{\"id\":\"td\",\"type\":\"APPROVAL\",\"name\":\"丁科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptD) + "\"}}," + "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"end\",\"type\":\"END\"}" @@ -277,10 +315,10 @@ public abstract class FlowTestSupport { + "\"code\":\"" + codeOf() + "\",\"name\":\"出口并行测试流程\",\"start\":\"start\"," + "\"nodes\":[" + "{\"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\":\"p1\",\"type\":\"APPROVAL\",\"name\":\"并行甲\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}}," - + "{\"id\":\"p2\",\"type\":\"APPROVAL\",\"name\":\"并行乙\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}}," + + "{\"id\":\"p1\",\"type\":\"APPROVAL\",\"name\":\"并行甲\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}}," + + "{\"id\":\"p2\",\"type\":\"APPROVAL\",\"name\":\"并行乙\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}}," + "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"}," + "{\"id\":\"end\",\"type\":\"END\"}" + "]," @@ -299,12 +337,12 @@ public abstract class FlowTestSupport { + "\"code\":\"" + codeOf() + "\",\"name\":\"主线并行尾验流程\",\"start\":\"start\"," + "\"nodes\":[" + "{\"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\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ka\"}}," - + "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"kb\"}}," + + "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk("ka") + "\"}}," + + "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk("kb") + "\"}}," + "{\"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\"}" + "]," + "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"split1\"},"