changelog(order-v3): 出行人 4 接口 (#2517/#2518/#2519/#2520)

- #2517 list 路径迁移 /travelers → /traveler/list
- #2518 batch-edit 真实业务+9错误码
- #2519 add 路径迁移+4错误码+@Idempotent+@Lock4j
- #2520 delete 方法+路径迁移+4错误码+整事务

测试服 9443 真测全 

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot 2026-05-18 19:31:03 +08:00
父节点 2059566991
当前提交 e9117da736
共有 4 个文件被更改,包括 827 次插入0 次删除

查看文件

@ -0,0 +1,188 @@
# 出行人列表: 路径迁移 /travelers → /traveler/list
> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2531
> **Issue**: #2517
> **日期**: 2026-05-18
> **影响范围**: 管理后台订单详情概览 Tab 出行人区块 / F27 出行人补全列表读取
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `68fd00f85`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)
---
## ⚠️ 关键变化(破坏性路径迁移)
接口路径从 v3 早期临时路径 `/travelers` 改为文档 V5.48 §2.1 正式路径 `/traveler/list`:
| 维度 | before(老路径,即将下线) | after(本 PR 起生效) |
|---|---|---|
| 方法 | GET | GET |
| 路径 | `/v3/admin/order/{id}/travelers` | `/v3/admin/order/{id}/traveler/list` |
**对前端的影响**: 调用方需把请求 URL 从 `/travelers` 替换为 `/traveler/list`
**响应结构无变化**(仍是 `Result<List<TravelerVO>>` + 16 字段),前端字段映射不需要改。
---
## 一、背景
V5.48 文档 §2.1 已把出行人列表正式路径定为 `/traveler/list`(与 add / batch-edit / delete 等 §2.2~§2.4 接口的路径风格统一: 一级名词 `traveler` + 二级动词)。早期实现用了简短复数形式 `/travelers`,本 PR 完成对齐。
同时清理了 TravelerConverter 内残留的 Mock 占位注释,补齐 `transportPlanIds` 字段的真实化(从桥接表 `order_transport_plan_traveler` LEFT JOIN 取真值,而非占位空数组)。
---
## 二、变更接口清单
| # | 接口 | 方法 | 老路径 | 新路径 | 变更类型 |
|---|------|------|--------|--------|----------|
| 1 | 订单出行人列表 | GET | `/v3/admin/order/{id}/travelers` | `/v3/admin/order/{id}/traveler/list` | 破坏性路径迁移 |
---
## 三、接口详情
### 1. 订单出行人列表 `GET /v3/admin/order/{id}/traveler/list`
**VO**: `TravelerVO`(16 字段,无 *Label 衍生字段)
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID(雪花 ID 字符串安全形式) |
#### 出参 `Result<List<TravelerVO>>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Long | 出行人 ID |
| `orderId` | Long | 订单 ID |
| `travelerType` | String | ADULT / CHILD / YOUNG_CHILD / BABY |
| `name` | String? | 姓名(占位行为 null) |
| `gender` | String? | MALE / FEMALE / UNKNOWN |
| `birthday` | LocalDate? | 出生日期 |
| `idType` | String? | ID_CARD / PASSPORT / BIRTH_CERT |
| `idNo` | String? | 证件号(admin 明文,DB 加密) |
| `nationality` | String | 国籍(默认"中国") |
| `race` | String | 民族(默认"汉族") |
| `phone` | String? | 出行人手机(admin 明文) |
| `emergencyContact` | String? | 紧急联系人姓名 |
| `emergencyPhone` | String? | 紧急联系人电话(admin 明文) |
| `roomGroupNo` | Integer? | 同住分组号 |
| `profileStatus` | String | PENDING / COMPLETED |
| `transportPlanIds` | List\<Long\> | 关联大交通批次 ID 列表(来自桥接表) |
#### 请求示例
```
GET /v3/admin/order/60123456789012/traveler/list
Authorization: Bearer {admin_jwt}
```
#### 响应示例
```json
{
"code": 200,
"data": [
{
"id": 70123456789012,
"orderId": 60123456789012,
"travelerType": "ADULT",
"name": "张三",
"gender": "MALE",
"birthday": "1985-08-12",
"idType": "ID_CARD",
"idNo": "220103198508121234",
"nationality": "中国",
"race": "汉族",
"phone": "13800002046",
"emergencyContact": "李四",
"emergencyPhone": "13900008888",
"roomGroupNo": 1,
"profileStatus": "COMPLETED",
"transportPlanIds": [80012345, 80012346]
}
],
"msg": "success"
}
```
#### 空数据响应
```json
{ "code": 200, "data": [], "msg": "success" }
```
#### 错误响应
| 错误码 | 含义 |
|---|---|
| `581100` | 出行人主段位—— 订单 / 出行人不存在(本接口下订单不存在时返回空集合即 200 + `data:[]`,不报错;此码主要给 batch-edit / add / delete 用) |
---
## 四、契约约束
- 仅 admin JWT 可访问,网关层鉴权(无 token → 网关 401);跨公司读取由订单详情上下文接口在更上层校验,本接口本身只看订单 ID。
- 敏感字段(idNo / phone / emergencyPhone)**明文返回**,前端勿做二次脱敏渲染(B 端定制师业务诉求,后端已通过 EncryptTypeHandler 在 DB 层加密,VO 层明文)。
---
## 五、数据库行为
只读接口,**不产生任何 DB 写入**。
读关联表: `order_traveler` + LEFT JOIN `order_transport_plan_traveler`(取 `transportPlanIds` 真值)。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 / 该订单下无出行人 → 200 + `data: []`(不 404,不抛异常)
- 出行人有未关联任何大交通的 → `transportPlanIds: []`
- 占位出行人(`name=null` `idNo=null`) → 字段为 null,不异常
---
## 七、不影响范围
- **仅影响**: 管理后台调用 `/v3/admin/order/{id}/travelers` 的请求 URL 拼接
- **零影响**:
- 响应结构 / 字段名 / 字段类型 / 字段值(VO 完全不变)
- mp 端出行人列表接口(走 `/v3/mp/...`,独立路径,未涉及)
- 订单详情主聚合接口 `/v3/admin/order/{id}``overview.travelers` 嵌套数组(已复用同 VO,本次未改)
- Feign 跨服务 internal 接口 `/internal/order/orders/{orderId}/travelers`(internal 路径独立,未涉及)
---
## 八、测试环境已验证
```
GET https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/list
Authorization: Bearer {admin_jwt}
→ 200 + List<TravelerVO> 16 字段齐全 ✓
→ transportPlanIds 真值(非占位空数组)✓
→ 老路径 /travelers 已无返回(404)✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #2124 | #2123 | /traveler/list 入参精简 + 敏感字段改明文(早期路径用 /travelers) | 部分有效(敏感字段口径保留,路径被本 PR 覆盖) |
| #2125 | - | detail.overview.travelers 复用 TravelerVO 完整字段 | ✅ 有效 |
| **本 PR #2531** | **#2517** | 路径正式迁 /traveler/list + transportPlanIds 真实化 | ✅ 最新 |
---
## 十、相关文档
- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.1
- 关联 Issue: [wx/HL#2517](https://git.1814.love:8443/wx/HL/issues/2517)
- 关联 PR: [wx/HL#2531](https://git.1814.love:8443/wx/HL/pulls/2531)

查看文件

@ -0,0 +1,233 @@
# 出行人批量编辑: POST /traveler/batch-edit 真实业务化
> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2535
> **Issue**: #2518
> **日期**: 2026-05-18
> **影响范围**: 管理后台 F27 出行人补全(B 端定制师代填)
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `83dbeedb4`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)
---
## ⚠️ 关键变化
1. **请求体字段名**: 文档原稿 §2.2 写的是 `items`,**实际 VO 定义为 `travelers`**,前端按 `travelers` 提交(下文示例为准)。
2. **从 Mock 占位变真实业务**: 之前是空骨架返回固定假数据,本 PR 接通真实 UPDATE + 12301 校验 + 合同冻结 + 同住分组校验 + 资料状态联动。
3. **新增 9 个错误码段位**: 581101 / 581102 / 581103 / 581104 / 581105 / 581111 / 581112 / 581113 / 581114(段位安排见下)。
4. **加幂等 @Idempotent(3s)**: 同订单 3 秒窗口内重复提交直接拒绝,防止快速点击 / 网络重试导致补全双写。
---
## 一、背景
文档 V5.48 §2.2 定义 admin 端批量编辑出行人,任一行 12301 字段(国籍 / 民族)校验失败整体回滚,补全完成后异步重算 `profile_status` 并触发"已完善"系统标签。本 PR 完成真实业务化,以及合同 signed 状态对证件号的冻结约束。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 |
|---|------|------|------|----------|
| 1 | 出行人批量编辑 | POST | `/v3/admin/order/{id}/traveler/batch-edit` | 真实业务化 + 9 个新错误码 |
---
## 三、接口详情
### 1. 出行人批量编辑 `POST /v3/admin/order/{id}/traveler/batch-edit`
**VO**: `TravelerBatchEditReqVO` + `TravelerEditItem`(嵌套数组每行)
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `travelers` | Body | List\<TravelerEditItem\> | ✅ | 待修改出行人数组(≤30) |
**TravelerEditItem 每行字段**:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | Long | ✅ | 出行人 ID(必须属于该 orderId) |
| `name` | String | ❌ | 姓名 |
| `gender` | String | ❌ | MALE / FEMALE / UNKNOWN |
| `birthday` | LocalDate | ❌ | 出生日期 |
| `idType` | String | ❌ | ID_CARD / PASSPORT / BIRTH_CERT |
| `idNo` | String | ❌ | 证件号(明文传,DB 加密) |
| `nationality` | String | ❌ | 国籍(默认中国,不可设为空字符串) |
| `race` | String | ❌ | 民族(默认汉族,不可设为空字符串) |
| `phone` | String | ❌ | 出行人手机(明文传,DB 加密) |
| `emergencyContact` | String | ❌ | 紧急联系人姓名 |
| `emergencyPhone` | String | ❌ | 紧急联系人电话(明文传) |
| `roomGroupNo` | Integer | ❌ | 同住分组号 |
#### 出参 `Result<TravelerBatchEditRespVO>`
| 字段 | 类型 | 说明 |
|------|------|------|
| `updatedCount` | Integer | 实际更新行数 |
| `completedCount` | Integer | 本次操作后变为 COMPLETED 的行数 |
| `pendingCount` | Integer | 仍为 PENDING 的行数 |
| `allCompleted` | Boolean | 该订单所有出行人是否已完善(联动 5 项 checklist) |
#### 请求示例
```json
POST /v3/admin/order/60123456789012/traveler/batch-edit
Authorization: Bearer {admin_jwt}
{
"travelers": [
{
"id": 70123456789012,
"name": "张三",
"gender": "MALE",
"birthday": "1985-08-12",
"idType": "ID_CARD",
"idNo": "220103198508121234",
"nationality": "中国",
"race": "汉族",
"phone": "13800002046",
"roomGroupNo": 1
},
{
"id": 70123456789013,
"name": "张小宝",
"gender": "MALE",
"birthday": "2018-05-01",
"idType": "BIRTH_CERT",
"idNo": "J012345678",
"roomGroupNo": 1
}
]
}
```
#### 响应示例
```json
{
"code": 200,
"data": {
"updatedCount": 2,
"completedCount": 2,
"pendingCount": 0,
"allCompleted": true
},
"msg": "success"
}
```
#### 错误响应清单(本工单新增段位)
| 错误码 | 含义 |
|---|---|
| `581100` | 出行人不存在 |
| `581101` | 12301 必报字段缺失(国籍 / 民族不能为空字符串) |
| `581102` | 订单不存在,无法编辑出行人 |
| `581103` | 性别枚举不合法(应为 MALE / FEMALE / UNKNOWN) |
| `581104` | 同住分组号超出订单家庭数上限 |
| `581105` | 批量大小超限(>30,@Size 已 fallback,Service 层防御性二次校验) |
| `581110` | 出行人 ID 不属于该订单 |
| `581111` | 已签电子合同后禁止修改证件号(合同 signed 后 idNo 冻结) |
| `581112` | 证件号格式不合法(身份证 18 位 / 护照 5-20 位) |
| `581113` | 手机号格式非法(应为 11 位数字) |
| `581114` | 出生日期不能晚于今天 |
| `581119` | 出行人证件号重复(同订单内 idNo 去重) |
错误响应体示例:
```json
{
"code": 581111,
"msg": "已签电子合同后禁止修改证件号",
"data": null
}
```
---
## 四、契约约束
### 校验顺序(任一失败整事务回滚)
1. 批量大小 ≤30(581105)
2. 订单存在(581102)
3. 身份证去重(581119,同请求体内自查重)
4. 姓名硬拦截(异常态字符,跟 TravelerNameValidator 共享口径)
5. 12301 字段非空字符串(581101)
6. 格式校验: idNo / phone / birthday(581112 / 581113 / 581114) + gender 枚举(581103) + roomGroupNo 范围(581104)
7. 归属校验: 每行 id 必须属于该 orderId(581110)
8. **已签电子合同冻结**: `contract_status = SIGNED` 时禁改证件号(581111)
### 并发控制
- `@Idempotent(timeout=3s)`: 同订单 3 秒重复提交直接拒绝
- `@Lock4j(expire=30000ms)`: 同订单批量更新加 30 秒分布式锁,防 admin / mp 同时提交竞态
---
## 五、数据库行为
- UPDATE N 行 `order_traveler`(只更非 null 字段,null 入参不覆盖原值)
- 派生 `profile_status`(若 idType/idNo/name/birthday/gender 齐全 → COMPLETED 否则 PENDING)
- INSERT 1 行 `order_status_log`,reason="出行人补全"
- 触发"已完善"系统标签事件(联动 confirm-checklist 的 TRAVELER_COMPLETE)
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 入参 travelers 为 null / 空数组 → 400(@NotEmpty)
- 入参 travelers.size > 30 → 400(@Size,@Idempotent 之前拦截)
- 订单不存在 → 581102
- 重复提交(3 秒内) → 接口直接拒绝(@Idempotent 拦截)
- 已签电子合同 → 581111(只冻结 idNo,其他字段仍可改)
---
## 七、不影响范围
- **仅影响**: 管理后台 F27 出行人补全表单
- **零影响**:
- 单个新增 `/traveler/add`(#2519 独立接口)
- 软删 `/traveler/{travelerId}/delete`(#2520 独立接口)
- 列表 `/traveler/list`(只读,#2517)
- mp 端出行人编辑接口
- 大交通批次 / 桥接表(本接口不动 transport_plan)
---
## 八、测试环境已验证
```
POST https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/batch-edit
Body: {"travelers":[{id, name, ...}]}
→ 200 + updatedCount/completedCount/pendingCount/allCompleted ✓
反例:
- 12301 字段为空 → 581101 ✓
- 出行人不属于订单 → 581110 ✓
- 合同 SIGNED 改 idNo → 581111 ✓
- 重复提交(3 秒内) → 接口拒绝 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #1984 | - | [§2 traveler skeleton] 9 接口空骨架 + Mock ServiceImpl | ❌ 被本 PR 真实化覆盖 |
| **本 PR #2535** | **#2518** | batch-edit 接通真实业务 + 9 错误码 + 幂等 / 锁 | ✅ 最新 |
---
## 十、相关文档
- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.2
- 关联 Issue: [wx/HL#2518](https://git.1814.love:8443/wx/HL/issues/2518)
- 关联 PR: [wx/HL#2535](https://git.1814.love:8443/wx/HL/pulls/2535)

查看文件

@ -0,0 +1,218 @@
# 出行人新增: 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)

查看文件

@ -0,0 +1,188 @@
# 出行人软删: DELETE → POST /traveler/{travelerId}/delete 路径迁移 + 真实业务化
> **服务**: hl-order-service-v3 (端口 8084 / 二期)
> **PR**: #2537
> **Issue**: #2520
> **日期**: 2026-05-18
> **影响范围**: 管理后台 F27 改人数场景(临时取消同行 1 人)
> **存放目录**: `changelogs-v2/2026-05/`(二期 v3 专属,带 -v2 后缀)
> **部署 commit**: dev-v3 `34c294465`
> **测试服已验证**: ✅(/@qa 通过 9443 网关 + 真 admin token round-trip)
---
## ⚠️ 关键变化
1. **破坏性路径 + 方法迁移**:
- **方法**: DELETE → POST(对齐 v3 风格,所有写操作统一 POST)
- **路径**: `/travelers/{travelerId}``/traveler/{travelerId}/delete`(对齐 V5.48 §2.4)
2. **从 Mock 占位变真实业务**: 4 道校验 + 软删 + 桥接表级联软删 + count-1 + status_log。
3. **新增 2 个错误码**: `581106`(已签合同禁删) / `581107`(最后 1 成人禁删)。
- ⚠️ 注意码段: **不是 581140/581141**,大交通段位 581120+ 已被占用,出行人主段位空挡是 581106/581107 紧贴主块(文档原稿写的 581119/581120 已被 TRAVELER_ID_CARD_DUPLICATE / TRANSPORT_PLAN_NOT_FOUND 占用)。
4. **加幂等 + 锁整事务**: `@Idempotent(3s) + @Lock4j(30s)`
---
## 一、背景
文档 V5.48 §2.4 定义: 软删除单行 `order_traveler`,同步:
1. UPDATE `order_main.<type>Count` -1
2. UPDATE `order_transport_plan_traveler.deleted=1`(级联解除其在大交通桥接表的所有关联)
3. INSERT `order_status_log`,reason="删除出行人"
并有 2 条业务硬约束:
- **已签电子合同(contract_status = SIGNED)的订单禁止删人**(合同已固定参与人,删除会破坏合同人员一致性)
- **订单最后 1 位成人禁止删除**(无成人订单逻辑上无效,会影响保险 / 大交通 / 房型分配)
---
## 二、变更接口清单
| # | 接口 | 方法 | 老路径 | 新路径 | 变更类型 |
|---|------|------|--------|--------|----------|
| 1 | 出行人软删 | DELETE → **POST** | `/v3/admin/order/{id}/travelers/{travelerId}` | `/v3/admin/order/{id}/traveler/{travelerId}/delete` | 破坏性方法 + 路径迁移 + 真实业务化 |
---
## 三、接口详情
### 1. 出行人软删 `POST /v3/admin/order/{id}/traveler/{travelerId}/delete`
#### 入参
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | Path | Long | ✅ | 订单 ID |
| `travelerId` | Path | Long | ✅ | 出行人 ID |
(请求体: 无)
#### 出参 `Result<Boolean>`
```json
{
"code": 200,
"data": true,
"msg": "success"
}
```
#### 请求示例
```
POST /v3/admin/order/60123456789012/traveler/70123456789013/delete
Authorization: Bearer {admin_jwt}
```
#### 错误响应清单(本工单新增段位)
| 错误码 | 含义 |
|---|---|
| `581100` | 出行人不存在 |
| `581102` | 订单不存在,无法删除出行人 |
| `581106` | **已签电子合同,禁止删除出行人**(本 PR 新增) |
| `581107` | **出行人是订单最后 1 位成人,禁止删除**(本 PR 新增) |
| `581110` | 出行人 ID 不属于该订单 |
> 段位说明: 文档原稿 §2.4 期望 `581119 / 581120`,但 581119 已被 TRAVELER_ID_CARD_DUPLICATE 占用,581120 已被 TRANSPORT_PLAN_NOT_FOUND 占用。本 PR 落到出行人主段位空挡 **581106 / 581107**,紧贴主块,保留 581140-581144 给 §2.6 大交通后续工单。
错误响应体示例:
```json
{
"code": 581106,
"msg": "已签电子合同,禁止删除出行人",
"data": null
}
```
---
## 四、契约约束
### 校验顺序(任一失败整事务回滚,4 道闸)
1. **订单存在性**(581102): 查 `order_main` by `id`
2. **出行人归属**(581110): 出行人的 `order_id` 必须等于 path `id`(防越权 / 错传)
3. **合同冻结**(581106): 订单 `contract_status = SIGNED` → 拒绝
4. **最后 1 成人**(581107): 当前操作是 ADULT 且订单未软删的 ADULT 行数 = 1 → 拒绝
### 并发控制
- `@Idempotent(timeout=3s)`: 同 `(orderId, travelerId)` 3 秒重复提交直接拒绝(防快速双击)
- `@Lock4j(expire=30000ms)`: 同订单 30 秒锁,与 batch-edit / add 互斥,防 count 字段 race
---
## 五、数据库行为
整事务:
| 操作 | 表 | 说明 |
|------|------|------|
| UPDATE | `order_traveler` | `deleted=1` `deleted_at=NOW()` |
| UPDATE | `order_transport_plan_traveler` | 该出行人所有未软删的桥接行 `deleted=1`(级联解除大交通关联) |
| UPDATE | `order_main.<type>Count` | -1(按 `travelerType` 选择: adultCount / childCount / youngChildCount / babyCount) |
| INSERT | `order_status_log` | reason="删除出行人",flowStatus 不变(from == to) |
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 → 581102
- 出行人不属于订单(越权) → 581110
- 合同 SIGNED → 581106(优先于人数校验)
- 删最后 1 成人 → 581107(允许删 CHILD / YOUNG_CHILD / BABY 即使他们也是最后 1 个)
- 出行人有大交通关联 → 自动级联软删桥接表,不阻断主流程
- 出行人已被软删过(重复删) → 581100(查不到行)
---
## 七、不影响范围
- **仅影响**: 管理后台 F27 改人数(删除)场景
- **零影响**:
- 批量编辑 `/traveler/batch-edit`(#2518 独立接口)
- 新增 `/traveler/add`(#2519 独立接口)
- 列表 `/traveler/list`(只读)
- 大交通错误码段位 581120-581129(保留)
- 老路径 `DELETE /travelers/{travelerId}` 已下线,前端调用必须改用 `POST /traveler/{travelerId}/delete`
---
## 八、测试环境已验证
```
POST https://web.test.1814.love:9443/v3/admin/order/60123456789012/traveler/70123456789013/delete
Authorization: Bearer {admin_jwt}
→ 200 + data: true ✓
→ order_traveler.deleted=1 ✓
→ order_transport_plan_traveler 级联 deleted=1 ✓
→ order_main.child_count -1 ✓
→ order_status_log 新增 1 行 reason=删除出行人 ✓
反例:
- 合同 SIGNED 删人 → 581106 ✓
- 删最后 1 成人 → 581107 ✓
- travelerId 不属于该订单 → 581110 ✓
- DELETE 老方法老路径 → 404 / 405 ✓
```
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #1984 | - | [§2 traveler skeleton] 空骨架 Mock | ❌ 被本 PR 真实化覆盖 |
| #2535 | #2518 | batch-edit 真实化(同期工单,共用 581100-581114) | ✅ 有效 |
| #2536 | #2519 | add 真实化(同期工单,581115-581118) | ✅ 有效 |
| **本 PR #2537** | **#2520** | delete 接通真实业务 + 方法+路径迁移 + 2 错误码(581106/581107) | ✅ 最新 |
---
## 十、相关文档
- API SPEC: `D:/work2/HL-v3/docs/order-v3/api/API-SPEC-V5.48.html` §2.4
- 关联 Issue: [wx/HL#2520](https://git.1814.love:8443/wx/HL/issues/2520)
- 关联 PR: [wx/HL#2537](https://git.1814.love:8443/wx/HL/pulls/2537)