feat(flow): 回调链路迁平台 flow-callback starter + 表单级流程绑定
build / build-push (push) Failing after 8s

- 回调迁 starter(2026-09-20):删本地实现 FlowCallbackService/Controller/
  FilterConfiguration 及旧测试,X-Flw 验签/幂等/乱序门由 starter 承担,
  业务映射入 SvcFlowCallbackHandler(biz-type svc_item);端点仍 /callback/flow,
  存量注册 callback_url 不变、secret 零轮换,镜像表 flow_callback_reg
  由 sql/20260920_flow_callback_starter_migration.sql 迁移
- 表单级流程绑定:ServiceForm 增 flowGuid,事项未绑流程时回退取绑定表单
  最新版本(effectiveFlowGuid),绑定校验/在线办理口径同步
- FlowReconcileJob/FlowCenterClient/OpenApiAuthEnforceConfiguration 等
  SDK 适配微调;补 .gitignore(target/、日志)
- 全量 mvn test 本地绿(含新增 SvcFlowCallbackHandlerTest/
  ServiceItemEffectiveFlowTest)
This commit is contained in:
chenxing
2026-09-21 09:37:33 +08:00
parent 27e19923fb
commit b08d076f0b
26 changed files with 568 additions and 1052 deletions
@@ -0,0 +1,138 @@
package com.yunhai.svchall.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.platform.flowcallback.FlowCallbackHandler;
import com.yunhai.platform.openapi.flow.model.FlowCallbackEvent;
import com.yunhai.svchall.entity.ServiceApply;
import com.yunhai.svchall.enums.ApplyFlowStatusEnum;
import com.yunhai.svchall.repository.ServiceApplyRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 流程中心回调业务映射(2026-09-20 迁 yh-flow-callback-spring-boot-starter):
* X-Flw 验签(starter 可选过滤器)、ping、必填校验、bizType 过滤、eventId 幂等、
* instanceVersion 乱序门由 starter 接收管线承担;本类只做办件镜像状态机推进——
* 按 bizId 行锁定位 + 单据级乱序闸门 + 事件映射(submitted/task.*→10;completed→20;
* rejected→30;withdrawn→40)+ 终态不回退守卫 + 结果明细快照 + 终态 finishTime。
* 应答语义不变:本方法抛任何异常 → starter 回 5xx(flow-center 持续重试追平,免对账);
* 正常返回(含办件不存在/未知事件等不可重试场景)→ 200。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SvcFlowCallbackHandler implements FlowCallbackHandler {
/** 本应用业务类型(flow-center apply/bizType 固定值,uk(sch_id, biz_type, biz_id)) */
public static final String BIZ_TYPE = "svc_item";
private final ServiceApplyRepository applyRepository;
private final ObjectMapper objectMapper;
@Override
public void onEvent(FlowCallbackEvent event) {
Integer applyId;
try {
applyId = Integer.valueOf(event.getBizId().trim());
} catch (NumberFormatException e) {
log.warn("[svc-hall] 回调 bizId 非法(丢弃):{}", event.getBizId());
return;
}
// 行锁串行化:并发推送(flow-center 推送池)对同一办件的处理排队,防丢更新
ServiceApply apply = applyRepository.findByIdForUpdate(applyId).orElse(null);
if (apply == null) {
log.warn("[svc-hall] 回调找不到对应办件 bizId={} eventId={}(忽略)",
event.getBizId(), event.getEventId());
return;
}
Integer version = event.getInstanceVersion();
// 单据级乱序闸门(starter 表级闸门之外的第二道:以行锁后读到的办件版本为准)——
// 严格小于已消费版本才丢弃(同版本多事件合法:submitted+task.created 同 v1)
if (version != null && apply.getLastInstanceVersion() != null
&& version < apply.getLastInstanceVersion()) {
log.info("[svc-hall] 回调乱序丢弃 instanceId={} incoming={} last={}",
event.getInstanceId(), version, apply.getLastInstanceVersion());
return;
}
advance(apply, event.getEvent(), version, event);
log.info("[svc-hall] 办件镜像状态推进 applyId={} instanceId={} event={} → status={}",
applyId, event.getInstanceId(), event.getEvent(), apply.getFlowStatus());
}
/** 状态推进(幂等:同一事件重放由 eventId 去重拦截,此处只需正确覆盖目标态) */
private void advance(ServiceApply apply, String event, Integer version, FlowCallbackEvent payload) {
Integer status = mapStatus(event);
if (status == null) {
log.warn("[svc-hall] 未知回调事件类型:{}(忽略)", event);
return;
}
// P1-3 终态不回退守卫:终态事件与最后一个 task.handled 同 instanceVersion,若 handled
// 在 completed/rejected 之后落库(flow-center 推送池并发不保序),无守卫会把 20/30 打回 10
// 并用 handled 明细覆盖已落定的 flow_result_json。重提不冲突:resubmit 成功后先以
// 窄列原子置 10,新一轮事件到达时行已非终态。
if (ApplyFlowStatusEnum.byCode(apply.getFlowStatus()).terminal()) {
if (!ApplyFlowStatusEnum.byCode(status).terminal()) {
log.warn("[svc-hall] 终态不回退:applyId={} flowStatus={} 拒绝非终态事件 {}(v{})",
apply.getId(), apply.getFlowStatus(), event, version);
return;
}
if (apply.getFlowResultJson() != null && !apply.getFlowResultJson().isEmpty()) {
log.warn("[svc-hall] 终态结果已落定:applyId={} flowStatus={} 拒绝覆盖 flow_result_json(事件 {})",
apply.getId(), apply.getFlowStatus(), event);
return;
}
}
Map<String, Object> result = new HashMap<>();
result.put("event", event);
Map<String, Object> data = payload.getData();
if (data != null) {
copyIfPresent(data, result, "result");
copyIfPresent(data, result, "opinion");
copyIfPresent(data, result, "rejectNode");
}
try {
apply.setFlowResultJson(objectMapper.writeValueAsString(result));
} catch (Exception ignored) {
// 明细序列化失败不影响状态推进
}
apply.setFlowStatus(status);
if (version != null) {
apply.setLastInstanceVersion(version);
}
if (status.equals(ApplyFlowStatusEnum.COMPLETED.getCode())
|| status.equals(ApplyFlowStatusEnum.REJECTED.getCode())
|| status.equals(ApplyFlowStatusEnum.WITHDRAWN.getCode())) {
apply.setFinishTime(LocalDateTime.now());
}
applyRepository.save(apply);
}
private static void copyIfPresent(Map<String, Object> data, Map<String, Object> target, String key) {
Object v = data.get(key);
if (v != null) {
target.put(key, String.valueOf(v));
}
}
private static Integer mapStatus(String event) {
switch (event) {
case "flow.submitted":
case "flow.task.created":
case "flow.task.handled":
return ApplyFlowStatusEnum.APPROVING.getCode();
case "flow.completed":
return ApplyFlowStatusEnum.COMPLETED.getCode();
case "flow.rejected":
return ApplyFlowStatusEnum.REJECTED.getCode();
case "flow.withdrawn":
return ApplyFlowStatusEnum.WITHDRAWN.getCode();
default:
return null;
}
}
}