diff --git a/changelogs-v2/2026-10/23_9201_核单车辆费用写入明细条数上限-修改接口-管理后台.md b/changelogs-v2/2026-10/23_9201_核单车辆费用写入明细条数上限-修改接口-管理后台.md new file mode 100644 index 00000000..719d844c --- /dev/null +++ b/changelogs-v2/2026-10/23_9201_核单车辆费用写入明细条数上限-修改接口-管理后台.md @@ -0,0 +1,202 @@ +--- +schema: "hl-changelog/v2" +ticket: "9201" +title: "核单车辆费用写入内部接口:明细条数上限 2000,超限写库前失败 605610" +consumer: "internal" +author: "chewu3(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "唯一调用方 order-v3 的合法载荷上界 2000 条(逐日派车拓扑不变量);当前冻结写入口另有更紧的 1000 条审计上限。超限在首次数据库副作用之前抛 605610,消息含实际条数与上限,零部分写入。已随 dev-v3 57de68b70 部署 TEST(task 5c950897),两实例 UP。" +updated_at: "2026-10-23" +base: "dev-v3" +--- + +# hl-fleet-service: 核单车辆费用写入明细条数设上限 + +> **服务**: hl-fleet-service(内部 Feign 口,网关不放行) +> **PR**: `#9208`(已合入 `dev-v3`,合并提交 `57de68b70`) +> **Issue**: #9201 + +--- + +## ⚠️ 关键变化 + +🟢 `POST /internal/fleet/reconciliation/vehicle-fee` 的 `items` 新增条数上限 **2000**:超过即返回 `605610`,且在取数据库栅栏、判关账、任何 upsert 之前失败——**不会出现部分明细已写入**。 + +🟢 锁租约由 30s 延长到 120s(服务端 Redis 租约,调用方不可见)。原因:2000 条最坏 7 次 DB 往返/条 ≈ 14000 次,30s 在慢库下不够。 + +🔴 此前无上限:调用方传入多少条就逐条写多少条,持锁时间随笔数线性增长,可能超过租约后仍继续写库。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 核单车辆费用写入 | POST | `/internal/fleet/reconciliation/vehicle-fee` | 请求体新增条数上限 | items > 2000 返回 605610,零写入 | + +--- + +## 三、接口详情 + +### 1. 核单车辆费用写入 `POST /internal/fleet/reconciliation/vehicle-fee` + +**VO**: `SettlementVehicleFeeWriteDTO`(hl-common-core,唯一调用方 order-v3) + +#### 使用场景 + +order-v3 结账冻结车辆费用后,经命令发件箱异步把逐日车辆费用明细回传车务落库(`OrderFleetCommandOutboxProcessor` → `FleetAssignmentExpandFeignClient#writeSettlementVehicleFee`)。一条 `items` 对应订单侧一条 `FLEET_DAILY` 冻结行,服务端逐条按 `(orderId, assignmentId, serviceDate)` 唯一键 upsert。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId / orderNo | Body | Long / String | ✅ | - | 订单身份 | +| settledAt | Body | LocalDateTime | 否 | - | 结账时刻 | +| items | Body | List | ✅ | **1~2000 条**(新增上限) | 每条一个 assignmentId + serviceDate | +| items[].assignmentId | Body | Long | ✅ | - | 车务派车行 ID | +| items[].driverId | Body | Long | ✅ | - | 司机 | +| items[].serviceDate | Body | LocalDate | ✅ | - | 服务日 | +| items[].actualAmount | Body | BigDecimal | ✅ | 两位小数 | 该日实际车费 | +| items[].vehicleId | Body | Long | 否 | - | 为空时跳过车辆快照校验 | + +#### 出参 + +| 字段 | 类型 | 说明 | +|------|------|------| +| code / message / success | - | 成功 `200`;超限 `605610` | +| data | null | 本接口无业务返回体 | + +#### 请求示例 + +```http +POST /internal/fleet/reconciliation/vehicle-fee HTTP/1.1 +Host: hl-fleet-service +Content-Type: application/json + +{ + "orderId": 2112275846201069569, + "orderNo": "SO202610230001", + "settledAt": "2026-10-23 10:30:00", + "items": [ + { "assignmentId": 9001, "driverId": 501, "serviceDate": "2026-10-28", "actualAmount": "1500.00", "vehicleId": 4401 } + ] +} +``` + +#### 响应示例 + +```json +{ "code": 200, "message": "成功", "data": null, "success": true } +``` + +#### 空数据 / 降级响应 + +`items` 为空数组时维持原行为:只记一条 info 日志,直接返回 `200`,不写库。服务端降级不适用于本接口(失败即整笔失败,由发件箱重放)。 + +#### 错误响应 + +超过 2000 条(新增): + +```json +{ + "code": 605610, + "message": "核单车辆费用写入明细条数超过上限:实际 2001 条,上限 2000 条(orderId=… orderNo=…)", + "success": false, + "data": null +} +``` + +单条结构非法(645610 同码族、原有行为):`605610` + 具体缺字段说明。 + +#### 业务边界 + +- 条数检查在 `@Lock4j` 租约之后(AOP 时序)、数据库栅栏与全部写库之前;超限请求不产生任何行级写入。 +- 上限 2000 取自 `FleetAssignmentPlanConstants.MAX_DAILY_PLAN_CELLS`(逐日派车拓扑硬不变量),不是拍脑袋值:调用方每条 items 来自一条冻结 `FLEET_DAILY` 行,而冻结行数受同一拓扑校验约束。 +- 当前冻结写入口另按 `VehicleFeeAuditSnapshot.MAX_AUDIT_ITEMS = 1000` 限制,故正常新单实际不会到 2000;上限留 2 倍余量,避免误拒更早历史数据形成的大单。 + +--- + +## 四、契约约束与正确调用方式 + +- 一次请求的 `items` 必须 ≤ 2000,且**不要**把「整团全部历史日期」拼进同一次写入来绕开分页。 +- 超限返回是**可判定的拒绝**:调用方应记录实际条数并告警,不要盲目重试同一 payload(重试仍 605610)。 +- 调用方 Feign 默认 `readTimeout=30000`:2000 条慢库档(3~5ms/次往返)可能超过 30s,超时后按发件箱重放;服务端 upsert 以 `(orderId, assignmentId, serviceDate)` 唯一键收敛,重放不会产生重复行。 + +--- + +## 五、数据库行为 + +| 场景 | 结果 | +|------|------| +| items ≤ 2000 | 逐条 upsert(先按唯一键查,有则 update,无则 insert) | +| items > 2000 | 零写入,直接 605610 | + +--- + +## 六、边界行为 + +- 恰好 2000 条:照常写入(上限含边界)。 +- `items=null` 或空数组:不触发上限检查,原行为返回 200。 +- 判断发生在 Redis 租约之后是已知取舍(租约由 AOP 在方法体前取得);业务副作用(数据库栅栏、关账判断、写行)全在检查之后。 +- 锁键 `fleet:recon-vehicle-fee:{orderId}`、事务边界、章节锁序均未改,仅 `expire` 30s→120s。 + +## 六.6、修改前后对比 + +| 场景 | 改前 | 改后 | +|------|------|------| +| items = 2001 | 逐条写 2001 行,持锁时间不受控 | 605610,零写入 | +| items = 2000 | 逐条写入(可能超过 30s 租约) | 逐条写入,租约 120s | +| items 为空 | 200,info 日志 | 同左(不变) | + +## 六.7、影响评估 + +- **是否破坏向后兼容**:否。唯一调用方 order-v3 的合法载荷上界 2000,正常链路不触发新拒绝。 +- **调用方是否必须同步上线**:否。 +- **回滚**:revert #9208 并重部署 hl-fleet-service,恢复 30s 租约与无上限行为。 + +--- + +## 七、不影响范围 + +- 其他 `/internal/fleet/reconciliation/**` 接口、保险费用写入、关账判断逻辑。 +- 单条 upsert 语义与唯一键(未改动)。 +- 管理后台核单页面与前端契约(零变化)。 + +--- + +## 八、测试环境已验证 + +**环境**:TEST **验证时间**:2026-10-23 10:39~10:45 +**构建身份**:hl-fleet-service `dev-v3 @ 57de68b70`,部署 task `5c950897`;两实例 `8187/8087 is UP after 7s`;部署状态回读 `dev-v3 57de68b70 0/N ok`。 + +本接口不经网关、唯一调用方为 order-v3,测试服未做载荷级直连实测;契约由以下读数支撑: + +- 单元:`ReconciliationVehicleFeeServiceTest` 14 例 0 失败,含 `write_itemsOverCap_throws605610BeforeAnyFenceOrWrite`(2001 条 → 605610、消息含 2001/2000、断言零 DB 交互)与 `write_itemsAtCap_passesGuardAndWritesEveryRow`(2000 条照常写 2000 行)。 +- 变异:把边界改成 `<= 上限+1` → 两条用例红(`Expecting code to raise a throwable`),还原后 14 例复绿。 +- 冒烟:网关只读探活 `GET /admin/fleet/dispatch-groups/page` 200 + 3 个团详情 200(`CHECKED=3 FAIL=0`);两实例部署窗口日志 `total_ERROR=0`。 + +--- + +## 十、相关文档 + +- Issue `#9201`;PR `#9208` +- 门禁与量化过程见 `E:/work/reports/9201/收口报告.md`(2000 上界推导、单条 3/5/7 次 DB 往返、租约余量) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#9201](https://git.1814.love/wx/HL/issues/9201) +- **PR**: [#9208](https://git.1814.love/wx/HL/pulls/9208) +- **Merge commit**: [57de68b70](https://git.1814.love/wx/HL/commit/57de68b7033de26be47c3d96fb324fd375352c0a) + +### 联系人 + +- **后端负责人**: @chewu3