# [feat] 订单管理端「出行人智能批量导入」接口

- **日期**: 2026-05-06
- **PR**: wx/HL #1714 (Closes #1713) + #1716 hotfix
- **后端**: wx (Claude)
- **前端 @**: mmg
- **状态**: ✅ 测试服全链路网关 round-trip 15/15 通过(2026-05-06 15:50 部署 5149e329)

---

## 接口

```
POST {GATEWAY}/admin/order-traveler/smart-parse
Header: Authorization: Bearer <admin token>
Body:
{
  "rawText": "张三 350101199001011231 13800138000\n李四 110101199201021237 13900139000"
}
```

## 响应结构

```jsonc
{
  "code": 200,
  "data": {
    "totalLines": 2,
    "successCount": 2,
    "failCount": 0,
    "travelers": [
      {
        "lineNo": 1,
        "name": "张三",
        "idCardType": "ID_CARD",
        "idCardTypeLabel": "身份证",
        "idCardNo": "350101199001011231",
        "phone": "13800138000",
        "gender": "1",                 // "1"男 / "2"女 / null(身份证解析失败)
        "birthday": "1990-01-01",      // yyyy-MM-dd 字符串
        "travelerType": "ADULT",
        "travelerTypeLabel": "成人"     // 中文必返非 null
      }
    ],
    "errors": [
      // { lineNo, raw(脱敏), reason(中文), severity: "FAIL" | "WARNING" }
      // FAIL = 行解析失败,计入 failCount
      // WARNING = 行成功但有提示(多身份证号取首/重复 idCard 等),不计入 failCount
      // 恒等式: successCount + failCount == totalLines
    ]
  }
}
```

## 关键约束

- **顺序无关识别**: 每行任意顺序写姓名/身份证/手机号都能正确归位;18 位身份证→`idCardNo` 自动派生 `birthday/gender/travelerType`,11 位 1 开头→`phone`,其他→`name`
- **容错**: 表头跳过(三 token AND)、行首序号剥离(`1./1、/1)`)、BOM/零宽字符 strip、半角/全角/制表符空格切分
- **校验**: 18 位 GB 11643-1999 校验码(不支持 15 位旧版)
- **字段对齐**: ParsedTraveler 100% 对齐 `AdminOrderTravelerAddReqVO`,可直接 `formData = parsedTraveler` 一键回填
- **`birthday` 序列化为 `"yyyy-MM-dd"` 字符串**(已 @JsonFormat,前端 DatePicker 直接吃)
- **`*Label` 必返中文非 null**(P0 #1679,字典失败 throw 不兜底)
- **后端不直接落库**: 仅解析返回,前端拿到结果回填出行人表格(可编辑),用户确认后调原 `POST /admin/order/{orderId}/travelers` 入库

## 限制

| 限制 | 值 | 错误码 |
|------|-----|--------|
| 单次行数 | ≤ 100 | `SMART_PARSE_LINE_LIMIT_EXCEEDED` (504003) |
| 单次文本 | ≤ 20000 字符 | `SMART_PARSE_TEXT_TOO_LONG` (504004) + @Size 兜底 |
| 单行长度 | ≤ 500 字符 | 行内失败,reason=单行超长 |
| 限流 | 每用户 10 次 / 分钟 | `@RateLimiter` |
| 字典服务 | 失败 throw | `DICT_SERVICE_UNAVAILABLE` (504005) |

## 失败原因(中文)

- `身份证号校验失败` — 18 位但末位校验码错
- `不支持 15 位身份证,请用 18 位`
- `缺少身份证号` / `缺少姓名` / `未识别到姓名`
- `本行检测到多个身份证号,已取第一个` (warning)
- `身份证号在本次导入内重复` (warning,双胞胎/同名场景不强制去重)
- `单行超长(>500),疑似格式错误`
- `姓名含非法字符`

## 前端 UI 落点(@mmg)

参考用户提供的截图("快捷导入"弹窗):
- 入口: 订单详情 → 出行人信息 Tab → 「添加出行人」旁加「智能导入」按钮
- 弹窗: 多行 Textarea + 提示文案「导入要求:一人一行 姓名 身份证 手机号码之间用空格隔开」
- 确定 → 调本接口 → 拿 `travelers[]` 回填出行人表格(每行一条,可编辑)
- `errors[]` 高亮失败行,允许用户在原文本框修正后再次提交
- `successCount=0` 时弹「全部解析失败,请检查格式」
- `errors[i].lineNo` 是已剔除空行/表头/序号干扰后的数据行序号(非物理行号)

## 兼容性

- 仅新增接口,无 DDL / 无字典数据变更 / 无 Nacos 配置变更
- 复用现有 `AdminOrderTravelerController` 的 add 接口入库,流程不变
- 限流走项目自研 `@RateLimiter`,Redis 挂时自动放行(降级)

## 测试覆盖

- 单测: Service 49 + Controller MockMvc 9 + PiiMaskUtil 18 + IdCardChecksumUtil 13 = **89/89 全绿**
- CR: 9.6/10 通过(P0 红线 @OperationLog AOP 移除防 PII 落审计)
- 真 round-trip(网关/真容器/真限流/真 Feign): 部署测试服后补
