Files
flow-center-service/src/main/java/com/yunhai/flowcenter/engine/ConditionRouteService.java
T
chenxing 92619508f0 feat(condition): CONDITION 判断方式二选一——EXPRESSION 内置 Aviator 表达式(沙箱)+ SERVICE 原口径;JudgeBody 补 submitterId
- ExpressionEvaluator:Aviator 5.4.3 沙箱(FEATURE_SET 只留 If、ALLOWED_CLASS_SET 空=禁类访问/反射)、编译缓存、
  顶层变量缺失抛错挂起(Aviator 缺失变量按 nil 且 nil 视作最小值,<= 兜底出口会静默放行——宁挂起不误放行)
- ConditionRouteService.decide 分流:EXPRESSION 按出边顺序取首个 true(逐边结果摘要落 raw_response),
  全 false/异常挂起沿用 SERVICE 纪律(route_log outlet=NULL + EXPRESSION 常量 + 告警 + 管理端重跑)
- DefValidator 发布期分层校验:EXPRESSION 出口缺条件/语法错/违禁表达式硬拦;表单变量放行(运行时核对);
  内置变量疑似拼错 warn;judgeMode 非法值拦
- /admin/router/eval 调试端点(同一求值核心,不落库不走流程)
- 单测 22 新增 + 全量 170 绿;E2E 18/18(发布/分支选路/挂起留痕/重跑/调试)见 local-debug/four-topics-a-e2e.py
2026-10-10 18:52:40 +08:00

218 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(一次一行)。
*
* <p><b>失败策略(已定,勿改):挂起 + 管理端重跑,禁止配默认出口</b>——超时/非 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<String> 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<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(发布期已保证非空唯一) */
private static List<String> outletNames(FlowDefModel model, FlowDefModel.Node node) {
List<String> names = new ArrayList<>();
for (FlowDefModel.Edge e : model.outEdges(node.id)) {
names.add(e.name);
}
return names;
}
/** form_data_json 原样透传为 variables(SDK 发起参数不加新字段;解析失败=空对象) */
private Map<String, Object> 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<Map<String, Object>>() {
});
} 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<String> 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<String> outlets,
String error, int attempt) {
try {
List<Integer> 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<Integer> alertUserIds() {
List<Integer> 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;
}
}