文件
hl-api-changelog/changelogs-v2/2026-09/12_7526_团期物料门复判与兜底重扫-新增接口-管理后台.md
Mimingguang a0d72720a6
changelog-filename-gate / validate (push) Failing after 3s
chore(changelog): 7526 前端交付回写 verified
团期物料门复判与兜底重扫:hl-admin 36c9064a 在团期详情页
onContractActionSuccess 补 fetchDetailSafe 重拉主详情,出具/签署触发物料门
自动推进时状态立即刷新。frontmatter 翻 verified + owner + ref + status_note。
2026-09-12 07:46:16 +08:00

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:00 Quartz 刚跑过未推进 → 03:37:09 管理端「作废重开保险」→ 03:37:10 团期 RESOURCE_PREPARING → MATERIAL_PREPARING,新增 BATCH_RESOURCE_READY(operator_type=ADMIN)。
  • Job 路径:直接 SQL 改出具态绕过事件 → 团期确认仍卡住 → cron 于 03:27:00 自行触发,sys_job_log 1044 SUCCESS → 团期推进,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
  • 前端:待认领(团期详情页在合同保险面板动作后需重新拉取团期状态)