diff --git a/changelogs/2026-04/2026-04-23_admin-order-transport.md b/changelogs/2026-04/2026-04-23_admin-order-transport.md new file mode 100644 index 0000000..03c040f --- /dev/null +++ b/changelogs/2026-04/2026-04-23_admin-order-transport.md @@ -0,0 +1,386 @@ +# 管理后台 · 订单交通信息(接送站)5 个 CRUD + 飞常准查询 7 个 + +**日期**:2026-04-23 +**影响**:**管理后台** 订单详情·行程安排·交通信息(接送站)板块 +**PR**:#1285(/transport 路径别名) +**关联 Issue**:#1284(404 修复) + +--- + +## 概述 + +统一管理后台「交通信息」板块的接口契约。**管理后台前端之前对接的是一份已废弃的 v1 小程序契约**(`legType / departureStation / travelDate / ...`),和后端现有新契约(`direction / departStation / departTime + arriveTime / ...`)完全不匹配,导致: + +1. `POST /admin/order/{orderId}/transport` **404**(#1285 已修:Controller 同时支持 `/transport` 和 `/arrival` 两个前缀) +2. 字段反序列化不上 → **400 `方向不能为空`、`出行人列表不能为空`** + +本次梳理:管理后台前端请**完全对齐小程序端 `/mp/order/{orderId}/arrival` 的 DTO 结构**(字段名、必填规则都一样,仅路径前缀不同)。同时后端已开放 `/admin/transport/*` 共 7 个飞常准查询接口,供前端自动补全航班/车次时刻。 + +--- + +## 一、订单交通批次 5 个 CRUD + +### 基础信息 + +- **鉴权**:Admin Bearer token +- **创建者**:`creatorType=ADMIN`(定制师代录;C 端小程序同样接口 `creatorType=USER`) +- **DTO**:后端 `ArrivalPlanSaveReqVO`(和小程序 `MpArrivalPlanSaveReqVO` 完全一致,字段名一一对应) + +### 路径表 + +**URL 支持两个前缀别名(PR #1285)**,任选其一,推荐 `/transport`(与 DB 表 `order_transport_plan` 对齐): + +| 功能 | 方法 | 路径 | +|---|---|---| +| 查全量批次 | GET | `/admin/order/{orderId}/transport` 或 `/admin/order/{orderId}/arrival` | +| 新增单批次 | POST | `/admin/order/{orderId}/transport` 或 `/admin/order/{orderId}/arrival` | +| 全量替换(多批次一次性提交) | POST | `/admin/order/{orderId}/transport/batch` 或 `/admin/order/{orderId}/arrival/batch` | +| 修改单批次 | PUT | `/admin/order/{orderId}/transport/plan/{planId}` 或 `/admin/order/{orderId}/arrival/plan/{planId}` | +| 删除单批次 | DELETE | `/admin/order/{orderId}/transport/plan/{planId}` 或 `/admin/order/{orderId}/arrival/plan/{planId}` | + +--- + +### 1. 查全量批次 + +``` +GET /admin/order/{orderId}/transport +``` + +#### 入参 + +| 参数 | 位置 | 类型 | 必填 | +|---|---|---|---| +| orderId | Path | Long | ✅ | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| orderId | Long | 订单ID | +| arrivals | `List` | 到达批次(接站) | +| departures | `List` | 离开批次(送站) | + +`ArrivalPlanRespVO` 字段结构见第 2 节出参。 + +--- + +### 2. 新增单批次 + +``` +POST /admin/order/{orderId}/transport +``` + +**创建者类型**:`creatorType=ADMIN`。 + +#### 入参 + +| 参数 | 位置 | 类型 | 必填 | +|---|---|---|---| +| orderId | Path | Long | ✅ | + +**Body `ArrivalPlanSaveReqVO`**(完整字段表): + +| 字段 | 类型 | 必填规则 | 约束 | 说明 | +|---|---|---|---|---| +| **direction** | String | ✅ 始终必填 | - | 方向。`ARRIVAL`=到达/接站;`DEPARTURE`=离开/送站。字典 `transport_direction` | +| **transportType** | String | ✅ 始终必填 | - | 交通方式。`FLIGHT`=飞机,`TRAIN`=火车,`SELF_DRIVE`=自驾。字典 `transport_type`(本模块子集) | +| transportNo | String | FLIGHT/TRAIN 必填;SELF_DRIVE 不填 | ≤32 | 航班号/车次号,如 `CA1234` / `G71` | +| carrier | String | 可选 | ≤64 | 航司/铁路公司,如 `中国国航` | +| departStation | String | DEPARTURE+FLIGHT/TRAIN 必填;其他场景可选 | ≤64 | 出发站/机场 | +| arriveStation | String | ARRIVAL+FLIGHT/TRAIN 必填;其他场景可选 | ≤64 | 到达站/机场 | +| departTime | LocalDateTime | DEPARTURE+FLIGHT/TRAIN 必填;其他场景可选 | - | 出发时间,`yyyy-MM-dd HH:mm:ss` | +| arriveTime | LocalDateTime | ARRIVAL+FLIGHT/TRAIN 必填;其他场景可选 | - | 到达时间,`yyyy-MM-dd HH:mm:ss` | +| selfDrivePeriod | String | SELF_DRIVE 必填 | - | 自驾时段:`MORNING` / `AFTERNOON` / `EVENING`。字典 `self_drive_period` | +| selfDriveEta | LocalDateTime | 可选 | - | 自驾预计到达精确时间 | +| remark | String | 可选 | ≤255 | 用户备注 | +| **travelerIds** | `List` | ✅ 始终非空 | 至少 1 人 | 本批次涉及的出行人 ID 列表;每个 ID **必须属于本订单** | + +> ⚠️ **字段间联动**:字段必填规则由 Service 层按 `transportType + direction` 组合校验,不是单注解表达式。前端请按上表规则做前置校验,否则会 500。 + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| planId | Long | 批次ID | +| orderId | Long | 订单ID | +| direction | String | 方向 | +| directionLabel | String | 方向中文标签 | +| transportType | String | 交通方式 | +| transportTypeLabel | String | 交通方式中文标签 | +| transportNo | String | 航班号/车次号 | +| carrier | String | 航司/铁路公司 | +| departStation | String | 出发站 | +| arriveStation | String | 到达站 | +| departTime | LocalDateTime | 出发时间 | +| arriveTime | LocalDateTime | 到达时间 | +| selfDrivePeriod | String | 自驾时段 | +| selfDrivePeriodLabel | String | 自驾时段中文标签 | +| selfDriveEta | LocalDateTime | 自驾预计到达 | +| remark | String | 备注 | +| creatorType | String | `USER` / `ADMIN` | +| createTime | LocalDateTime | 创建时间 | +| travelers | `List` | 本批次涉及的出行人 | + +**`ArrivalPlanTravelerSimpleVO`**: + +| 字段 | 类型 | 说明 | +|---|---|---| +| travelerId | Long | 出行人 ID | +| name | String | 姓名 | +| travelerType | String | `ADULT` / `CHILD` / `YOUNG_CHILD` / `BABY` | + +#### 请求示例(接站 ARRIVAL) + +```json +{ + "direction": "ARRIVAL", + "transportType": "FLIGHT", + "transportNo": "CA1234", + "carrier": "中国国航", + "departStation": "上海虹桥T2", + "arriveStation": "海拉尔东山", + "departTime": "2026-07-10 08:30:00", + "arriveTime": "2026-07-10 11:00:00", + "remark": "带小孩,需要婴儿座椅", + "travelerIds": [10001, 10002] +} +``` + +#### 请求示例(送站 DEPARTURE) + +```json +{ + "direction": "DEPARTURE", + "transportType": "FLIGHT", + "transportNo": "CA5678", + "carrier": "中国国航", + "departStation": "海拉尔东山", + "arriveStation": "上海虹桥T2", + "departTime": "2026-07-15 14:00:00", + "arriveTime": "2026-07-15 17:00:00", + "travelerIds": [10001, 10002] +} +``` + +#### 请求示例(自驾 SELF_DRIVE) + +```json +{ + "direction": "ARRIVAL", + "transportType": "SELF_DRIVE", + "selfDrivePeriod": "AFTERNOON", + "selfDriveEta": "2026-07-10 15:30:00", + "remark": "自驾从漠河过来", + "travelerIds": [10001] +} +``` + +--- + +### 3. 全量替换(多批次一次性提交) + +``` +POST /admin/order/{orderId}/transport/batch +``` + +先软删当前所有批次,再按请求体批量创建。空数组 `[]` = 清空订单全部交通批次。 + +#### 入参 + +| 参数 | 位置 | 类型 | 必填 | +|---|---|---|---| +| orderId | Path | Long | ✅ | + +Body:`List`(每项结构同第 2 节)。 + +#### 出参 + +`Result`,结构同第 1 节。 + +#### 使用场景 + +管理后台「一次性代录全部交通」按钮。例如定制师拿到客户的去程和回程航班信息,一次性把 2 条(或多条,如一家分两批)全部提交。 + +--- + +### 4. 修改单批次 + +``` +PUT /admin/order/{orderId}/transport/plan/{planId} +``` + +**覆盖语义**:按字段全量覆盖,包括 `travelerIds`(替换旧关联)。 + +#### 入参 + +| 参数 | 位置 | 类型 | 必填 | +|---|---|---|---| +| orderId | Path | Long | ✅ | +| planId | Path | Long | ✅ | + +Body:`ArrivalPlanSaveReqVO`(结构同第 2 节)。 + +#### 出参 + +`Result`。 + +--- + +### 5. 删除单批次 + +``` +DELETE /admin/order/{orderId}/transport/plan/{planId} +``` + +**级联**:软删关联的出行人绑定行。 + +#### 入参 + +| 参数 | 位置 | 类型 | 必填 | +|---|---|---|---| +| orderId | Path | Long | ✅ | +| planId | Path | Long | ✅ | + +#### 出参 + +`Result`。 + +--- + +## 二、航班/火车查询 7 个(`/admin/transport/*`) + +数据源:飞常准官方 API,`resource-service` 侧做 Redis 缓存后透传。与小程序端 `/mp/transport/*` **底层共享同一批数据**。 + +**用途**:前端表单里用户输入航班号后,后端返回精确的起飞/到达时间、机场/航站楼等,**前端自动回填 `departTime / arriveTime / departStation / arriveStation`**,用户不必手动填时分秒。 + +### 路径表 + +| 功能 | 路径 | 核心入参 | +|---|---|---| +| 按航班号查航班 | `GET /admin/transport/flight/by-flight-no` | `flightNo`、`date` | +| 按机场三字码查航班 | `GET /admin/transport/flight/by-airport` | `dep`、`arr`、`date` | +| 按城市三字码查航班 | `GET /admin/transport/flight/by-city` | `depCity`、`arrCity`、`date` | +| 按车次号查火车(含经停站) | `GET /admin/transport/train/by-train-no` | `trainNo`、`date` | +| 按车次+出发/到达站查火车 | `GET /admin/transport/train/by-train-no-stations` | `trainNo`、`dep`、`arr`、`date` | +| 按出发/到达站查火车 | `GET /admin/transport/train/by-station` | `dep`、`arr`、`date` | +| 按出发/到达城市查火车 | `GET /admin/transport/train/by-city` | `depCity`、`arrCity`、`date` | + +> ⚠️ 注意:admin 端路径风格和 mp 端**略有差异**(admin 端按航班号的叫 `/flight/by-flight-no`,mp 端叫 `/flight`;按车次号的叫 `/train/by-train-no`,mp 端叫 `/train`)。其他 5 个路径完全一致。 + +### 航班返回 `FlightVO` 字段 + +| 字段 | 类型 | 说明 | +|---|---|---| +| flightNo | String | 航班号 | +| airline | String | 航空公司名称 | +| category | String | `国内` / `国际` / `地区` | +| departAirportCode | String | 出发机场三字码 | +| arriveAirportCode | String | 到达机场三字码 | +| departAirport | String | 出发机场名称 | +| arriveAirport | String | 到达机场名称 | +| departCity | String | 出发城市 | +| arriveCity | String | 到达城市 | +| planDepartTime | String | 计划起飞时间 | +| planArriveTime | String | 计划到达时间 | +| actualDepartTime | String | 实际起飞时间(未起飞为 null) | +| actualArriveTime | String | 实际到达时间(未到达为 null) | +| status | String | `计划` / `起飞` / `到达` / `延误` / `取消` / `返航` / `备降` 等 | +| stopFlag | String | `0`=不经停,`1`=经停 1 次,`n`=经停 n 次 | +| shareFlag | String | `0`=否,`1`=是(共享航班) | +| shareFlightNo | String | 共享航班号 | +| departTerminal | String | 出发航站楼 | +| arriveTerminal | String | 到达航站楼 | +| boardGate | String | 登机口 | +| arriveExit | String | 到达出口 | +| departTimezone | String | 出发时区偏移(秒) | +| arriveTimezone | String | 到达时区偏移(秒) | + +### 火车返回 `TrainVO` 字段 + +| 字段 | 类型 | 说明 | +|---|---|---| +| trainNo | String | 车次 | +| departStation | String | 出发车站 | +| arriveStation | String | 到达车站 | +| shutdown | String | `0`=否,`1`=停运 | +| planDepartTime | String | 计划出发时间 | +| planArriveTime | String | 计划到达时间 | +| estimatedDepartTime | String | 预计出发时间(仅 `by-train-no-stations` 返回) | +| estimatedArriveTime | String | 预计到达时间(仅 `by-train-no-stations` 返回) | +| actualDepartTime | String | 实际出发时间(仅 `by-train-no-stations` 返回) | +| actualArriveTime | String | 实际到达时间(仅 `by-train-no-stations` 返回) | +| departStatus | String | 出发状态:`计划`/`正点`/`晚点`/`出发`(仅 `by-train-no-stations`) | +| arriveStatus | String | 到达状态:`计划`/`正点`/`晚点`/`到达`(仅 `by-train-no-stations`) | +| duration | Integer | 运行时长(分钟) | +| stops | `List` | 经停站列表(仅 `by-train-no` 返回) | + +**`TrainStopVO`**: + +| 字段 | 类型 | 说明 | +|---|---|---| +| stationName | String | 车站名称 | +| arriveTime | String | 到站时间 | +| departTime | String | 发车时间 | + +### 边界 + +- 查不到数据(航班号无效/停运):返回 `data: []` +- 飞常准上游异常/超时:500,`message` 含上游错误码 +- 日期格式:`yyyy-MM-dd`,格式错误返回 500 +- 所有三字码不区分大小写(内部 `toUpperCase`) + +--- + +## 三、字典依赖 + +| 字典类型 | 值 | +|---|---| +| `transport_direction` | `ARRIVAL` / `DEPARTURE` | +| `transport_type` | `FLIGHT` / `TRAIN` / `SELF_DRIVE`(本模块子集) | +| `self_drive_period` | `MORNING` / `AFTERNOON` / `EVENING` | +| `creator_type` | `USER` / `ADMIN` | +| `traveler_type` | `ADULT` / `CHILD` / `YOUNG_CHILD` / `BABY` | + +所有 `xxxLabel` 字段由后端透传中文展示值,前端无需再次映射。 + +--- + +## 四、前端迁移指南(重要 ⚠️) + +### 字段改名对照表 + +管理后台前端**之前在用 v1 小程序遗留契约**,请按下表迁移: + +| 旧契约(v1 遗留) | 新契约(当前) | 说明 | +|---|---|---| +| `legType` (`OUTBOUND` / `RETURN`) | **`direction`** (`ARRIVAL` / `DEPARTURE`) | **字段名+值域都变了**。`OUTBOUND` 去程=客户到达目的地 → `ARRIVAL`;`RETURN` 回程=客户从目的地离开 → `DEPARTURE` | +| `departureStation` | **`departStation`** | 去掉 `ure` 词尾 | +| `arrivalStation` | **`arriveStation`** | 去掉 `al` 词尾 | +| `travelDate`(一个日期) | **`departTime` + `arriveTime`**(两个 LocalDateTime) | 拆成出发时间和到达时间,两者都要有时分秒 | +| 缺失 | **`travelerIds`(必填)** | 新增必填字段,至少 1 人,必须属于本订单 | + +### 推荐 UX + +用户填写交通信息时: + +1. **方向**:单选 `接站 (ARRIVAL)` / `送站 (DEPARTURE)` +2. **交通方式**:单选 `飞机 (FLIGHT)` / `火车 (TRAIN)` / `自驾 (SELF_DRIVE)` +3. **航班号/车次号**:输入后立即调 `GET /admin/transport/flight/by-flight-no` 或 `GET /admin/transport/train/by-train-no` +4. 从返回列表让用户选具体班次,**自动回填** `departStation / arriveStation / departTime / arriveTime / carrier` +5. 自驾场景:改为输入 `selfDrivePeriod` + 可选 `selfDriveEta` +6. **出行人选择**:多选框,默认全选订单所有出行人(用户可取消部分),最少 1 人 +7. 提交时按上表 DTO 校验格式 + +### 参考实现 + +小程序端已上线该 UI,表单交互和字段校验可**直接参考 `MpArrivalController` 的使用**(changelog: `2026-04-21_mp-order-arrival.md`)。唯一差别是路径前缀 `/admin` 和 `creatorType=ADMIN`。 + +--- + +## 五、关联 PR 与历史 + +- #1285(本次):Controller 同时支持 `/transport` 和 `/arrival` 两个前缀别名。修 404。 +- #1262(2026-04-23):配房差价 execute 自动写 `order_surcharge` / `order_discount`(source=ROOM_UPGRADE),与本模块无关但同日相邻。 +- 2026-04-21 MP 端 `/mp/order/{orderId}/arrival` 已上线,DTO 结构与本次 admin 端一致。 + +**待废弃**:`MpOrderTransportController`(`/mp/order/{orderId}/transport`,v1 遗留)已标 `@Deprecated`,请**不要再参考它的字段命名**。管理后台前端之前误以此为契约,本次必须迁移。