- 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
218 lines
11 KiB
Java
218 lines
11 KiB
Java
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;
|
||
}
|
||
}
|