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.entity.FlwRouter; import com.yunhai.flowcenter.notify.UserMsgClient; import com.yunhai.flowcenter.repository.FlwRouterRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 条件路由判断编排(2026-09-24 §4.4):查 flw_router 注册 → 组装判断契约体(variables 透传 * form_data_json)→ {@link RouteJudgeClient} 同步调用 → 结果落 flw_route_log(一次一行)。 * *

失败策略(已定,勿改):挂起 + 管理端重跑,禁止配默认出口——超时/非 2xx/ * outlet 未命中/注册缺失一律返回 null(route_log 落 outlet=NULL 行 + 告警日志与站内信), * 节点保持在途(currentNodes 不动、办件仍 10),管理端 reroute 重新调判断。 * "校长室免审"这类判断失败默认放行是安全事故,宁挂起。 * 退回重走 CONDITION 必然重新调判断(单据可能已改):attempt 递增,留痕只增不改。 */ @Slf4j @Service @RequiredArgsConstructor public class ConditionRouteService { private final FlwRouterRepository routerRepository; private final RouteLogStore routeLogStore; private final RouteJudgeClient judgeClient; private final UserMsgClient userMsgClient; private final ObjectMapper objectMapper; /** * 判断并留痕。 * * @return 命中的出口名(必属 outlets);失败返回 null(已落 outlet=NULL 留痕行 + 告警) */ public String decide(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node) { List outlets = outletNames(model, node); if (node.expressionJudge()) { return decideByExpression(instance, model, node, outlets); } FlwRouter router = routerRepository.findByRouterKey(node.routerKey).orElse(null); if (router == null || router.getStatus() == null || router.getStatus() != 1) { log.error("[flow-center] 路由判断失败:routerKey={} 未注册或已停用(挂起):instance={} node={}", node.routerKey, instance.getId(), node.id); return persistFailure(instance, node, outlets, "routerKey 未注册或已停用:" + node.routerKey); } RouteJudgeClient.JudgeBody body = new RouteJudgeClient.JudgeBody( instance.getSchId(), instance.getId(), instance.getVersion(), instance.getBizType(), instance.getBizId(), instance.getSubmitterId(), node.id, node.name, node.routerKey, outlets, parseVariables(instance.getFormDataJson())); try { RouteJudgeClient.JudgeResult result = judgeClient.call(router, body); if (!outlets.contains(result.outlet)) { log.error("[flow-center] 路由判断返回出口未命中 outlets(挂起):instance={} node={} outlet={} outlets={}", instance.getId(), node.id, result.outlet, outlets); return persistFailure(instance, node, outlets, "返回出口未命中 outlets:" + result.outlet + ";raw=" + result.rawResponse); } persist(instance, node, node.routerKey, result.outlet, result.rawResponse); log.info("[flow-center] 路由判断命中:instance={} node={} router={} outlet={}", instance.getId(), node.id, node.routerKey, result.outlet); return result.outlet; } catch (Exception e) { log.error("[flow-center] 路由判断调用失败(挂起):instance={} node={} router={} {}", instance.getId(), node.id, node.routerKey, e.toString()); return persistFailure(instance, node, outlets, "调用失败:" + e); } } /** 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 outlets) { Map 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 builtinEnv(FlwInstance instance, FlowDefModel.Node node, Map 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(发布期已保证非空唯一) */ private static List outletNames(FlowDefModel model, FlowDefModel.Node node) { List names = new ArrayList<>(); for (FlowDefModel.Edge e : model.outEdges(node.id)) { names.add(e.name); } return names; } /** form_data_json 原样透传为 variables(SDK 发起参数不加新字段;解析失败=空对象) */ private Map parseVariables(String formDataJson) { if (formDataJson == null || formDataJson.trim().isEmpty()) { return new java.util.HashMap<>(); } try { return objectMapper.readValue(formDataJson, new com.fasterxml.jackson.core.type.TypeReference>() { }); } catch (Exception e) { log.warn("[flow-center] form_data_json 非 JSON 对象,variables 透传空:{}", e.toString()); return new java.util.HashMap<>(); } } private String persistFailure(FlwInstance instance, FlowDefModel.Node node, List outlets, String error) { int attempt = persist(instance, node, node.expressionJudge() ? EXPRESSION_ROUTER_KEY : node.routerKey, null, truncate(error)); alertSuspended(instance, node, outlets, error, attempt); return null; } /** 落一行留痕(REQUIRES_NEW:外部调用已真实发生,不随外层回滚),返回本轮 attempt(告警 msgNo 用) */ private int persist(FlwInstance instance, FlowDefModel.Node node, String routerKeyLabel, String outlet, String raw) { FlwRouteLog logRow = new FlwRouteLog(); logRow.setSchId(instance.getSchId()); logRow.setInstanceId(instance.getId()); logRow.setNodeId(node.id); logRow.setNodeName(node.name); logRow.setRouterKey(routerKeyLabel); logRow.setOutlet(outlet); logRow.setRawResponse(raw); return routeLogStore.append(logRow).getAttempt(); } /** 挂起告警本校管理员(复用回调挂起告警接收人配置;UserMsg 不可达只记日志,不影响挂起落定) */ private void alertSuspended(FlwInstance instance, FlowDefModel.Node node, List outlets, String error, int attempt) { try { List admins = alertUserIds(); if (admins.isEmpty()) { return; } userMsgClient.sendUserMsg(instance.getSchId().intValue(), admins, 1, "flow.route", String.valueOf(instance.getId()), "条件路由挂起告警", "办件 " + instance.getId() + "(" + instance.getBizTitle() + ")在节点「" + (node.name == null || node.name.isEmpty() ? node.id : node.name) + "」路由判断失败已挂起,请在流程管理端重跑路由。出口候选:" + outlets + "。最近错误:" + truncate(error), null, null, "route-hang:" + instance.getId() + ":" + node.id + ":" + attempt, null); } catch (Exception e) { log.warn("[flow-center] 路由挂起告警发送失败:instance={} {}", instance.getId(), e.toString()); } } private List alertUserIds() { List userIds = new ArrayList<>(); String configured = alertUserIdsConfig; if (configured == null || configured.trim().isEmpty()) { return userIds; } for (String s : configured.split(",")) { if (!s.trim().isEmpty()) { userIds.add(Integer.valueOf(s.trim())); } } return userIds; } /** 告警接收人(与回调挂起告警同源配置,@Value 注入见构造) */ @org.springframework.beans.factory.annotation.Value("${flow-center.callback.alert-user-ids:}") private String alertUserIdsConfig; private static String truncate(String s) { if (s == null) { return null; } return s.length() > 1000 ? s.substring(0, 1000) : s; } }