# 出行人新增: POST /traveler/add 路径迁移 + 真实业务化

> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2536
> **Issue**: #2519
> **日期**: 2026-05-18
> **影响范围**: 管理后台 F27 改人数场景(临时加 1 个出行人)
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `16e861a7d`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)

---

## ⚠️ 关键变化

1. **破坏性路径迁移**: 老 `POST /v3/admin/order/{id}/travelers` → 新 `POST /v3/admin/order/{id}/traveler/add`(对齐 V5.48 §2.3 命名规范)
2. **从 Mock 占位变真实业务**: 三道校验 + INSERT + UPDATE order_main 人数 + INSERT order_status_log,而不是直接 insert 不校验。
3. **新增 4 个错误码**: 581115 / 581116 / 581117 / 581118
4. **加幂等 @Idempotent(3s) + 分布式锁 @Lock4j(30s)**: 防止与 batch-edit / 自身重复提交并发,导致 order_main 人数计数 race。

---

## 一、背景

文档 V5.48 §2.3 定义: 当订单实际出行人数 > 创单声明人数(典型场景: 客户带了未声明的孩子)时,定制师 B 端临时加人。后端必须同步 UPDATE `order_main.<type>Count`,且**不允许已结算 / 已取消 / 退款中的订单加人**(避免影响财务封账与退款冲账)。

---

## 二、变更接口清单

| # | 接口 | 方法 | 老路径 | 新路径 | 变更类型 |
|---|------|------|--------|--------|----------|
| 1 | 单个出行人新增 | POST | `/v3/admin/order/{id}/travelers` | `/v3/admin/order/{id}/traveler/add` | 破坏性路径迁移 + 真实业务化 |

---

## 三、接口详情

### 1. 单个出行人新增 `POST /v3/admin/order/{id}/traveler/add`

**VO**: `TravelerCreateReqVO`(请求体) / `TravelerVO`(响应,16 字段)

#### 入参

| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `travelerType` | Body | String | ✅ | ADULT / CHILD / YOUNG_CHILD / BABY |
| `name` | Body | String | ❌ | 姓名(可后填) |
| `gender` | Body | String | ❌ | MALE / FEMALE / UNKNOWN |
| `birthday` | Body | LocalDate | ❌ | 出生日期 |
| `idType` | Body | String | ❌ | ID_CARD / PASSPORT / BIRTH_CERT |
| `idNo` | Body | String | ❌ | 证件号(明文传,DB 加密) |
| `nationality` | Body | String | ❌ | 国籍(默认"中国") |
| `race` | Body | String | ❌ | 民族(默认"汉族") |
| `phone` | Body | String | ❌ | 出行人手机(明文传) |
| `emergencyContact` | Body | String | ❌ | 紧急联系人姓名 |
| `emergencyPhone` | Body | String | ❌ | 紧急联系人电话 |
| `roomGroupNo` | Body | Integer | ❌ | 同住分组号 |

#### 出参 `Result<TravelerVO>`

与 §2.1 列表接口完全相同的 16 字段 VO(含 `id` / `orderId` / `travelerType` / `name` / `gender` / `birthday` / `idType` / `idNo` / `nationality` / `race` / `phone` / `emergencyContact` / `emergencyPhone` / `roomGroupNo` / `profileStatus` / `transportPlanIds`)。

#### 请求示例

```json
POST /v3/admin/order/60123456789012/traveler/add
Authorization: Bearer {admin_jwt}

{
  "travelerType": "CHILD",
  "name": "王小明",
  "gender": "MALE",
  "birthday": "2018-06-20",
  "idType": "BIRTH_CERT",
  "idNo": "J012345678",
  "nationality": "中国",
  "race": "汉族",
  "roomGroupNo": 1
}
```

#### 响应示例

```json
{
  "code": 200,
  "data": {
    "id": 70123456789014,
    "orderId": 60123456789012,
    "travelerType": "CHILD",
    "name": "王小明",
    "gender": "MALE",
    "birthday": "2018-06-20",
    "idType": "BIRTH_CERT",
    "idNo": "J012345678",
    "nationality": "中国",
    "race": "汉族",
    "phone": null,
    "emergencyContact": null,
    "emergencyPhone": null,
    "roomGroupNo": 1,
    "profileStatus": "COMPLETED",
    "transportPlanIds": []
  },
  "msg": "success"
}
```

