package com.yunhai.flowcenter.engine; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.ObjectMapper; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; import java.util.List; import java.util.Map; /** * 流程定义 JSON 模型(概览 §5.1 规范,一期 5 节点)。 * 序列化/反序列化统一走 {@link #parse(ObjectMapper, String)},未知属性忽略(向前兼容)。 */ @JsonIgnoreProperties(ignoreUnknown = true) public class FlowDefModel { /** 节点类型(一期 5 种,二期按需扩展 NOTIFY/CONDITION) */ public static final String TYPE_START = "START"; public static final String TYPE_END = "END"; public static final String TYPE_APPROVAL = "APPROVAL"; public static final String TYPE_PARALLEL_SPLIT = "PARALLEL_SPLIT"; public static final String TYPE_PARALLEL_JOIN = "PARALLEL_JOIN"; /** 抄送节点(2026-09-23 二期第一批):瞬时放行,逐人落 flw_cc + CC_SENT,不建任务不阻塞 */ public static final String TYPE_NOTIFY = "NOTIFY"; /** * 条件分支节点(2026-09-24 二期第二批 §4):激活时同步调外部判断服务(flw_router 注册, * routerKey 寻址)按返回出口名选路;失败挂起待管理端重跑,禁止配默认出口。 * 出边=互斥分支,name 必填且唯一(出口名即判断接口的 outlets 契约)。 */ 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_ANY = "ANY"; /** 拒绝策略(一期仅 END_REJECTED) */ public static final String REJECT_END_REJECTED = "END_REJECTED"; /** 办理人类型(概览 §六:一期 DEPT_KEY/USERS;SUBMITTER_DEPT/FORM_FIELD 二期) */ public static final String ASSIGNEE_DEPT_KEY = "DEPT_KEY"; public static final String ASSIGNEE_USERS = "USERS"; /** 发起时指派选择模式:SINGLE=恰 1 人;MULTI=多选(min~max) */ public static final String ASSIGN_MODE_SINGLE = "SINGLE"; public static final String ASSIGN_MODE_MULTI = "MULTI"; public String code; public String name; public String start; public List nodes; public List edges; public Settings settings; /** * 画布坐标(nodeId → 左上角),仅前端设计器/查看器复用;引擎不消费。 * 缺失时前端自行做自动布局,故可选、可空。 */ public Map layout; @JsonIgnoreProperties(ignoreUnknown = true) public static class Node { public String id; public String type; public String name; public Assignee assignee; /** NOTIFY 抄送接收人(复用 Assignee 结构:DEPT_KEY/USERS 两档,解析走 AssigneeResolver) */ public Assignee receivers; /** CONDITION 判断服务注册键(flw_router.router_key,发布期校验须已注册且启用) */ 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 */ public String countersign; /** PARALLEL_JOIN 汇聚条件,一期仅 ALL */ public String waitFor; /** 办理时限小时数(自然时间,任务 arrive_time + N 小时;可空) */ public Integer deadlineHours; /** 发起时指派策略(2026-10-13 方案):存在即允许指派;缺省/缺失=不允许(向前兼容) */ public AssignPolicy assignPolicy; } @JsonIgnoreProperties(ignoreUnknown = true) public static class Assignee { /** DEPT_KEY | USERS */ public String type; public String deptKey; public List userIds; } /** 发起时指派策略:mode 必填(容错 null=MULTI);min/max 可空(缺省 1/候选全量数);required 仅子应用 UI 用 */ @JsonIgnoreProperties(ignoreUnknown = true) public static class AssignPolicy { /** SINGLE | MULTI */ public String mode; public Integer min; public Integer max; public Boolean required; } @JsonIgnoreProperties(ignoreUnknown = true) public static class Edge { public String from; public String to; /** * 出边名(按钮文案)。单出口可空;多出口节点必填且同节点内唯一, * 办理人在办理页选它决定走哪条边(见 {@link #edgeByName})。 */ public String name; /** 出口条件(二期条件自动路由预留;一期只校验透传,引擎不消费) */ public String condition; } /** 画布坐标(前端设计器专用) */ @JsonIgnoreProperties(ignoreUnknown = true) public static class Layout { public Integer x; public Integer y; } @JsonIgnoreProperties(ignoreUnknown = true) public static class Settings { public String rejectPolicy = REJECT_END_REJECTED; public Urge urge = new Urge(); } @JsonIgnoreProperties(ignoreUnknown = true) public static class Urge { public Boolean enabled = true; public Integer intervalHours = 24; public Integer maxTimes = 3; } public static FlowDefModel parse(ObjectMapper objectMapper, String json) { try { FlowDefModel model = objectMapper.readValue(json, FlowDefModel.class); if (model == null || model.nodes == null || model.edges == null) { throw new BizException(ErrorCode.PARAM_ERROR, "流程定义 JSON 缺少 nodes/edges"); } if (model.settings == null) { model.settings = new Settings(); } return model; } catch (BizException e) { throw e; } catch (Exception e) { throw new BizException(ErrorCode.PARAM_ERROR, "流程定义 JSON 不合法:" + e.getMessage()); } } public Node node(String nodeId) { for (Node n : nodes) { if (n.id != null && n.id.equals(nodeId)) { return n; } } return null; } public Node nodeById(String nodeId) { Node n = node(nodeId); if (n == null) { throw new BizException(ErrorCode.PARAM_ERROR, "流程定义缺少节点:" + nodeId); } return n; } /** 出边集合(保持定义顺序) */ public List outEdges(String nodeId) { java.util.List out = new java.util.ArrayList<>(); for (Edge e : edges) { if (nodeId != null && nodeId.equals(e.from)) { out.add(e); } } return out; } public List inEdges(String nodeId) { java.util.List in = new java.util.ArrayList<>(); for (Edge e : edges) { if (nodeId != null && nodeId.equals(e.to)) { in.add(e); } } return in; } /** 按出边名取边(多出口节点路由用);未命中返回 null */ public Edge edgeByName(String fromNodeId, String name) { if (name == null) { return null; } for (Edge e : outEdges(fromNodeId)) { if (name.equals(e.name)) { return e; } } return null; } }