feat(engine): NOTIFY 抄送 + CONDITION 条件路由 + JOIN scope 修复(两批全栈)
build / build-push (push) Successful in 47s
build / build-push (push) Successful in 47s
- NOTIFY:TYPE_NOTIFY 瞬时抄送(flw_cc 逐人留痕 + CC_SENT 事件 + 站内通知不加待办角标)、/api/flow/cc/my、轨迹 kind=CC 与 oapi progress 抄送步、示意图青框卡片
- CONDITION:flw_router 判断服务注册 + flw_route_log 决策留痕(REQUIRES_NEW,reroute 回滚不丢失败行)+ 同步判断调用(服务目录寻址 + 平台签名,3s×3 退避重试)+ 失败挂起/管理端重跑(不配默认出口)+ 可选事件 flow.route.decided
- JOIN scope:branch_key 路径前缀编码 + scope 级到齐放行 + DefValidator SPLIT/JOIN 配对与键长守卫(存量无点号键零迁移,历史直连图回退实例级口径)
- 管理端:/api/flow/admin/router/* CRUD + POST /api/flow/admin/instance/{id}/reroute
- 生产 SQL:sql/20260923_create_flw_cc.sql(含 flw_task 时间列升 DATETIME(3))、20260924_create_flw_router_and_route_log.sql
- 测试:全量 125 绿(FlowEngineIT 24,含条件/嵌套并行/退回重走用例)
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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);
|
||||
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(), 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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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, null, truncate(error));
|
||||
alertSuspended(instance, node, outlets, error, attempt);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 落一行留痕(REQUIRES_NEW:外部调用已真实发生,不随外层回滚),返回本轮 attempt(告警 msgNo 用) */
|
||||
private int persist(FlwInstance instance, FlowDefModel.Node node, 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(node.routerKey);
|
||||
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, "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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user