diff --git a/changelogs/2026-05/06_feat_order_traveler_smart_import.md b/changelogs/2026-05/06_feat_order_traveler_smart_import.md new file mode 100644 index 0000000..ca3a304 --- /dev/null +++ b/changelogs/2026-05/06_feat_order_traveler_smart_import.md @@ -0,0 +1,101 @@ +# [feat] 订单管理端「出行人智能批量导入」接口 + +- **日期**: 2026-05-06 +- **PR**: wx/HL #1714 (Closes #1713) +- **后端**: wx (Claude) +- **前端 @**: mmg + +--- + +## 接口 + +``` +POST {GATEWAY}/admin/order-traveler/smart-parse +Header: Authorization: Bearer +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(中文) } + ] + } +} +``` + +## 关键约束 + +- **顺序无关识别**: 每行任意顺序写姓名/身份证/手机号都能正确归位;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): 部署测试服后补