docs(changelog): #7666 接送机服务日回填(内部)CAS 落空按库内真实值回显(修复)
changelog-filename-gate / validate (push) Failing after 2s

POST /v3/internal/order/orders/{orderId}/requirement/transfer/service-dates/backfill:CAS 落空时按库内当前读作答,
库内仍空答 NO_TRANSPORT(不再失真答 ALREADY_PRESENT),库内有日期答 ALREADY_PRESENT + 库内真实值。
补正 13_7439 内部分册第 4 个端点的 outcome 口径。测试服 order-v3 dev-v3@79e3b9da7 直连实例实测。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015LU2ERekNLbvhsMqzFruHo
这个提交包含在:
API Changelog Bot
2026-09-14 22:37:38 +08:00
共同撰写人 Claude Opus 5
父节点 234e557852
当前提交 523c1faf99
@@ -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<TransferServiceDatesBackfillRespVO>`,字段 `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` + 实际库里为空」的答复。如有据此跳过处理的批量脚本记录,建议对那批订单重跑一次回填端点。