# 出行人新增: 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.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` 与 §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.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)