团期物料门复判与兜底重扫:hl-admin 36c9064a 在团期详情页 onContractActionSuccess 补 fetchDetailSafe 重拉主详情,出具/签署触发物料门 自动推进时状态立即刷新。frontmatter 翻 verified + owner + ref + status_note。
14 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7526 | 团期物料门复判——手工复判端点 + 出具态事件触发 + 兜底重扫 Job | admin | jw(GIT) | 新增接口 | deployed | verified | verified | mmg | 36c9064a | 2026-09-12 | 前端 2026-09-12 已交付(hl-admin 36c9064a):实证缺口在团期详情页 onContractActionSuccess 只刷合同看板/芯片明细、未重拉团期主详情(同页其它回调均调 fetchDetailSafe);补 store.fetchDetailSafe(id) 重拉主详情含 batchStatus,出具/签署触发物料门自动推进时状态立即刷新。运维端点 recheck-material-gate 与兜底 Job /v3/internal/**(网关403)前端不对接。checkpoint 7 项全绿(含生产构建)。 | 2026-09-12 | dev-v3 |
order-v3/user: 团期物料门复判与兜底重扫
服务: hl-order-service-v3 / hl-user-service
PR: #7559
Issue: #7526
⚠️ 关键变化
🔴 本单修的是一个「团期永久卡死」的死锁,不是新功能:团期从「资源准备中」推进到「物料准备中」有三道门,其中第三道要求全团活跃子订单合同已签、保险已承保;而合同与保险恰恰是四项资源(房/车/导/摄)配齐之后才被允许出具的。门变真的那一刻——客户签完合同、保单出单成功——原先没有任何代码去复判,团期于是永久停在「资源准备中」。本单补上两条触发源,门条件一行未动。
🔴 推进现在可能在你没有发起任何团期操作时发生:出具态变更(合同签署完成 / 保险承保完成 / 出具态被清空)会在事务提交后自动复判一次;另有兜底 Job 每 5 分钟重扫。前端若缓存了团期状态,需要在合同保险面板动作之后重新拉取团期状态,不能假定「我没点推进它就不会变」。
🔴 新端点是给运维定点解卡用的,不是给业务流程用的:recheck-material-gate 只在团期处于「资源准备中」时可用,其余状态一律返回 589561。它不改任何门条件,只是手工敲一次门,并在没推进时告诉你是哪道门挡着。
一、背景
团期状态机中「资源准备中 → 物料准备中」这一跳的判定(GroupBatchService#maybeAdvanceToMaterialPreparing)是 private 方法,全仓只有 5 个调用点,语义分别是「房/车/导/摄某一项 ready 翻真」与「成团」。
这 5 个时刻全部发生在合同签署之前——判据 GroupBatchContractResolver#issuable 要求「状态为资源准备中且四项 ready 全真」才放行出具,也就是说合同/保险只可能在四项配齐之后才出具、才签署。于是:四项翻真时第三道门必然判假;等第三道门真正变真时,已经无人敲门。
线上表现:团期四项资源都配齐了、每户合同也签了保险也出了,团期状态却一直停在「资源准备中」,物料准备无法开始,且没有任何报错。
本单补两条触发源:事件(实时主链路) + 兜底 Job(最终一致保险丝),并新增一个运维手工复判端点。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 手工复判物料门 | POST | /v3/admin/order/group-batch/{groupBatchId}/recheck-material-gate |
新增 | 运维定点解卡;未推进时回执指明挡门原因 |
| 2 | 物料门兜底重扫 | POST | /v3/internal/jobs/group-batch-material-gate/run |
新增 | 服务间接口,由 Quartz 经 Feign 触发;公网网关不放行,前端无需对接 |
三、接口详情
1. 手工复判物料门 POST /v3/admin/order/group-batch/{groupBatchId}/recheck-material-gate
VO: Long → Result<GroupBatchMaterialGateRespVO>
使用场景
运维发现某个团期四项资源都配齐、合同保险也都出齐了,状态却还停在「资源准备中」时,手工敲一次门。推进成功即解卡;没推进则回执直接告诉你是哪道门挡着、哪一户挡着,不必再去翻库。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 团期主订单 ID | 团期ID |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| advanced | Boolean | 本次调用是否把团期推进到了「物料准备中」 |
| currentStatus | String | 复判后的团期状态 code |
| currentStatusName | String | 复判后的团期状态中文名 |
| blockedGate | String | 挡门标识,RESOURCE_NOT_READY=四项资源未配齐 / CONTRACT_INSURANCE_NOT_READY=合同或保险未出齐;已推进或疑似并发时为 null |
| blockedOrderId | String | 挡门的子订单 ID(字符串序列化防精度丢失);非逐户挡门时为 null |
| blockedReason | String | 人可读的挡门原因;已推进时为 null |
请求示例
POST /v3/admin/order/group-batch/2097500233511362561/recheck-material-gate
响应示例
{
"code": 200,
"message": "成功",
"data": {
"advanced": true,
"currentStatus": "MATERIAL_PREPARING",
"currentStatusName": "物料准备中",
"blockedGate": null,
"blockedOrderId": null,
"blockedReason": null
},
"traceId": null,
"success": true
}
空数据 / 降级响应
门未开时 HTTP 200 + code=200,advanced=false 并带挡门诊断(这是正常回执,不是错误):
{
"code": 200,
"message": "成功",
"data": {
"advanced": false,
"currentStatus": "RESOURCE_PREPARING",
"currentStatusName": "资源准备中",
"blockedGate": "CONTRACT_INSURANCE_NOT_READY",
"blockedOrderId": "2097500234186702849",
"blockedReason": "子订单 2097500234186702849 合同状态 GENERATED(需 SIGNED)"
},
"traceId": null,
"success": true
}
三道门重读都满足却没推进(被并发改写)时,blockedGate 留 null、不编造原因:
{
"code": 200,
"message": "成功",
"data": {
"advanced": false,
"currentStatus": "RESOURCE_PREPARING",
"currentStatusName": "资源准备中",
"blockedGate": null,
"blockedOrderId": null,
"blockedReason": "三道门复判均满足但状态未变,可能被并发操作改写,请重试一次"
},
"traceId": null,
"success": true
}
错误响应
{
"code": 589561,
"message": "团期当前状态为「招募中」,不在「资源准备中」,无需复判物料门",
"data": null,
"traceId": null,
"success": false
}
{
"code": 589500,
"message": "团期不存在",
"data": null,
"traceId": null,
"success": false
}
业务边界
- 需要
group-batch:manage权限 - 仅「资源准备中」可用;其余状态(含已推进到「物料准备中」及之后)一律
589561,这是设计行为而非失败 - 幂等:重复调用与调用一次等价,时间线只由 CAS 赢家写一条
- 本端点不改任何门条件,推进判据仍在
GroupBatchService#maybeAdvanceToMaterialPreparing;回执里的诊断是事后重读一次得出的解释
2. 物料门兜底重扫 POST /v3/internal/jobs/group-batch-material-gate/run
VO: void → Result<Integer>
使用场景
服务间接口。由 hl-user-service 的 Quartz(sys_job.job_id = 1044,cron 0 2/5 * * * ?)经 Feign 触发,游标分批扫「资源准备中 + 四项 ready 全真」的团期逐条复判。公网网关不放行 /v3/internal/**(返 403),前端无需也无法对接,此处列出仅供核对契约边界。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| X-Internal-Token | Header | String | ✅ | 内部服务令牌 | 由 InternalAuthInterceptor 校验 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| data | Integer | 本轮成功推进到「物料准备中」的团期数;未抢到分布式锁返回 0 |
请求示例
POST /v3/internal/jobs/group-batch-material-gate/run
响应示例
{
"code": 200,
"message": "成功",
"data": 1,
"traceId": null,
"success": true
}
空数据 / 降级响应
无可推进团期时返回 data=0;未抢到分布式锁(另一实例正在跑)同样返回 data=0,这不是失败。
错误响应
{
"code": 403,
"message": "接口不可访问",
"data": null,
"traceId": null,
"success": false
}
业务边界
- 分布式锁 key 为
group-batch-material-gate,TTL 5 分钟,与自动成团 / 生命周期推进 / enrolled 对账各自独立 - 单条团期复判失败只记日志、不中断本轮
- 每批上限 300 条,按
group_batch_id升序游标推进 - 与事件路径调同一个幂等入口,重复触发无副作用
四、契约约束与正确调用方式
blockedOrderId是字符串:Long 精度在 JS 侧会丢,出参已@JsonSerialize(ToStringSerializer),前端按字符串处理,不要parseInt。advanced=false不是错误:code仍为 200、success仍为 true。判「有没有推进」看data.advanced,判「调用有没有成功」看code。blockedGate为 null 且advanced=false表示疑似并发,提示用户重试即可,不要据此展示「原因未知」的错误页。- 不要用本端点做业务流程推进:正常链路由出具态事件自动完成,本端点是运维兜底。
五、数据库行为
| 表 | 行为 |
|---|---|
order_group_batch |
门全过时 CAS 更新 batch_status:RESOURCE_PREPARING → MATERIAL_PREPARING;CAS 落空不重试、不报错 |
group_batch_status_log |
推进成功时新增一条 event_type=BATCH_RESOURCE_READY、change_type=STATUS、content=四项资源全就绪 + 合同保险出齐。事件路径触发的 operator_type=ADMIN,Job 路径触发的 operator_type=SYSTEM,可据此区分来源。写入失败只 WARN 降级,不影响推进 |
sys_job(hl_user_service) |
新增 job_id=1044 团期物料门兜底重扫,invoke_target=groupBatchMaterialGateJob.run(),cron 0 2/5 * * * ?,misfire_policy=FIRE_ONCE,concurrent=FALSE,status=PAUSED(迁移 V20260912_110) |
无新增表、无新增字段、无 DDL。合同/保险是否出齐仍按 order_main.contract_status / insurance_status 自评估,不落团期级冗余列。
六、边界行为
- 已取消子订单不参与合同/保险门判定:
order_status=CANCELLED的子订单跳过,只被它挡住时视为门已过。 - 团期无活跃子订单:合同/保险门放行。
needs_guide/needs_photographer为 false 的团期:对应 ready 位已在成团时免闸置真,天然满足四项条件。- 合同/保险状态为 null(未出具):挡门回执文案渲染为「未出具」,不是字面
null。 - 软删团期:一律按「团期不存在」
589500处理。 - 事件监听器失败不重抛:出具态镜像已提交、无从回滚,监听器异常被吞掉并记 ERROR 日志,由兜底 Job 补上——这正是必须同时做「事件 + Job」两条路的原因。
七、不影响范围
- 门条件一行未动:四项 ready 与合同/保险的判据完全不变,不放宽也不收紧。
- 原有 5 个推进调用点未触碰(某项 ready 翻真 ×4 + 成团 ×1),行为与改前逐字相同。
- 合同/保险出具链路不变:
GroupBatchContractResolver#issuable零改动;两个镜像服务只在回写之后追加一次事件发布,发布失败只 WARN,不影响镜像本身。 - 不影响「物料准备中 → 待出行」及之后的任何一跳。
GroupBatchIssueGateContract及其实现类仅订正过期 javadoc,无运行时行为变更。
八、测试环境已验证
api.test.1814.love:9443,2026-09-12,分支 feature/7526-material-gate-reevaluate @ 32b9ea9a3(order-v3 + user-service 同批),验收后已合入 dev-v3 并把 TEST 换回主干(09e8cbfaa)。
- 先复现 bug 本体:团期四项 ready 全真、全团合同 SIGNED 保险 INSURED,静置 30 秒状态纹丝不动。
- 事件路径:
03:37:00Quartz 刚跑过未推进 →03:37:09管理端「作废重开保险」→03:37:10团期RESOURCE_PREPARING → MATERIAL_PREPARING,新增BATCH_RESOURCE_READY(operator_type=ADMIN)。 - Job 路径:直接 SQL 改出具态绕过事件 → 团期确认仍卡住 → cron 于
03:27:00自行触发,sys_job_log1044SUCCESS→ 团期推进,BATCH_RESOURCE_READY(operator_type=SYSTEM)。Quartz → 白名单 → 桥接 Bean → Feign 整链一次跑通。 - 幂等:已推进团期上再跑一次 cron + 手工复判端点各一次 → 状态不变、时间线不新增。
- 挡门回执:
blockedGate=CONTRACT_INSURANCE_NOT_READY、blockedOrderId指向真实未签子订单;保险缺失场景文案为「保险状态 未出具(需 INSURED)」。 - 错误码:对「招募中」团期调同端点 → HTTP 200 +
589561。 - 单测:新增 35 例 / 7 个类;
hl-user-service全量 3985 例 BUILD SUCCESS;order-v3 分两遍共 10169 例,ArchUnit 6 个类全绿。
十、相关文档
- Issue #7526、PR #7559
- 前置:#7023(合同/保险门)、#7249(出具判据改判四项 ready)、#7105 / #7243(团期合同保险面板 GB-ADM-030/031)
关联 / 联系人
- 后端:jw
- 前端:待认领(团期详情页在合同保险面板动作后需重新拉取团期状态)