From e9117da7365123e9ed30b64e46b973db5069d2ef Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 18 May 2026 19:31:03 +0800 Subject: [PATCH] =?UTF-8?q?changelog(order-v3):=20=E5=87=BA=E8=A1=8C?= =?UTF-8?q?=E4=BA=BA=204=20=E6=8E=A5=E5=8F=A3=20(#2517/#2518/#2519/#2520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #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) --- .../2026-05/18_2517_traveler-list-path.md | 188 ++++++++++++++ .../2026-05/18_2518_traveler-batch-edit.md | 233 ++++++++++++++++++ changelogs-v2/2026-05/18_2519_traveler-add.md | 218 ++++++++++++++++ .../2026-05/18_2520_traveler-delete.md | 188 ++++++++++++++ 4 files changed, 827 insertions(+) create mode 100644 changelogs-v2/2026-05/18_2517_traveler-list-path.md create mode 100644 changelogs-v2/2026-05/18_2518_traveler-batch-edit.md create mode 100644 changelogs-v2/2026-05/18_2519_traveler-add.md create mode 100644 changelogs-v2/2026-05/18_2520_traveler-delete.md diff --git a/changelogs-v2/2026-05/18_2517_traveler-list-path.md b/changelogs-v2/2026-05/18_2517_traveler-list-path.md new file mode 100644 index 0000000..23b6c09 --- /dev/null +++ b/changelogs-v2/2026-05/18_2517_traveler-list-path.md @@ -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>` + 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>` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `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\ | 关联大交通批次 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 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) diff --git a/changelogs-v2/2026-05/18_2518_traveler-batch-edit.md b/changelogs-v2/2026-05/18_2518_traveler-batch-edit.md new file mode 100644 index 0000000..22f9f5a --- /dev/null +++ b/changelogs-v2/2026-05/18_2518_traveler-batch-edit.md @@ -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\ | ✅ | 待修改出行人数组(≤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` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `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) diff --git a/changelogs-v2/2026-05/18_2519_traveler-add.md b/changelogs-v2/2026-05/18_2519_traveler-add.md new file mode 100644 index 0000000..87e32cb --- /dev/null +++ b/changelogs-v2/2026-05/18_2519_traveler-add.md @@ -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.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) diff --git a/changelogs-v2/2026-05/18_2520_traveler-delete.md b/changelogs-v2/2026-05/18_2520_traveler-delete.md new file mode 100644 index 0000000..62af89f --- /dev/null +++ b/changelogs-v2/2026-05/18_2520_traveler-delete.md @@ -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.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` + +```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.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)