Compare commits
4
Commits
a1f99fd7e0
...
a06b610d9d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a06b610d9d | ||
|
|
4293c9628a | ||
|
|
d6b7a68ec3 | ||
|
|
92619508f0 |
@@ -70,6 +70,14 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 条件路由表达式引擎(2026-10-10 四议题):CONDITION 节点 judgeMode=EXPRESSION 时
|
||||||
|
出边 condition 用 AviatorScript 求值(JDK8 兼容;5.4.3 含安全修复,沙箱见 ExpressionEvaluator) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.aviator</groupId>
|
||||||
|
<artifactId>aviator</artifactId>
|
||||||
|
<version>5.4.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ public class FlowAdminController {
|
|||||||
private final FlwInstanceRepository instanceRepository;
|
private final FlwInstanceRepository instanceRepository;
|
||||||
private final FlowDiagramService diagramService;
|
private final FlowDiagramService diagramService;
|
||||||
private final com.yunhai.flowcenter.service.DiagramImageService imageService;
|
private final com.yunhai.flowcenter.service.DiagramImageService imageService;
|
||||||
|
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
||||||
|
private final com.yunhai.flowcenter.service.FlowAppAdminService appAdminService;
|
||||||
|
private final com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService;
|
||||||
|
|
||||||
// ==================== 流程定义 ====================
|
// ==================== 流程定义 ====================
|
||||||
|
|
||||||
@@ -65,6 +68,27 @@ public class FlowAdminController {
|
|||||||
return defAdminService.distinctAppKeys(adminGuard.requireAdmin(user, null));
|
return defAdminService.distinctAppKeys(adminGuard.requireAdmin(user, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 应用设置(2026-10-10 四议题) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用管理列表:本校"可对接流程"白名单(flw_app)。附带一次学校池静默同步
|
||||||
|
* (UserBase list-by-school;不可达降级本地行)。开通过程在 UserBase 管理端做。
|
||||||
|
*/
|
||||||
|
@GetMapping("/app/list")
|
||||||
|
public List<com.yunhai.flowcenter.entity.FlwApp> appList(BoaUser user) {
|
||||||
|
return appAdminService.list(adminGuard.requireAdmin(user, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开关某应用是否对接流程(停用后 defSave 归属应用校验拦截新引用) */
|
||||||
|
@PostMapping("/app/enable")
|
||||||
|
public void appEnable(BoaUser user,
|
||||||
|
@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) {
|
||||||
|
Long schId = adminGuard.requireAdmin(user, null);
|
||||||
|
appAdminService.enable(schId,
|
||||||
|
body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id"))),
|
||||||
|
Boolean.parseBoolean(String.valueOf(body.get("enabled"))));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/def/get")
|
@GetMapping("/def/get")
|
||||||
public FlwDef defGet(BoaUser user, @RequestParam("id") Long id) {
|
public FlwDef defGet(BoaUser user, @RequestParam("id") Long id) {
|
||||||
return defAdminService.get(adminGuard.requireAdmin(user, null), id);
|
return defAdminService.get(adminGuard.requireAdmin(user, null), id);
|
||||||
@@ -81,6 +105,18 @@ public class FlowAdminController {
|
|||||||
(String) body.get("appKey"));
|
(String) body.get("appKey"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元数据原地编辑(2026-10-10 议题2):只改 name/appKey 不升版本(version/flowGuid/status/contentJson
|
||||||
|
* 结构均不动,仅同步 contentJson 顶层 name);已发布亦可。结构改动请走编辑器升版本。
|
||||||
|
*/
|
||||||
|
@PostMapping("/def/updateMeta")
|
||||||
|
public FlwDef defUpdateMeta(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
|
Long schId = adminGuard.requireAdmin(user, null);
|
||||||
|
return defAdminService.updateMeta(schId,
|
||||||
|
body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id"))),
|
||||||
|
(String) body.get("name"), (String) body.get("appKey"));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/def/publish/{id}")
|
@PostMapping("/def/publish/{id}")
|
||||||
public FlwDef defPublish(BoaUser user, @PathVariable("id") Long id) {
|
public FlwDef defPublish(BoaUser user, @PathVariable("id") Long id) {
|
||||||
return defAdminService.publish(adminGuard.requireAdmin(user, null), id);
|
return defAdminService.publish(adminGuard.requireAdmin(user, null), id);
|
||||||
@@ -126,35 +162,91 @@ public class FlowAdminController {
|
|||||||
.body(imageService.renderPng(diagramService.build(def, null, mode)));
|
.body(imageService.renderPng(diagramService.build(def, null, mode)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 办理人映射 ====================
|
// ==================== 科室管理(2026-10-10 四议题·议题3,取代 /map/* 映射 CRUD) ====================
|
||||||
|
|
||||||
@GetMapping("/map/list")
|
/** 科室列表(编辑器 ctx.depts 同结构:deptKey=id 字符串 + 启用成员数) */
|
||||||
public List<FlwAssigneeMap> mapList(BoaUser user,
|
@GetMapping("/dept/list")
|
||||||
@RequestParam(value = "deptKey", required = false) String deptKey) {
|
public List<com.yunhai.flowcenter.service.FlowDeptAdminService.DeptVo> deptList(BoaUser user) {
|
||||||
return mapAdminService.listByDept(adminGuard.requireAdmin(user, null), deptKey);
|
return deptAdminService.list(adminGuard.requireAdmin(user, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/map/save")
|
@PostMapping("/dept/create")
|
||||||
public FlwAssigneeMap mapSave(BoaUser user, @RequestBody Map<String, Object> body) {
|
public com.yunhai.flowcenter.entity.FlwDept deptCreate(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
|
return deptAdminService.create(adminGuard.requireAdmin(user, null), (String) body.get("deptName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/dept/rename")
|
||||||
|
public com.yunhai.flowcenter.entity.FlwDept deptRename(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
Long schId = adminGuard.requireAdmin(user, null);
|
Long schId = adminGuard.requireAdmin(user, null);
|
||||||
return mapAdminService.save(schId,
|
return deptAdminService.rename(schId, longVal(body.get("id")), (String) body.get("deptName"));
|
||||||
(String) body.get("deptKey"), (String) body.get("deptName"),
|
|
||||||
body.get("userId") == null ? null : Integer.valueOf(String.valueOf(body.get("userId"))),
|
|
||||||
(String) body.get("userName"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/map/disable/{id}")
|
/** 停用/启用科室(停用=解析不出办理人、发布校验拦新引用;成员不动) */
|
||||||
public void mapDisable(BoaUser user, @PathVariable("id") Long id) {
|
@PostMapping("/dept/status")
|
||||||
mapAdminService.disable(adminGuard.requireAdmin(user, null), id);
|
public void deptStatus(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
|
Long schId = adminGuard.requireAdmin(user, null);
|
||||||
|
deptAdminService.setStatus(schId, longVal(body.get("id")), boolVal(body.get("enabled")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 启用映射(停用行的恢复入口;DEPT_KEY 解析只取启用行) */
|
/** 删除科室(级联删成员;被流程引用的 deptKey 将解析失败——发布校验/办理时会拦) */
|
||||||
@PostMapping("/map/enable/{id}")
|
@PostMapping("/dept/delete")
|
||||||
public FlwAssigneeMap mapEnable(BoaUser user, @PathVariable("id") Long id) {
|
public void deptDelete(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
return mapAdminService.enable(adminGuard.requireAdmin(user, null), id);
|
Long schId = adminGuard.requireAdmin(user, null);
|
||||||
|
deptAdminService.delete(schId, longVal(body.get("id")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 成员检索(UserBase /oapi/user/byId) */
|
@GetMapping("/dept/members")
|
||||||
|
public List<com.yunhai.flowcenter.entity.FlwDeptMember> deptMembers(BoaUser user,
|
||||||
|
@RequestParam("deptId") Long deptId) {
|
||||||
|
return deptAdminService.members(adminGuard.requireAdmin(user, null), deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量加成员(已存在跳过;姓名经 UserBase 补全),返回新增数 */
|
||||||
|
@PostMapping("/dept/memberAdd")
|
||||||
|
public int deptMemberAdd(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
|
Long schId = adminGuard.requireAdmin(user, null);
|
||||||
|
List<Integer> userIds = new java.util.ArrayList<>();
|
||||||
|
Object raw = body.get("userIds");
|
||||||
|
if (raw instanceof List) {
|
||||||
|
for (Object o : (List<?>) raw) {
|
||||||
|
if (o != null) {
|
||||||
|
userIds.add(Integer.valueOf(String.valueOf(o)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deptAdminService.memberAdd(schId, longVal(body.get("deptId")), userIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/dept/memberRemove")
|
||||||
|
public void deptMemberRemove(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
|
Long schId = adminGuard.requireAdmin(user, null);
|
||||||
|
deptAdminService.memberRemove(schId, longVal(body.get("memberId")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一键拉取教师组·预览:学校子树内教师组容器 → 科室+组内成员(UserBase 组 id 即将落
|
||||||
|
* flw_dept.source_group_id)。导入前先展示给管理员确认,不落库。
|
||||||
|
*/
|
||||||
|
@GetMapping("/dept/groupPreview")
|
||||||
|
public List<com.yunhai.flowcenter.service.FlowMapAdminService.GroupDeptVo> groupPreview(BoaUser user) {
|
||||||
|
return mapAdminService.previewGroupDepts(adminGuard.requireAdmin(user, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一键拉取教师组·导入:upsert flw_dept(source_group_id 幂等,重复拉取=刷新组名与成员集) */
|
||||||
|
@PostMapping("/dept/importGroups")
|
||||||
|
public com.yunhai.flowcenter.service.FlowDeptAdminService.GroupImportResultVo importGroups(BoaUser user) {
|
||||||
|
return deptAdminService.importGroups(adminGuard.requireAdmin(user, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long longVal(Object o) {
|
||||||
|
return o == null ? null : Long.valueOf(String.valueOf(o));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean boolVal(Object o) {
|
||||||
|
return o == null ? false : Boolean.parseBoolean(String.valueOf(o));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 成员检索(UserBase /oapi/user/byId;编辑器选人/姓名补全共用,非映射 CRUD,保留) */
|
||||||
@GetMapping("/map/lookupUser")
|
@GetMapping("/map/lookupUser")
|
||||||
public com.yunhai.platform.utils.model.BoaUser lookupUser(BoaUser user,
|
public com.yunhai.platform.utils.model.BoaUser lookupUser(BoaUser user,
|
||||||
@RequestParam("userId") Integer userId) {
|
@RequestParam("userId") Integer userId) {
|
||||||
@@ -162,21 +254,6 @@ public class FlowAdminController {
|
|||||||
return mapAdminService.lookupUser(userId);
|
return mapAdminService.lookupUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 一键拉取教师组·预览:学校子树内教师组容器 → 科室+组内成员(deptKey=UserBase 组 id)。
|
|
||||||
* 导入前先展示给管理员确认,不落库。
|
|
||||||
*/
|
|
||||||
@GetMapping("/map/groupPreview")
|
|
||||||
public List<com.yunhai.flowcenter.service.FlowMapAdminService.GroupDeptVo> groupPreview(BoaUser user) {
|
|
||||||
return mapAdminService.previewGroupDepts(adminGuard.requireAdmin(user, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 一键拉取教师组·导入:批量落映射(已存在且启用中的 sch+dept+user 跳过),返回计数 */
|
|
||||||
@PostMapping("/map/importGroups")
|
|
||||||
public com.yunhai.flowcenter.service.FlowMapAdminService.GroupImportResultVo importGroups(BoaUser user) {
|
|
||||||
return mapAdminService.importGroups(adminGuard.requireAdmin(user, null));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 选人组件数据源 ====================
|
// ==================== 选人组件数据源 ====================
|
||||||
|
|
||||||
/** 选人组件树:学校组树(组→组内用户叶子),管理端「选择用户」弹窗共用 */
|
/** 选人组件树:学校组树(组→组内用户叶子),管理端「选择用户」弹窗共用 */
|
||||||
@@ -288,6 +365,47 @@ public class FlowAdminController {
|
|||||||
routerAdminService.reroute(adminGuard.requireAdmin(user, null), id);
|
routerAdminService.reroute(adminGuard.requireAdmin(user, null), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表达式调试求值(2026-10-10 四议题):body {expression, variablesJson},与运行时同一求值核心
|
||||||
|
* (ExpressionEvaluator);不落库、不走流程,发布前/挂起排查用。内置变量需在 variablesJson 里
|
||||||
|
* 自行提供样例值(线上是引擎注入的办件上下文)。
|
||||||
|
*/
|
||||||
|
@PostMapping("/router/eval")
|
||||||
|
public Map<String, Object> routerEval(BoaUser user, @RequestBody Map<String, Object> body) {
|
||||||
|
adminGuard.requireAdmin(user, null);
|
||||||
|
String expression = body.get("expression") == null ? "" : String.valueOf(body.get("expression")).trim();
|
||||||
|
String variablesJson = body.get("variablesJson") == null ? "" : String.valueOf(body.get("variablesJson")).trim();
|
||||||
|
Map<String, Object> result = new java.util.HashMap<>();
|
||||||
|
if (expression.isEmpty()) {
|
||||||
|
result.put("result", false);
|
||||||
|
result.put("error", "表达式为空");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Map<String, Object> env;
|
||||||
|
if (variablesJson.isEmpty()) {
|
||||||
|
env = new java.util.HashMap<>();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
env = objectMapper.readValue(variablesJson,
|
||||||
|
new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("result", false);
|
||||||
|
result.put("error", "样例变量不是合法 JSON 对象:" + e.getMessage());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Object value = com.yunhai.flowcenter.engine.ExpressionEvaluator.evaluate(expression, env);
|
||||||
|
result.put("result", Boolean.TRUE.equals(value));
|
||||||
|
result.put("value", String.valueOf(value));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.put("result", false);
|
||||||
|
result.put("error", String.valueOf(e.getMessage() != null ? e.getMessage() : e.toString()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 办件查询 ====================
|
// ==================== 办件查询 ====================
|
||||||
|
|
||||||
@GetMapping("/instance/page")
|
@GetMapping("/instance/page")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.yunhai.flowcenter.engine;
|
package com.yunhai.flowcenter.engine;
|
||||||
|
|
||||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
import com.yunhai.flowcenter.entity.FlwDeptMember;
|
||||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
import com.yunhai.flowcenter.service.FlowDeptAdminService;
|
||||||
import com.yunhai.platform.apistd.BizException;
|
import com.yunhai.platform.apistd.BizException;
|
||||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
@@ -14,15 +14,15 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 办理人解析(概览 §六):DEPT_KEY 查显式映射表(零跨服务依赖);USERS 定义直配,
|
* 办理人解析(概览 §六):DEPT_KEY 查科室管理(flw_dept+flw_dept_member,2026-10-10 切源,
|
||||||
* 姓名经 UserBase /oapi/user/byId 补全(失败降级空名不阻塞)。SUBMITTER_DEPT/FORM_FIELD 二期。
|
* 零跨服务依赖);USERS 定义直配,姓名经 UserBase /oapi/user/byId 补全(失败降级空名不阻塞)。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AssigneeResolver {
|
public class AssigneeResolver {
|
||||||
|
|
||||||
private final FlwAssigneeMapRepository mapRepository;
|
private final FlowDeptAdminService deptAdminService;
|
||||||
private final OpenApiUserClient userClient;
|
private final OpenApiUserClient userClient;
|
||||||
|
|
||||||
public List<AssigneeRef> resolve(Long schId, FlowDefModel.Node node) {
|
public List<AssigneeRef> resolve(Long schId, FlowDefModel.Node node) {
|
||||||
@@ -32,8 +32,7 @@ public class AssigneeResolver {
|
|||||||
}
|
}
|
||||||
List<AssigneeRef> refs = new ArrayList<>();
|
List<AssigneeRef> refs = new ArrayList<>();
|
||||||
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
|
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
|
||||||
List<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, a.deptKey);
|
for (FlwDeptMember m : deptAdminService.resolveEnabled(schId, a.deptKey)) {
|
||||||
for (FlwAssigneeMap m : maps) {
|
|
||||||
refs.add(new AssigneeRef(m.getUserId(), m.getUserName()));
|
refs.add(new AssigneeRef(m.getUserId(), m.getUserName()));
|
||||||
}
|
}
|
||||||
} else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) {
|
} else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) {
|
||||||
@@ -47,14 +46,14 @@ public class AssigneeResolver {
|
|||||||
}
|
}
|
||||||
if (refs.isEmpty()) {
|
if (refs.isEmpty()) {
|
||||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||||
"节点[" + node.id + "]无可用办理人(映射可能已被停用)");
|
"节点[" + node.id + "]无可用办理人(科室可能已被停用或成员为空)");
|
||||||
}
|
}
|
||||||
return refs;
|
return refs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 抄送接收人解析(NOTIFY 专用,宽容口径):结构不合法照抛(发布期校验本应拦住,属脏定义),
|
* 抄送接收人解析(NOTIFY 专用,宽容口径):结构不合法照抛(发布期校验本应拦住,属脏定义),
|
||||||
* 但解析不出人(映射被停用/名单为空)只返回空列表由调用方记日志——抄送是知会不是审批,
|
* 但解析不出人(科室被停用/名单为空)只返回空列表由调用方记日志——抄送是知会不是审批,
|
||||||
* 绝不能因抄送对象缺失卡住流程推进。与 {@link #resolve} 的强口径相反。
|
* 绝不能因抄送对象缺失卡住流程推进。与 {@link #resolve} 的强口径相反。
|
||||||
*/
|
*/
|
||||||
public List<AssigneeRef> resolveTolerant(Long schId, FlowDefModel.Node node) {
|
public List<AssigneeRef> resolveTolerant(Long schId, FlowDefModel.Node node) {
|
||||||
@@ -64,7 +63,7 @@ public class AssigneeResolver {
|
|||||||
}
|
}
|
||||||
List<AssigneeRef> refs = new ArrayList<>();
|
List<AssigneeRef> refs = new ArrayList<>();
|
||||||
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
|
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
|
||||||
for (FlwAssigneeMap m : mapRepository.findEnabled(schId, a.deptKey)) {
|
for (FlwDeptMember m : deptAdminService.resolveEnabled(schId, a.deptKey)) {
|
||||||
refs.add(new AssigneeRef(m.getUserId(), m.getUserName()));
|
refs.add(new AssigneeRef(m.getUserId(), m.getUserName()));
|
||||||
}
|
}
|
||||||
} else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) {
|
} else if (FlowDefModel.ASSIGNEE_USERS.equals(a.type)) {
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ public class ConditionRouteService {
|
|||||||
*/
|
*/
|
||||||
public String decide(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node) {
|
public String decide(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node) {
|
||||||
List<String> outlets = outletNames(model, node);
|
List<String> outlets = outletNames(model, node);
|
||||||
|
if (node.expressionJudge()) {
|
||||||
|
return decideByExpression(instance, model, node, outlets);
|
||||||
|
}
|
||||||
FlwRouter router = routerRepository.findByRouterKey(node.routerKey).orElse(null);
|
FlwRouter router = routerRepository.findByRouterKey(node.routerKey).orElse(null);
|
||||||
if (router == null || router.getStatus() == null || router.getStatus() != 1) {
|
if (router == null || router.getStatus() == null || router.getStatus() != 1) {
|
||||||
log.error("[flow-center] 路由判断失败:routerKey={} 未注册或已停用(挂起):instance={} node={}",
|
log.error("[flow-center] 路由判断失败:routerKey={} 未注册或已停用(挂起):instance={} node={}",
|
||||||
@@ -50,7 +53,8 @@ public class ConditionRouteService {
|
|||||||
}
|
}
|
||||||
RouteJudgeClient.JudgeBody body = new RouteJudgeClient.JudgeBody(
|
RouteJudgeClient.JudgeBody body = new RouteJudgeClient.JudgeBody(
|
||||||
instance.getSchId(), instance.getId(), instance.getVersion(),
|
instance.getSchId(), instance.getId(), instance.getVersion(),
|
||||||
instance.getBizType(), instance.getBizId(), node.id, node.name,
|
instance.getBizType(), instance.getBizId(), instance.getSubmitterId(),
|
||||||
|
node.id, node.name,
|
||||||
node.routerKey, outlets, parseVariables(instance.getFormDataJson()));
|
node.routerKey, outlets, parseVariables(instance.getFormDataJson()));
|
||||||
try {
|
try {
|
||||||
RouteJudgeClient.JudgeResult result = judgeClient.call(router, body);
|
RouteJudgeClient.JudgeResult result = judgeClient.call(router, body);
|
||||||
@@ -60,7 +64,7 @@ public class ConditionRouteService {
|
|||||||
return persistFailure(instance, node, outlets,
|
return persistFailure(instance, node, outlets,
|
||||||
"返回出口未命中 outlets:" + result.outlet + ";raw=" + result.rawResponse);
|
"返回出口未命中 outlets:" + result.outlet + ";raw=" + result.rawResponse);
|
||||||
}
|
}
|
||||||
persist(instance, node, result.outlet, result.rawResponse);
|
persist(instance, node, node.routerKey, result.outlet, result.rawResponse);
|
||||||
log.info("[flow-center] 路由判断命中:instance={} node={} router={} outlet={}",
|
log.info("[flow-center] 路由判断命中:instance={} node={} router={} outlet={}",
|
||||||
instance.getId(), node.id, node.routerKey, result.outlet);
|
instance.getId(), node.id, node.routerKey, result.outlet);
|
||||||
return result.outlet;
|
return result.outlet;
|
||||||
@@ -71,6 +75,54 @@ public class ConditionRouteService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** EXPRESSION 模式留痕的 router_key 快照(真实注册键不适用;列 NOT NULL) */
|
||||||
|
static final String EXPRESSION_ROUTER_KEY = "EXPRESSION";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EXPRESSION 模式(2026-10-10 四议题):按出边定义顺序对 condition 求值,首个 true 的出口名
|
||||||
|
* 即结果;逐边结果摘要落 raw_response。全 false / 求值异常 → 挂起,沿用 SERVICE 模式纪律
|
||||||
|
* (留痕 outlet=NULL + 告警 + 管理端重跑),不配默认出口。
|
||||||
|
*/
|
||||||
|
private String decideByExpression(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node,
|
||||||
|
List<String> outlets) {
|
||||||
|
Map<String, Object> env = builtinEnv(instance, node, parseVariables(instance.getFormDataJson()));
|
||||||
|
StringBuilder trace = new StringBuilder("EXPRESSION judge: ");
|
||||||
|
try {
|
||||||
|
for (FlowDefModel.Edge e : model.outEdges(node.id)) {
|
||||||
|
Object result = ExpressionEvaluator.evaluate(e.condition, env);
|
||||||
|
trace.append("[").append(e.name).append("] ").append(e.condition)
|
||||||
|
.append(" => ").append(result).append("; ");
|
||||||
|
if (Boolean.TRUE.equals(result)) {
|
||||||
|
int attempt = persist(instance, node, EXPRESSION_ROUTER_KEY, e.name, trace.toString());
|
||||||
|
log.info("[flow-center] 表达式判断命中:instance={} node={} outlet={} attempt={}",
|
||||||
|
instance.getId(), node.id, e.name, attempt);
|
||||||
|
return e.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.error("[flow-center] 表达式全部未命中(挂起):instance={} node={} outlets={} trace={}",
|
||||||
|
instance.getId(), node.id, outlets, trace);
|
||||||
|
return persistFailure(instance, node, outlets, "表达式全部未命中:" + trace);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[flow-center] 表达式求值异常(挂起):instance={} node={} {}",
|
||||||
|
instance.getId(), node.id, e.toString());
|
||||||
|
return persistFailure(instance, node, outlets, "表达式求值异常:" + e + ";" + trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 内置变量注入(EXPRESSION 求值 env;SERVICE 模式的 JudgeBody 同名字段一一对应) */
|
||||||
|
static Map<String, Object> builtinEnv(FlwInstance instance, FlowDefModel.Node node,
|
||||||
|
Map<String, Object> variables) {
|
||||||
|
variables.put("schId", instance.getSchId());
|
||||||
|
variables.put("instanceId", instance.getId());
|
||||||
|
variables.put("instanceVersion", instance.getVersion());
|
||||||
|
variables.put("bizType", instance.getBizType());
|
||||||
|
variables.put("bizId", instance.getBizId());
|
||||||
|
variables.put("nodeId", node.id);
|
||||||
|
variables.put("nodeName", node.name);
|
||||||
|
variables.put("submitterId", instance.getSubmitterId());
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
/** CONDITION 出边名即判断契约的 outlets(发布期已保证非空唯一) */
|
/** CONDITION 出边名即判断契约的 outlets(发布期已保证非空唯一) */
|
||||||
private static List<String> outletNames(FlowDefModel model, FlowDefModel.Node node) {
|
private static List<String> outletNames(FlowDefModel model, FlowDefModel.Node node) {
|
||||||
List<String> names = new ArrayList<>();
|
List<String> names = new ArrayList<>();
|
||||||
@@ -97,19 +149,21 @@ public class ConditionRouteService {
|
|||||||
|
|
||||||
private String persistFailure(FlwInstance instance, FlowDefModel.Node node, List<String> outlets,
|
private String persistFailure(FlwInstance instance, FlowDefModel.Node node, List<String> outlets,
|
||||||
String error) {
|
String error) {
|
||||||
int attempt = persist(instance, node, null, truncate(error));
|
int attempt = persist(instance, node, node.expressionJudge() ? EXPRESSION_ROUTER_KEY : node.routerKey,
|
||||||
|
null, truncate(error));
|
||||||
alertSuspended(instance, node, outlets, error, attempt);
|
alertSuspended(instance, node, outlets, error, attempt);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 落一行留痕(REQUIRES_NEW:外部调用已真实发生,不随外层回滚),返回本轮 attempt(告警 msgNo 用) */
|
/** 落一行留痕(REQUIRES_NEW:外部调用已真实发生,不随外层回滚),返回本轮 attempt(告警 msgNo 用) */
|
||||||
private int persist(FlwInstance instance, FlowDefModel.Node node, String outlet, String raw) {
|
private int persist(FlwInstance instance, FlowDefModel.Node node, String routerKeyLabel,
|
||||||
|
String outlet, String raw) {
|
||||||
FlwRouteLog logRow = new FlwRouteLog();
|
FlwRouteLog logRow = new FlwRouteLog();
|
||||||
logRow.setSchId(instance.getSchId());
|
logRow.setSchId(instance.getSchId());
|
||||||
logRow.setInstanceId(instance.getId());
|
logRow.setInstanceId(instance.getId());
|
||||||
logRow.setNodeId(node.id);
|
logRow.setNodeId(node.id);
|
||||||
logRow.setNodeName(node.name);
|
logRow.setNodeName(node.name);
|
||||||
logRow.setRouterKey(node.routerKey);
|
logRow.setRouterKey(routerKeyLabel);
|
||||||
logRow.setOutlet(outlet);
|
logRow.setOutlet(outlet);
|
||||||
logRow.setRawResponse(raw);
|
logRow.setRawResponse(raw);
|
||||||
return routeLogStore.append(logRow).getAttempt();
|
return routeLogStore.append(logRow).getAttempt();
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ public final class DefValidator {
|
|||||||
validateNotify(n, enabledDeptKeys, problems);
|
validateNotify(n, enabledDeptKeys, problems);
|
||||||
break;
|
break;
|
||||||
case FlowDefModel.TYPE_CONDITION:
|
case FlowDefModel.TYPE_CONDITION:
|
||||||
validateCondition(n, enabledRouterKeys, problems);
|
validateCondition(model, n, enabledRouterKeys, problems);
|
||||||
break;
|
break;
|
||||||
case FlowDefModel.TYPE_PARALLEL_SPLIT:
|
case FlowDefModel.TYPE_PARALLEL_SPLIT:
|
||||||
case FlowDefModel.TYPE_PARALLEL_JOIN:
|
case FlowDefModel.TYPE_PARALLEL_JOIN:
|
||||||
@@ -243,12 +243,37 @@ public final class DefValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CONDITION 条件分支节点校验(2026-09-24 §4):routerKey 必填且已注册启用——运行时才发现
|
* CONDITION 条件分支节点校验(2026-09-24 §4):SERVICE 模式 routerKey 必填且已注册启用——
|
||||||
* 无效只会挂起,发布期就拦;出边=互斥分支,出口名即判断接口的 outlets 契约(命名/唯一/
|
* 运行时才发现无效只会挂起,发布期就拦;出边=互斥分支,出口名即判断接口的 outlets 契约
|
||||||
* 同分支上下文由度约束段复用 {@link #validateOutlets} 口径)。
|
* (命名/唯一/同分支上下文由度约束段复用 {@link #validateOutlets} 口径)。
|
||||||
|
*
|
||||||
|
* <p>EXPRESSION 模式(2026-10-10 四议题)分层校验:语法错/出口缺条件 → 发布期硬拦
|
||||||
|
* (表单变量是子应用运行时传的,发布期无从核对 schema,一律放行、缺失按 null 注入求值)。
|
||||||
*/
|
*/
|
||||||
private static void validateCondition(FlowDefModel.Node n, Set<String> enabledRouterKeys,
|
private static void validateCondition(FlowDefModel model, FlowDefModel.Node n, Set<String> enabledRouterKeys,
|
||||||
List<String> problems) {
|
List<String> problems) {
|
||||||
|
if (n.judgeMode != null && !FlowDefModel.JUDGE_MODE_SERVICE.equals(n.judgeMode)
|
||||||
|
&& !FlowDefModel.JUDGE_MODE_EXPRESSION.equals(n.judgeMode)) {
|
||||||
|
problems.add(nodeLabel(n) + " 判断方式取值非法:" + n.judgeMode + "(支持 SERVICE/EXPRESSION)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n.expressionJudge()) {
|
||||||
|
for (FlowDefModel.Edge e : model.outEdges(n.id)) {
|
||||||
|
if (e.condition == null || e.condition.trim().isEmpty()) {
|
||||||
|
problems.add(nodeLabel(n) + " 的出口「" + e.name
|
||||||
|
+ "」缺少条件表达式(EXPRESSION 模式每个出口必须配置)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
warnBuiltinMisspelling(n, e);
|
||||||
|
ExpressionEvaluator.compile(e.condition.trim());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
problems.add(nodeLabel(n) + " 的出口「" + e.name + "」表达式不合法:"
|
||||||
|
+ ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (n.routerKey == null || n.routerKey.trim().isEmpty()) {
|
if (n.routerKey == null || n.routerKey.trim().isEmpty()) {
|
||||||
problems.add(nodeLabel(n) + " 缺少判断服务(routerKey)配置");
|
problems.add(nodeLabel(n) + " 缺少判断服务(routerKey)配置");
|
||||||
} else if (enabledRouterKeys != null && !enabledRouterKeys.contains(n.routerKey.trim())) {
|
} else if (enabledRouterKeys != null && !enabledRouterKeys.contains(n.routerKey.trim())) {
|
||||||
@@ -257,6 +282,34 @@ public final class DefValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 表达式内置变量白名单(ConditionRouteService.builtinEnv 注入的同名集) */
|
||||||
|
private static final Set<String> EXPRESSION_BUILTINS = new HashSet<>(java.util.Arrays.asList(
|
||||||
|
"schId", "instanceId", "instanceVersion", "bizType", "bizId",
|
||||||
|
"nodeId", "nodeName", "submitterId"));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内置变量疑似拼错提示(warn 不硬拦):变量名与某内置变量仅大小写不一致(如 schid)时
|
||||||
|
* 提示一次——拼错的表达式静默把 null 与数字比较,全部不命中挂起,很难排查。
|
||||||
|
*/
|
||||||
|
private static void warnBuiltinMisspelling(FlowDefModel.Node n, FlowDefModel.Edge e) {
|
||||||
|
try {
|
||||||
|
for (String v : ExpressionEvaluator.variableNames(e.condition.trim())) {
|
||||||
|
if (!EXPRESSION_BUILTINS.contains(v)) {
|
||||||
|
for (String b : EXPRESSION_BUILTINS) {
|
||||||
|
if (b.equalsIgnoreCase(v)) {
|
||||||
|
LOG.warn("[flow-center] 节点 {} 出口「{}」表达式变量 {} 疑似内置变量 {} 拼写错误",
|
||||||
|
n.id, e.name, v, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// 语法错时拿不到变量名,交给 compile 路径报问题
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(DefValidator.class);
|
||||||
|
|
||||||
private static void validateApproval(FlowDefModel.Node n, Set<String> enabledDeptKeys, List<String> problems) { FlowDefModel.Assignee a = n.assignee;
|
private static void validateApproval(FlowDefModel.Node n, Set<String> enabledDeptKeys, List<String> problems) { FlowDefModel.Assignee a = n.assignee;
|
||||||
if (a == null || a.type == null) {
|
if (a == null || a.type == null) {
|
||||||
problems.add(nodeLabel(n) + " 缺少 assignee 配置");
|
problems.add(nodeLabel(n) + " 缺少 assignee 配置");
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.yunhai.flowcenter.engine;
|
||||||
|
|
||||||
|
import com.googlecode.aviator.AviatorEvaluator;
|
||||||
|
import com.googlecode.aviator.AviatorEvaluatorInstance;
|
||||||
|
import com.googlecode.aviator.Expression;
|
||||||
|
import com.googlecode.aviator.Feature;
|
||||||
|
import com.googlecode.aviator.Options;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 条件路由表达式求值核心(2026-10-10 四议题 Phase 4):CONDITION 节点 judgeMode=EXPRESSION 时,
|
||||||
|
* 出边 condition 即 AviatorScript 表达式,按出边定义顺序取首个求值为 true 的出口。
|
||||||
|
* 发布期校验(DefValidator)、运行时(ConditionRouteService)、调试端点(/admin/router/eval)
|
||||||
|
* 共用本类——三处口径天然一致。
|
||||||
|
*
|
||||||
|
* <p><b>沙箱纪律(勿放宽)</b>:FEATURE_SET 只留 If(三元/条件),NewInstance/Module/Use/
|
||||||
|
* Static* 全部不在集合内即禁用;ALLOWED_CLASS_SET=空不可变集 = 禁一切 Java 类访问(反射/静态
|
||||||
|
* 方法/类字面量);表达式只能引用变量与 Aviator 内置纯函数(math/string/seq)。
|
||||||
|
*
|
||||||
|
* <p><b>缺失变量口径(宁挂起不误放行)</b>:顶层变量缺失(拼写错/表单未传)直接抛错由调用方
|
||||||
|
* 挂起——Aviator 的 nil 参与大小比较时视作最小值,{@code x <= n} 这类兜底出口会对缺失字段
|
||||||
|
* <i>静默放行</i>,必须拦住。判空请显式写 {@code x == nil}(嵌套属性缺失走 nil 语义,
|
||||||
|
* {@code form.amount == nil} 可用)。env 中显式为 null 的值仍按 nil 语义求值。
|
||||||
|
*
|
||||||
|
* <p>线程安全:{@link AviatorEvaluatorInstance} 本身线程安全,编译结果带缓存(key=表达式串)。
|
||||||
|
*/
|
||||||
|
public final class ExpressionEvaluator {
|
||||||
|
|
||||||
|
private ExpressionEvaluator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final AviatorEvaluatorInstance INSTANCE = createSandboxed();
|
||||||
|
|
||||||
|
private static AviatorEvaluatorInstance createSandboxed() {
|
||||||
|
AviatorEvaluatorInstance instance = AviatorEvaluator.newInstance();
|
||||||
|
instance.setOption(Options.FEATURE_SET, Feature.asSet(Feature.If));
|
||||||
|
instance.setOption(Options.ALLOWED_CLASS_SET, Collections.emptySet());
|
||||||
|
instance.setOption(Options.NIL_WHEN_PROPERTY_NOT_FOUND, Boolean.TRUE);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 求值(编译缓存复用)。顶层变量缺失直接抛错(调用方挂起)——Aviator 对缺失顶层变量一律
|
||||||
|
* 按 nil 求值且不可配置,而 nil 视作最小值会让 {@code x <= n} 这类兜底出口静默放行。
|
||||||
|
* 嵌套属性(form.amount)缺失走 nil 语义,判空请显式 {@code form.amount == nil}。
|
||||||
|
*
|
||||||
|
* @return 求值结果;调用方仅把 Boolean.TRUE 视为命中(null/非布尔一律不命中——宁挂起不误放行)
|
||||||
|
*/
|
||||||
|
public static Object evaluate(String expression, Map<String, Object> env) {
|
||||||
|
Expression compiled = compile(expression);
|
||||||
|
for (String name : compiled.getVariableNames()) {
|
||||||
|
if (!env.containsKey(name)) {
|
||||||
|
throw new IllegalStateException("表达式变量未提供:" + name
|
||||||
|
+ "(拼写错误或表单未传;宁挂起不误放行,不按 nil 求值)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compiled.execute(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 语法校验/预编译(发布期拦语法错;与运行时走同一编译路径) */
|
||||||
|
public static Expression compile(String expression) {
|
||||||
|
return INSTANCE.compile(expression, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表达式引用的顶层变量名(发布期"内置变量疑似拼错"提示用) */
|
||||||
|
public static List<String> variableNames(String expression) {
|
||||||
|
return compile(expression).getVariableNames();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ public class FlowDefModel {
|
|||||||
*/
|
*/
|
||||||
public static final String TYPE_CONDITION = "CONDITION";
|
public static final String TYPE_CONDITION = "CONDITION";
|
||||||
|
|
||||||
|
/** CONDITION 判断方式(2026-10-10 四议题):外部判断服务(缺省=旧口径,routerKey 寻址) */
|
||||||
|
public static final String JUDGE_MODE_SERVICE = "SERVICE";
|
||||||
|
/** CONDITION 判断方式:内置表达式引擎(出边 condition 即 AviatorScript 表达式) */
|
||||||
|
public static final String JUDGE_MODE_EXPRESSION = "EXPRESSION";
|
||||||
|
|
||||||
/** 会签模式 */
|
/** 会签模式 */
|
||||||
public static final String COUNTERSIGN_ALL = "ALL";
|
public static final String COUNTERSIGN_ALL = "ALL";
|
||||||
public static final String COUNTERSIGN_ANY = "ANY";
|
public static final String COUNTERSIGN_ANY = "ANY";
|
||||||
@@ -63,6 +68,13 @@ public class FlowDefModel {
|
|||||||
public Assignee receivers;
|
public Assignee receivers;
|
||||||
/** CONDITION 判断服务注册键(flw_router.router_key,发布期校验须已注册且启用) */
|
/** CONDITION 判断服务注册键(flw_router.router_key,发布期校验须已注册且启用) */
|
||||||
public String routerKey;
|
public String routerKey;
|
||||||
|
/** CONDITION 判断方式:SERVICE(缺省,routerKey 外部服务)|EXPRESSION(出边 condition 表达式) */
|
||||||
|
public String judgeMode;
|
||||||
|
|
||||||
|
/** 是否表达式判断(null 视作 SERVICE,旧 contentJson 无此字段天然走服务口径) */
|
||||||
|
public boolean expressionJudge() {
|
||||||
|
return JUDGE_MODE_EXPRESSION.equals(judgeMode);
|
||||||
|
}
|
||||||
/** ALL(默认)|ANY */
|
/** ALL(默认)|ANY */
|
||||||
public String countersign;
|
public String countersign;
|
||||||
/** PARALLEL_JOIN 汇聚条件,一期仅 ALL */
|
/** PARALLEL_JOIN 汇聚条件,一期仅 ALL */
|
||||||
|
|||||||
@@ -38,13 +38,14 @@ public class RouteJudgeClient {
|
|||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断请求体(契约冻结:query 带 schId,业务方按 outlets 返回 {outlet}) */
|
/** 判断请求体(契约冻结:query 带 schId,业务方按 outlets 返回 {outlet};submitterId 2026-10-10 增补,老实现忽略即可) */
|
||||||
public static class JudgeBody {
|
public static class JudgeBody {
|
||||||
public final Long schId;
|
public final Long schId;
|
||||||
public final Long instanceId;
|
public final Long instanceId;
|
||||||
public final Integer instanceVersion;
|
public final Integer instanceVersion;
|
||||||
public final String bizType;
|
public final String bizType;
|
||||||
public final String bizId;
|
public final String bizId;
|
||||||
|
public final Integer submitterId;
|
||||||
public final String nodeId;
|
public final String nodeId;
|
||||||
public final String nodeName;
|
public final String nodeName;
|
||||||
public final String routerKey;
|
public final String routerKey;
|
||||||
@@ -52,13 +53,14 @@ public class RouteJudgeClient {
|
|||||||
public final Map<String, Object> variables;
|
public final Map<String, Object> variables;
|
||||||
|
|
||||||
public JudgeBody(Long schId, Long instanceId, Integer instanceVersion, String bizType, String bizId,
|
public JudgeBody(Long schId, Long instanceId, Integer instanceVersion, String bizType, String bizId,
|
||||||
String nodeId, String nodeName, String routerKey, List<String> outlets,
|
Integer submitterId, String nodeId, String nodeName, String routerKey,
|
||||||
Map<String, Object> variables) {
|
List<String> outlets, Map<String, Object> variables) {
|
||||||
this.schId = schId;
|
this.schId = schId;
|
||||||
this.instanceId = instanceId;
|
this.instanceId = instanceId;
|
||||||
this.instanceVersion = instanceVersion;
|
this.instanceVersion = instanceVersion;
|
||||||
this.bizType = bizType;
|
this.bizType = bizType;
|
||||||
this.bizId = bizId;
|
this.bizId = bizId;
|
||||||
|
this.submitterId = submitterId;
|
||||||
this.nodeId = nodeId;
|
this.nodeId = nodeId;
|
||||||
this.nodeName = nodeName;
|
this.nodeName = nodeName;
|
||||||
this.routerKey = routerKey;
|
this.routerKey = routerKey;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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 四议题·应用设置):本校"哪些应用可对接流程"的白名单。
|
||||||
|
* 行来源=应用设置页从学校池(UserBase /oapi/app/list-by-school,学校组单点已开通)同步,
|
||||||
|
* 同步即 enabled=1;管理员可停用(停用后 defSave 的归属应用校验拦截新引用)。
|
||||||
|
* app_name 是学校池快照(含学校别名),每次同步静默刷新。
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "flw_app",
|
||||||
|
uniqueConstraints = @UniqueConstraint(name = "uk_flw_app_sch_key", columnNames = {"sch_id", "app_key"}),
|
||||||
|
indexes = @Index(name = "idx_flw_app_sch_enabled", columnList = "sch_id, enabled"))
|
||||||
|
public class FlwApp extends BaseEntity {
|
||||||
|
|
||||||
|
/** 租户键 */
|
||||||
|
@Column(name = "sch_id", nullable = false)
|
||||||
|
private Long schId;
|
||||||
|
|
||||||
|
/** 子应用 appKey(UserBase application_info.app_key 的字符串形态,与 flw_def.app_key 同口径) */
|
||||||
|
@Column(name = "app_key", nullable = false, length = 64)
|
||||||
|
private String appKey;
|
||||||
|
|
||||||
|
/** 应用名快照(学校池返回值,含学校别名;同步时刷新) */
|
||||||
|
@Column(name = "app_name", nullable = false, length = 128)
|
||||||
|
private String appName;
|
||||||
|
|
||||||
|
/** 1=对接流程(白名单内);0=停用(defSave 校验拦截) */
|
||||||
|
@Column(name = "enabled", nullable = false)
|
||||||
|
private Integer enabled = 1;
|
||||||
|
|
||||||
|
/** 展示排序(学校池 orderNumber 快照,可空=0) */
|
||||||
|
@Column(name = "sort_no")
|
||||||
|
private Integer sortNo;
|
||||||
|
}
|
||||||
@@ -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,19 @@
|
|||||||
|
package com.yunhai.flowcenter.repository;
|
||||||
|
|
||||||
|
import com.yunhai.flowcenter.entity.FlwApp;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface FlwAppRepository extends JpaRepository<FlwApp, Long> {
|
||||||
|
|
||||||
|
List<FlwApp> findBySchIdOrderBySortNoAscIdAsc(Long schId);
|
||||||
|
|
||||||
|
List<FlwApp> findBySchIdAndEnabledOrderBySortNoAscIdAsc(Long schId, Integer enabled);
|
||||||
|
|
||||||
|
/** defSave 归属应用白名单校验:本校启用集合是否包含该 appKey */
|
||||||
|
@Query("select count(a) > 0 from FlwApp a where a.schId = :schId and a.appKey = :appKey and a.enabled = 1")
|
||||||
|
boolean isEnabledApp(@Param("schId") Long schId, @Param("appKey") String appKey);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.yunhai.flowcenter.repository;
|
||||||
|
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDeptMember;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface FlwDeptMemberRepository extends JpaRepository<FlwDeptMember, Long> {
|
||||||
|
|
||||||
|
List<FlwDeptMember> findByDeptIdOrderByIdAsc(Long deptId);
|
||||||
|
|
||||||
|
List<FlwDeptMember> findByDeptIdAndStatusOrderByIdAsc(Long deptId, Integer status);
|
||||||
|
|
||||||
|
Optional<FlwDeptMember> findByDeptIdAndUserId(Long deptId, Integer userId);
|
||||||
|
|
||||||
|
void deleteByDeptId(Long deptId);
|
||||||
|
|
||||||
|
long countByDeptIdAndStatus(Long deptId, Integer status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.yunhai.flowcenter.repository;
|
||||||
|
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDept;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface FlwDeptRepository extends JpaRepository<FlwDept, Long> {
|
||||||
|
|
||||||
|
List<FlwDept> findBySchIdOrderByIdAsc(Long schId);
|
||||||
|
|
||||||
|
List<FlwDept> findBySchIdAndStatusOrderByIdAsc(Long schId, Integer status);
|
||||||
|
|
||||||
|
Optional<FlwDept> findBySchIdAndId(Long schId, Long id);
|
||||||
|
|
||||||
|
/** 一键拉取幂等:同源 UserBase 组只建一行 */
|
||||||
|
Optional<FlwDept> findBySchIdAndSourceGroupId(Long schId, Integer sourceGroupId);
|
||||||
|
}
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
package com.yunhai.flowcenter.service;
|
package com.yunhai.flowcenter.service;
|
||||||
|
|
||||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 科室口径文案的唯一出口(2026-09-18):DEPT_KEY 节点的"科室名"取值只在 {@link #mappedDeptName}
|
* 科室口径文案的唯一出口(2026-09-18 建,2026-10-10 切源 flw_dept):DEPT_KEY 节点的"科室名"
|
||||||
* 里实现一次——流程图(FlowDiagramService 的科室视图)与流程轨迹(FlowQueryService 的科室口径)
|
* 取值只在这里实现一次——流程图(FlowDiagramService 的科室视图)与流程轨迹(FlowQueryService
|
||||||
* 共用,避免两边对同一份映射表给出不同名字。
|
* 的科室口径)共用,避免两边对同一份科室表给出不同名字。
|
||||||
*/
|
*/
|
||||||
final class DeptLabelSupport {
|
final class DeptLabelSupport {
|
||||||
|
|
||||||
@@ -15,18 +11,14 @@ final class DeptLabelSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 映射行里的科室名:取第一个非空 deptName(一个 deptKey 的表现通常一致;科室名可空,
|
* 科室名(flw_dept.dept_name):科室不存在/已删返回 null,由调用方决定回落链
|
||||||
* 空则返回 null 由调用方决定回落链)。
|
* (科室不存在≠科室停用——停用科室仍有名字,轨迹/图示照常显示)。
|
||||||
*/
|
*/
|
||||||
static String mappedDeptName(List<FlwAssigneeMap> maps) {
|
static String deptName(FlowDeptAdminService deptAdminService, Long schId, String deptKey) {
|
||||||
if (maps == null) {
|
try {
|
||||||
|
return deptAdminService.deptName(schId, deptKey);
|
||||||
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
for (FlwAssigneeMap m : maps) {
|
|
||||||
if (m.getDeptName() != null && !m.getDeptName().trim().isEmpty()) {
|
|
||||||
return m.getDeptName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.yunhai.flowcenter.service;
|
||||||
|
|
||||||
|
import com.yunhai.flowcenter.entity.FlwApp;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwAppRepository;
|
||||||
|
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||||||
|
import com.yunhai.platform.utils.model.BoaApp;
|
||||||
|
import com.yunhai.platform.utils.model.BoaAppType;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用设置·应用管理(2026-10-10 四议题):flw_app 白名单的同步与开关。
|
||||||
|
*
|
||||||
|
* <p><b>源池口径(拍板 D7)</b>:UserBase {@code /oapi/app/list-by-school}——学校组单点已开通
|
||||||
|
* (sys_group_app_rel,不并子组/角色模块),clientType 传 null 不过滤端(拍板 D3,避免漏纯移动端应用)。
|
||||||
|
* 同步即入白名单 enabled=1(拍板 D2:只做最优实现,不做存量 appKey 兜底——不在学校池的应用
|
||||||
|
* 进不了白名单,defSave 引用会被拦,属拍板接受的后果);池内应用名变化静默刷新(学校别名);
|
||||||
|
* 学校池已移除的本地行保留(enabled 交管理员处置)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FlowAppAdminService {
|
||||||
|
|
||||||
|
private final FlwAppRepository appRepository;
|
||||||
|
private final OpenApiAppClient appClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表(管理端应用设置页)。附带一次静默学校池同步:UserBase 不可达/学校未开通任何应用时
|
||||||
|
* 降级只返回本地行(首次使用前 flw_app 为空表是正常态,管理员可先在 UserBase 侧开通)。
|
||||||
|
*/
|
||||||
|
public List<FlwApp> list(Long schId) {
|
||||||
|
try {
|
||||||
|
syncFromSchoolPool(schId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[flow-center] 学校池同步失败,降级返回本地应用行:sch={} {}", schId, e.toString());
|
||||||
|
}
|
||||||
|
return appRepository.findBySchIdOrderBySortNoAscIdAsc(schId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开关某应用是否对接流程(停用后 defSave 归属应用校验拦截新引用;存量定义不受影响) */
|
||||||
|
@Transactional
|
||||||
|
public FlwApp enable(Long schId, Long id, boolean enabled) {
|
||||||
|
FlwApp app = appRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new com.yunhai.platform.apistd.BizException(
|
||||||
|
com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR, "应用行不存在:" + id));
|
||||||
|
if (!app.getSchId().equals(schId)) {
|
||||||
|
throw new com.yunhai.platform.apistd.BizException(
|
||||||
|
com.yunhai.platform.utils.exception.ErrorCode.FORBIDDEN, "应用行不属于该学校");
|
||||||
|
}
|
||||||
|
app.setEnabled(enabled ? 1 : 0);
|
||||||
|
return appRepository.save(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 学校池 → flw_app diff 同步(新增 enabled=1 / 改名静默刷新 / 池外保留)。
|
||||||
|
* 幂等:按 (sch_id, app_key) 唯一键 upsert。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void syncFromSchoolPool(Long schId) {
|
||||||
|
List<BoaAppType> pool = appClient.getSchoolApp(schId.intValue(), null);
|
||||||
|
Map<String, BoaApp> poolApps = new HashMap<>();
|
||||||
|
for (BoaAppType type : pool) {
|
||||||
|
for (BoaApp app : type.getApps()) {
|
||||||
|
if (app.getAppKey() != null) {
|
||||||
|
poolApps.put(String.valueOf(app.getAppKey()), app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, FlwApp> local = new HashMap<>();
|
||||||
|
for (FlwApp row : appRepository.findBySchIdOrderBySortNoAscIdAsc(schId)) {
|
||||||
|
local.put(row.getAppKey(), row);
|
||||||
|
}
|
||||||
|
int inserted = 0;
|
||||||
|
int refreshed = 0;
|
||||||
|
for (Map.Entry<String, BoaApp> e : poolApps.entrySet()) {
|
||||||
|
BoaApp poolApp = e.getValue();
|
||||||
|
FlwApp row = local.get(e.getKey());
|
||||||
|
if (row == null) {
|
||||||
|
row = new FlwApp();
|
||||||
|
row.setSchId(schId);
|
||||||
|
row.setAppKey(e.getKey());
|
||||||
|
row.setAppName(poolApp.getName() == null || poolApp.getName().isEmpty()
|
||||||
|
? e.getKey() : poolApp.getName());
|
||||||
|
row.setEnabled(1);
|
||||||
|
row.setSortNo(poolApp.getOrderNumber() == null ? 0 : poolApp.getOrderNumber());
|
||||||
|
appRepository.save(row);
|
||||||
|
inserted++;
|
||||||
|
} else if (poolApp.getName() != null && !poolApp.getName().isEmpty()
|
||||||
|
&& !poolApp.getName().equals(row.getAppName())) {
|
||||||
|
// 学校别名变化:静默刷新名称快照(开关不动)
|
||||||
|
row.setAppName(poolApp.getName());
|
||||||
|
row.setSortNo(poolApp.getOrderNumber() == null ? row.getSortNo() : poolApp.getOrderNumber());
|
||||||
|
appRepository.save(row);
|
||||||
|
refreshed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inserted > 0 || refreshed > 0) {
|
||||||
|
log.info("[flow-center] 学校池同步:sch={} 池内 {} 应用,新增 {} 行,刷新名 {} 行",
|
||||||
|
schId, poolApps.size(), inserted, refreshed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,9 +33,10 @@ import java.util.stream.Collectors;
|
|||||||
public class FlowDefAdminService {
|
public class FlowDefAdminService {
|
||||||
|
|
||||||
private final FlwDefRepository defRepository;
|
private final FlwDefRepository defRepository;
|
||||||
private final FlwAssigneeMapRepository mapRepository;
|
|
||||||
private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
|
private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
|
||||||
private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository;
|
private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository;
|
||||||
|
private final com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
|
||||||
|
private final FlowDeptAdminService deptAdminService;
|
||||||
private final FlowEngine flowEngine;
|
private final FlowEngine flowEngine;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
@@ -79,6 +80,12 @@ public class FlowDefAdminService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public FlwDef save(Long schId, Long id, String code, String name, String contentJson, String baseGuid, String appKey) {
|
public FlwDef save(Long schId, Long id, String code, String name, String contentJson, String baseGuid, String appKey) {
|
||||||
String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
|
String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
|
||||||
|
// 归属应用白名单(2026-10-10 应用设置):显式传入非空时必须 ∈ flw_app(sch, enabled=1);
|
||||||
|
// 不传(升版本继承源线)不拦。不在学校池/未开启的应用由此拦下(拍板 D2:不做存量兜底)
|
||||||
|
if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||||
|
"归属应用未开启或不存在(" + cleanAppKey + "):请在 流程管理 > 应用设置 中开启后重试");
|
||||||
|
}
|
||||||
// JSON 先解析再落库(语法错误不进库)
|
// JSON 先解析再落库(语法错误不进库)
|
||||||
FlowDefModel parsed = FlowDefModel.parse(objectMapper, contentJson);
|
FlowDefModel parsed = FlowDefModel.parse(objectMapper, contentJson);
|
||||||
if (code == null || code.trim().isEmpty() || name == null || name.trim().isEmpty()) {
|
if (code == null || code.trim().isEmpty() || name == null || name.trim().isEmpty()) {
|
||||||
@@ -191,9 +198,52 @@ public class FlowDefAdminService {
|
|||||||
return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
|
return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元数据原地编辑(2026-10-10 四议题·议题2,拍板 D4):已发布/停用/草稿均可只改 name/appKey
|
||||||
|
* 而<b>不升版本</b>——结构改动仍走 save 的升版本路径,两者完全隔离(本方法不碰
|
||||||
|
* code/flow_guid/version/status,不进升版本分支)。appKey 校验同 save 口径(显式传入须在白名单)。
|
||||||
|
* content_json 顶层 name 同步更新(只动顶层字段不动结构,消除"行 name 与 JSON name 的脏窗口";
|
||||||
|
* 亦修复 API 直调方经 parsed.name 覆盖行字段时读到旧名的口径)。
|
||||||
|
* 并发说明:与升版本路径无共享可变行(升版本建新行),不加锁。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public FlwDef updateMeta(Long schId, Long id, String name, String appKey) {
|
||||||
|
FlwDef def = requireDef(schId, id);
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "流程名称不能为空");
|
||||||
|
}
|
||||||
|
String cleanName = name.trim();
|
||||||
|
String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
|
||||||
|
if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||||
|
"归属应用未开启或不存在(" + cleanAppKey + "):请在 流程管理 > 应用设置 中开启后重试");
|
||||||
|
}
|
||||||
|
def.setName(cleanName);
|
||||||
|
def.setAppKey(cleanAppKey);
|
||||||
|
def.setContentJson(syncTopLevelName(def.getContentJson(), cleanName));
|
||||||
|
try {
|
||||||
|
return defRepository.save(def);
|
||||||
|
} catch (DataIntegrityViolationException e) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突:该流程可能正被他人同时编辑,请刷新后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** contentJson 顶层 name 同步(ObjectNode 保字段顺序;结构零改动),返回新 JSON 串 */
|
||||||
|
private String syncTopLevelName(String contentJson, String name) {
|
||||||
|
try {
|
||||||
|
com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode)
|
||||||
|
objectMapper.readTree(contentJson);
|
||||||
|
root.put("name", name);
|
||||||
|
return objectMapper.writeValueAsString(root);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// contentJson 在 save 期已保证可解析;防御口径:解析失败不同步(行 name 已更新,JSON 维持原值)
|
||||||
|
return contentJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Set<String> enabledDeptKeys(Long schId) {
|
private Set<String> enabledDeptKeys(Long schId) {
|
||||||
return mapRepository.findEnabledBySchId(schId).stream()
|
// 2026-10-10 科室管理切源:发布校验白名单改查 flw_dept/flw_dept_member(拍板 D1)
|
||||||
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet());
|
return deptAdminService.enabledDeptKeys(schId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */
|
/** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package com.yunhai.flowcenter.service;
|
||||||
|
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDept;
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDeptMember;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwDeptMemberRepository;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwDeptRepository;
|
||||||
|
import com.yunhai.platform.apistd.BizException;
|
||||||
|
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 科室管理(2026-10-10 四议题·议题3,拍板 D1 选 B):"科室组+成员"模型取代单用户映射行。
|
||||||
|
* deptKey=本地自增主键;不做存量迁移(旧 flw_assignee_map 的 deptKey=UserBase 组 id,切源后
|
||||||
|
* 解析失败属预期,需在本科室管理重建后重新配置流程定义)。一键拉取保留为初始化语义
|
||||||
|
* (source_group_id 幂等防重复建组,重复拉取=刷新组名与成员集)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FlowDeptAdminService {
|
||||||
|
|
||||||
|
private final FlwDeptRepository deptRepository;
|
||||||
|
private final FlwDeptMemberRepository memberRepository;
|
||||||
|
/** 复用旧映射服务的组树拉取(previewGroupDepts 口径:教师组容器子组=科室+成员)与用户检索 */
|
||||||
|
private final FlowMapAdminService mapAdminService;
|
||||||
|
|
||||||
|
// ==================== 读模型(编辑器/校验共用) ====================
|
||||||
|
|
||||||
|
/** 科室列表(编辑器 ctx.depts 结构:deptKey=id 字符串;count=启用成员数,停用科室恒 0) */
|
||||||
|
public List<DeptVo> list(Long schId) {
|
||||||
|
List<DeptVo> out = new ArrayList<>();
|
||||||
|
for (FlwDept dept : deptRepository.findBySchIdOrderByIdAsc(schId)) {
|
||||||
|
DeptVo vo = new DeptVo();
|
||||||
|
vo.setDeptKey(String.valueOf(dept.getId()));
|
||||||
|
vo.setDeptName(dept.getDeptName());
|
||||||
|
vo.setStatus(dept.getStatus());
|
||||||
|
vo.setCount(dept.getStatus() == 0
|
||||||
|
? (int) memberRepository.countByDeptIdAndStatus(dept.getId(), 0) : 0);
|
||||||
|
out.add(vo);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发布校验白名单:启用且有 ≥1 启用成员的科室 deptKey 全集(替代旧 enabledDeptKeys) */
|
||||||
|
public Set<String> enabledDeptKeys(Long schId) {
|
||||||
|
Set<String> keys = new HashSet<>();
|
||||||
|
for (FlwDept dept : deptRepository.findBySchIdAndStatusOrderByIdAsc(schId, 0)) {
|
||||||
|
if (memberRepository.countByDeptIdAndStatus(dept.getId(), 0) > 0) {
|
||||||
|
keys.add(String.valueOf(dept.getId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 办理人解析(AssigneeResolver/图示/轨迹共用,宽容口径):deptKey 不存在或科室停用返回
|
||||||
|
* 空列表(旧 findEnabled 同语义——示意图/轨迹渲染遇已删科室不炸;强口径由调用方判空抛) */
|
||||||
|
public List<FlwDeptMember> resolveEnabled(Long schId, String deptKey) {
|
||||||
|
FlwDept dept = findDept(schId, deptKey);
|
||||||
|
if (dept == null || dept.getStatus() != 0) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return memberRepository.findByDeptIdAndStatusOrderByIdAsc(dept.getId(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 科室名(DeptLabelSupport 唯一出口:流程图科室视图 + 轨迹富化) */
|
||||||
|
public String deptName(Long schId, String deptKey) {
|
||||||
|
FlwDept dept = findDept(schId, deptKey);
|
||||||
|
return dept == null ? null : dept.getDeptName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 科室 CRUD ====================
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public FlwDept create(Long schId, String deptName) {
|
||||||
|
String name = requireName(deptName);
|
||||||
|
FlwDept dept = new FlwDept();
|
||||||
|
dept.setSchId(schId);
|
||||||
|
dept.setDeptName(name);
|
||||||
|
dept.setStatus(0);
|
||||||
|
return deptRepository.save(dept);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public FlwDept rename(Long schId, Long id, String deptName) {
|
||||||
|
FlwDept dept = requireDeptById(schId, id);
|
||||||
|
dept.setDeptName(requireName(deptName));
|
||||||
|
return deptRepository.save(dept);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停用=科室级(成员不动,恢复即回);停用科室解析不出办理人、发布校验拦新引用 */
|
||||||
|
@Transactional
|
||||||
|
public FlwDept setStatus(Long schId, Long id, boolean enabled) {
|
||||||
|
FlwDept dept = requireDeptById(schId, id);
|
||||||
|
dept.setStatus(enabled ? 0 : 1);
|
||||||
|
return deptRepository.save(dept);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除科室(级联物理删成员;被流程定义引用的 deptKey 将解析失败——发布校验/办理时会拦) */
|
||||||
|
@Transactional
|
||||||
|
public void delete(Long schId, Long id) {
|
||||||
|
FlwDept dept = requireDeptById(schId, id);
|
||||||
|
memberRepository.deleteByDeptId(dept.getId());
|
||||||
|
deptRepository.delete(dept);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 成员维护 ====================
|
||||||
|
|
||||||
|
public List<FlwDeptMember> members(Long schId, Long deptId) {
|
||||||
|
requireDeptById(schId, deptId);
|
||||||
|
return memberRepository.findByDeptIdOrderByIdAsc(deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量加成员(姓名经 UserBase 补全,失败降级 #id;已存在跳过) */
|
||||||
|
@Transactional
|
||||||
|
public int memberAdd(Long schId, Long deptId, List<Integer> userIds) {
|
||||||
|
FlwDept dept = requireDeptById(schId, deptId);
|
||||||
|
int added = 0;
|
||||||
|
for (Integer userId : userIds == null ? new ArrayList<Integer>() : userIds) {
|
||||||
|
if (userId == null || memberRepository.findByDeptIdAndUserId(dept.getId(), userId).isPresent()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FlwDeptMember m = new FlwDeptMember();
|
||||||
|
m.setDeptId(dept.getId());
|
||||||
|
m.setUserId(userId);
|
||||||
|
m.setUserName(safeName(userId));
|
||||||
|
m.setStatus(0);
|
||||||
|
memberRepository.save(m);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void memberRemove(Long schId, Long memberId) {
|
||||||
|
FlwDeptMember m = memberRepository.findById(memberId)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "成员行不存在:" + memberId));
|
||||||
|
requireDeptById(schId, m.getDeptId());
|
||||||
|
memberRepository.delete(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 一键拉取(初始化语义) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取导入:按 {@link FlowMapAdminService#previewGroupDepts} 口径 upsert——
|
||||||
|
* source_group_id 命中则刷新组名+成员集(删除不在拉取集内的旧成员),未命中新建+灌成员。
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public GroupImportResultVo importGroups(Long schId) {
|
||||||
|
List<FlowMapAdminService.GroupDeptVo> depts = mapAdminService.previewGroupDepts(schId);
|
||||||
|
GroupImportResultVo result = new GroupImportResultVo();
|
||||||
|
for (FlowMapAdminService.GroupDeptVo dept : depts) {
|
||||||
|
Integer sourceGroupId = Integer.valueOf(dept.getDeptKey()); // 预览口径:deptKey=UserBase 组 id
|
||||||
|
FlwDept row = deptRepository.findBySchIdAndSourceGroupId(schId, sourceGroupId).orElse(null);
|
||||||
|
if (row == null) {
|
||||||
|
row = new FlwDept();
|
||||||
|
row.setSchId(schId);
|
||||||
|
row.setSourceGroupId(sourceGroupId);
|
||||||
|
row.setStatus(0);
|
||||||
|
result.setCreated(result.getCreated() + 1);
|
||||||
|
} else {
|
||||||
|
result.setRefreshed(result.getRefreshed() + 1);
|
||||||
|
}
|
||||||
|
row.setDeptName(dept.getDeptName());
|
||||||
|
row = deptRepository.save(row);
|
||||||
|
// 成员集全量对齐(uk 幂等 + 删不在集内的旧成员)
|
||||||
|
Set<Integer> pulled = new HashSet<>();
|
||||||
|
for (FlowMapAdminService.MemberVo m : dept.getUsers()) {
|
||||||
|
if (m.getUserId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pulled.add(m.getUserId());
|
||||||
|
Optional<FlwDeptMember> existing = memberRepository.findByDeptIdAndUserId(row.getId(), m.getUserId());
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
FlwDeptMember em = existing.get();
|
||||||
|
em.setUserName(m.getUserName() == null ? "" : m.getUserName());
|
||||||
|
em.setStatus(0);
|
||||||
|
memberRepository.save(em);
|
||||||
|
} else {
|
||||||
|
FlwDeptMember nm = new FlwDeptMember();
|
||||||
|
nm.setDeptId(row.getId());
|
||||||
|
nm.setUserId(m.getUserId());
|
||||||
|
nm.setUserName(m.getUserName() == null ? "" : m.getUserName());
|
||||||
|
nm.setStatus(0);
|
||||||
|
memberRepository.save(nm);
|
||||||
|
result.setUsers(result.getUsers() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (FlwDeptMember old : memberRepository.findByDeptIdOrderByIdAsc(row.getId())) {
|
||||||
|
if (!pulled.contains(old.getUserId())) {
|
||||||
|
memberRepository.delete(old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("[flow-center] 科室一键拉取:sch={} 拉取 {} 科室(新建 {} 刷新 {}),新增成员 {}",
|
||||||
|
schId, depts.size(), result.getCreated(), result.getRefreshed(), result.getUsers());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 内部 ====================
|
||||||
|
|
||||||
|
private FlwDept requireDeptById(Long schId, Long id) {
|
||||||
|
FlwDept dept = deptRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "科室不存在:" + id));
|
||||||
|
if (!dept.getSchId().equals(schId)) {
|
||||||
|
throw new BizException(ErrorCode.FORBIDDEN, "科室不属于该学校");
|
||||||
|
}
|
||||||
|
return dept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwDept requireDept(Long schId, String deptKey) {
|
||||||
|
FlwDept dept = findDept(schId, deptKey);
|
||||||
|
if (dept == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "科室不存在:deptKey=" + deptKey);
|
||||||
|
}
|
||||||
|
return dept;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwDept findDept(Long schId, String deptKey) {
|
||||||
|
if (deptKey == null || deptKey.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return deptRepository.findBySchIdAndId(schId, Long.valueOf(deptKey.trim())).orElse(null);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireName(String deptName) {
|
||||||
|
if (deptName == null || deptName.trim().isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "科室名称不能为空");
|
||||||
|
}
|
||||||
|
return deptName.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeName(Integer userId) {
|
||||||
|
try {
|
||||||
|
return mapAdminService.lookupUser(userId).getName();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "#" + userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== VO ====================
|
||||||
|
|
||||||
|
/** 编辑器/管理页科室行(deptKey=自增 id 字符串,与 DeptOption 前端结构对齐) */
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class DeptVo {
|
||||||
|
private String deptKey;
|
||||||
|
private String deptName;
|
||||||
|
private Integer status;
|
||||||
|
private int count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class GroupImportResultVo {
|
||||||
|
private int created;
|
||||||
|
private int refreshed;
|
||||||
|
private int users;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package com.yunhai.flowcenter.service;
|
|||||||
|
|
||||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
import com.yunhai.flowcenter.entity.FlwDeptMember;
|
||||||
import com.yunhai.flowcenter.entity.FlwCc;
|
import com.yunhai.flowcenter.entity.FlwCc;
|
||||||
import com.yunhai.flowcenter.entity.FlwDef;
|
import com.yunhai.flowcenter.entity.FlwDef;
|
||||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||||
@@ -10,7 +10,6 @@ import com.yunhai.flowcenter.entity.FlwRouteLog;
|
|||||||
import com.yunhai.flowcenter.entity.FlwTask;
|
import com.yunhai.flowcenter.entity.FlwTask;
|
||||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||||
import com.yunhai.flowcenter.model.FlowModels;
|
import com.yunhai.flowcenter.model.FlowModels;
|
||||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
|
||||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||||
@@ -53,7 +52,7 @@ public class FlowDiagramService {
|
|||||||
private static final String USERS_HINT_DEPT_VIEW = "指定办理人";
|
private static final String USERS_HINT_DEPT_VIEW = "指定办理人";
|
||||||
|
|
||||||
private final FlwDefRepository defRepository;
|
private final FlwDefRepository defRepository;
|
||||||
private final FlwAssigneeMapRepository mapRepository;
|
private final FlowDeptAdminService deptAdminService;
|
||||||
private final FlwInstanceRepository instanceRepository;
|
private final FlwInstanceRepository instanceRepository;
|
||||||
private final FlwTaskRepository taskRepository;
|
private final FlwTaskRepository taskRepository;
|
||||||
private final FlwCcRepository ccRepository;
|
private final FlwCcRepository ccRepository;
|
||||||
@@ -222,9 +221,9 @@ public class FlowDiagramService {
|
|||||||
vo.setAssigneeType(r.type);
|
vo.setAssigneeType(r.type);
|
||||||
vo.setDeptKey(r.deptKey);
|
vo.setDeptKey(r.deptKey);
|
||||||
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(r.type)) {
|
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(r.type)) {
|
||||||
List<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, r.deptKey);
|
List<FlwDeptMember> members = deptAdminService.resolveEnabled(schId, r.deptKey);
|
||||||
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + maps.size() + "人"
|
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送科室 · " + members.size() + "人"
|
||||||
: "抄送 " + namesWithSuffix(memberNames(maps), new ArrayList<>()));
|
: "抄送 " + namesWithSuffix(memberNames(members), new ArrayList<>()));
|
||||||
} else if (FlowDefModel.ASSIGNEE_USERS.equals(r.type)) {
|
} else if (FlowDefModel.ASSIGNEE_USERS.equals(r.type)) {
|
||||||
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送指定人员"
|
vo.setSubLabel(MODE_DEPT.equals(mode) ? "抄送指定人员"
|
||||||
: "抄送 " + namesWithSuffix(userNames(r.userIds), new ArrayList<>()));
|
: "抄送 " + namesWithSuffix(userNames(r.userIds), new ArrayList<>()));
|
||||||
@@ -243,14 +242,14 @@ public class FlowDiagramService {
|
|||||||
vo.setAssigneeType(a.type);
|
vo.setAssigneeType(a.type);
|
||||||
vo.setDeptKey(a.deptKey);
|
vo.setDeptKey(a.deptKey);
|
||||||
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
|
if (FlowDefModel.ASSIGNEE_DEPT_KEY.equals(a.type)) {
|
||||||
List<FlwAssigneeMap> maps = mapRepository.findEnabled(schId, a.deptKey);
|
List<FlwDeptMember> members = deptAdminService.resolveEnabled(schId, a.deptKey);
|
||||||
if (MODE_USER.equals(mode)) {
|
if (MODE_USER.equals(mode)) {
|
||||||
vo.setSubLabel(namesWithSuffix(memberNames(maps), suffixes(n)));
|
vo.setSubLabel(namesWithSuffix(memberNames(members), suffixes(n)));
|
||||||
} else {
|
} else {
|
||||||
// dept 视图与 auto 的 DEPT_KEY 自适应同文案:科室名 + 启用映射人数
|
// dept 视图与 auto 的 DEPT_KEY 自适应同文案:科室名 + 启用成员人数
|
||||||
vo.setLabel(deptLabel(n, a, maps));
|
vo.setLabel(deptLabel(n, a, schId));
|
||||||
List<String> parts = new ArrayList<>();
|
List<String> parts = new ArrayList<>();
|
||||||
parts.add("科室 · " + maps.size() + "人");
|
parts.add("科室 · " + members.size() + "人");
|
||||||
parts.addAll(suffixes(n));
|
parts.addAll(suffixes(n));
|
||||||
vo.setSubLabel(String.join(" · ", parts));
|
vo.setSubLabel(String.join(" · ", parts));
|
||||||
}
|
}
|
||||||
@@ -264,14 +263,14 @@ public class FlowDiagramService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 科室视图 label 兜底链:有映射行取 deptName(全空→deptKey);无映射行→节点名(再退 deptKey) */
|
/** 科室视图 label 兜底链:科室存在取 dept_name;科室不存在→节点名(再退 deptKey) */
|
||||||
private String deptLabel(FlowDefModel.Node n, FlowDefModel.Assignee a, List<FlwAssigneeMap> maps) {
|
private String deptLabel(FlowDefModel.Node n, FlowDefModel.Assignee a, Long schId) {
|
||||||
// 科室名取值与流程轨迹科室口径共用(DeptLabelSupport),只保留本视图特有的兜底链
|
// 科室名取值与流程轨迹科室口径共用(DeptLabelSupport),只保留本视图特有的兜底链
|
||||||
String deptName = DeptLabelSupport.mappedDeptName(maps);
|
String deptName = DeptLabelSupport.deptName(deptAdminService, schId, a.deptKey);
|
||||||
if (deptName != null) {
|
if (deptName != null) {
|
||||||
return deptName;
|
return deptName;
|
||||||
}
|
}
|
||||||
return maps.isEmpty() ? firstNonBlank(n.name, a.deptKey) : firstNonBlank(a.deptKey, n.name);
|
return firstNonBlank(n.name, a.deptKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 人员名单(顿号连接)+ 会签/时限后缀(" · "分隔);名单与后缀均空则返回 null */
|
/** 人员名单(顿号连接)+ 会签/时限后缀(" · "分隔);名单与后缀均空则返回 null */
|
||||||
@@ -308,9 +307,9 @@ public class FlowDiagramService {
|
|||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> memberNames(List<FlwAssigneeMap> maps) {
|
private List<String> memberNames(List<FlwDeptMember> members) {
|
||||||
List<String> names = new ArrayList<>();
|
List<String> names = new ArrayList<>();
|
||||||
for (FlwAssigneeMap m : maps) {
|
for (FlwDeptMember m : members) {
|
||||||
names.add(blankToId(m.getUserName(), m.getUserId()));
|
names.add(blankToId(m.getUserName(), m.getUserId()));
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import com.yunhai.flowcenter.entity.FlwRouteLog;
|
|||||||
import com.yunhai.flowcenter.entity.FlwTask;
|
import com.yunhai.flowcenter.entity.FlwTask;
|
||||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||||
import com.yunhai.flowcenter.model.FlowModels;
|
import com.yunhai.flowcenter.model.FlowModels;
|
||||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
|
||||||
import com.yunhai.flowcenter.repository.FlwBranchRepository;
|
import com.yunhai.flowcenter.repository.FlwBranchRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||||
@@ -47,7 +46,7 @@ public class FlowQueryService {
|
|||||||
private final FlwCcRepository ccRepository;
|
private final FlwCcRepository ccRepository;
|
||||||
private final FlwRouteLogRepository routeLogRepository;
|
private final FlwRouteLogRepository routeLogRepository;
|
||||||
private final FlwDefRepository defRepository;
|
private final FlwDefRepository defRepository;
|
||||||
private final FlwAssigneeMapRepository mapRepository;
|
private final FlowDeptAdminService deptAdminService;
|
||||||
private final FlowEngine flowEngine;
|
private final FlowEngine flowEngine;
|
||||||
|
|
||||||
public BasePageResult<FlowModels.InstanceItem> pageMy(Long schId, Integer submitterId,
|
public BasePageResult<FlowModels.InstanceItem> pageMy(Long schId, Integer submitterId,
|
||||||
@@ -476,7 +475,7 @@ public class FlowQueryService {
|
|||||||
}
|
}
|
||||||
row.put("deptKey", assignee.deptKey);
|
row.put("deptKey", assignee.deptKey);
|
||||||
String deptName = deptNames.computeIfAbsent(assignee.deptKey,
|
String deptName = deptNames.computeIfAbsent(assignee.deptKey,
|
||||||
k -> DeptLabelSupport.mappedDeptName(mapRepository.findEnabled(schId, k)));
|
k -> DeptLabelSupport.deptName(deptAdminService, schId, k));
|
||||||
if (deptName != null) {
|
if (deptName != null) {
|
||||||
row.put("deptName", deptName);
|
row.put("deptName", deptName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- 2026-10-10 四议题·应用设置:流程对接应用白名单表
|
||||||
|
-- ddl-auto=update 会自动建表;本脚本为生产备案(生产建议停自动 DDL 后手工执行)
|
||||||
|
CREATE TABLE IF NOT EXISTS `flw_app` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`sch_id` BIGINT NOT NULL COMMENT '租户键',
|
||||||
|
`app_key` VARCHAR(64) NOT NULL COMMENT '子应用 appKey(UserBase application_info.app_key 字符串形态)',
|
||||||
|
`app_name` VARCHAR(128) NOT NULL COMMENT '应用名快照(学校池返回值,含学校别名;同步刷新)',
|
||||||
|
`enabled` INT NOT NULL DEFAULT 1 COMMENT '1=对接流程(白名单);0=停用(defSave 校验拦截)',
|
||||||
|
`sort_no` INT NULL COMMENT '展示排序(学校池 orderNumber 快照)',
|
||||||
|
`create_time` DATETIME(6) NULL,
|
||||||
|
`update_time` DATETIME(6) NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_flw_app_sch_key` (`sch_id`, `app_key`),
|
||||||
|
KEY `idx_flw_app_sch_enabled` (`sch_id`, `enabled`)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '流程对接应用白名单(应用设置)';
|
||||||
@@ -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 = '科室成员';
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- flow-center 四议题(2026-10-10):UserBase app_module 变更
|
||||||
|
-- ① 新增 107「应用设置」(/admin/app-setting,应用管理 + 科室管理两个 tab)
|
||||||
|
-- ② 下线 104「办理人映射」(/admin/maps 由 应用设置-科室管理 承载,Part E 上线时执行 ②段)
|
||||||
|
-- 纪律:AI 不代跑,dev / prod 由人工执行;MySQL 5.7 兼容;幂等可重复执行。
|
||||||
|
-- 前置:flow-center 前端(含 AppSetting.vue 路由)已发布,否则菜单点进去 404。
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- 步骤 0:校验身份链(app_module.app_id 存的是 application_info 主键,非 app_key)
|
||||||
|
SELECT id AS app_id_pk, app_key, name
|
||||||
|
FROM application_info
|
||||||
|
WHERE id = 100011 OR app_key = 100019;
|
||||||
|
-- 期望同一行(id=100011 且 app_key=100019);不同则把 @app_id 改成 app_key=100019 那行的 id。
|
||||||
|
|
||||||
|
SET @app_id = 100011;
|
||||||
|
SET @operator = 0;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- ① 新增 107 应用设置(幂等:不存在才插;path 与 routes.ts 逐字一致)
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
INSERT INTO app_module
|
||||||
|
(app_id, parent_id, name, custom_id, path, icon, level, sort_no, create_user, create_time)
|
||||||
|
SELECT @app_id, NULL, '应用设置', '107', '/admin/app-setting', NULL, 1, 107, @operator, NOW()
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM app_module m WHERE m.app_id = @app_id AND m.custom_id = '107'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ①授权(角色路径,@role_id 换成实际选中的启用角色;见 flow-center接入UserBase菜单.sql §3.1 查询)
|
||||||
|
SET @role_id = 0; -- ← 改这里
|
||||||
|
INSERT INTO sys_app_role_module_rel (role_id, module_id, create_user, create_time)
|
||||||
|
SELECT @role_id, m.id, @operator, NOW()
|
||||||
|
FROM app_module m
|
||||||
|
WHERE m.app_id = @app_id AND m.custom_id = '107'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM sys_app_role_module_rel r
|
||||||
|
WHERE r.role_id = @role_id AND r.module_id = m.id);
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- ② 下线 104 办理人映射(Part E 科室管理上线时执行;先删授权再删模块行)
|
||||||
|
-- 如需保留回滚能力,可改为 UPDATE app_module SET sort_no = 9999 隐藏(菜单仍下发)。
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- DELETE r FROM sys_app_role_module_rel r
|
||||||
|
-- JOIN app_module m ON m.id = r.module_id
|
||||||
|
-- WHERE m.app_id = @app_id AND m.custom_id = '104';
|
||||||
|
-- DELETE p FROM sys_group_module_perm p
|
||||||
|
-- JOIN app_module m ON m.id = p.module_id
|
||||||
|
-- WHERE m.app_id = @app_id AND m.custom_id = '104';
|
||||||
|
-- DELETE m FROM app_module m
|
||||||
|
-- WHERE m.app_id = @app_id AND m.custom_id = '104';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
-- 验收
|
||||||
|
-- ---------------------------------------------------------------------
|
||||||
|
SELECT id, app_id, custom_id, name, path, level, sort_no
|
||||||
|
FROM app_module
|
||||||
|
WHERE app_id = @app_id
|
||||||
|
ORDER BY CAST(custom_id AS UNSIGNED);
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package com.yunhai.flowcenter.engine;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||||
|
import com.yunhai.flowcenter.entity.FlwRouteLog;
|
||||||
|
import com.yunhai.flowcenter.notify.UserMsgClient;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CONDITION 判断编排单测(2026-10-10 四议题 Phase 4):EXPRESSION 模式分流——按出边顺序
|
||||||
|
* 首个 true 命中、全 false/求值异常挂起(留痕 outlet=NULL + router_key=EXPRESSION + 告警)、
|
||||||
|
* SERVICE 模式回归不受影响。挂起推进链路由 FlowEngineIT 覆盖。
|
||||||
|
*/
|
||||||
|
class ConditionRouteServiceExpressionTest {
|
||||||
|
|
||||||
|
private FlwRouterRepository routerRepository;
|
||||||
|
private RouteLogStore routeLogStore;
|
||||||
|
private RouteJudgeClient judgeClient;
|
||||||
|
private ConditionRouteService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
routerRepository = mock(FlwRouterRepository.class);
|
||||||
|
routeLogStore = mock(RouteLogStore.class);
|
||||||
|
judgeClient = mock(RouteJudgeClient.class);
|
||||||
|
UserMsgClient userMsgClient = mock(UserMsgClient.class);
|
||||||
|
when(routeLogStore.append(any(FlwRouteLog.class))).thenAnswer(inv -> {
|
||||||
|
FlwRouteLog row = inv.getArgument(0);
|
||||||
|
row.setAttempt(1);
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
service = new ConditionRouteService(routerRepository, routeLogStore, judgeClient,
|
||||||
|
userMsgClient, new ObjectMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwInstance instance(String formDataJson) {
|
||||||
|
FlwInstance inst = new FlwInstance();
|
||||||
|
inst.setId(7L);
|
||||||
|
inst.setSchId(1L);
|
||||||
|
inst.setVersion(3);
|
||||||
|
inst.setBizType("leave");
|
||||||
|
inst.setBizId("456");
|
||||||
|
inst.setSubmitterId(990001);
|
||||||
|
inst.setFormDataJson(formDataJson);
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CONDITION 表达式模型:出边顺序即求值优先级(出口一 → 出口二,互补对) */
|
||||||
|
private FlowDefModel model() {
|
||||||
|
return expressionModel("amount > 1000", "amount <= 1000");
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowDefModel expressionModel(String cond1, String cond2) {
|
||||||
|
return FlowDefModel.parse(new ObjectMapper(), "{"
|
||||||
|
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||||
|
+ "\"nodes\":["
|
||||||
|
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||||
|
+ "{\"id\":\"cond\",\"type\":\"CONDITION\",\"name\":\"判断\",\"judgeMode\":\"EXPRESSION\"},"
|
||||||
|
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||||
|
+ "\"edges\":["
|
||||||
|
+ "{\"from\":\"start\",\"to\":\"cond\"},"
|
||||||
|
+ "{\"from\":\"cond\",\"to\":\"end\",\"name\":\"出口一\",\"condition\":\"" + cond1 + "\"},"
|
||||||
|
+ "{\"from\":\"cond\",\"to\":\"end\",\"name\":\"出口二\",\"condition\":\"" + cond2 + "\"}]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionAllFalse_suspendsWithFailureRow() {
|
||||||
|
FlowDefModel model = expressionModel("amount > 1000", "amount > 2000");
|
||||||
|
FlowDefModel.Node node = model.nodeById("cond");
|
||||||
|
|
||||||
|
String outlet = service.decide(instance("{\"amount\":500}"), model, node);
|
||||||
|
|
||||||
|
assertNull(outlet);
|
||||||
|
ArgumentCaptor<FlwRouteLog> captor = ArgumentCaptor.forClass(FlwRouteLog.class);
|
||||||
|
verify(routeLogStore, times(1)).append(captor.capture());
|
||||||
|
assertNull(captor.getValue().getOutlet());
|
||||||
|
assertEquals("EXPRESSION", captor.getValue().getRouterKey());
|
||||||
|
assertTrue(captor.getValue().getRawResponse().contains("表达式全部未命中"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionMissingVariable_suspendsWithTypeSafeError() {
|
||||||
|
// 表单未传 amount(拼写错/漏传同款):直接挂起,不留静默放行口子
|
||||||
|
FlowDefModel model = model();
|
||||||
|
FlowDefModel.Node node = model.nodeById("cond");
|
||||||
|
|
||||||
|
String outlet = service.decide(instance("{\"other\":1}"), model, node);
|
||||||
|
|
||||||
|
assertNull(outlet);
|
||||||
|
ArgumentCaptor<FlwRouteLog> captor = ArgumentCaptor.forClass(FlwRouteLog.class);
|
||||||
|
verify(routeLogStore, times(1)).append(captor.capture());
|
||||||
|
assertTrue(captor.getValue().getRawResponse().contains("表达式求值异常"));
|
||||||
|
assertTrue(captor.getValue().getRawResponse().contains("amount"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionSecondEdgeTaken_whenFirstFalse() {
|
||||||
|
FlowDefModel model = model();
|
||||||
|
FlowDefModel.Node node = model.nodeById("cond");
|
||||||
|
|
||||||
|
String outlet = service.decide(instance("{\"amount\":500}"), model, node);
|
||||||
|
|
||||||
|
assertEquals("出口二", outlet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionEvalError_suspends() {
|
||||||
|
FlowDefModel model = FlowDefModel.parse(new ObjectMapper(), "{"
|
||||||
|
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||||
|
+ "\"nodes\":["
|
||||||
|
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||||
|
+ "{\"id\":\"cond\",\"type\":\"CONDITION\",\"name\":\"判断\",\"judgeMode\":\"EXPRESSION\"},"
|
||||||
|
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||||
|
+ "\"edges\":["
|
||||||
|
+ "{\"from\":\"start\",\"to\":\"cond\"},"
|
||||||
|
+ "{\"from\":\"cond\",\"to\":\"end\",\"name\":\"出口一\",\"condition\":\"amount - 'x'\"}]}");
|
||||||
|
FlowDefModel.Node node = model.nodeById("cond");
|
||||||
|
|
||||||
|
String outlet = service.decide(instance("{\"amount\":1}"), model, node);
|
||||||
|
|
||||||
|
assertNull(outlet);
|
||||||
|
ArgumentCaptor<FlwRouteLog> captor = ArgumentCaptor.forClass(FlwRouteLog.class);
|
||||||
|
verify(routeLogStore, times(1)).append(captor.capture());
|
||||||
|
assertTrue(captor.getValue().getRawResponse().contains("表达式求值异常"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void builtinVariables_availableInEnv() {
|
||||||
|
Map<String, Object> env = ConditionRouteService.builtinEnv(instance(null),
|
||||||
|
model().nodeById("cond"), new HashMap<>());
|
||||||
|
assertEquals(1L, env.get("schId"));
|
||||||
|
assertEquals(7L, env.get("instanceId"));
|
||||||
|
assertEquals(3, env.get("instanceVersion"));
|
||||||
|
assertEquals("leave", env.get("bizType"));
|
||||||
|
assertEquals("456", env.get("bizId"));
|
||||||
|
assertEquals("cond", env.get("nodeId"));
|
||||||
|
assertEquals(990001, env.get("submitterId"));
|
||||||
|
|
||||||
|
Object hit = ExpressionEvaluator.evaluate("bizType == 'leave' && submitterId == 990001", env);
|
||||||
|
assertTrue((Boolean) hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullJudgeMode_stillServicePath_routerMissSuspends() {
|
||||||
|
FlowDefModel model = FlowDefModel.parse(new ObjectMapper(), "{"
|
||||||
|
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||||
|
+ "\"nodes\":["
|
||||||
|
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||||
|
+ "{\"id\":\"cond\",\"type\":\"CONDITION\",\"name\":\"判断\",\"routerKey\":\"rk\"},"
|
||||||
|
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||||
|
+ "\"edges\":["
|
||||||
|
+ "{\"from\":\"start\",\"to\":\"cond\"},"
|
||||||
|
+ "{\"from\":\"cond\",\"to\":\"end\",\"name\":\"出口一\"},"
|
||||||
|
+ "{\"from\":\"cond\",\"to\":\"end\",\"name\":\"出口二\"}]}");
|
||||||
|
FlowDefModel.Node node = model.nodeById("cond");
|
||||||
|
when(routerRepository.findByRouterKey("rk")).thenReturn(java.util.Optional.empty());
|
||||||
|
|
||||||
|
assertNull(service.decide(instance(null), model, node));
|
||||||
|
ArgumentCaptor<FlwRouteLog> captor = ArgumentCaptor.forClass(FlwRouteLog.class);
|
||||||
|
verify(routeLogStore, times(1)).append(captor.capture());
|
||||||
|
assertEquals("rk", captor.getValue().getRouterKey());
|
||||||
|
assertTrue(captor.getValue().getRawResponse().contains("未注册或已停用"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -381,6 +381,78 @@ class DefValidatorTest {
|
|||||||
new HashSet<>(Collections.singletonList("rk1")));
|
new HashSet<>(Collections.singletonList("rk1")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== CONDITION EXPRESSION 模式(2026-10-10 四议题) =====
|
||||||
|
|
||||||
|
private String expressionDef(String judgeMode, String cond1, String cond2) {
|
||||||
|
return "{"
|
||||||
|
+ "\"code\":\"c1\",\"name\":\"流程\",\"start\":\"start\","
|
||||||
|
+ "\"nodes\":["
|
||||||
|
+ "{\"id\":\"start\",\"type\":\"START\"},"
|
||||||
|
+ "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\""
|
||||||
|
+ (judgeMode == null ? "" : ",\"judgeMode\":\"" + judgeMode + "\"") + "},"
|
||||||
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ybb\"}},"
|
||||||
|
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ybb\"}},"
|
||||||
|
+ "{\"id\":\"end\",\"type\":\"END\"}],"
|
||||||
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"cond1\"},"
|
||||||
|
+ "{\"from\":\"cond1\",\"to\":\"n1\",\"name\":\"甲线\",\"condition\":\"" + cond1 + "\"},"
|
||||||
|
+ "{\"from\":\"cond1\",\"to\":\"n2\",\"name\":\"乙线\",\"condition\":\"" + cond2 + "\"},"
|
||||||
|
+ "{\"from\":\"n1\",\"to\":\"end\"},{\"from\":\"n2\",\"to\":\"end\"}]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionMode_validConditions_passes() {
|
||||||
|
DefValidator.validate(parse(expressionDef("EXPRESSION", "amount > 1000", "amount <= 1000")),
|
||||||
|
new HashSet<>(Collections.singletonList("ybb")), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionMode_missingCondition_fails() {
|
||||||
|
BizException e = assertThrows(BizException.class, () -> DefValidator.validate(
|
||||||
|
parse(expressionDef("EXPRESSION", "amount > 1000", "")),
|
||||||
|
new HashSet<>(Collections.singletonList("ybb")), null));
|
||||||
|
assertTrue(e.getMessage().contains("缺少条件表达式"));
|
||||||
|
assertTrue(e.getMessage().contains("乙线"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionMode_syntaxError_fails() {
|
||||||
|
BizException e = assertThrows(BizException.class, () -> DefValidator.validate(
|
||||||
|
parse(expressionDef("EXPRESSION", "amount >", "amount <= 1000")),
|
||||||
|
new HashSet<>(Collections.singletonList("ybb")), null));
|
||||||
|
assertTrue(e.getMessage().contains("表达式不合法"));
|
||||||
|
assertTrue(e.getMessage().contains("甲线"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionMode_dangerousExpression_fails() {
|
||||||
|
BizException e = assertThrows(BizException.class, () -> DefValidator.validate(
|
||||||
|
parse(expressionDef("EXPRESSION", "new java.util.Date()", "amount <= 1000")),
|
||||||
|
new HashSet<>(Collections.singletonList("ybb")), null));
|
||||||
|
assertTrue(e.getMessage().contains("表达式不合法"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionMode_formVariables_passPublishTime() {
|
||||||
|
// 表单变量(子应用运行时传)发布期不拦:缺失按 null 注入求值
|
||||||
|
DefValidator.validate(parse(expressionDef("EXPRESSION", "form.level == 'A'", "true")),
|
||||||
|
new HashSet<>(Collections.singletonList("ybb")), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void expressionMode_skipsRouterKeyCheck() {
|
||||||
|
// EXPRESSION 模式不配 routerKey(enabledRouterKeys=null 也不查注册表)
|
||||||
|
DefValidator.validate(parse(expressionDef("EXPRESSION", "true", "false")),
|
||||||
|
new HashSet<>(Collections.singletonList("ybb")), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void judgeMode_illegalValue_fails() {
|
||||||
|
BizException e = assertThrows(BizException.class, () -> DefValidator.validate(
|
||||||
|
parse(expressionDef("WHATEVER", "true", "false")),
|
||||||
|
new HashSet<>(Collections.singletonList("ybb")), null));
|
||||||
|
assertTrue(e.getMessage().contains("判断方式取值非法"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void condition_missingRouterKey_fails() {
|
void condition_missingRouterKey_fails() {
|
||||||
String json = "{"
|
String json = "{"
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.yunhai.flowcenter.engine;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表达式求值器单测(2026-10-10 四议题 Phase 4):沙箱纪律(禁类访问/反射/new)、
|
||||||
|
* 缺失变量补 null、nil 属性语义、非布尔不命中。发布校验/运行时/调试端点共用本核心。
|
||||||
|
*/
|
||||||
|
class ExpressionEvaluatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void basicComparison_trueAndFalse() {
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
env.put("amount", 2000);
|
||||||
|
assertTrue((Boolean) ExpressionEvaluator.evaluate("amount > 1000", env));
|
||||||
|
assertFalse((Boolean) ExpressionEvaluator.evaluate("amount < 1000", env));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingTopLevelVariable_throws_typoFailsClosed() {
|
||||||
|
// 宁挂起不误放行:缺失变量直接抛错(nil 视作最小值,<= 兜底出口会静默放行,不能补 null)
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
assertThrows(Exception.class, () -> ExpressionEvaluator.evaluate("missing > 1000", env));
|
||||||
|
assertThrows(Exception.class, () -> ExpressionEvaluator.evaluate("missing <= 1000", env));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explicitNullValue_nilComparisons_documentedSemantics() {
|
||||||
|
// env 显式 null 仍走 nil 语义:== nil 可判空;<= 对 nil 为 true(Aviator 视 nil 为最小值)
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
env.put("amount", null);
|
||||||
|
assertTrue((Boolean) ExpressionEvaluator.evaluate("amount == nil", env));
|
||||||
|
assertTrue((Boolean) ExpressionEvaluator.evaluate("amount <= 1000", env));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nestedMapPropertyMissing_returnsNil() {
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
env.put("form", new HashMap<String, Object>());
|
||||||
|
Object r = ExpressionEvaluator.evaluate("form.amount == nil", env);
|
||||||
|
assertTrue((Boolean) r);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonBooleanResult_notTreatedAsTrue() {
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
env.put("amount", 2000);
|
||||||
|
Object r = ExpressionEvaluator.evaluate("amount + 1", env);
|
||||||
|
assertEquals(2001L, ((Number) r).longValue());
|
||||||
|
assertFalse(Boolean.TRUE.equals(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stringFunction_allowed() {
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
env.put("title", "请假申请");
|
||||||
|
assertTrue((Boolean) ExpressionEvaluator.evaluate("string.contains(title, '请假')", env));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sandbox_rejectsNewInstance() {
|
||||||
|
assertThrows(Exception.class, ()
|
||||||
|
-> ExpressionEvaluator.evaluate("new java.util.Date()", new HashMap<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sandbox_rejectsStaticMethodAccess() {
|
||||||
|
assertThrows(Exception.class, ()
|
||||||
|
-> ExpressionEvaluator.evaluate("System.currentTimeMillis() > 0", new HashMap<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sandbox_rejectsChainedReflection() {
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
env.put("title", "x");
|
||||||
|
assertThrows(Exception.class, ()
|
||||||
|
-> ExpressionEvaluator.evaluate("title.getClass().forName('java.lang.Runtime')", env));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sandbox_bareClassReference_isInertVariableLookup() {
|
||||||
|
// 裸类名在属性语法糖下只是嵌套 map 查找(java→lang→Runtime),顶层变量 java 缺失被
|
||||||
|
// 求值前核对拦下——不构成类访问;真正的类访问(new/静态方法/链式反射)由上面三个用例证明被拒
|
||||||
|
assertThrows(IllegalStateException.class, ()
|
||||||
|
-> ExpressionEvaluator.evaluate("java.lang.Runtime", new HashMap<>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void builtinVariableStyle_comparisonAcrossNumberTypes() {
|
||||||
|
Map<String, Object> env = new HashMap<>();
|
||||||
|
env.put("submitterId", 990001);
|
||||||
|
assertTrue((Boolean) ExpressionEvaluator.evaluate("submitterId == 990001", env));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syntaxError_thrownAtCompile() {
|
||||||
|
assertThrows(Exception.class, () -> ExpressionEvaluator.compile("amount >"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -228,7 +228,7 @@ class FlowEngineIT extends FlowTestSupport {
|
|||||||
assertEquals(2, trace.get(3).get("round"));
|
assertEquals(2, trace.get(3).get("round"));
|
||||||
// 科室口径富化(DEPT_KEY 节点):deptName 取映射里的科室名,前端"按科室"据此换掉人名
|
// 科室口径富化(DEPT_KEY 节点):deptName 取映射里的科室名,前端"按科室"据此换掉人名
|
||||||
assertEquals("DEPT_KEY", trace.get(3).get("assigneeType"));
|
assertEquals("DEPT_KEY", trace.get(3).get("assigneeType"));
|
||||||
assertEquals("ybb", trace.get(3).get("deptKey"));
|
assertEquals(dk("ybb"), trace.get(3).get("deptKey"));
|
||||||
assertEquals("ybb科", trace.get(3).get("deptName"));
|
assertEquals("ybb科", trace.get(3).get("deptName"));
|
||||||
|
|
||||||
// ② 已办去重:同一办件一行,结果是最后一次(同意),次数=2
|
// ② 已办去重:同一办件一行,结果是最后一次(同意),次数=2
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class RouteJudgeClientTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private RouteJudgeClient.JudgeBody body() {
|
private RouteJudgeClient.JudgeBody body() {
|
||||||
return new RouteJudgeClient.JudgeBody(1L, 2L, 3, "notice", "456",
|
return new RouteJudgeClient.JudgeBody(1L, 2L, 3, "notice", "456", 990001,
|
||||||
"node_r1", "判断", "rk", Collections.singletonList("甲线"), new HashMap<>());
|
"node_r1", "判断", "rk", Collections.singletonList("甲线"), new HashMap<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.yunhai.flowcenter.service;
|
||||||
|
|
||||||
|
import com.yunhai.flowcenter.entity.FlwApp;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwAppRepository;
|
||||||
|
import com.yunhai.platform.openapi.base.OpenApiAppClient;
|
||||||
|
import com.yunhai.platform.utils.model.BoaApp;
|
||||||
|
import com.yunhai.platform.utils.model.BoaAppType;
|
||||||
|
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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
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.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):学校池 diff 同步(新增 enabled=1/改名刷新/池外保留)、
|
||||||
|
* UserBase 不可达降级、开关与跨校守卫。口径拍板:D2 只做最优实现(不做存量 appKey 兜底)、
|
||||||
|
* D3 clientType=null 不过滤端、D7 学校组单点池。
|
||||||
|
*/
|
||||||
|
class FlowAppAdminServiceTest {
|
||||||
|
|
||||||
|
private static final Long SCH = 990001L;
|
||||||
|
|
||||||
|
private FlwAppRepository appRepository;
|
||||||
|
private OpenApiAppClient appClient;
|
||||||
|
private FlowAppAdminService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
appRepository = mock(FlwAppRepository.class);
|
||||||
|
appClient = mock(OpenApiAppClient.class);
|
||||||
|
service = new FlowAppAdminService(appRepository, appClient);
|
||||||
|
when(appRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BoaAppType type(BoaApp... apps) {
|
||||||
|
BoaAppType t = new BoaAppType();
|
||||||
|
t.setId(2);
|
||||||
|
t.setName("办公管理");
|
||||||
|
t.setApps(Arrays.asList(apps));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BoaApp app(Integer appKey, String name, Integer orderNumber) {
|
||||||
|
BoaApp a = new BoaApp();
|
||||||
|
a.setAppKey(appKey);
|
||||||
|
a.setName(name);
|
||||||
|
a.setOrderNumber(orderNumber);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwApp row(String appKey, String name, int enabled) {
|
||||||
|
FlwApp r = new FlwApp();
|
||||||
|
r.setId((long) appKey.hashCode());
|
||||||
|
r.setSchId(SCH);
|
||||||
|
r.setAppKey(appKey);
|
||||||
|
r.setAppName(name);
|
||||||
|
r.setEnabled(enabled);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sync_池内新应用_插入且默认启用() {
|
||||||
|
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.singletonList(
|
||||||
|
type(app(100002, "通知公告", 1))));
|
||||||
|
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
service.syncFromSchoolPool(SCH);
|
||||||
|
|
||||||
|
ArgumentCaptor<FlwApp> cap = ArgumentCaptor.forClass(FlwApp.class);
|
||||||
|
verify(appRepository, times(1)).save(cap.capture());
|
||||||
|
assertEquals("100002", cap.getValue().getAppKey());
|
||||||
|
assertEquals("通知公告", cap.getValue().getAppName());
|
||||||
|
assertEquals(1, cap.getValue().getEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sync_改名静默刷新_开关不动_池外行保留() {
|
||||||
|
FlwApp renamed = row("100002", "通知公告", 1);
|
||||||
|
FlwApp untouchedEnabled = row("100008", "请假", 0); // 池外且已停用:保留不动
|
||||||
|
FlwApp untouchedName = row("100010", "旧名", 1); // 池外:名称也不动
|
||||||
|
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.singletonList(
|
||||||
|
type(app(100002, "通知公告(校内别名)", 9))));
|
||||||
|
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH))
|
||||||
|
.thenReturn(Arrays.asList(renamed, untouchedEnabled, untouchedName));
|
||||||
|
|
||||||
|
service.syncFromSchoolPool(SCH);
|
||||||
|
|
||||||
|
verify(appRepository, times(1)).save(any(FlwApp.class));
|
||||||
|
assertEquals("通知公告(校内别名)", renamed.getAppName());
|
||||||
|
assertEquals(1, renamed.getEnabled());
|
||||||
|
assertEquals("旧名", untouchedName.getAppName());
|
||||||
|
assertEquals(0, untouchedEnabled.getEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void list_UserBase不可达_降级返回本地行() {
|
||||||
|
when(appClient.getSchoolApp(any(), any())).thenThrow(new RuntimeException("connect timeout"));
|
||||||
|
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH))
|
||||||
|
.thenReturn(Collections.singletonList(row("100002", "通知公告", 1)));
|
||||||
|
|
||||||
|
List<FlwApp> rows = service.list(SCH);
|
||||||
|
|
||||||
|
assertEquals(1, rows.size());
|
||||||
|
assertEquals("100002", rows.get(0).getAppKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void list_正常链路_先同步再返回() {
|
||||||
|
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.emptyList());
|
||||||
|
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
|
||||||
|
assertEquals(0, service.list(SCH).size());
|
||||||
|
verify(appClient, times(1)).getSchoolApp(990001, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enable_切换与守卫() {
|
||||||
|
FlwApp r = row("100002", "通知公告", 1);
|
||||||
|
when(appRepository.findById(r.getId())).thenReturn(java.util.Optional.of(r));
|
||||||
|
|
||||||
|
service.enable(SCH, r.getId(), false);
|
||||||
|
assertEquals(0, r.getEnabled());
|
||||||
|
|
||||||
|
service.enable(SCH, r.getId(), true);
|
||||||
|
assertEquals(1, r.getEnabled());
|
||||||
|
|
||||||
|
FlwApp other = row("100002", "x", 1);
|
||||||
|
other.setSchId(2L);
|
||||||
|
when(appRepository.findById(other.getId())).thenReturn(java.util.Optional.of(other));
|
||||||
|
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> service.enable(SCH, other.getId(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sync_池为空_零写入() {
|
||||||
|
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.emptyList());
|
||||||
|
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
service.syncFromSchoolPool(SCH);
|
||||||
|
|
||||||
|
verify(appRepository, never()).save(any());
|
||||||
|
assertTrue(service.list(SCH).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||||
import com.yunhai.flowcenter.entity.FlwDef;
|
import com.yunhai.flowcenter.entity.FlwDef;
|
||||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
|
||||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||||
import com.yunhai.platform.apistd.BizException;
|
import com.yunhai.platform.apistd.BizException;
|
||||||
@@ -37,18 +36,23 @@ class FlowDefAdminServiceSaveTest {
|
|||||||
private static final String JSON = "{\"nodes\":[],\"edges\":[]}";
|
private static final String JSON = "{\"nodes\":[],\"edges\":[]}";
|
||||||
|
|
||||||
private FlwDefRepository defRepository;
|
private FlwDefRepository defRepository;
|
||||||
|
private com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
|
||||||
private FlowDefAdminService service;
|
private FlowDefAdminService service;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
defRepository = mock(FlwDefRepository.class);
|
defRepository = mock(FlwDefRepository.class);
|
||||||
|
appRepository = mock(com.yunhai.flowcenter.repository.FlwAppRepository.class);
|
||||||
service = new FlowDefAdminService(defRepository,
|
service = new FlowDefAdminService(defRepository,
|
||||||
mock(FlwAssigneeMapRepository.class),
|
|
||||||
mock(FlwRouterRepository.class),
|
mock(FlwRouterRepository.class),
|
||||||
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
|
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
|
||||||
|
appRepository,
|
||||||
|
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class),
|
||||||
mock(FlowEngine.class),
|
mock(FlowEngine.class),
|
||||||
new ObjectMapper());
|
new ObjectMapper());
|
||||||
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
// 归属应用白名单:非空 appKey 一律放行(校验拦截语义由专门用例覆盖)
|
||||||
|
when(appRepository.isEnabledApp(any(), any())).thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FlwDef def(long id, String flowGuid, String code, int version, int status) {
|
private FlwDef def(long id, String flowGuid, String code, int version, int status) {
|
||||||
@@ -168,4 +172,23 @@ class FlowDefAdminServiceSaveTest {
|
|||||||
() -> service.save(SCH, 9L, "work-leave", "请假", JSON, null, null));
|
() -> service.save(SCH, 9L, "work-leave", "请假", JSON, null, null));
|
||||||
assertTrue(e.getMessage().contains("保存冲突"));
|
assertTrue(e.getMessage().contains("保存冲突"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appKey不在启用白名单_保存被拦() {
|
||||||
|
when(appRepository.isEnabledApp(SCH, "999999")).thenReturn(false);
|
||||||
|
BizException e = assertThrows(BizException.class,
|
||||||
|
() -> service.save(SCH, null, "work-leave", "请假", JSON, null, "999999"));
|
||||||
|
assertTrue(e.getMessage().contains("应用设置"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appKey不传_白名单校验不触发() {
|
||||||
|
// 升版本继承源线(cleanAppKey=null)不查白名单——存量线升版本不受切换影响
|
||||||
|
when(defRepository.findById(9L)).thenReturn(Optional.of(def(9L, "G", "work-leave", 4, FlwEnums.DefStatus.PUBLISHED.getCode())));
|
||||||
|
when(defRepository.maxVersionByGuid(SCH, "G")).thenReturn(4);
|
||||||
|
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, "G")).thenReturn(Optional.empty());
|
||||||
|
FlwDef saved = service.save(SCH, 9L, "work-leave", "请假", JSON, null, null);
|
||||||
|
org.mockito.Mockito.verify(appRepository, org.mockito.Mockito.never()).isEnabledApp(any(), any());
|
||||||
|
assertNull(saved.getAppKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.yunhai.flowcenter.service;
|
||||||
|
|
||||||
|
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.FlwDefRepository;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
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.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updateMeta 原地编辑单测(2026-10-10 议题2,拍板 D4):
|
||||||
|
* 三不变(version/flowGuid/status;contentJson 仅顶层 name)、appKey 白名单校验、
|
||||||
|
* contentJson 无顶层 name 时补写、name 非空校验。
|
||||||
|
*/
|
||||||
|
class FlowDefAdminServiceUpdateMetaTest {
|
||||||
|
|
||||||
|
private static final Long SCH = 3L;
|
||||||
|
|
||||||
|
private FlwDefRepository defRepository;
|
||||||
|
private FlwAppRepository appRepository;
|
||||||
|
private FlowDefAdminService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
defRepository = mock(FlwDefRepository.class);
|
||||||
|
appRepository = mock(FlwAppRepository.class);
|
||||||
|
service = new FlowDefAdminService(defRepository,
|
||||||
|
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));
|
||||||
|
when(appRepository.isEnabledApp(any(), any())).thenReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwDef published(String contentJson) {
|
||||||
|
FlwDef d = new FlwDef();
|
||||||
|
d.setId(9L);
|
||||||
|
d.setSchId(SCH);
|
||||||
|
d.setFlowGuid("guid123");
|
||||||
|
d.setCode("work-leave");
|
||||||
|
d.setName("旧名");
|
||||||
|
d.setAppKey("100002");
|
||||||
|
d.setVersion(4);
|
||||||
|
d.setStatus(1);
|
||||||
|
d.setContentJson(contentJson);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 三不变_contentJson仅顶层name() throws Exception {
|
||||||
|
String json = "{\"code\":\"work-leave\",\"name\":\"旧名\",\"start\":\"start\","
|
||||||
|
+ "\"nodes\":[{\"id\":\"start\",\"type\":\"START\"}],"
|
||||||
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"end\"}]}";
|
||||||
|
FlwDef def = published(json);
|
||||||
|
when(defRepository.findById(9L)).thenReturn(Optional.of(def));
|
||||||
|
|
||||||
|
FlwDef saved = service.updateMeta(SCH, 9L, "新名称", "100008");
|
||||||
|
|
||||||
|
assertEquals(4, saved.getVersion());
|
||||||
|
assertEquals("guid123", saved.getFlowGuid());
|
||||||
|
assertEquals(1, saved.getStatus());
|
||||||
|
assertEquals("work-leave", saved.getCode());
|
||||||
|
assertEquals("新名称", saved.getName());
|
||||||
|
assertEquals("100008", saved.getAppKey());
|
||||||
|
assertEquals("新名称", new ObjectMapper().readTree(saved.getContentJson()).path("name").asText());
|
||||||
|
// 结构零改动:节点/边/其余顶层字段原样保留
|
||||||
|
assertEquals("work-leave", new ObjectMapper().readTree(saved.getContentJson()).path("code").asText());
|
||||||
|
assertTrue(saved.getContentJson().contains("\"type\":\"START\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contentJson无顶层name_补写() throws Exception {
|
||||||
|
FlwDef def = published("{\"code\":\"c\",\"start\":\"start\",\"nodes\":[],\"edges\":[]}");
|
||||||
|
when(defRepository.findById(9L)).thenReturn(Optional.of(def));
|
||||||
|
|
||||||
|
FlwDef saved = service.updateMeta(SCH, 9L, "补写名", null);
|
||||||
|
|
||||||
|
assertEquals("补写名", new ObjectMapper().readTree(saved.getContentJson()).path("name").asText());
|
||||||
|
assertNull(saved.getAppKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appKey白名单外_被拦() {
|
||||||
|
when(appRepository.isEnabledApp(SCH, "888888")).thenReturn(false);
|
||||||
|
when(defRepository.findById(9L)).thenReturn(Optional.of(published("{}")));
|
||||||
|
|
||||||
|
Exception e = assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||||||
|
() -> service.updateMeta(SCH, 9L, "n", "888888"));
|
||||||
|
assertTrue(e.getMessage().contains("应用设置"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void name空_被拦() {
|
||||||
|
when(defRepository.findById(9L)).thenReturn(Optional.of(published("{}")));
|
||||||
|
assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||||||
|
() -> service.updateMeta(SCH, 9L, " ", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 跨校_被拒() {
|
||||||
|
FlwDef other = published("{}");
|
||||||
|
other.setSchId(2L);
|
||||||
|
when(defRepository.findById(9L)).thenReturn(Optional.of(other));
|
||||||
|
assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||||||
|
() -> service.updateMeta(SCH, 9L, "n", null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package com.yunhai.flowcenter.service;
|
||||||
|
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDept;
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDeptMember;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwDeptMemberRepository;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwDeptRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 科室管理单测(2026-10-10 四议题·议题3,拍板 D1):CRUD 守卫、enabledDeptKeys 白名单口径、
|
||||||
|
* 解析宽容口径(deptKey 不存在/科室停用返回空,示意图/轨迹不炸)、一键拉取幂等
|
||||||
|
* (source_group_id 命中刷新组名+成员集对齐,删不在集内旧成员)。
|
||||||
|
*/
|
||||||
|
class FlowDeptAdminServiceTest {
|
||||||
|
|
||||||
|
private static final Long SCH = 990001L;
|
||||||
|
|
||||||
|
private FlwDeptRepository deptRepository;
|
||||||
|
private FlwDeptMemberRepository memberRepository;
|
||||||
|
private FlowMapAdminService mapAdminService;
|
||||||
|
private FlowDeptAdminService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
deptRepository = mock(FlwDeptRepository.class);
|
||||||
|
memberRepository = mock(FlwDeptMemberRepository.class);
|
||||||
|
mapAdminService = mock(FlowMapAdminService.class);
|
||||||
|
service = new FlowDeptAdminService(deptRepository, memberRepository, mapAdminService);
|
||||||
|
when(deptRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
when(memberRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwDept dept(long id, String name, int status, Integer sourceGroupId) {
|
||||||
|
FlwDept d = new FlwDept();
|
||||||
|
d.setId(id);
|
||||||
|
d.setSchId(SCH);
|
||||||
|
d.setDeptName(name);
|
||||||
|
d.setStatus(status);
|
||||||
|
d.setSourceGroupId(sourceGroupId);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwDeptMember member(long deptId, int userId, String name) {
|
||||||
|
FlwDeptMember m = new FlwDeptMember();
|
||||||
|
m.setDeptId(deptId);
|
||||||
|
m.setUserId(userId);
|
||||||
|
m.setUserName(name);
|
||||||
|
m.setStatus(0);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowMapAdminService.GroupDeptVo groupVo(int groupId, String name, FlowMapAdminService.MemberVo... users) {
|
||||||
|
FlowMapAdminService.GroupDeptVo vo = new FlowMapAdminService.GroupDeptVo();
|
||||||
|
vo.setDeptKey(String.valueOf(groupId));
|
||||||
|
vo.setDeptName(name);
|
||||||
|
vo.setUsers(Arrays.asList(users));
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowMapAdminService.MemberVo userVo(int userId, String name) {
|
||||||
|
return new FlowMapAdminService.MemberVo(userId, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void list_启用户口count_停用科室恒0() {
|
||||||
|
FlwDept on = dept(1L, "人事处", 0, null);
|
||||||
|
FlwDept off = dept(2L, "教务处", 1, null);
|
||||||
|
when(deptRepository.findBySchIdOrderByIdAsc(SCH)).thenReturn(Arrays.asList(on, off));
|
||||||
|
when(memberRepository.countByDeptIdAndStatus(1L, 0)).thenReturn(3L);
|
||||||
|
when(memberRepository.countByDeptIdAndStatus(2L, 0)).thenReturn(5L);
|
||||||
|
|
||||||
|
List<FlowDeptAdminService.DeptVo> rows = service.list(SCH);
|
||||||
|
|
||||||
|
assertEquals(2, rows.size());
|
||||||
|
assertEquals("1", rows.get(0).getDeptKey());
|
||||||
|
assertEquals(3, rows.get(0).getCount());
|
||||||
|
assertEquals(0, rows.get(1).getCount(), "停用科室 count 恒 0(编辑器提示'无启用人员')");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enabledDeptKeys_仅含有启用成员的启用科室() {
|
||||||
|
FlwDept with = dept(1L, "人事处", 0, null);
|
||||||
|
FlwDept empty = dept(2L, "教务处", 0, null);
|
||||||
|
FlwDept off = dept(3L, "保卫处", 1, null);
|
||||||
|
when(deptRepository.findBySchIdAndStatusOrderByIdAsc(SCH, 0)).thenReturn(Arrays.asList(with, empty));
|
||||||
|
when(memberRepository.countByDeptIdAndStatus(1L, 0)).thenReturn(2L);
|
||||||
|
when(memberRepository.countByDeptIdAndStatus(2L, 0)).thenReturn(0L);
|
||||||
|
|
||||||
|
assertEquals(Collections.singleton("1"), service.enabledDeptKeys(SCH));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveEnabled_宽容口径() {
|
||||||
|
// deptKey 不存在(旧映射 deptKey=UserBase 组 id 切源后即此态)→ 空,不抛
|
||||||
|
when(deptRepository.findBySchIdAndId(SCH, 123L)).thenReturn(Optional.empty());
|
||||||
|
assertTrue(service.resolveEnabled(SCH, "123").isEmpty());
|
||||||
|
// 非数字 deptKey → 空,不抛(脏定义防御)
|
||||||
|
assertTrue(service.resolveEnabled(SCH, "abc").isEmpty());
|
||||||
|
// 科室停用 → 空
|
||||||
|
when(deptRepository.findBySchIdAndId(SCH, 2L)).thenReturn(Optional.of(dept(2L, "教务处", 1, null)));
|
||||||
|
assertTrue(service.resolveEnabled(SCH, "2").isEmpty());
|
||||||
|
// 启用科室 → 启用成员
|
||||||
|
FlwDept on = dept(1L, "人事处", 0, null);
|
||||||
|
when(deptRepository.findBySchIdAndId(SCH, 1L)).thenReturn(Optional.of(on));
|
||||||
|
when(memberRepository.findByDeptIdAndStatusOrderByIdAsc(1L, 0))
|
||||||
|
.thenReturn(Arrays.asList(member(1L, 11, "张三"), member(1L, 12, "李四")));
|
||||||
|
assertEquals(2, service.resolveEnabled(SCH, "1").size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deptName_切源唯一出口() {
|
||||||
|
when(deptRepository.findBySchIdAndId(SCH, 1L)).thenReturn(Optional.of(dept(1L, "人事处", 0, null)));
|
||||||
|
assertEquals("人事处", service.deptName(SCH, "1"));
|
||||||
|
when(deptRepository.findBySchIdAndId(SCH, 9L)).thenReturn(Optional.empty());
|
||||||
|
assertNull(service.deptName(SCH, "9"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void create_name必填() {
|
||||||
|
assertEquals("新科室", service.create(SCH, " 新科室 ").getDeptName());
|
||||||
|
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> service.create(SCH, " "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void delete_级联删成员_跨校拒() {
|
||||||
|
FlwDept d = dept(1L, "人事处", 0, null);
|
||||||
|
when(deptRepository.findById(1L)).thenReturn(Optional.of(d));
|
||||||
|
service.delete(SCH, 1L);
|
||||||
|
verify(memberRepository).deleteByDeptId(1L);
|
||||||
|
verify(deptRepository).delete(d);
|
||||||
|
|
||||||
|
FlwDept other = dept(2L, "x", 0, null);
|
||||||
|
other.setSchId(2L);
|
||||||
|
when(deptRepository.findById(2L)).thenReturn(Optional.of(other));
|
||||||
|
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> service.delete(SCH, 2L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void memberAdd_跳过已存在_失败降级井号() {
|
||||||
|
FlwDept d = dept(1L, "人事处", 0, null);
|
||||||
|
when(deptRepository.findById(1L)).thenReturn(Optional.of(d));
|
||||||
|
when(memberRepository.findByDeptIdAndUserId(1L, 11)).thenReturn(Optional.of(member(1L, 11, "张三")));
|
||||||
|
when(mapAdminService.lookupUser(12)).thenThrow(new RuntimeException("userbase down"));
|
||||||
|
|
||||||
|
int added = service.memberAdd(SCH, 1L, Arrays.asList(11, 12));
|
||||||
|
|
||||||
|
assertEquals(1, added);
|
||||||
|
ArgumentCaptor<FlwDeptMember> cap = ArgumentCaptor.forClass(FlwDeptMember.class);
|
||||||
|
verify(memberRepository, times(1)).save(cap.capture());
|
||||||
|
assertEquals(12, cap.getValue().getUserId());
|
||||||
|
assertEquals("#12", cap.getValue().getUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importGroups_首拉新建_重复拉取刷新并对齐成员集() {
|
||||||
|
when(mapAdminService.previewGroupDepts(SCH)).thenReturn(Arrays.asList(
|
||||||
|
groupVo(7001, "人事处", userVo(11, "张三"), userVo(12, "李四"))));
|
||||||
|
// 首拉:无既有行 → 新建 + 灌成员
|
||||||
|
FlwDept created = dept(101L, "人事处", 0, 7001);
|
||||||
|
when(deptRepository.findBySchIdAndSourceGroupId(SCH, 7001)).thenReturn(Optional.empty());
|
||||||
|
when(deptRepository.save(any())).thenReturn(created);
|
||||||
|
when(memberRepository.findByDeptIdOrderByIdAsc(101L)).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
FlowDeptAdminService.GroupImportResultVo r1 = service.importGroups(SCH);
|
||||||
|
assertEquals(1, r1.getCreated());
|
||||||
|
assertEquals(2, r1.getUsers());
|
||||||
|
verify(memberRepository, times(2)).save(any(FlwDeptMember.class));
|
||||||
|
|
||||||
|
// 重复拉取:命中 source_group_id → 刷新组名;成员集对齐(11 留、12 删、13 增)
|
||||||
|
FlwDept existing = dept(101L, "人事处旧名", 0, 7001);
|
||||||
|
when(deptRepository.findBySchIdAndSourceGroupId(SCH, 7001)).thenReturn(Optional.of(existing));
|
||||||
|
when(mapAdminService.previewGroupDepts(SCH)).thenReturn(Arrays.asList(
|
||||||
|
groupVo(7001, "人事处新名", userVo(11, "张三"), userVo(13, "王五"))));
|
||||||
|
FlwDeptMember old12 = member(101L, 12, "李四");
|
||||||
|
FlwDeptMember old11 = member(101L, 11, "张三");
|
||||||
|
when(memberRepository.findByDeptIdOrderByIdAsc(101L)).thenReturn(Arrays.asList(old11, old12));
|
||||||
|
when(memberRepository.findByDeptIdAndUserId(eq(101L), anyInt())).thenReturn(Optional.empty());
|
||||||
|
when(memberRepository.findByDeptIdAndUserId(101L, 11)).thenReturn(Optional.of(old11));
|
||||||
|
|
||||||
|
FlowDeptAdminService.GroupImportResultVo r2 = service.importGroups(SCH);
|
||||||
|
assertEquals(1, r2.getRefreshed());
|
||||||
|
assertEquals(1, r2.getUsers());
|
||||||
|
assertEquals("人事处新名", existing.getDeptName());
|
||||||
|
verify(memberRepository).delete(old12);
|
||||||
|
verify(memberRepository, never()).delete(old11);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package com.yunhai.flowcenter.service;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.yunhai.flowcenter.controller.api.FlowAdminController;
|
import com.yunhai.flowcenter.controller.api.FlowAdminController;
|
||||||
import com.yunhai.flowcenter.controller.api.FlowUserController;
|
import com.yunhai.flowcenter.controller.api.FlowUserController;
|
||||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
import com.yunhai.flowcenter.entity.FlwDeptMember;
|
||||||
import com.yunhai.flowcenter.entity.FlwDef;
|
import com.yunhai.flowcenter.entity.FlwDef;
|
||||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||||
import com.yunhai.flowcenter.entity.FlwTask;
|
import com.yunhai.flowcenter.entity.FlwTask;
|
||||||
@@ -11,7 +11,6 @@ import com.yunhai.flowcenter.engine.AssigneeResolver;
|
|||||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||||
import com.yunhai.flowcenter.model.FlowModels;
|
import com.yunhai.flowcenter.model.FlowModels;
|
||||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
|
||||||
import com.yunhai.flowcenter.repository.FlwBranchRepository;
|
import com.yunhai.flowcenter.repository.FlwBranchRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||||
@@ -85,7 +84,7 @@ class FlowDiagramServiceTest {
|
|||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n2\"},{\"from\":\"n2\",\"to\":\"end\"}]}";
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n2\"},{\"from\":\"n2\",\"to\":\"end\"}]}";
|
||||||
|
|
||||||
private FlwDefRepository defRepository;
|
private FlwDefRepository defRepository;
|
||||||
private FlwAssigneeMapRepository mapRepository;
|
private com.yunhai.flowcenter.service.FlowDeptAdminService deptAdminService;
|
||||||
private FlwTaskRepository taskRepository;
|
private FlwTaskRepository taskRepository;
|
||||||
private com.yunhai.flowcenter.repository.FlwCcRepository ccRepository;
|
private com.yunhai.flowcenter.repository.FlwCcRepository ccRepository;
|
||||||
private OpenApiUserClient userClient;
|
private OpenApiUserClient userClient;
|
||||||
@@ -94,7 +93,7 @@ class FlowDiagramServiceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
defRepository = mock(FlwDefRepository.class);
|
defRepository = mock(FlwDefRepository.class);
|
||||||
mapRepository = mock(FlwAssigneeMapRepository.class);
|
deptAdminService = mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class);
|
||||||
taskRepository = mock(FlwTaskRepository.class);
|
taskRepository = mock(FlwTaskRepository.class);
|
||||||
ccRepository = mock(com.yunhai.flowcenter.repository.FlwCcRepository.class);
|
ccRepository = mock(com.yunhai.flowcenter.repository.FlwCcRepository.class);
|
||||||
userClient = mock(OpenApiUserClient.class);
|
userClient = mock(OpenApiUserClient.class);
|
||||||
@@ -107,7 +106,7 @@ class FlowDiagramServiceTest {
|
|||||||
taskRepository, mock(FlwBranchRepository.class), ccRepository,
|
taskRepository, mock(FlwBranchRepository.class), ccRepository,
|
||||||
mock(AssigneeResolver.class), mock(com.yunhai.flowcenter.engine.ConditionRouteService.class),
|
mock(AssigneeResolver.class), mock(com.yunhai.flowcenter.engine.ConditionRouteService.class),
|
||||||
mock(ApplicationEventPublisher.class));
|
mock(ApplicationEventPublisher.class));
|
||||||
service = new FlowDiagramService(defRepository, mapRepository,
|
service = new FlowDiagramService(defRepository, deptAdminService,
|
||||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||||
taskRepository, ccRepository,
|
taskRepository, ccRepository,
|
||||||
mock(com.yunhai.flowcenter.repository.FlwRouteLogRepository.class),
|
mock(com.yunhai.flowcenter.repository.FlwRouteLogRepository.class),
|
||||||
@@ -127,11 +126,9 @@ class FlowDiagramServiceTest {
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
private FlwAssigneeMap mapRow(String deptName, int userId, String userName) {
|
private FlwDeptMember memberRow(int userId, String userName) {
|
||||||
FlwAssigneeMap m = new FlwAssigneeMap();
|
FlwDeptMember m = new FlwDeptMember();
|
||||||
m.setSchId(SCH);
|
m.setDeptId(7L);
|
||||||
m.setDeptKey("rs");
|
|
||||||
m.setDeptName(deptName);
|
|
||||||
m.setUserId(userId);
|
m.setUserId(userId);
|
||||||
m.setUserName(userName);
|
m.setUserName(userName);
|
||||||
m.setStatus(0);
|
m.setStatus(0);
|
||||||
@@ -153,8 +150,9 @@ class FlowDiagramServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void c1_deptView_deptKeyNode_labelDeptName_subLabelCountAndDeadline() {
|
void c1_deptView_deptKeyNode_labelDeptName_subLabelCountAndDeadline() {
|
||||||
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
|
||||||
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四")));
|
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
||||||
|
memberRow(11, "张三"), memberRow(12, "李四")));
|
||||||
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
|
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
|
||||||
FlowModels.NodeVo n1 = node(vo, "n1");
|
FlowModels.NodeVo n1 = node(vo, "n1");
|
||||||
assertEquals("人事处", n1.getLabel());
|
assertEquals("人事处", n1.getLabel());
|
||||||
@@ -170,30 +168,32 @@ class FlowDiagramServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void c1_deptView_fallbackChain_deptKeyThenNodeName() {
|
void c1_deptView_fallbackChain_deptKeyThenNodeName() {
|
||||||
// 映射行全无名 → deptKey 兜底
|
// 科室已被删(deptName=null)→ 节点名兜底(成员解析空,人数照实)
|
||||||
when(mapRepository.findEnabled(SCH, "rs"))
|
when(deptAdminService.resolveEnabled(SCH, "rs"))
|
||||||
.thenReturn(Collections.singletonList(mapRow("", 11, "张三")));
|
.thenReturn(Collections.singletonList(memberRow(11, "张三")));
|
||||||
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
|
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
|
||||||
assertEquals("rs", n1.getLabel());
|
|
||||||
assertEquals("科室 · 1人 · 时限48h", n1.getSubLabel());
|
|
||||||
// 无映射行 → 节点名兜底(人数照实为 0)
|
|
||||||
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Collections.emptyList());
|
|
||||||
n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
|
|
||||||
assertEquals("人事处审批", n1.getLabel());
|
assertEquals("人事处审批", n1.getLabel());
|
||||||
|
assertEquals("科室 · 1人 · 时限48h", n1.getSubLabel());
|
||||||
|
// 科室存在但成员为空 → 科室名照常,人数 0
|
||||||
|
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
|
||||||
|
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Collections.emptyList());
|
||||||
|
n1 = node(service.build(def(DEPT_DEF_JSON), null, "dept"), "n1");
|
||||||
|
assertEquals("人事处", n1.getLabel());
|
||||||
assertEquals("科室 · 0人 · 时限48h", n1.getSubLabel());
|
assertEquals("科室 · 0人 · 时限48h", n1.getSubLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void c1_anyCountersign_appendsSuffix() {
|
void c1_anyCountersign_appendsSuffix() {
|
||||||
when(mapRepository.findEnabled(SCH, "rs"))
|
when(deptAdminService.resolveEnabled(SCH, "rs"))
|
||||||
.thenReturn(Collections.singletonList(mapRow("人事处", 11, "张三")));
|
.thenReturn(Collections.singletonList(memberRow(11, "张三")));
|
||||||
FlowModels.NodeVo n1 = node(service.build(def(ANY_DEF_JSON), null, "dept"), "n1");
|
FlowModels.NodeVo n1 = node(service.build(def(ANY_DEF_JSON), null, "dept"), "n1");
|
||||||
assertEquals("科室 · 1人 · 任一人通过", n1.getSubLabel());
|
assertEquals("科室 · 1人 · 任一人通过", n1.getSubLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void c1_fixedLabels_andEdgesCarried() {
|
void c1_fixedLabels_andEdgesCarried() {
|
||||||
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
||||||
|
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
|
||||||
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
|
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, "dept");
|
||||||
assertEquals("开始", node(vo, "start").getLabel());
|
assertEquals("开始", node(vo, "start").getLabel());
|
||||||
assertEquals("并行分发", node(vo, "sp1").getLabel());
|
assertEquals("并行分发", node(vo, "sp1").getLabel());
|
||||||
@@ -215,8 +215,9 @@ class FlowDiagramServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void c1_autoView_deptKeyNode_rendersDeptStyle() {
|
void c1_autoView_deptKeyNode_rendersDeptStyle() {
|
||||||
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
|
||||||
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四")));
|
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
||||||
|
memberRow(11, "张三"), memberRow(12, "李四")));
|
||||||
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "auto"), "n1");
|
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "auto"), "n1");
|
||||||
assertEquals("人事处", n1.getLabel());
|
assertEquals("人事处", n1.getLabel());
|
||||||
assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel());
|
assertEquals("科室 · 2人 · 时限48h", n1.getSubLabel());
|
||||||
@@ -226,8 +227,9 @@ class FlowDiagramServiceTest {
|
|||||||
/** 2026-09-18:mode 缺省改为人员视图(原 auto 自适应)——老深链显式带 auto 仍按自适应出图 */
|
/** 2026-09-18:mode 缺省改为人员视图(原 auto 自适应)——老深链显式带 auto 仍按自适应出图 */
|
||||||
@Test
|
@Test
|
||||||
void c1_defaultMode_isUserView() {
|
void c1_defaultMode_isUserView() {
|
||||||
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
|
||||||
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, "李四")));
|
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
||||||
|
memberRow(11, "张三"), memberRow(12, "李四")));
|
||||||
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, null);
|
FlowModels.DiagramVo vo = service.build(def(DEPT_DEF_JSON), null, null);
|
||||||
assertEquals("user", vo.getMode());
|
assertEquals("user", vo.getMode());
|
||||||
FlowModels.NodeVo n1 = node(vo, "n1");
|
FlowModels.NodeVo n1 = node(vo, "n1");
|
||||||
@@ -288,8 +290,9 @@ class FlowDiagramServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void c3_userView_deptKeyNode_listsMemberNames_blankNameDegradesToId() {
|
void c3_userView_deptKeyNode_listsMemberNames_blankNameDegradesToId() {
|
||||||
when(mapRepository.findEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
when(deptAdminService.deptName(SCH, "rs")).thenReturn("人事处");
|
||||||
mapRow("人事处", 11, "张三"), mapRow("人事处", 12, null)));
|
when(deptAdminService.resolveEnabled(SCH, "rs")).thenReturn(Arrays.asList(
|
||||||
|
memberRow(11, "张三"), memberRow(12, null)));
|
||||||
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "user"), "n1");
|
FlowModels.NodeVo n1 = node(service.build(def(DEPT_DEF_JSON), null, "user"), "n1");
|
||||||
// 人员视图:label=节点名,subLabel=映射成员名(空名降级 #id)+ 会签/时限后缀
|
// 人员视图:label=节点名,subLabel=映射成员名(空名降级 #id)+ 会签/时限后缀
|
||||||
assertEquals("人事处审批", n1.getLabel());
|
assertEquals("人事处审批", n1.getLabel());
|
||||||
@@ -304,7 +307,8 @@ class FlowDiagramServiceTest {
|
|||||||
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
when(defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||||||
SCH, "g1", FlwEnums.DefStatus.PUBLISHED.getCode()))
|
SCH, "g1", FlwEnums.DefStatus.PUBLISHED.getCode()))
|
||||||
.thenReturn(Optional.of(def));
|
.thenReturn(Optional.of(def));
|
||||||
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
||||||
|
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
|
||||||
FlowModels.DiagramVo vo = service.byGuid(SCH, "g1", null);
|
FlowModels.DiagramVo vo = service.byGuid(SCH, "g1", null);
|
||||||
assertEquals("教师入职", vo.getName());
|
assertEquals("教师入职", vo.getName());
|
||||||
assertEquals(Integer.valueOf(2), vo.getVersion());
|
assertEquals(Integer.valueOf(2), vo.getVersion());
|
||||||
@@ -349,7 +353,8 @@ class FlowDiagramServiceTest {
|
|||||||
instance.setSchId(SCH);
|
instance.setSchId(SCH);
|
||||||
instance.setDefId(12L);
|
instance.setDefId(12L);
|
||||||
when(defRepository.findById(12L)).thenReturn(Optional.of(def(DEPT_DEF_JSON)));
|
when(defRepository.findById(12L)).thenReturn(Optional.of(def(DEPT_DEF_JSON)));
|
||||||
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
||||||
|
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
|
||||||
FlowModels.DiagramVo vo = service.byInstance(instance, "dept");
|
FlowModels.DiagramVo vo = service.byInstance(instance, "dept");
|
||||||
assertEquals(Long.valueOf(100L), vo.getInstanceId());
|
assertEquals(Long.valueOf(100L), vo.getInstanceId());
|
||||||
assertEquals(Long.valueOf(12L), vo.getDefId());
|
assertEquals(Long.valueOf(12L), vo.getDefId());
|
||||||
@@ -360,7 +365,8 @@ class FlowDiagramServiceTest {
|
|||||||
void c6_build_draftDef_ok() {
|
void c6_build_draftDef_ok() {
|
||||||
FlwDef draft = def(DEPT_DEF_JSON);
|
FlwDef draft = def(DEPT_DEF_JSON);
|
||||||
draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
draft.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
||||||
when(mapRepository.findEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
when(deptAdminService.resolveEnabled(anyLong(), anyString())).thenReturn(Collections.emptyList());
|
||||||
|
when(deptAdminService.deptName(anyLong(), anyString())).thenReturn(null);
|
||||||
FlowModels.DiagramVo vo = service.build(draft, null, "auto");
|
FlowModels.DiagramVo vo = service.build(draft, null, "auto");
|
||||||
assertEquals(Integer.valueOf(0), vo.getStatus());
|
assertEquals(Integer.valueOf(0), vo.getStatus());
|
||||||
assertEquals(5, vo.getNodes().size());
|
assertEquals(5, vo.getNodes().size());
|
||||||
@@ -482,7 +488,10 @@ class FlowDiagramServiceTest {
|
|||||||
mock(FlowMapAdminService.class), mock(FlowCallbackAdminService.class),
|
mock(FlowMapAdminService.class), mock(FlowCallbackAdminService.class),
|
||||||
mock(com.yunhai.flowcenter.service.FlowRouterAdminService.class),
|
mock(com.yunhai.flowcenter.service.FlowRouterAdminService.class),
|
||||||
mock(FlwInstanceRepository.class), diagramService,
|
mock(FlwInstanceRepository.class), diagramService,
|
||||||
mock(com.yunhai.flowcenter.service.DiagramImageService.class));
|
mock(com.yunhai.flowcenter.service.DiagramImageService.class),
|
||||||
|
new ObjectMapper(),
|
||||||
|
mock(com.yunhai.flowcenter.service.FlowAppAdminService.class),
|
||||||
|
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
|
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
package com.yunhai.flowcenter.support;
|
package com.yunhai.flowcenter.support;
|
||||||
|
|
||||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
|
||||||
import com.yunhai.flowcenter.entity.FlwCallbackLog;
|
import com.yunhai.flowcenter.entity.FlwCallbackLog;
|
||||||
import com.yunhai.flowcenter.entity.FlwCallbackReg;
|
import com.yunhai.flowcenter.entity.FlwCallbackReg;
|
||||||
import com.yunhai.flowcenter.entity.FlwDef;
|
import com.yunhai.flowcenter.entity.FlwDef;
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDept;
|
||||||
|
import com.yunhai.flowcenter.entity.FlwDeptMember;
|
||||||
import com.yunhai.flowcenter.entity.FlwRouteLog;
|
import com.yunhai.flowcenter.entity.FlwRouteLog;
|
||||||
import com.yunhai.flowcenter.entity.FlwRouter;
|
import com.yunhai.flowcenter.entity.FlwRouter;
|
||||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
|
||||||
import com.yunhai.flowcenter.repository.FlwCallbackLogRepository;
|
import com.yunhai.flowcenter.repository.FlwCallbackLogRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
import com.yunhai.flowcenter.repository.FlwCcRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwDeptMemberRepository;
|
||||||
|
import com.yunhai.flowcenter.repository.FlwDeptRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwRouteLogRepository;
|
import com.yunhai.flowcenter.repository.FlwRouteLogRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IT 公共基类:专用测试库 bsch_flowcenter_test,每例清表(引擎测试不能开测试事务——
|
* IT 公共基类:专用测试库 bsch_flowcenter_test,每例清表(引擎测试不能开测试事务——
|
||||||
* AFTER_COMMIT 推进监听器依赖真实提交);提供定义落库/发布、映射注册的快捷方法。
|
* AFTER_COMMIT 推进监听器依赖真实提交);提供定义落库/发布、科室注册的快捷方法。
|
||||||
|
* 2026-10-10 科室切源:DEPT_KEY 解析读 flw_dept/flw_dept_member,deptKey=科室自增 id——
|
||||||
|
* 用例仍以 tag("ybb"/"ka")书写,经 {@link #mapDept} 注册 tag→deptKey,def helper 输出
|
||||||
|
* JSON 时替换(dk()),用例主体零改动。
|
||||||
*/
|
*/
|
||||||
public abstract class FlowTestSupport {
|
public abstract class FlowTestSupport {
|
||||||
|
|
||||||
@@ -34,7 +41,9 @@ public abstract class FlowTestSupport {
|
|||||||
@Autowired
|
@Autowired
|
||||||
protected FlwTaskRepository taskRepository;
|
protected FlwTaskRepository taskRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
protected FlwAssigneeMapRepository mapRepository;
|
protected FlwDeptRepository deptRepository;
|
||||||
|
@Autowired
|
||||||
|
protected FlwDeptMemberRepository deptMemberRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
protected FlwCallbackRegRepository regRepository;
|
protected FlwCallbackRegRepository regRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -46,6 +55,14 @@ public abstract class FlowTestSupport {
|
|||||||
@Autowired
|
@Autowired
|
||||||
protected FlwRouteLogRepository routeLogRepository;
|
protected FlwRouteLogRepository routeLogRepository;
|
||||||
|
|
||||||
|
/** tag → 实际 deptKey(flw_dept.id 字符串);mapDept 注册,def helper 消费 */
|
||||||
|
private final Map<String, String> deptKeyByTag = new HashMap<>();
|
||||||
|
|
||||||
|
/** tag 对应的实际 deptKey(未注册的 tag 原样返回——脏定义防御用例依赖此行为) */
|
||||||
|
protected String dk(String tag) {
|
||||||
|
return deptKeyByTag.getOrDefault(tag, tag);
|
||||||
|
}
|
||||||
|
|
||||||
protected void cleanDb() {
|
protected void cleanDb() {
|
||||||
logRepository.deleteAll();
|
logRepository.deleteAll();
|
||||||
regRepository.deleteAll();
|
regRepository.deleteAll();
|
||||||
@@ -55,17 +72,38 @@ public abstract class FlowTestSupport {
|
|||||||
taskRepository.deleteAll();
|
taskRepository.deleteAll();
|
||||||
instanceRepository.deleteAll();
|
instanceRepository.deleteAll();
|
||||||
defRepository.deleteAll();
|
defRepository.deleteAll();
|
||||||
mapRepository.deleteAll();
|
deptMemberRepository.deleteAll();
|
||||||
|
deptRepository.deleteAll();
|
||||||
|
deptKeyByTag.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected FlwAssigneeMap mapDept(String deptKey, Integer userId, String userName) {
|
/**
|
||||||
FlwAssigneeMap map = new FlwAssigneeMap();
|
* 注册科室成员(tag 复用同一科室行;未注册过的 tag 新建科室并登记 deptKey)。
|
||||||
map.setSchId(SCH);
|
* 返回科室名(deptName=tag+"科",轨迹/图示断言用)。
|
||||||
map.setDeptKey(deptKey);
|
*/
|
||||||
map.setDeptName(deptKey + "科");
|
protected String mapDept(String tag, Integer userId, String userName) {
|
||||||
map.setUserId(userId);
|
FlwDept dept = deptKeyByTag.containsKey(tag) ? reloadDept(tag) : createDept(tag);
|
||||||
map.setUserName(userName);
|
deptKeyByTag.put(tag, String.valueOf(dept.getId()));
|
||||||
return mapRepository.save(map);
|
FlwDeptMember member = new FlwDeptMember();
|
||||||
|
member.setDeptId(dept.getId());
|
||||||
|
member.setUserId(userId);
|
||||||
|
member.setUserName(userName);
|
||||||
|
member.setStatus(0);
|
||||||
|
deptMemberRepository.save(member);
|
||||||
|
return dept.getDeptName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwDept reloadDept(String tag) {
|
||||||
|
return deptRepository.findById(Long.valueOf(deptKeyByTag.get(tag)))
|
||||||
|
.orElseThrow(() -> new IllegalStateException("科室行丢失:" + tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlwDept createDept(String tag) {
|
||||||
|
FlwDept dept = new FlwDept();
|
||||||
|
dept.setSchId(SCH);
|
||||||
|
dept.setDeptName(tag + "科");
|
||||||
|
dept.setStatus(0);
|
||||||
|
return deptRepository.save(dept);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected FlwDef saveAndPublishDef(String code, String name, String contentJson) {
|
protected FlwDef saveAndPublishDef(String code, String name, String contentJson) {
|
||||||
@@ -88,7 +126,7 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"" + nodeName + "\","
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"" + nodeName + "\","
|
||||||
+ "\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptKey + "\"},\"countersign\":\"ALL\"},"
|
+ "\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptKey) + "\"},\"countersign\":\"ALL\"},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
|
||||||
@@ -102,8 +140,8 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||||
+ "{\"id\":\"t_a\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"t_a\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"t_b\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}},"
|
+ "{\"id\":\"t_b\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
|
||||||
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
@@ -121,7 +159,7 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送校领导\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301,302]}},"
|
+ "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送校领导\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301,302]}},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"cc1\"},{\"from\":\"cc1\",\"to\":\"n1\"},"
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"cc1\"},{\"from\":\"cc1\",\"to\":\"n1\"},"
|
||||||
@@ -136,7 +174,7 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301]}},"
|
+ "{\"id\":\"cc1\",\"type\":\"NOTIFY\",\"name\":\"抄送\",\"receivers\":{\"type\":\"USERS\",\"userIds\":[301]}},"
|
||||||
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
@@ -158,8 +196,8 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"},"
|
+ "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"乙线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}},"
|
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"乙线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"cond1\"},"
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"cond1\"},"
|
||||||
@@ -178,9 +216,9 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"主线条件测试流程\",\"start\":\"start\","
|
+ "\"code\":\"" + codeOf() + "\",\"name\":\"主线条件测试流程\",\"start\":\"start\","
|
||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"n0\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}},"
|
+ "{\"id\":\"n0\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
|
||||||
+ "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"},"
|
+ "{\"id\":\"cond1\",\"type\":\"CONDITION\",\"name\":\"条件判断\",\"routerKey\":\"" + routerKey + "\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"甲线审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n0\"},"
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n0\"},"
|
||||||
@@ -213,8 +251,8 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"多出口测试流程\",\"start\":\"start\","
|
+ "\"code\":\"" + codeOf() + "\",\"name\":\"多出口测试流程\",\"start\":\"start\","
|
||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}},"
|
+ "{\"id\":\"n2\",\"type\":\"APPROVAL\",\"name\":\"复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},"
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},"
|
||||||
@@ -230,7 +268,7 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"夡签测试流程\",\"start\":\"start\","
|
+ "\"code\":\"" + codeOf() + "\",\"name\":\"夡签测试流程\",\"start\":\"start\","
|
||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"任一人审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptKey + "\"},\"countersign\":\"ANY\"},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"任一人审批\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptKey) + "\"},\"countersign\":\"ANY\"},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"end\"}],"
|
||||||
@@ -248,13 +286,13 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"嵌套并行测试流程\",\"start\":\"start\","
|
+ "\"code\":\"" + codeOf() + "\",\"name\":\"嵌套并行测试流程\",\"start\":\"start\","
|
||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
|
||||||
+ "{\"id\":\"s1\",\"type\":\"PARALLEL_SPLIT\"},"
|
+ "{\"id\":\"s1\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||||
+ "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}},"
|
+ "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
|
||||||
+ "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"},"
|
+ "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||||
+ "{\"id\":\"tc\",\"type\":\"APPROVAL\",\"name\":\"丙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptC + "\"}},"
|
+ "{\"id\":\"tc\",\"type\":\"APPROVAL\",\"name\":\"丙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptC) + "\"}},"
|
||||||
+ "{\"id\":\"td\",\"type\":\"APPROVAL\",\"name\":\"丁科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptD + "\"}},"
|
+ "{\"id\":\"td\",\"type\":\"APPROVAL\",\"name\":\"丁科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptD) + "\"}},"
|
||||||
+ "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
+ "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||||
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
@@ -277,10 +315,10 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"出口并行测试流程\",\"start\":\"start\","
|
+ "\"code\":\"" + codeOf() + "\",\"name\":\"出口并行测试流程\",\"start\":\"start\","
|
||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"分流审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"分流审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
|
||||||
+ "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"},"
|
+ "{\"id\":\"s2\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||||
+ "{\"id\":\"p1\",\"type\":\"APPROVAL\",\"name\":\"并行甲\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"p1\",\"type\":\"APPROVAL\",\"name\":\"并行甲\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"p2\",\"type\":\"APPROVAL\",\"name\":\"并行乙\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptB + "\"}},"
|
+ "{\"id\":\"p2\",\"type\":\"APPROVAL\",\"name\":\"并行乙\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptB) + "\"}},"
|
||||||
+ "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
+ "{\"id\":\"join2\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
@@ -299,12 +337,12 @@ public abstract class FlowTestSupport {
|
|||||||
+ "\"code\":\"" + codeOf() + "\",\"name\":\"主线并行尾验流程\",\"start\":\"start\","
|
+ "\"code\":\"" + codeOf() + "\",\"name\":\"主线并行尾验流程\",\"start\":\"start\","
|
||||||
+ "\"nodes\":["
|
+ "\"nodes\":["
|
||||||
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
+ "{\"id\":\"start\",\"type\":\"START\",\"name\":\"开始\"},"
|
||||||
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptMain + "\"}},"
|
+ "{\"id\":\"n1\",\"type\":\"APPROVAL\",\"name\":\"入口\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptMain) + "\"}},"
|
||||||
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
+ "{\"id\":\"split1\",\"type\":\"PARALLEL_SPLIT\"},"
|
||||||
+ "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"ka\"}},"
|
+ "{\"id\":\"ta\",\"type\":\"APPROVAL\",\"name\":\"甲科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk("ka") + "\"}},"
|
||||||
+ "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"kb\"}},"
|
+ "{\"id\":\"tb\",\"type\":\"APPROVAL\",\"name\":\"乙科审核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk("kb") + "\"}},"
|
||||||
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
+ "{\"id\":\"join1\",\"type\":\"PARALLEL_JOIN\",\"waitFor\":\"ALL\"},"
|
||||||
+ "{\"id\":\"n3\",\"type\":\"APPROVAL\",\"name\":\"出口复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + deptA + "\"}},"
|
+ "{\"id\":\"n3\",\"type\":\"APPROVAL\",\"name\":\"出口复核\",\"assignee\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"" + dk(deptA) + "\"}},"
|
||||||
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
+ "{\"id\":\"end\",\"type\":\"END\"}"
|
||||||
+ "],"
|
+ "],"
|
||||||
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"split1\"},"
|
+ "\"edges\":[{\"from\":\"start\",\"to\":\"n1\"},{\"from\":\"n1\",\"to\":\"split1\"},"
|
||||||
|
|||||||
Reference in New Issue
Block a user