diff --git a/changelogs-v2/2026-09/14_7666_接送机服务日回填CAS落空按库内真实值回显-内部接口-修复-管理后台.md b/changelogs-v2/2026-09/14_7666_接送机服务日回填CAS落空按库内真实值回显-内部接口-修复-管理后台.md new file mode 100644 index 00000000..91964607 --- /dev/null +++ b/changelogs-v2/2026-09/14_7666_接送机服务日回填CAS落空按库内真实值回显-内部接口-修复-管理后台.md @@ -0,0 +1,129 @@ +--- +schema: "hl-changelog/v2" +ticket: "7666" +title: "接送机服务日回填(内部):CAS 落空时按库内真实值回显,NO_TRANSPORT 多一种成因" +consumer: "internal" +author: "wx(GIT)" +change_type: "修复" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "内部接口(/v3/internal/**,不经网关),收件人是后端调用方(#7443 展开前置、运维批量补数脚本),不是 mmg。不改请求/响应结构、不新增 outcome 取值、不新增错误码;只改 CAS 落空这一分支的 outcome 与 serviceDates 取值口径。本文件补正 13_7439_团期车务地基-内部接口-修改接口-管理后台.md 第 4 个端点的 outcome 说明。" +updated_at: "2026-09-14" +base: "dev-v3" +--- + +# order-v3: 接送机服务日回填(内部)CAS 落空后按库内真实值回显(修复) + +> **服务**: hl-order-service-v3(`RequirementService#backfillTransferServiceDates` → `fillCasMissOutcomeFromDb`) +> **消费方**: 后端内部调用方(#7443 展开前置、运维批量补数脚本),`/v3/internal/**` 不经 JWT、网关不暴露 +> **PR**: #7692(squash `a37fd8b447`) +> **Issue**: #7666 +> **日期**: 2026-09-14 +> **影响范围**: 1 个内部接口的一个分支(首次读为空、派生出日期、但 CAS 匹配 0 行);**不涉及**请求/响应结构、outcome 取值集合、错误码、路由与数据库 + +--- + +## ⚠️ 关键变化 + +🔴 **CAS 落空时不再无条件答 `ALREADY_PRESENT`,改为按库内当前读的真实值作答。** + +| 情形 | 改动前 | 改动后 | +|---|---|---| +| CAS 落空,库内该行此刻**有**服务日 | `ALREADY_PRESENT`,`serviceDates` = 本次调用自己派生的日期(可能与库内不同) | `ALREADY_PRESENT`,`serviceDates` = **库内真实值** | +| CAS 落空,库内该行此刻**仍为空**(NULL 或 `[]`) | `ALREADY_PRESENT`,`serviceDates` = 本次派生的日期(**失真**:库里其实没有) | **`NO_TRANSPORT`**,`serviceDates=[]` | +| CAS 落空,重读时已无活跃 TRANSFER 需求 | `ALREADY_PRESENT` | **`NOT_FOUND`**,`requirementId=null`,`serviceDates=[]` | +| 首次读就有服务日 / 回填成功 / 无活跃需求 / 派生不出日期 | 各自原样 | **不变** | + +🟢 CAS 没有落空的三种正常结局(`BACKFILLED` / 首次读 `ALREADY_PRESENT` / 派生不出的 `NO_TRANSPORT`)与 `NOT_FOUND`,逐字段不变。 + +--- + +## 一、背景 + +`backfillTransferServiceDates` 的流程是: +1. 先做一次快照读; +2. 用大交通派生出服务日; +3. 用 `WHERE status = 'PENDING_REVIEW'` 做 CAS 写回。 + +CAS 匹配 0 行,说明这一行在首次读之后被别的操作推离了 `PENDING_REVIEW`。推走它的操作不一定写了服务日,它可能是提交车务、打回或其他写口。 + +改动前,这个分支一律按「本来就有」作答,并把**自己派生的日期**当成库内的值返回。调用方拿到 `ALREADY_PRESENT` 就会继续调严格 GET,而库里可能仍是空的,于是在下一步被 809007 打掉。更糟的是,调用方会以为这一单「不用再管」。 + +本单改为:CAS 落空后,对同一行做一次当前读(`selectLatestByOrderIdForUpdate`),按库内真实值作答。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|------|------|------|------|----------|------| +| 1 | 回填接送机服务日(内部) | POST | `/v3/internal/order/orders/{orderId}/requirement/transfer/service-dates/backfill` | 修复 | CAS 落空分支按库内当前读作答 | + +--- + +## 三、接口详情 + +### 1. 回填接送机服务日(内部) + +- **方法 / 路径**:`POST /v3/internal/order/orders/{orderId}/requirement/transfer/service-dates/backfill` +- **路径参数**:`orderId`(订单 ID) +- **请求体**:无,**不变** +- **鉴权**:请求头 `X-Internal-Token`,**不变** +- **响应**:`Result`,字段 `requirementId` / `outcome` / `serviceDates`,**结构不变** +- **错误码**:不新增 + +**outcome 取值口径(补正 #7439 内部分册第 4 个端点的说明)**: + +| outcome | 含义(改动后) | +|---|---| +| `BACKFILLED` | 本次补齐服务日并推进 `PENDING_REVIEW → PENDING`;`serviceDates` 为本次写入的值 | +| `ALREADY_PRESENT` | **库内**该行确有非空服务日,本次未改动;`serviceDates` 为库内值(首次读就有,或 CAS 落空后当前读到了) | +| `NO_TRANSPORT` | 本次未写、未推状态,**且库内该行仍没有服务日**。两种成因:①大交通派生不出日期,该行停在 `PENDING_REVIEW`;②CAS 落空(该行已被推离 `PENDING_REVIEW`)后回读,库内仍为 NULL 或 `[]` | +| `NOT_FOUND` | 无活跃 TRANSFER 需求(首次读即无,或 CAS 落空后回读时已无);`requirementId=null` | + +**请求 / 响应实测原文**: +- 环境:2026-09-14,测试环境,直连实例 `http://192.168.100.236:8086`(内部接口不经网关)。 +- 部署:`hl-order-service-v3` 为 `dev-v3@79e3b9da7`,含本单,部署时间 21:59:54。 +- 数据:前置状态由 SQL 直更构造,事后已还原。 + +① CAS 落空、库内**有**日期。另一连接持行锁,把该行改为 `DONE` + 哨兵日期 `2099-01-01` 后提交;返回的是库内哨兵值,不是本次派生的日期: + +``` +POST /v3/internal/order/orders/2098381524394024962/requirement/transfer/service-dates/backfill + +HTTP 200 +{"code":200,"message":"成功","data":{"requirementId":"2098381840044761089","outcome":"ALREADY_PRESENT","serviceDates":["2099-01-01"]},"traceId":null,"success":true} +``` + +② CAS 落空、库内**仍为空**。该行已是 `PENDING`,`service_dates` 为 NULL,大交通能派生出日期: + +``` +POST /v3/internal/order/orders/2098372734110027778/requirement/transfer/service-dates/backfill + +HTTP 200 +{"code":200,"message":"成功","data":{"requirementId":"2098959908610183170","outcome":"NO_TRANSPORT","serviceDates":[]},"traceId":null,"success":true} +``` + +③ 回归,回填成功: + +``` +POST /v3/internal/order/orders/2098374023258722305/requirement/transfer/service-dates/backfill + +HTTP 200 +{"code":200,"message":"成功","data":{"requirementId":"2098959909633593346","outcome":"BACKFILLED","serviceDates":["2026-09-11","2026-09-18"]},"traceId":null,"success":true} +``` + +--- + +## 四、调用方需要做什么 + +- **分流规则不变**: + - `NO_TRANSPORT` → 就地失败关闭,**不得**再调 `GET .../requirement/vehicle?kind=TRANSFER`; + - `BACKFILLED` / `ALREADY_PRESENT` → 可以继续调 GET。 +- **不要**再把 `NO_TRANSPORT` 解读成「客人一定没填大交通」:它现在也可能表示「这一行被别的写口推走了,库里仍没有日期」。只要按上面的分流规则就地失败关闭即可;如需区分成因,看 order-v3 日志里的 `接送机服务日回填 CAS 落空且库内仍无服务日`(WARN,带 `status`)。 +- 改动前,调用方在并发下可能拿到过「`ALREADY_PRESENT` + 实际库里为空」的答复。如有据此跳过处理的批量脚本记录,建议对那批订单重跑一次回填端点。