#### 错误响应清单(本工单新增段位)

| 错误码 | 含义 |
|---|---|
| `581100` | 出行人主段位—— 订单不存在 |
| `581102` | 订单不存在,无法编辑出行人 |
| `581115` | 实际人数已等于声明人数,请先调整订单人数再新增出行人 |
| `581116` | 当前订单状态禁止新增出行人(已结算 / 已取消 / 退款中) |
| `581117` | 出行人类型与现有同住分组冲突 |
| `581118` | 新增出行人缺少必填字段(travelerType 必填) |

错误响应体示例:

```json
{
  "code": 581116,
  "msg": "当前订单状态禁止新增出行人(已结算 / 已取消 / 退款中)",
  "data": null
}
```

---

## 四、契约约束

### 校验顺序(任一失败整事务回滚)

1. **订单存在**(581102)
2. **订单状态白名单**: 拒绝 SETTLED / CANCELLED / REFUNDING(581116)
3. **实际 vs 声明人数**: `count(order_traveler 未软删) >= sum(adultCount + childCount + youngChildCount + babyCount)` 时拒绝(581115,要求先调订单人数)
4. **同住分组冲突**(581117,可选字段校验)
5. **必填**: travelerType(581118)

### 并发控制

- `@Idempotent(timeout=3s)`: 同订单 3 秒重复提交直接拒绝
- `@Lock4j(expire=30000ms,keys="order:traveler:add:" + #id)`: 同订单 30 秒锁,防与 batch-edit 并发导致 order_main 人数计数 race

---

## 五、数据库行为

- INSERT 1 行 `order_traveler`(派生 `profile_status`)
- UPDATE `order_main.<type>Count` +1(按 `travelerType` 选择: adultCount / childCount / youngChildCount / babyCount)
- INSERT 1 行 `order_status_log`,reason="新增出行人",flowStatus 不变(from == to)

| travelerType | order_main 字段 | delta |
|---|---|---|
| ADULT | `adult_count` | +1 |
| CHILD | `child_count` | +1 |
| YOUNG_CHILD | `young_child_count` | +1 |
| BABY | `baby_count` | +1 |

---

## 六、边界行为

- 未登录 → 401(网关拦截)
- 订单不存在 → 581102
- 订单 status = SETTLED / CANCELLED / REFUNDING → 581116(优先于人数校验)
- 实际人数已等于声明 → 581115(典型场景: 客户原本说 2 大 1 小现已 2 大 1 小,要先把声明改成 2 大 2 小再加人)
- travelerType 未识别 → 防御性 return(不报错也不更新人数,实际由前置 @NotBlank 拦截)

---

## 七、不影响范围

- **仅影响**: 管理后台 F27 改人数(新增)场景
- **零影响**:
  - 批量编辑 `/traveler/batch-edit`(#2518 独立接口)
  - 软删 `/traveler/{travelerId}/delete`(#2520 独立接口)
  - 列表 `/traveler/list`(只读)
  - mp 端出行人接口
  - 老路径 `POST /travelers` 已下线,前端调用必须改用 `/traveler/add`

---

## 八、测试环境已验证

```
POST https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/add
  Body: {"travelerType":"CHILD","name":"王小明","birthday":"2018-06-20",...}
  → 200 + TravelerVO(含 id / profileStatus / transportPlanIds) ✓
  → order_main.child_count +1 ✓
  → order_status_log 新增 1 行 reason=新增出行人 ✓
反例:
  - 订单 status=SETTLED → 581116 ✓
  - 实际人数 = 声明人数 → 581115 ✓
  - 老路径 POST /travelers → 404 ✓
```

---

## 九、相关历史 PR

| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #1984 | - | [§2 traveler skeleton] 空骨架 Mock | ❌ 被本 PR 真实化覆盖 |
| #2535 | #2518 | batch-edit 真实业务化(同期工单,共用 581100-581114 段位) | ✅ 有效 |
| **本 PR #2536** | **#2519** | add 接通真实业务 + 路径迁移 + 4 错误码(581115-581118) | ✅ 最新 |

---

## 十、相关文档

- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.3
- 关联 Issue: [wx/HL#2519](https://git.1814.love:8443/wx/HL/issues/2519)
- 关联 PR: [wx/HL#2536](https://git.1814.love:8443/wx/HL/pulls/2536)
