# 大交通批次新增: 接口路径破坏性迁移 + 4 个新错误码

> **存放目录**: 二期(v3,`order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084)
> **PR**: #2545 (commit `2c66137c7`)
> **Issue**: #2522
> **日期**: 2026-05-18
> **影响范围**: 管理后台「行程安排 - 接送站」区块 - 大交通批次新增弹窗

---

## ⚠️ 关键变化(破坏性 - 前端必同步)

一行红字说清:
- **本次变了什么**: 新增大交通批次接口路径从 `/transport-plans` 改为 `/transport-plan/add`,并新增 4 个错误码 `581140`-`581143`
- **前端以前以为的是什么**: `POST /v3/admin/order/{id}/transport-plans` + Body `TransportPlanReqVO`
- **实际现在是什么**: `POST /v3/admin/order/{id}/transport-plan/add` + Body `TransportPlanReqVO`(字段不变)

**配套破坏性**: §2.6 大交通批次模块 4 个接口路径风格 + 动词 + Method 全部统一迁移,见 #2521/#2523/#2524 changelog。

---

## 一、背景

V5.48 §2.6.2 将原 `POST /transport-plans` 拆为 `POST /transport-plan/add`,并补齐"字段组合 / 出行人合法性 / 时间顺序 / 同方向冲突"4 类业务校验错误码,与文档 §2.6 错误码段位 `581140`-`581143` 一一对应。

`orderId` 从 path 取,Body 不传;操作人从 JWT 派生(对齐 §2.6 通用约定)。

---

## 二、变更接口清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 大交通批次新增 | POST | `/v3/admin/order/{id}/transport-plan/add` | 路径迁移 + 错误码新增 | 旧 `/transport-plans` 下线;4 个业务错误码上线 |

---

## 三、接口详情

### 1. 大交通批次新增 `POST /v3/admin/order/{id}/transport-plan/add`

**VO**: `TransportPlanReqVO`(入参) / `TransportPlanVO`(出参)

#### 入参 Body `TransportPlanReqVO`

| 字段 | 类型 | 必填 | 约束 | 说明 |
|------|------|:----:|------|------|
| `direction` | String | ✅ | 枚举 | `ARRIVAL` / `DEPARTURE` |
| `transportType` | String | ✅ | 枚举 | `FLIGHT` / `TRAIN` / `SELF_DRIVE` |
| `transportNo` | String | 条件 | ≤50 | 航班号 / 车次号;`SELF_DRIVE` 时必须为 null |
| `carrier` | String | ❌ | ≤50 | 航司 / 铁路公司 |
| `departStation` | String | ❌ | ≤100 | 出发站 |
| `arriveStation` | String | ❌ | ≤100 | 到达站 |
| `departTime` | LocalDateTime | 条件 | - | `FLIGHT`/`TRAIN` 必填;`SELF_DRIVE` 必须为 null |
| `arriveTime` | LocalDateTime | 条件 | - | `FLIGHT`/`TRAIN` 必填;`SELF_DRIVE` 必须为 null;必须 ≥ `departTime` |
| `selfDrivePeriod` | String | 条件 | 枚举 | 仅 `SELF_DRIVE` 必填: `MORNING` / `AFTERNOON` / `EVENING` |
| `selfDriveEta` | LocalDateTime | ❌ | - | 仅 `SELF_DRIVE` 可选 |
| `travelerIds` | List<Long> | ✅ | size≥1 | 关联出行人 ID,至少 1 个 |
| `remark` | String | ❌ | ≤500 | 备注 |

#### 出参 `Result<TransportPlanVO>`

字段同 #2521 列表的元素结构(含 `travelers: [{id, name}]` 嵌套)。

#### 请求示例

```json
POST /v3/admin/order/60123456789012/transport-plan/add

{
  "direction": "ARRIVAL",
  "transportType": "FLIGHT",
  "transportNo": "CA1234",
  "carrier": "中国国际航空",
  "departStation": "北京首都T3",
  "arriveStation": "长春龙嘉",
  "departTime": "2026-06-01T08:30:00",
  "arriveTime": "2026-06-01T10:15:00",
  "selfDrivePeriod": null,
  "selfDriveEta": null,
  "travelerIds": [70123456789012, 70123456789013],
  "remark": "需要接机举牌"
}
```

#### 响应示例

```json
{
  "code": 200,
  "data": {
    "id": "80012345",
    "orderId": "60123456789012",
    "direction": "ARRIVAL",
    "transportType": "FLIGHT",
    "transportNo": "CA1234",
    "carrier": "中国国际航空",
    "departStation": "北京首都T3",
    "arriveStation": "长春龙嘉",
    "departTime": "2026-06-01T08:30:00",
    "arriveTime": "2026-06-01T10:15:00",
    "selfDrivePeriod": null,
    "selfDriveEta": null,
    "travelers": [
      {"id": "70123456789012", "name": "张三"},
      {"id": "70123456789013", "name": "王小明"}
    ],
    "remark": "需要接机举牌"
  },
  "msg": "success"
}
```

#### 错误响应

| 错误码 | 含义 | 触发场景示例 |
|--------|------|--------------|
| `581140` | 大交通字段组合不合法 | `FLIGHT` 缺 `transportNo` / `SELF_DRIVE` 错带 `transportNo` 或 `departTime` |
| `581141` | travelerIds 含订单外的出行人 | 提交的 traveler ID 不属于当前订单或已软删 |
| `581142` | 出发时间晚于到达时间 | `departTime > arriveTime` |
| `581143` | 同方向同一出行人已在另一 plan | 张三已在 ARRIVAL plan A,再为 ARRIVAL plan B 提交张三 |
| `581100` | 订单不存在 | path 中 orderId 无效 |

示例 `581140`:

```json
{ "code": 581140, "msg": "大交通字段组合不合法: FLIGHT 类型必须填 transportNo", "data": null }
```

示例 `581143`:

```json
{ "code": 581143, "msg": "同方向同一出行人已在另一 plan: 张三(70123456789012) 已存在 ARRIVAL 批次 80012340", "data": null }
```

---

## 四、契约约束与正确调用方式

### ✅ 正确 / ❌ 错误 payload 对照

| 场景 | payload 关键字段 | 结果 |
|------|------------------|------|
| ✅ FLIGHT 完整 | `transportType:FLIGHT, transportNo:CA1234, departTime/arriveTime 有值` | 201 |
| ✅ SELF_DRIVE 完整 | `transportType:SELF_DRIVE, transportNo:null, departTime:null, arriveTime:null, selfDrivePeriod:MORNING` | 201 |
| ❌ FLIGHT 缺航班号 | `transportType:FLIGHT, transportNo:null` | `581140` |
| ❌ SELF_DRIVE 带航班号 | `transportType:SELF_DRIVE, transportNo:CA1234` | `581140` |
| ❌ 时间倒挂 | `departTime:10:00, arriveTime:08:00` | `581142` |
| ❌ travelerIds 含他人 | `travelerIds:[别单出行人]` | `581141` |
| ❌ 同方向重复占用 | 同方向同 traveler 在另一 plan | `581143` |

### 并发与幂等

- `@Idempotent(timeout = 3)`: 同 admin 3 秒内重复 POST 同请求体直接拿首次结果,防双击双提
- `@Lock4j(keys = "#id")`: 同订单 30 秒锁,串行化 add/edit/delete,保证桥接表一致

### 同方向冲突规则

- 「同方向」= `direction` 相同的所有 plan(同 ARRIVAL 之间冲突,同 DEPARTURE 之间冲突)
- 「同一出行人」= 同一 `traveler_id`
- 不同方向不冲突(同一人可同时在 1 个 ARRIVAL plan + 1 个 DEPARTURE plan)
- 软删的 plan / 桥接行不参与冲突判定

---

## 五、数据库行为

单事务执行,失败整体回滚:

| 步骤 | 表 | 动作 |
|------|-----|------|
| 1 | `order_transport_plan` | INSERT 新行,雪花 ID,`deleted=0`,`create_time`/`update_time` 自动填 |
| 2 | `order_transport_plan_traveler` | INSERT N 行(N = `travelerIds.size()`),每行 `{plan_id, traveler_id, deleted:0}` |

校验顺序(任一失败即抛业务异常,事务回滚):

1. 订单存在性 → `581100`
2. 字段组合校验(类型 × 字段必填矩阵)→ `581140`
3. 时间顺序校验 → `581142`
4. travelerIds 归属校验(SELECT order_traveler WHERE order_id 比对)→ `581141`
5. 同方向同 traveler 占用校验(SELECT bridge WHERE direction = ? AND traveler_id IN ?)→ `581143`

---

## 六、边界行为

- 未登录 → 401(网关拦截)
- 订单不存在 → `581100`
- `travelerIds` 重复值 → 后端去重后再校验(不报错)
- `SELF_DRIVE` 仅传 `selfDrivePeriod` 不传 `selfDriveEta` → 通过(ETA 选填)
- 一次提交超过单订单合理上限(>10 plan)→ 不在此层拦截,业务上仅靠订单状态自然限制

---

## 七、不影响范围

- **仅影响**: 管理后台 F21 「接送站」新增弹窗
- **零影响**:
  - C 端订单详情 / 抵达计划(`order_arrival_plan` 表完全独立,见 detail 文档 §10.2 共存边界)
  - 出行人模块自身 CRUD
  - 订单状态机(plan 增删不直接变 `order_main.status`)

---

## 八、测试环境已验证

测试服 9443 网关 + 真 admin token round-trip 验证:

```
POST https://web.test.1814.love:9443/v3/admin/order/{orderId}/transport-plan/add
  → 200 + 返回新 plan(含 travelers 嵌套)✓
  → FLIGHT 缺 transportNo → 581140 ✓
  → SELF_DRIVE 错带 transportNo → 581140 ✓
  → departTime > arriveTime → 581142 ✓
  → travelerIds 含别单 → 581141 ✓
  → 同方向同 traveler 重复 → 581143 ✓
  → 重复 POST 3s 内同请求体 → 幂等返同结果 ✓
```

(commit `15e2c7eeb` 含 hotfix #2550,4 接口一并 9443 真测过)

---

## 九、相关历史 PR

| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| **本 PR #2545** | **#2522** | 路径迁移 + 错误码 581140-581143 上线 | ✅ 最新 |

---

## 十、相关文档

- 关联 Issue: [wx/HL#2522](https://git.1814.love:8443/wx/HL/issues/2522)
- 关联 PR: [wx/HL#2545](https://git.1814.love:8443/wx/HL/pulls/2545)
- API 文档: `docs/order-v3/api/API-SPEC-V5.48.html` §2.6.2
- 配套破坏性: 见 `18_#2521_transport-plan-list-path.md` / `18_#2523_transport-plan-edit.md` / `18_#2524_transport-plan-delete.md`
