# 🔧 出行人批量编辑：资料完整度与完成统计统一为订单级手机号语义（#5203）

> **PR**: [#5210](https://git.1814.love:8443/wx/HL/pulls/5210)
> **Issue**: [#5203](https://git.1814.love:8443/wx/HL/issues/5203)
> **日期**: 2026-07-24
> **消费端**: 一期小程序 MP BFF
> **接口**: `POST /mp/v3/order/{id}/traveler/batch-edit`

## 1. 变更背景

出行人资料完整度与订单签约、确认门禁此前存在两套手机号口径：逐人完成状态可能要求每名成人都有手机号，但订单门禁只要求整单至少一名出行人有手机号。

本次统一为：

- 单名出行人的资料完整度只检查 `name`、`gender`、`birthday`、`idType`、`idNo` 五项；
- `phone` 不再影响该出行人的完成状态；
- 订单整体仍必须至少有一名出行人填写手机号；
- 接口字段名、类型和层级不变，但 `completedCount`、`pendingCount`、`allCompleted` 的统计结果可能变化。

## 2. 变更接口

| 接口 | 方法 | 路径 | 变更类型 |
|---|---|---|---|
| 客户批量补全出行人 | POST | `/mp/v3/order/{id}/traveler/batch-edit` | 响应字段语义修改 |

## 3. 完整接口契约

### 3.1 调用约束

| 项目 | 契约 |
|---|---|
| 认证 | 需要小程序登录态 |
| 可编辑订单状态 | `PENDING_PAY`（待支付）、`CUSTOMIZING`（定制中） |
| 订单归属 | 只能编辑当前登录用户自己的订单 |
| 幂等 | 同一订单 3 秒内重复提交返回 `100502` |
| 批量上限 | 每次 1～30 名出行人 |
| 写入语义 | `id=null` 为新增，`id` 非空为更新；未出现在数组中的已有出行人不会被删除 |

### 3.2 路径参数

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | Long | 是 | 订单 ID；建议以字符串形式传递，避免大整数精度丢失 |

### 3.3 请求体

请求类型：`TravelerBatchEditReqVO`

| 字段 | 类型 | 必填 | 约束与语义 |
|---|---|---|---|
| `travelers` | `TravelerEditItem[]` | 是 | 1～30 项 |
| `travelers[].id` | Long / null | 否 | `null` 表示新增；非空表示更新，且必须属于路径中的订单 |
| `travelers[].name` | String / null | 条件必填 | 新增项的 `name`、`idType`、`idNo` 至少一项非空；有值时长度 2～30，只允许中文、英文、中点 `·`、连字符 `-`、空格 |
| `travelers[].gender` | String / null | 否 | `0`、`1`、`2`；该字段为空时资料状态为 `PENDING` |
| `travelers[].birthday` | String | 是 | `yyyy-MM-dd`，不得晚于当天；用于派生出行人类型 |
| `travelers[].idType` | String / null | 条件必填 | 取值见第 4 节；与 `idNo` 配套 |
| `travelers[].idNo` | String / null | 条件必填 | `ID_CARD` 为 18 位数字或末位 `X/x`；其他证件为 5～30 位字母、数字或连字符 |
| `travelers[].nationality` | String / null | 否 | 允许不传或传 `null`，不允许显式传空字符串 |
| `travelers[].race` | String / null | 否 | 允许不传或传 `null`，不允许显式传空字符串 |
| `travelers[].phone` | String / null | 否 | 有值时必须为 11 位数字；更新时 `null` 表示保留原值，空字符串表示清空 |
| `travelers[].emergencyContact` | String / null | 否 | 出行人级紧急联系人姓名 |
| `travelers[].emergencyPhone` | String / null | 否 | 有值时必须为 11 位数字 |
| `travelers[].roomGroupNo` | Integer / null | 否 | 最小为 1，最大不超过订单声明总人数 |

更新已有出行人时，除必填的 `birthday` 外，其他可选字段传 `null` 表示保留原值。

### 3.4 响应

响应类型：`Result<TravelerBatchEditRespVO>`

统一响应字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | Integer | `200` 表示成功，其他值见第 5 节 |
| `message` | String | 响应文案 |
| `data` | Object / null | 成功时为批量编辑统计，失败时通常为 `null` |
| `traceId` | String / null | 链路追踪 ID，可能为空 |
| `success` | Boolean | `code == 200` 时为 `true` |

`data` 字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `createdCount` | Integer | 本次请求中 `id=null` 的新增数量 |
| `updatedCount` | Integer | 本次请求中 `id` 非空的更新数量 |
| `completedCount` | Integer | 操作完成后，订单内五项资料均完整的出行人总数 |
| `pendingCount` | Integer | 操作完成后，订单内五项资料仍有缺失的出行人总数 |
| `allCompleted` | Boolean | 同时满足“订单声明人数大于 0、实际人数等于声明人数、`pendingCount=0`、整单至少一名出行人有手机号”时为 `true` |

五项资料指：`name`、`gender`、`birthday`、`idType`、`idNo`。手机号不计入单名出行人的完成状态，但仍计入 `allCompleted` 的订单级门禁。

## 4. 枚举与数据字典

### 4.1 `gender`

| 值 | 中文 | 完整度语义 |
|---|---|---|
| `0` | 未知 | 有值，满足 `gender` 完整度 |
| `1` | 男 | 有值，满足 `gender` 完整度 |
| `2` | 女 | 有值，满足 `gender` 完整度 |

### 4.2 资料完成状态

该状态不单独出现在本接口响应中，但直接决定 `completedCount` 和 `pendingCount`。

| 值 | 中文 | 判定 |
|---|---|---|
| `COMPLETED` | 已完善 | `name/gender/birthday/idType/idNo` 五项全部非空 |
| `PENDING` | 待完善 | 上述五项任一为空 |

### 4.3 `idType`

| 值 | 中文 |
|---|---|
| `ID_CARD` | 身份证 |
| `PASSPORT` | 护照 |
| `HK_MACAU_PASS` | 港澳通行证 |
| `HONGKONG_RESIDENT_PASS` | 回乡证 |
| `TAIWAN_PASS` | 台湾通行证 |
| `MILITARY_ID` | 军官证 |
| `OTHER` | 其他 |

### 4.4 出行人类型

出行人类型不由请求体传入，而是根据 `birthday` 自动派生，并用于校验订单各类型人数配额。

| 年龄 | 值 | 中文 |
|---|---|---|
| 未满 2 周岁 | `BABY` | 幼童 |
| 2～6 周岁 | `YOUNG_CHILD` | 小童 |
| 7～17 周岁 | `CHILD` | 儿童 |
| 18 周岁及以上 | `ADULT` | 成人 |

## 5. 错误码

| code | message / 含义 | 触发场景 |
|---|---|---|
| `401` | 未认证 | 未携带有效小程序登录态 |
| `500` | 出行人服务不可用，请稍后重试 | BFF 无法调用出行人服务 |
| `100001` | 参数非法 | `travelers` 为空、超过 30 项、缺少 `birthday`、日期格式错误等请求校验失败 |
| `100502` | 出行人补全处理中，请勿重复提交 | 同一订单 3 秒内重复提交 |
| `100503` | 资源被占用，请稍后重试 | 同一订单存在并发写入且未能取得操作权 |
| `100701` | 姓名长度异常(2-30字符) | 非空姓名长度不在 2～30 字符 |
| `100702` | 姓名含非法字符 | 姓名包含允许字符集之外的内容 |
| `100703` | 姓名含敏感词 | 姓名命中敏感词 |
| `100704` | 姓名格式不正确 | 同一字符连续重复 5 次及以上 |
| `581101` | 12301 必报字段缺失（国籍 / 民族不能为空字符串） | `nationality` 或 `race` 显式传空字符串 |
| `581102` | 订单不存在，无法编辑出行人 | 处理过程中订单不存在 |
| `581103` | 性别编码不合法（应为 1=男/2=女/0=未知） | 非空 `gender` 不在 `0/1/2` |
| `581104` | 同住分组号超出订单家庭数上限 | `roomGroupNo < 1` 或超过订单声明总人数 |
| `581110` | 出行人 ID 不属于该订单 | 更新项的 `id` 不属于路径订单 |
| `581111` | 已签电子合同后禁止修改证件号 | 已签约记录尝试修改 `idNo` |
| `581112` | 证件号格式不合法，请检查证件类型与号码是否匹配 | `idType` 非法或 `idNo` 格式不匹配 |
| `581113` | 手机号格式非法（应为 11 位数字） | 非空 `phone` 或 `emergencyPhone` 不是 11 位数字 |
| `581114` | 出生日期不能晚于今天 | `birthday` 为未来日期 |
| `581118` | 新增出行人缺少必填字段 | 新增项的 `name/idType/idNo` 全部为空 |
| `581119` | 出行人证件号重复 | 同一请求或订单内出现重复证件号 |
| `581122` | 订单不属于当前用户 | 订单不存在或不属于当前登录用户 |
| `581145` | 订单已确认，出行人信息不可再经小程序修改，如需变更请联系定制师 | 订单状态不在 `PENDING_PAY/CUSTOMIZING` 白名单 |
| `581149` | 出行人类型人数超出订单人数配置 | 根据生日派生后的某类出行人数超过订单声明配额 |

## 6. 示例

### 6.1 典型成功：两人五项完整，仅一人有手机号

**请求**

```http
POST /mp/v3/order/2079576729147338754/traveler/batch-edit
Authorization: Bearer <mp-token>
Content-Type: application/json
```

```json
{
  "travelers": [
    {
      "id": "2079576729147338801",
      "name": "张三",
      "gender": "1",
      "birthday": "1990-01-01",
      "idType": "PASSPORT",
      "idNo": "P1234567",
      "nationality": "中国",
      "race": "汉族",
      "phone": "13800138000",
      "emergencyContact": null,
      "emergencyPhone": null,
      "roomGroupNo": 1
    },
    {
      "id": "2079576729147338802",
      "name": "李四",
      "gender": "2",
      "birthday": "1992-02-02",
      "idType": "PASSPORT",
      "idNo": "P7654321",
      "nationality": "中国",
      "race": "汉族",
      "phone": "",
      "emergencyContact": null,
      "emergencyPhone": null,
      "roomGroupNo": 1
    }
  ]
}
```

**响应**

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "createdCount": 0,
    "updatedCount": 2,
    "completedCount": 2,
    "pendingCount": 0,
    "allCompleted": true
  },
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": true
}
```

### 6.2 边界成功：五项全部完整，但整单没有手机号

假设订单声明人数和实际人数均为 2，且请求将最后一部手机号清空。

**请求**

```http
POST /mp/v3/order/2079576729147338754/traveler/batch-edit
Authorization: Bearer <mp-token>
Content-Type: application/json
```

```json
{
  "travelers": [
    {
      "id": "2079576729147338801",
      "name": "张三",
      "gender": "1",
      "birthday": "1990-01-01",
      "idType": "PASSPORT",
      "idNo": "P1234567",
      "phone": ""
    },
    {
      "id": "2079576729147338802",
      "name": "李四",
      "gender": "2",
      "birthday": "1992-02-02",
      "idType": "PASSPORT",
      "idNo": "P7654321",
      "phone": ""
    }
  ]
}
```

**响应**

```json
{
  "code": 200,
  "message": "成功",
  "data": {
    "createdCount": 0,
    "updatedCount": 2,
    "completedCount": 2,
    "pendingCount": 0,
    "allCompleted": false
  },
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": true
}
```

这里 `completedCount=2` 表示两人的五项资料都完整；`allCompleted=false` 表示订单级“至少一名出行人有手机号”门禁未满足。

### 6.3 业务失败：订单已确认

**请求**

```http
POST /mp/v3/order/2079576729147338754/traveler/batch-edit
Authorization: Bearer <mp-token>
Content-Type: application/json
```

```json
{
  "travelers": [
    {
      "id": "2079576729147338801",
      "name": "张三",
      "gender": "1",
      "birthday": "1990-01-01",
      "idType": "PASSPORT",
      "idNo": "P1234567",
      "phone": "13800138000"
    }
  ]
}
```

**响应**

```json
{
  "code": 581145,
  "message": "订单已确认，出行人信息不可再经小程序修改，如需变更请联系定制师",
  "data": null,
  "traceId": "a1b2c3d4-e5f6-7890",
  "success": false
}
```

## 7. 修改前后对比

| 场景 | 修改前 | 修改后 |
|---|---|---|
| 单名成人五项完整但本人无手机号 | 可能计入 `pendingCount` | 计入 `completedCount` |
| 同行人已有手机号 | 仍可能要求每名成人各自填写 | 整单手机号门禁已满足 |
| 五项完整且整单无手机号 | 逐人完成状态与手机号门禁混合 | `completedCount` 可等于实际人数，但 `allCompleted=false` |
| 请求 / 响应结构 | 现有字段 | 不变 |

## 8. 消费注意事项

- 不要按“每名成人必须有手机号”在本地重算资料完成状态。
- `completedCount` 和 `pendingCount` 是操作后订单内的总量，不是本次请求中发生状态变化的行数。
- 判断本接口是否已满足整单补全条件，以响应 `allCompleted` 为准；它已同时包含人数、五项资料和订单级手机号门禁。
- `phone=null` 在更新场景表示保留原值；需要清空手机号时传空字符串。

## 9. 关联

- **Issue**: [#5203](https://git.1814.love:8443/wx/HL/issues/5203)
- **PR**: [#5210](https://git.1814.love:8443/wx/HL/pulls/5210)
- **Merge commit**: [88d0aec8375b56b5b8141984645a6998f8a42609](https://git.1814.love:8443/wx/HL/commit/88d0aec8375b56b5b8141984645a6998f8a42609)
