13 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 | 7965 | 改派幂等重放:assignmentSlotId 不再被抹成 null,重放与首调同值 | admin | jw(GIT) | 修改接口 | deployed | verified | pending | mmg | 改派 POST /admin/fleet/assignments/{assignmentId}/change 带 requestId 时,同一 requestId 的幂等重放此前恒返回 assignmentSlotId=null——而响应用 NON_NULL 省略 null,等于这个字段在重放的响应体里直接消失,首调却带着真实值。本次把回执冻结口径改回照抄首调的值,重放与首调返回同一个 assignmentSlotId。入参、路径、其余出参字段、错误码、判权一律不变;首次调用的行为也完全不变,只有「同 requestId 重放」这一条路径的返回值变了。后端已合并 dev-v3(d31bb3209)并部署 TEST。 | 2026-09-20 | dev-v3 |
fleet: 改派幂等重放不再抹掉 assignmentSlotId
存放目录:
changelogs-v2/{YYYY-MM}/(管理后台)服务: hl-fleet-service (端口 8087) PR: #7997 Issue: #7965 日期: 2026-09-20 影响范围: 派车「改派」提交的重试/重放路径
⚠️ 关键变化
- 只有「同一
requestId重放」这条路径的返回值变了:assignmentSlotId从恒null(响应体里该字段直接消失)改为与首次调用同值。 - 首次调用的行为一个字都没变:它本来就返回真实值。
- 不是新增字段、不是删字段:
assignmentSlotId一直在响应 VO 里;变的是它在重放时的取值。 - 入参、路径、方法、其余出参、错误码、判权全部不变。
一、背景
改派带 requestId 是幂等键:网络重试、前端重复提交时,第二次调用不会再改一次派单,而是把首次冻结的结果原样重放回来。
#7067「派单去槽位化」把回执写侧的 assignmentSlotId 无条件冻结成了 null,而产出侧(AssignmentService → AssignmentConverter → ChangeAssignmentRespVO)仍在计算并返回真实值。于是同一个 requestId 调两次,拿到两份不一样的「同一个结果」:首调有这个字段,重放没有(响应用 NON_NULL 省略 null,字段整个不出现)。
这不是「哪个值才对」的问题——assignmentSlotId 该不该退役是另一件事;错的是两条路径不同值,这是幂等回执的定义性失败。本次只把两侧拉齐:回执照抄首调的值。将来若真要退役这个字段,写口与回执一起退,两侧仍然同值。
缺陷自 2026-09-06(913062466)起带病运行 13 天没被发现,因为唯一能抓住它的用例 AssignmentChangeReceiptMysqlTest 被系统属性 fleet.mysql.provider 门控,平时整类跳过、PR 全绿。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 修改派单(改派) | POST | /admin/fleet/assignments/{assignmentId}/change |
修改 | 同 requestId 幂等重放的 assignmentSlotId 由恒 null 改为与首调同值 |
三、接口详情
1. 修改派单(改派) POST /admin/fleet/assignments/{assignmentId}/change
VO: Result<ChangeAssignmentRespVO>
使用场景
派车管理后台改派:按车辆槽位和生效日换车、换司机或同时替换。前端提交时带上本次操作的 requestId;网络超时重试或用户重复点击时,用同一个 requestId 再调一次,后端不会重复改派,而是把首次的结果重放回来。
入参
本次入参一个没动,下表只列与本次相关的字段。
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| assignmentId | Path | Long | ✅ | 当前车辆槽位任一派单 ID | 不存在返回 605009(不变) |
| effectiveDate | Body | LocalDate | ✅ | 须落在该派单服务日期区间内 | 不在区间返回 605028(不变) |
| requestId | Body | String | ❌ | 幂等键,由前端为「本次操作」生成一次 | 传了才有重放语义;同键不同业务载荷返回 605059(不变) |
出参
仅列与本次相关的字段,其余出参(assignmentId、assignmentStatus、effectiveDate、affectedDays、protocolPrice、vehicleFeeTotal、dailyVehicleFees、otherVehicles、warningCode、sendItinerarySms 等)本次一个没动。
| 字段 | 类型 | 说明 |
|---|---|---|
| assignmentSlotId | Long | 展示用历史槽位号,不参与任何判定。本次变更点:首调一直是真实值;同 requestId 重放此前恒 null(字段在响应体里消失),现在与首调同值 |
| previousAssignmentGroupId | Long | 改派前的派车组身份(不变) |
| newAssignmentGroupId | Long | 改派后的新派车组身份(不变) |
| assignmentStatus | String | 改派后派单状态(不变) |
请求示例
{
"effectiveDate": "2027-08-28",
"newDriverId": 2089686360926380033,
"reason": "#7965 AC-1 取证",
"requestId": "ac1-7965-20260920"
}
响应示例
(同一个 requestId 连调两次,两次响应体一致)
{
"code": 200,
"message": "成功",
"data": {
"assignmentId": 359948910304825344,
"assignmentSlotId": 359947685400285184,
"previousAssignmentGroupId": 359947685400285184,
"newAssignmentGroupId": 359948910271270912,
"assignmentStatus": "assigned",
"effectiveDate": "2027-08-28",
"affectedDays": 1
},
"success": true
}
空数据 / 降级响应
本接口不返回空数据形态:要么改派成功返回上述结构,要么返回错误码。
assignmentSlotId 本身可以是 null(该派单没有历史槽位号、也取不到锚点组身份时),此时首调与重放同为 null——不变量是「两次同值」,不是「一定非空」。
{
"code": 200,
"message": "成功",
"data": {
"assignmentId": 359948910304825344,
"previousAssignmentGroupId": null,
"newAssignmentGroupId": 359948910271270912,
"assignmentStatus": "assigned"
},
"success": true
}
注意:响应用
NON_NULL省略 null 字段,所以assignmentSlotId为 null 时该键不出现,不是出现一个null。前端取值要按「键可能不存在」写。
错误响应
{ "code": 605059, "message": "同一请求标识的业务载荷不一致", "data": null, "success": false }
| code | 触发条件 |
|---|---|
| 605059 | 同 requestId 但业务载荷不同(本次不变,且不产生改派副作用) |
| 605009 | 派单不存在(本次不变) |
| 605020 | 当前状态不允许改派(本次不变) |
| 605028 | 生效日不在派单服务日期范围内(本次不变) |
| 605041 | 最终基线复核不一致,data.dailyDifferences 给差异(本次不变) |
| 401 | 未登录(网关拦截) |
本次不新增、不修改任何错误码。
业务边界
- 不变量是「同
requestId的首调与重放返回同一个assignmentSlotId」,不是「该字段一定非空」。 - 该字段是展示用历史槽位号,
#7067之后不参与任何判定;不要拿它当主键、当筛选键、当幂等键。 - 不同
requestId的两次改派是两次独立操作,assignmentSlotId本来就可能不同,不在本不变量范围内。 - 同
requestId+ 不同业务载荷仍返回 605059,且不产生任何改派副作用(不变)。 - 不传
requestId时没有重放语义,每次调用都是一次真实改派。
四、契约约束与正确调用方式
| 场景 | 做法 |
|---|---|
| ✅ 改派提交 | 为「本次操作」生成一个 requestId,重试时原样复用,不要每次重新生成 |
✅ 重试后读 assignmentSlotId |
直接用,现在与首调同值;不需要为重放路径写兜底 |
✅ 渲染 assignmentSlotId |
按「键可能不存在」判空(该字段为 null 时整个键被省略) |
❌ 把 assignmentSlotId 当业务主键 / 幂等键 / 筛选键 |
它是展示用历史槽位号,不参与判定;身份走 newAssignmentGroupId |
❌ 重试时换一个新 requestId |
会被当成一次新的改派,真的再改一次 |
| ❌ 依赖「重放时该字段缺失」来区分首调与重放 | 旧行为,已被本次修复取消;要区分请用前端自己的请求状态 |
五、数据库行为
无表变更、无 Flyway 迁移、无索引变更。
- 唯一变化是改派回执表
fleet_assignment_change_receipt的outcome_json列新写入的内容:assignmentSlotId从「不写(NON_NULL 省略)」变为「照抄首调的值」。列本身不变,outcome_schema_version仍是 2(字段集没变,只是这个键从恒缺失变为有值;旧回执缺该键仍反序列化为 null,向后兼容)。 - 存量回执不订正(详见「六、边界行为」):2026-09-06 ~ 2026-09-20 修复部署前落库的回执行,
outcome_json里没有这个键,重放它们仍返回 null。
六、边界行为
- 同
requestId重放 →assignmentSlotId与首调同值(本次修复点)。 - 该字段本身为 null(无历史槽位号且取不到锚点组身份)→ 首调与重放同为 null,两次响应体里该键都不出现,不变量仍成立。
- 同
requestId+ 不同载荷 → 605059,不产生改派副作用(不变)。 - 不传
requestId→ 无重放语义,每次都是真实改派(不变)。 - 存量回执(修复部署前落库的) → 重放仍返回 null,本次不订正。理由:回执的语义是「冻结首调那一刻的答案」,事后回填等于用推导值覆盖冻结结果;而
requestId的实际重放窗口是「调用方当场重试」的秒级,存量行早已过了这个窗口,订正的收益接近零、破坏冻结语义的代价是实在的。存量读数与可反推结论见工单 #7965 AC-5。 - 改派本身的所有其他行为(基线复核 605041、旧 HOLD 605042、车费校验 605045/605049、全程槽 605064、行程短信
sendItinerarySms、ORDER_HAS_OTHER_VEHICLES提示)一律不变。
六.6、修改前后对比
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
assignmentSlotId(首次调用) |
真实值 | 真实值(不变) |
assignmentSlotId(同 requestId 重放) |
恒 null,NON_NULL 省略后该键在响应体里不出现 |
与首调同值(首调为 null 时同为 null) |
| 其余出参字段 | — | 不变(无新增、无删除、无改名、无类型变化) |
| 入参 / 路径 / 方法 / 错误码 / 判权 | — | 不变 |
行为级对比
| 场景 | 改前 | 改后 |
|---|---|---|
前端提交改派后超时重试(同 requestId) |
第二次响应里少一个字段,页面上槽位号「重试一下就没了」 | 两次响应体一致 |
| 调用方比对两次响应做一致性校验 | 必然不一致,且不报错(静默) | 一致 |
修复部署前落库的旧 requestId 重放 |
返回 null | 仍返回 null(存量不订正) |
六.7、影响评估
- 是否破坏向后兼容: 否。首次调用的行为完全不变;重放路径是从「少一个字段」变成「字段齐全」,是补齐不是削减。
- 前端是否必须同步上线: 否。此前若为重放路径写过「该字段可能缺失」的兜底,那段兜底可以保留(字段为 null 时仍会缺失),不需要改。
- 回滚: 回滚 PR #7997 即可。回滚后新写入的回执重新变回不带该字段;已写入的带值回执不受影响(读侧照常反序列化)。
- 风险: 低。改动是回执写侧一行取值,不触及改派本身的任何判定、状态机与资源占用。
七、不影响范围
- 仅影响: 本接口在「同
requestId幂等重放」路径上assignmentSlotId的取值。 - 零影响: 首次改派的全部行为与返回值;派单创建 / 取消 / 拒接 / 确认;车费计算与价格日历;行程短信;资源占用与 CAS;判权(
/admin/fleet/**仍要求VEHICLE_MANAGER或SUPER_ADMIN);网关路由;数据库表结构。 #7067「槽位身份退役」的其余部分不在本次范围——该字段目前仍是半退役状态(src/main里仍有多处写口),本次不推进也不回退它。
八、测试环境已验证
十、相关文档
- 关联 Issue: wx/HL#7965
- 关联 PR: wx/HL#7997
- 缺陷引入来源
#7067派单去槽位化: wx/HL#7067 - 门控测试类结构性盲区的处理见 wx/HL#7444