Files
svc-hall-service/src/main/java/com/yunhai/svchall/job/FlowReconcileJob.java
T
chenxing 05d2143ce1
build / build-push (push) Successful in 48s
feat(p3.2): bizType 方案B——常量换终值 svc-hall + 双值双查 + P1-02 行级绑定校验 + P1-03 R4 时序修复
- SvcFlowCallbackHandler.BIZ_TYPE → SvcBizTypes.CURRENT(svc-hall,H-1 实核 appKey=100008
  注册行 app_code);yml 回调白名单双值 [svc-hall, svc_item](收敛条件=另一类型不再可能
  产生事件,本轮不收敛;回滚也保双值)
- 双值双查补丁(先终值后存量,S0-H2 实测中心 svc_item 存量=0,次查为防御性兼容):
  ApplyViewGuard 办理人分支 / FlowReconcileJob 对账(命中判据=内容非空,兼容 miss 空载体)/
  progress 空态出参 / S2 审批代理 byBiz·todo(S2 已落)
- P1-02 行级事件绑定校验:事件 schId 与行一致、行有镜像实例时事件 instanceId 必须一致;
  不匹配 200 消费但不推进+告警日志(不抛 5xx 防重推风暴);行无值(首发回调早于回填)放行,
  doFlowApply 回填侧对实例不一致告警留痕(方案B 书面接受第二实例降级)
- P1-03 R4 时序修复:markResubmitted 清旧 flowResultJson(保留 lastInstanceVersion 跨轮
  乱序防护);Handler 终态守卫改版本感知(事件版本>已消费版本=新轮竞态窗口放行,不再以
  旧结果非空一刀切拒绝);非终态推进清残留 finishTime;三场景测试静态化注入
  (新轮回调先于 apply 响应/新轮快速终态/旧轮迟到重复)
- 测试断言按新语义更新:新件出站断言 svc-hall×3;全量 163/163 绿(Stage S3/P3.2)
2026-10-19 23:49:02 +08:00

112 lines
5.1 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.svchall.job;
import com.yunhai.svchall.entity.ServiceApply;
import com.yunhai.svchall.enums.ApplyFlowStatusEnum;
import com.yunhai.svchall.repository.ServiceApplyRepository;
import com.yunhai.svchall.service.FlowClient;
import com.yunhai.svchall.service.SvcBizTypes;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 对账兜底 Job(降级为应急闸,V2.0 Part B.4#5 → 契约冻结 v1.4):
* 升级后 flow-center 回调为<b>持续重试</b>(1m→5m→30m→2h→6h→60m 封顶)+ retryCount=6 挂起并
* UserMsg 告警恰一次 + 死信管理端手工处理——消费方免每日对账(本应用 5xx 语义已对齐),
* 故本 Job 默认停用(svchall.flow.reconcile-enabled=false);仅在大面积漂移事故应急时置 true
* 手工开闸。开启时语义不变:拉在途办件(审批中)→ 按 biz 键查 flow-center 真实状态 →
* 漂移以 flow-center 为准修正(对账直改状态,不写 service_flow_event 去重表与结果明细——
* 与回调链路的差异,仅应急用不混淆镜像口径)。
*/
@Slf4j
@Component
public class FlowReconcileJob {
private final ServiceApplyRepository applyRepository;
private final FlowClient flowClient;
private final boolean reconcileEnabled;
public FlowReconcileJob(ServiceApplyRepository applyRepository, FlowClient flowClient,
@Value("${svchall.flow.reconcile-enabled:false}") boolean reconcileEnabled) {
this.applyRepository = applyRepository;
this.flowClient = flowClient;
this.reconcileEnabled = reconcileEnabled;
}
@Scheduled(cron = "0 0 4 * * ?")
public void reconcile() {
if (!reconcileEnabled) {
return; // 默认停用:免对账契约下推拉主链路由回调持续重试承担
}
if (!flowClient.enabled()) {
return;
}
// 全租户口径:审批中的在途办件(分页上限 500,量级评估:全校审批 << 千/周)
List<ServiceApply> pending = applyRepository.findByFlowStatus(ApplyFlowStatusEnum.APPROVING.getCode(),
org.springframework.data.domain.PageRequest.of(0, 500));
int fixed = 0;
for (ServiceApply apply : pending) {
if (apply.getFlowInstanceId() == null) {
continue;
}
try {
// 双值双查(P3.2 方案B):先终值后存量,首个命中实例为准——命中判据用内容
// (id/status 非空)而非"非 null",对 miss 形态(运行时 data=null / 空载体)都稳
Map<String, Object> remote = null;
for (String bizType : SvcBizTypes.queryOrder()) {
Map<String, Object> candidate = flowClient.getInstanceByBiz(
apply.getSchId(), bizType, String.valueOf(apply.getId()));
if (candidate != null
&& (candidate.get("id") != null || candidate.get("status") != null)) {
remote = candidate;
break;
}
}
if (remote == null) {
continue;
}
Integer remoteStatus = toInt(remote.get("status"));
Integer remoteVersion = toInt(remote.get("version"));
if (remoteStatus != null && !remoteStatus.equals(apply.getFlowStatus())
&& (remoteVersion == null || remoteVersion > apply.getLastInstanceVersion())) {
apply.setFlowStatus(remoteStatus);
if (remoteVersion != null) {
apply.setLastInstanceVersion(remoteVersion);
}
if (remoteStatus.equals(ApplyFlowStatusEnum.COMPLETED.getCode())
|| remoteStatus.equals(ApplyFlowStatusEnum.REJECTED.getCode())
|| remoteStatus.equals(ApplyFlowStatusEnum.WITHDRAWN.getCode())) {
apply.setFinishTime(LocalDateTime.now());
}
applyRepository.save(apply);
fixed++;
}
} catch (Exception e) {
log.warn("[svc-hall] 对账跳过 applyId={}:{}", apply.getId(), e.toString());
}
}
if (fixed > 0 || !pending.isEmpty()) {
log.info("[svc-hall] 对账完成(应急闸开启中):在途 {} 条,修正 {} 条", pending.size(), fixed);
}
}
private static Integer toInt(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
try {
return Integer.parseInt(String.valueOf(value));
} catch (NumberFormatException e) {
return null;
}
}
}