feat(p3.2): bizType 方案B——常量换终值 svc-hall + 双值双查 + P1-02 行级绑定校验 + P1-03 R4 时序修复
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)
This commit is contained in:
chenxing
2026-10-19 23:49:02 +08:00
parent ed3c9e6a72
commit 05d2143ce1
8 changed files with 179 additions and 35 deletions
@@ -135,7 +135,7 @@ class ServiceApplyFlowSubmitTest {
ApplyVo vo = service.submit(user(9, 1), request());
assertEquals(ApplyFlowStatusEnum.PENDING.getCode(), vo.getFlowStatus(), "提交响应=PENDING 行,状态异步翻转");
ArgumentCaptor<String> bizId = ArgumentCaptor.forClass(String.class);
verify(flowClient).apply(eq(1L), eq("fg-1"), eq("svc_item"), bizId.capture(),
verify(flowClient).apply(eq(1L), eq("fg-1"), eq("svc-hall"), bizId.capture(),
anyString(), any(), any(), eq(9), eq("张三"), any());
assertEquals("42", bizId.getValue(), "bizId=办件 id(uk 幂等键)");
verify(applyRepository).markApproving(eq(42), eq(9011L), any(LocalDateTime.class));
@@ -148,7 +148,7 @@ class ServiceApplyFlowSubmitTest {
// router.replace 同形);路径参数 /apply-detail/{id} 会 no-match 空白页(走查 BP-R3-2)
service.submit(user(9, 1), request());
ArgumentCaptor<String> bizUrl = ArgumentCaptor.forClass(String.class);
verify(flowClient).apply(eq(1L), eq("fg-1"), eq("svc_item"), eq("42"),
verify(flowClient).apply(eq(1L), eq("fg-1"), eq("svc-hall"), eq("42"),
anyString(), bizUrl.capture(), any(), eq(9), eq("张三"), any());
assertEquals("/apply-detail?id=42", bizUrl.getValue());
}
@@ -249,7 +249,7 @@ class ServiceApplyFlowSubmitTest {
when(applyRepository.findByIdAndSchId(42, 1L)).thenReturn(Optional.of(apply));
service.resubmit(user(9, 1), 42);
// 同 bizId=42(flow-center R4 命中 30/40 → 复用同一办件行重启推进)
verify(flowClient).apply(eq(1L), eq("fg-1"), eq("svc_item"), eq("42"),
verify(flowClient).apply(eq(1L), eq("fg-1"), eq("svc-hall"), eq("42"),
anyString(), anyString(), any(), eq(9), eq("张三"), any());
verify(applyRepository).markResubmitted(eq(42), eq(9011L), any(LocalDateTime.class));
}
@@ -162,4 +162,81 @@ class SvcFlowCallbackHandlerTest {
assertThrows(org.springframework.dao.DataAccessResourceFailureException.class,
() -> handler.onEvent(event("e10", "flow.completed", 3)));
}
// ===== P1-02 行级事件绑定校验(双值白名单下"收"≠"属于该行") =====
@Test
void binding_schoolMismatch_rejectedWithoutAdvance() {
FlowCallbackEvent e = event("b1", "flow.completed", 3);
e.setSchId(2); // 办件行 schId=1
handler.onEvent(e); // 200 消费但不推进(不抛)
assertEquals(10, apply.getFlowStatus().intValue(), "学校不匹配拒收,状态不动");
assertNull(apply.getFlowResultJson());
org.mockito.Mockito.verify(applyRepository, org.mockito.Mockito.never()).save(any(ServiceApply.class));
}
@Test
void binding_instanceMismatch_rejectedWithoutAdvance() {
apply.setFlowInstanceId(100L); // 行已镜像实例 100
FlowCallbackEvent e = event("b2", "flow.completed", 3); // 事件实例 9527
handler.onEvent(e);
assertEquals(10, apply.getFlowStatus().intValue(), "实例不匹配拒收,状态不动");
org.mockito.Mockito.verify(applyRepository, org.mockito.Mockito.never()).save(any(ServiceApply.class));
}
@Test
void binding_rowWithoutInstance_firstCallbackAllowed() {
// 首发回调早于 apply 响应回填的合法窗口:行无镜像实例 → 放行推进
assertNull(apply.getFlowInstanceId());
handler.onEvent(event("b3", "flow.task.created", 2));
assertEquals(10, apply.getFlowStatus().intValue());
assertEquals(2, apply.getLastInstanceVersion().intValue());
}
// ===== P1-03 R4 时序(H-9 三场景静态化:可控顺序注入,无真实延迟) =====
/** 场景 1:新轮回调先于 apply 响应(行仍 30 终态,新轮 submitted v4 先到) */
@Test
void r4_newRoundCallbackBeforeApplyResponse_advances() {
apply.setFlowStatus(30);
apply.setFlowResultJson("{\"event\":\"flow.rejected\"}");
apply.setFinishTime(java.time.LocalDateTime.now());
apply.setLastInstanceVersion(3);
handler.onEvent(event("r1", "flow.submitted", 4));
assertEquals(10, apply.getFlowStatus().intValue(), "新轮(版本更高)回调放行,回开为 10");
assertEquals(4, apply.getLastInstanceVersion().intValue());
assertNull(apply.getFinishTime(), "回开为在途时清残留 finishTime");
assertTrue(apply.getFlowResultJson().contains("flow.submitted"));
}
/** 场景 2:新轮快速终态(行仍 30,新轮 completed v4 在 markResubmitted 前落定) */
@Test
void r4_newRoundQuickTerminal_overwritesOldResult() {
apply.setFlowStatus(30);
apply.setFlowResultJson("{\"event\":\"flow.rejected\",\"opinion\":\"旧轮意见\"}");
apply.setFinishTime(java.time.LocalDateTime.now());
apply.setLastInstanceVersion(3);
FlowCallbackEvent e = event("r2", "flow.completed", 4);
e.getData().put("result", "APPROVED");
handler.onEvent(e);
assertEquals(20, apply.getFlowStatus().intValue(), "新轮终态覆盖旧轮终态");
assertNotNull(apply.getFinishTime());
assertTrue(apply.getFlowResultJson().contains("APPROVED"), "结果快照按新轮覆盖");
assertEquals(4, apply.getLastInstanceVersion().intValue());
}
/** 场景 3:旧轮迟到/重复(版本更低被乱序门丢弃;同版本另一终态被结果守卫拒绝;同版本在途被终态守卫拒绝) */
@Test
void r4_oldRoundLateDuplicate_rejected() {
apply.setFlowStatus(30);
apply.setFlowResultJson("{\"event\":\"flow.rejected\"}");
apply.setLastInstanceVersion(3);
String frozen = apply.getFlowResultJson();
handler.onEvent(event("r3a", "flow.completed", 2)); // 旧版本:乱序门丢弃
handler.onEvent(event("r3b", "flow.withdrawn", 3)); // 同版本另一终态:结果已落定拒
handler.onEvent(event("r3c", "flow.task.handled", 3)); // 同版本在途:终态不回退拒
assertEquals(30, apply.getFlowStatus().intValue(), "旧轮事件全部拒收");
assertEquals(frozen, apply.getFlowResultJson(), "旧轮结果不被覆盖");
}
}