# 大交通批次 edit/delete 严格化 follow-up: 错误码 581121 → 581144 + edit 4 项业务校验补齐

> **存放目录**: 二期(v3,`order-v3` 标签)→ `changelogs-v2/2026-05/`
>
> **服务**: hl-order-v3 (端口 8084)
> **PR**: #2560 (commit `08deec4d1`)
> **Issue**: 无新增工单(审查 follow-up,语义对齐 #2523 / #2524 / #2526,原工单已 closed)
> **日期**: 2026-05-19
> **影响范围**: 管理后台「行程安排 - 接送站」区块 - 大交通批次编辑/删除接口

---

## ⚠️ 关键变化(前端必同步两点)

1. **#2524 delete 错误码变更(破坏性,小幅)**:大交通批次软删接口的「plan 不存在/跨单/已删」错误码 **`581121` → `581144`**。前端若对 `581121` 做了特殊 catch,需改为 `581144`,或合并到通用错误处理。错误消息文案同步改为「大交通批次不存在或不属于该订单」。
2. **#2523 edit 严格化(行为收紧)**:之前 edit 接口只校验 `direction` / `transportType` 两个枚举,本次补齐 add 同款 4 项业务校验(581140 字段组合 / 581141 出行人归属 / 581142 时间顺序 / 581143 同方向同一出行人占用),且 edit 加 `@Idempotent(3s)`,3 秒内重复提交直接返 `100502 处理中`。前端 add 已有的错误处理代码可直接复用到 edit。

> 这是 **#2546(#2523) / #2547(#2524) / #2554(#2526)** 三个原 PR 合并后,/@arch + /@cr 审查发现的尾巴一次性收完,不开新工单。

---

## 一、背景

V5.48 §2.6 大交通模块四接口在 #2521/#2522/#2523/#2524 完成路径风格 + Method 迁移后,审查发现两处实质语义偏差和一处注释笔误:

| 维度 | 原 PR 状态 | 本 follow-up 修正后 |
|------|-----------|---------------------|
| `editTransportPlan` 业务校验 | 仅枚举 direction/transportType,**不校验字段组合/出行人归属/时间顺序/同方向占用** | 全 4 项校验,完全对齐 add |
| `editTransportPlan` 防并发 | 仅 `@Lock4j(30s)`,**无幂等** | `@Lock4j(30s)` + `@Idempotent(3s)` |
| `deleteTransportPlan` 错误码 | `581121`(沿用 §2 baseline) | **`581144`**(对齐 §2.6.4 文档 + #2523 命名) |
| `TravelerErrorCode` 错误码区间注释 | "581131-581135" 整体看作启用 | "581131-581134 启用 + 581135 预留" |

错误码 `581121` 是 §2 出行人模块的"归属校验"基线值,但 §2.6 大交通批次模块已在 add/edit 中使用 `581140-581144` 段位,delete 也应统一使用 `581144`,前端可用同一段位错误处理覆盖全部 §2.6 接口。

---

## 二、变更接口清单

| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 大交通批次编辑 | POST | `/v3/admin/order/{id}/transport-plan/{planId}/edit` | 行为收紧 + 注解新增 | 补 4 项业务校验 + `@Idempotent(3s)` |
| 2 | 大交通批次软删 | POST | `/v3/admin/order/{id}/transport-plan/{planId}/delete` | 错误码变更(破坏性) | `581121` → `581144`,消息文案同步 |

---

## 三、接口详情

### 1. 大交通批次编辑 `POST /v3/admin/order/{id}/transport-plan/{planId}/edit`

入参 / 出参 / 路径 / Method **均与 PR #2546 一致**,仅以下行为变化:

#### 新增/补齐校验(对齐 add `POST .../transport-plan/add`)

校验顺序(从上到下,任一不通过立刻返错):

| 步骤 | 校验项 | 错误码 | 说明 |
|------|--------|--------|------|
| 1 | plan 存在 + 归属订单 + 未软删 | `581144` | (本 follow-up 改造点,见接口 2)|
| 2 | 订单存在性 | `581100` | |
| 3 | **字段组合合法性** | `581140` | FLIGHT/TRAIN/COACH 缺 `transportNo`,SELF_DRIVE 错带 `transportNo`/站点 等 |
| 4 | **时间顺序** | `581142` | `departTime > arriveTime` 拒绝(SELF_DRIVE 用 `selfDriveEta` 单点时间不触发此校验)|
| 5 | **travelerIds 归属订单** | `581141` | 任一 ID 不在 `order.travelerIds` → 整体拒绝 |
| 6 | **同方向同一出行人已被占用**(排除自己)| `581143` | 同 `direction` + 同 `travelerId` 在其他 active plan 中存在 → 拒绝;**自己的旧关联不算冲突** |

#### 新增注解

```java
@Idempotent(timeout = 3)            // 新增:3 秒内重复 POST 同 path+Body 返首次结果
@Lock4j(keys = "#id", expire = 30000)  // 已有
```

#### 错误响应示例

```json
// 字段组合非法(FLIGHT 缺 transportNo)
{ "code": 581140, "msg": "transportNo 必填(运输方式=FLIGHT)", "data": null }

// travelerIds 含订单外的出行人
{ "code": 581141, "msg": "出行人不属于该订单: travelerIds=[70123456789099]", "data": null }

// 时间顺序错误
{ "code": 581142, "msg": "departTime 不能晚于 arriveTime", "data": null }

// 同方向同 traveler 占用(其他 plan)
{ "code": 581143, "msg": "出行人在同方向已有另一批次: direction=ARRIVAL, travelerId=70123456789012, conflictPlanId=80012999", "data": null }

// 幂等命中(3 秒内重复 POST)
{ "code": 100502, "msg": "请求处理中,请稍后重试", "data": null }
```

> **edit 排除自身规则**:校验 `581143` 时,SQL `WHERE plan_id != #{currentPlanId}`,改自己不算冲突。该排除是 edit 接口独有,add 接口不存在 currentPlanId 概念。

---

### 2. 大交通批次软删 `POST /v3/admin/order/{id}/transport-plan/{planId}/delete`

入参 / 出参 / 路径 / Method / 数据库行为 **均与 PR #2547 一致**,仅错误码变化:

#### 错误响应变更

| 场景 | 变更前(PR #2547) | 变更后(PR #2560,**当前**) |
|------|------------------|---------------------------|
| plan 不存在 | `581121` "大交通批次不属于该订单" | **`581144` "大交通批次不存在或不属于该订单"** |
| plan 跨单(orderId 不匹配) | `581121` 同上 | **`581144`** 同上 |
| plan 已软删(deleted=1)再 delete | `581121` 同上 | **`581144`** 同上 |
| 订单不存在 | `581100` | `581100`(**不变**)|

示例响应:

```json
{ "code": 581144, "msg": "大交通批次不存在或不属于该订单: planId=80099999, orderId=60123456789012", "data": null }
```

> `581121` 在 §2.6 模块**不再返回**(仍保留在 §2 出行人模块的其他接口中)。

---

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

### edit 接口(前端 actionable)

| 场景 | payload | 结果 |
|------|---------|------|
| ✅ 合法 edit | FLIGHT + transportNo + 站点 + travelerIds 都属本单 + 时间顺序对 | 200 |
| ✅ 改自己旧关联的 traveler | edit Body `travelerIds=[A,B]`,A 在原 plan 已有 → 不触发 581143 | 200 |
| ❌ 缺 transportNo(FLIGHT) | `{"transportType":"FLIGHT","transportNo":null}` | `581140` |
| ❌ 时间倒挂 | `departTime=11:00, arriveTime=09:00` | `581142` |
| ❌ traveler 跨单 | `travelerIds=[订单外的 ID]` | `581141` |
| ❌ 同方向 A 已在别 plan | direction=ARRIVAL + travelerA 在 plan #B 占用 | `581143` |
| ❌ 3 秒内双击 | 第二次同 path+Body POST | `100502` 处理中 |

### delete 接口(前端 actionable)

| 错误码 | 前端建议 |
|--------|---------|
| 旧 `581121` | **移除**或合并到通用错误处理 |
| 新 `581144` | 提示「该批次不存在或已被删除」,刷新列表 |

### 前端代码迁移建议

```js
// 旧
if (resp.code === 581121) showToast('批次不属于该订单');

// 新(推荐合并 581121 + 581144,兼容过渡期)
if (resp.code === 581144 || resp.code === 581121) showToast('批次不存在或已被删除');
```

---

## 五、数据库行为

**无变化**。本 follow-up 仅修改 Service 层校验顺序 + Controller 注解 + 错误码常量值,DB schema / 表行为完全沿用 PR #2546 / #2547。

---

## 六、边界行为

### edit 接口

- 新校验全部在事务开始前完成,失败不写库
- `@Idempotent(3s)` 缓存键 = path + 用户 ID + Body hash,3 秒内重复同 Body 直接拿首次结果
- 同方向同 traveler 占用判定 SQL: `WHERE direction = ? AND traveler_id IN (?) AND plan_id != #{currentPlanId} AND deleted = 0`
- 已软删的 plan 不参与 `581143` 占用判定

### delete 接口

- `@Idempotent(3s)` + `@Lock4j(30s)` 沿用,不变
- 错误码常量从 `TRANSPORT_PLAN_NOT_BELONG_TO_ORDER(581121)` 改为 `TRANSPORT_PLAN_NOT_FOUND_OR_NOT_BELONG(581144)`

### 通用

- 未登录 → 401(网关拦截,不变)
- 历史 plan(v2 迁移数据)edit/delete 行为完全一致

---

## 七、不影响范围

- **仅影响**: 管理后台 F21 「接送站」编辑弹窗 + 删除按钮
- **零影响**:
  - C 端订单详情 / mp 抵达计划接口
  - §2.6.1 list 接口 / §2.6.2 add 接口(本 follow-up 不动)
  - 出行人 CRUD(traveler 表本身不动)
  - 订单状态机
  - `581121` 在 §2 出行人模块其他接口仍正常返回,语义未变
  - `581131-581134` 出行人模块错误码无功能变化,仅注释笔误修正

---

## 八、测试环境已验证

测试服 9443 网关 + 真 admin token round-trip **9/9 PASS**(/@qa 报告):

```
edit 接口严格化:
  ✓ FLIGHT 缺 transportNo → 581140
  ✓ travelerIds 含订单外 ID → 581141
  ✓ departTime > arriveTime → 581142
  ✓ 同方向 traveler 在别 plan 占用 → 581143
  ✓ 同方向 traveler 在自己 plan(改自己)→ 200(排除自身生效)
  ✓ 3 秒内重复 POST 同 Body → 100502 幂等命中

delete 接口错误码:
  ✓ planId 不存在 → 581144(原 581121)
  ✓ planId 跨单 → 581144(原 581121)
  ✓ 重复 delete 已软删 plan → 581144(原 581121)
```

本地单测 109/109 全绿(5 单测 + 7 IT,注解反射断言验证 `@Idempotent.timeout=3` / `@Lock4j.keys="#id"`)。

---

## 九、相关历史 PR

| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #2546 | #2523 | edit 接口 Method+路径迁移(原版只校验 2 枚举) | ✅ 仍有效,**本 follow-up 在其上补 4 校验 + 幂等** |
| #2547 | #2524 | delete 接口 Method+路径迁移(原版用 `581121`) | ✅ 仍有效,**本 follow-up 改错误码 → `581144`** |
| #2554 | #2526 | 出行人 smart-parse 新增接口(原版注释笔误)| ✅ 仍有效,**本 follow-up 仅修注释,无 API 变化** |
| **本 PR #2560** | (审查 follow-up,无新工单) | edit 严格化 + delete 错误码 + 注释笔误 | ✅ **最新** |

---

## 十、相关文档

- 关联 PR: [wx/HL#2560](https://git.1814.love:8443/wx/HL/pulls/2560)
- 原 PR(本 follow-up 的修正基础):
  - [wx/HL#2546](https://git.1814.love:8443/wx/HL/pulls/2546)(关联 [#2523](https://git.1814.love:8443/wx/HL/issues/2523))
  - [wx/HL#2547](https://git.1814.love:8443/wx/HL/pulls/2547)(关联 [#2524](https://git.1814.love:8443/wx/HL/issues/2524))
  - [wx/HL#2554](https://git.1814.love:8443/wx/HL/pulls/2554)(关联 [#2526](https://git.1814.love:8443/wx/HL/issues/2526))
- API 文档: `docs/order-v3/api/API-SPEC-V5.48.html` §2.6.3(edit)/ §2.6.4(delete)
- 同月配套 changelog:
  - `18_#2523_transport-plan-edit.md`
  - `18_#2524_transport-plan-delete.md`
  - `18_#2526_traveler-smart-parse.md`
