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.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<FlwAssigneeMap> 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<com.yunhai.flowcenter.service.FlowDeptAdminService.DeptVo> deptList(BoaUser user) {
return deptAdminService.list(adminGuard.requireAdmin(user, null));
}
@PostMapping("/map/save")
public FlwAssigneeMap mapSave(BoaUser user, @RequestBody Map<String, Object> body) {
@PostMapping("/dept/create")
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);
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<String, Object> 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<String, Object> body) {
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")
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<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;
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<AssigneeRef> resolve(Long schId, FlowDefModel.Node node) {
@@ -32,8 +32,7 @@ public class AssigneeResolver {
}
List<AssigneeRef> refs = new ArrayList<>();
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
List<FlwAssigneeMap> 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<AssigneeRef> resolveTolerant(Long schId, FlowDefModel.Node node) {
@@ -64,7 +63,7 @@ public class AssigneeResolver {
}
List<AssigneeRef> 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)) {
@@ -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;
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<FlwAssigneeMap> 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;
}
}
@@ -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<String> 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 启用键全集,全局不分校) */
@@ -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.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<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, r.deptKey);
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + maps.size() + "人"
: "抄送 " + namesWithSuffix(memberNames(maps), new ArrayList<>()));
List<FlwDeptMember> 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<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, a.deptKey);
List<FlwDeptMember> 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<String> 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<FlwAssigneeMap> 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<String> memberNames(List<FlwAssigneeMap> maps) {
private List<String> memberNames(List<FlwDeptMember> members) {
List<String> names = new ArrayList<>();
for (FlwAssigneeMap m : maps) {
for (FlwDeptMember m : members) {
names.add(blankToId(m.getUserName(), m.getUserId()));
}
return names;
@@ -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<FlowModels.InstanceItem> 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);
}