docs(changelogs-v2): 订单调整 snapshot/submit 契约重制(#4488/#4498/#4502/#4509/#4516)

snapshot 出参:basic 每次必返+承载订单号/产品名/团号+5金额,删顶层 discounts/surcharges/金额,行程改 days[].nodes[] 带价;
submit 入参:删 editReason/tabLocks/confirmDiffHash/basic/feeChanges,行程改 days[].nodes[];出参精简为仅 success。
这个提交包含在:
yaosutu 2026-06-27 18:14:19 +08:00
父节点 8941096a45
当前提交 ea9d7ed769

查看文件

@ -0,0 +1,217 @@
# 订单调整 snapshot / submit 契约重制——basic 承载订单信息+金额、行程 tab 定价化、submit 入出参大幅精简
> 端类型:管理后台
> 变更类型:修改接口(**破坏性**,前端需同步改造)
> 涉及接口:
> - `GET /v3/admin/order/{id}/adjustment/snapshot`(调整弹窗预填快照)
> - `POST /v3/admin/order/{id}/adjustment/submit`(调整统一提交)
## 背景
订单调整弹窗的预填快照与统一提交接口经一轮重制snapshot 的 `basic` 改为每次必返并承载订单身份+金额、行程 tab 升级为带价展示、删除前端用不到的优惠/附加费列表;submit 入参删除若干无用/已不支持字段、行程改嵌套结构、出参精简为只剩成功标志。**两个接口的入出参结构均有破坏性变化,前端需按本文同步改造。**
---
## 一、`GET /v3/admin/order/{id}/adjustment/snapshot`(出参重制)
### 入参(不变)
| 位置 | 字段 | 类型 | 必填 | 说明 |
|---|---|---|:---:|---|
| path | `id` | Long | 是 | 订单 ID |
| query | `scope` | String | 否 | 限定返回子域,逗号分隔,不传返全部。枚举:`BASIC`/`PEOPLE`/`SCHEDULE`/`ITINERARY`/`HOTEL_REQ`/`VEHICLE_REQ`**`FEE` 已移除** |
### 关键变更
1. **`basic` 每次必返**(不再受 `scope` 限制,传任意 scope 都返回 basic
2. **`basic` 新增订单身份字段**`orderNo`/`productName`/`teamNo`/`productType`(只读展示)。
3. **订单金额从顶层移入 `basic`**`orderAmount`/`surchargeAmount`/`discountAmount`/`receivableAmount`/`balanceAmount`。其中 `receivableAmount`(应收总额)为新增字段。
4. **删除顶层字段**`discounts[]``surcharges[]``orderAmount``surchargeAmount``discountAmount``balanceAmount`(金额已并入 basic;优惠/附加费列表下线)。
5. **行程 `itinerary` 改嵌套带价**:原顶层 `nodes[]` 删除,节点改挂在 `days[].nodes[]` 下,并新增单价/数量/小计/本日合计。
### 出参 `basic`OrderBasicVO
| 字段 | 类型 | 说明 |
|---|---|---|
| `orderNo` | String | 订单号(只读)🆕 |
| `productName` | String | 产品名(快照,只读)🆕 |
| `teamNo` | String | 团号(订金支付后生成,未支付为 null,只读🆕 |
| `productType` | String | 订单类型 CORE/ROUTE/CUSTOM/GROUP只读🆕 |
| `customerName` | String | 客户姓名(脱敏) |
| `customerPhone` | String | 客户手机(脱敏) |
| `emergencyContact` | String | 紧急联系人 |
| `customerRemark` | String | 客户备注 |
| `consultantRemark` | String | 顾问备注(内部) |
| `tags` | String[] | 订单标签 |
| `adultCount` / `childCount` / `youngChildCount` / `babyCount` | Integer | 成人/儿童/幼童/婴儿人数 |
| `orderAmount` | String(金额) | 订单基价(由顶层迁入)🔁 |
| `surchargeAmount` | String(金额) | 已有附加费合计(由顶层迁入)🔁 |
| `discountAmount` | String(金额) | 已有优惠合计(由顶层迁入)🔁 |
| `receivableAmount` | String(金额) | 应收总额 = 基价+附加费优惠,≥0,取消单为 0 🆕 |
| `balanceAmount` | String(金额) | 待付尾款 = 应收净已付,≥0,取消单为 0由顶层迁入🔁 |
> 金额字段均序列化为**字符串**(防 JS 精度丢失)。
### 出参 `itinerary`ItineraryEditVO,嵌套带价
| 路径 | 字段 | 类型 | 说明 |
|---|---|---|---|
| days[] | `id`/`dayNumber`/`dayDate`/`theme` | - | 行程天信息 |
| days[] | `nodes[]` | 数组 | 当天节点(**已过滤 HOTEL 节点**)🆕 |
| days[] | `dayTotal` | 金额 | 本日合计 = Σ节点 subtotal,后端派生只读 🆕 |
| days[].nodes[] | `id`/`dayId`/`nodeType`/`title`/`startTime`/`sortOrder` | - | 节点信息 |
| days[].nodes[] | `unitPrice` | 金额 | 单价(叙事展示;非 SCENIC 节点当前为 0🆕 |
| days[].nodes[] | `quantity` | Integer | 数量(默认 1🆕 |
| days[].nodes[] | `subtotal` | 金额 | 小计 = unitPrice×quantity,后端派生只读 🆕 |
> ⚠️ 原顶层 `itinerary.nodes[]` 已删除,全部迁入 `days[].nodes[]`
> 其余子域 `travelers` / `schedule` / `hotelRequirement` / `hotelDayDefaults` / `vehicleRequirement` / `lockedDays` / `editableTabLocksHint` 不变。
### snapshot 出参示例(节选)
```json
{
"code": 200,
"data": {
"basic": {
"orderNo": "HL202608010001",
"productName": "长白山深度5日游",
"teamNo": "GB202608001",
"productType": "CORE",
"customerName": "张*",
"adultCount": 4,
"orderAmount": "12000.00",
"surchargeAmount": "800.00",
"discountAmount": "200.00",
"receivableAmount": "12600.00",
"balanceAmount": "3000.00"
},
"itinerary": {
"days": [
{
"id": "1001", "dayNumber": 1, "dayDate": "2026-08-01", "theme": "天池徒步",
"nodes": [
{ "id": "2001", "nodeType": "SCENIC", "title": "长白山天池门票", "unitPrice": "128.00", "quantity": 1, "subtotal": "128.00" },
{ "id": "2002", "nodeType": "RESTAURANT", "title": "午餐", "unitPrice": "0", "quantity": 1, "subtotal": "0" }
],
"dayTotal": "128.00"
}
]
}
}
}
```
---
## 二、`POST /v3/admin/order/{id}/adjustment/submit`(入参 + 出参重制)
### 入参 `AdjustmentSubmitReqVO`
| 字段 | 类型 | 必填 | 说明 |
|---|---|:---:|---|
| `updates` | 对象 | 是 | 各子域改动(按需填,未改置 null,但不能全 null |
> ⚠️ **删除入参字段**`editReason`(原必填)、`tabLocks``confirmDiffHash`。这三个一律不再传。
### `updates`AdjustmentUpdatesVO—— 现仅 5 个子域
| 子域 | 类型 | 说明 |
|---|---|---|
| `travelers` | 对象 | 出行人增删改:`add[]` / `update[]`(含 id/ `remove[]`id 列表) |
| `schedule` | 对象 | 改期:`departDate`(新出发日 yyyy-MM-dd,本子域存在时必填,不可同原值 |
| `itinerary` | 对象 | 行程:`days[].nodes[]`(见下) |
| `hotelRequirement` | 对象 | 房需求完整新版本:`totalRoomCount`/`roomTypeSummary`/`specialTags[]`/`remark`/`days[]` |
| `vehicleRequirement` | 对象 | 车需求完整新版本:`vehicleType`/`requiredSeats`/`remark`/`fleet[]` |
> ⚠️ **删除子域**`basic`(订单基础信息不再在调整弹窗修改)、`feeChanges`(手动加优惠/附加费下线)。
### `updates.itinerary` 行程入参
| 路径 | 字段 | 类型 | 说明 |
|---|---|---|---|
| days[] | `id`/`dayNumber` | - | 定位天;`days` 数量比现有多/少 → 后端差量增/减天 |
| days[].nodes[] | `id` | Long | 定位要改的节点;为 null 视为新建(本期不支持,忽略) |
| days[].nodes[] | `unitPrice` / `quantity` | - | 传了即改单价/数量patch,不传不动 |
| days[].nodes[] | `deleted` | Boolean | true=删除该节点 |
### 出参 `AdjustmentSubmitRespVO`(精简)
| 字段 | 类型 | 说明 |
|---|---|---|
| `success` | Boolean | 提交是否成功 |
> ⚠️ 出参从原 15 字段精简为**仅 `success`**。删除:`appliedScopes`/`affectedRows`/`newHotelRequirementId`/`newHotelVersion`/`newVehicleRequirementId`/`newVehicleVersion`/`assignmentDeletedCount`/`statusLogId`/`changedDims`/`affectedDays`/`priceDelta`/`newDepartDate`/`newReturnDate`/`newFlowStatus`
> **失败时**统一走全局异常HTTP 200 + `Result{code: 587xxx, message: "..."}`,前端按已有错误码机制读 `code`/`message`
### submit 入参示例
```json
{
"updates": {
"schedule": { "departDate": "2026-09-01" },
"itinerary": {
"days": [
{ "id": "1001", "dayNumber": 1, "nodes": [
{ "id": "2001", "unitPrice": "150.00", "quantity": 2 },
{ "id": "2003", "deleted": true }
]}
]
},
"travelers": { "remove": [70001005] }
}
}
```
### submit 成功出参示例
```json
{ "code": 200, "message": "success", "data": { "success": true } }
```
### submit 失败出参示例(如:减员致有效总额低于已付)
```json
{ "code": 587032, "message": "调整后总额低于已支付金额,不允许本次调整", "data": null }
```
---
## 三、错误码submit 失败可能返回,前端按 code/message 展示)
| code | message | 含义 |
|---|---|---|
| 587002 | 订单已是终态,不可调整 | 订单 COMPLETED/CANCELLED 不可调整 |
| 587012 | updates 所有子领域均为空,无实际修改 | 未提交任何子域 |
| 587032 | 调整后总额低于已支付金额,不允许本次调整 | 防超付 |
| 587034 | 已出行,出行人不可调整 | 出行中改出行人被拦 |
| 587035 | 行程已确认,出发日期不可修改 | 改期窗口已关 |
| 587036 | 团期子订单不支持通过调整弹窗增删出行人,请前往团期管理台操作 | 团期出行人增删拦截 |
> 注:上表为常见项,完整错误码以后端 IErrorCode 段位为准;前端只需读 `code`/`message`
---
## 四、业务边界 / 注意
- **团期(GROUP)订单不能在调整弹窗改人数**:团期增删出行人被拦截,且基础信息子域已删,故团期人数变更不在本弹窗。非团期订单通过增减出行人(`travelers`)改人数。
- **行程单价为叙事展示价**,不进结算、不影响订单应付总额(改单价不触发算价)。
- **HOTEL 节点**不在 `itinerary.days[].nodes[]` 出现(配房默认值另走 `hotelDayDefaults`)。
- 人数/天数/行程改动触发的**自动价差**仍会写入订单加费/优惠(后端处理,前端无需关心)。
---
## 五、影响评估 / 前端改造点
1. snapshot金额与订单号/产品名/团号读取改从 `data.basic.*`(原顶层金额字段已无);不再渲染 `discounts`/`surcharges` 列表。
2. snapshot行程 tab 改读 `itinerary.days[].nodes[]`(含单价/数量/小计/本日合计),原顶层 `itinerary.nodes[]` 不存在。
3. submit请求体去掉 `editReason`/`tabLocks`/`confirmDiffHash`/`updates.basic`/`updates.feeChanges`;行程改提交 `days[].nodes[]`
4. submit响应只读 `data.success`(其余字段已删);失败读 `code`/`message`
---
## 六、关联
- Issue#4488(行程 tab 定价化)/ #4498(应收总额)/ #4502(金额挪 basic + basic 恒返 + 身份字段)/ #4509submit 删 editReason/tabLocks/ #4516submit 删 confirmDiffHash/basic/feeChanges + 出参精简)
- PR#4497 / #4500 / #4504 / #4521
- 负责人腰苏图yst