build / build-push (push) Successful in 48s
- 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)
112 lines
5.1 KiB
Java
112 lines
5.1 KiB
Java
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;
|
||
}
|
||
}
|
||
}
|