docs(changelog): #8629 幂等键补对象身份段、#8630 团期活跃子订单清零自动复位
changelog-filename-gate / validate (push) Failing after 1s
changelog-filename-gate / validate (push) Failing after 1s
- #8629 出行人/大交通新增的幂等键补上对象身份段,同订单录入第二个对象不再被误拒 - #8630 团期最后一户取消后自动复位 requirement_confirmed 与整团用车需求(CONFIRMED→DRAFT), 并写 BATCH_REQUIREMENT_REOPENED 时间线(trigger=ALL_SUB_ORDERS_CANCELLED) Refs #8629 Refs #8630 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
@@ -0,0 +1,432 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8629"
|
||||
title: "出行人/大交通批次单条新增:幂等键补身份段,同订单连续录入不同对象不再被误拦"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修改接口"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "hl-order-service-v3 dev-v3 提交 91a52ba46c;两个端点的 @Idempotent 幂等键从「仅订单 ID」改为「订单 ID + 出行人/批次身份摘要」,请求体与响应体结构均未变。测试服验证:同订单并发提交两个不同出行人(间隔 150ms)均返回 200 各自创建成功;同订单并发提交两次完全相同的出行人(间隔 150ms)后一条返回 code=100502。"
|
||||
updated_at: "2026-09-30"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 出行人/大交通批次单条新增:幂等键补身份段,同订单连续录入不同对象不再被误拦
|
||||
|
||||
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: [#8637](https://git.1814.love:8443/wx/HL/pulls/8637)
|
||||
> **Issue**: [#8629](https://git.1814.love:8443/wx/HL/issues/8629)
|
||||
> **日期**: 2026-09-30
|
||||
> **影响范围**: 管理后台订单详情页「出行人」「大交通」两个单条新增表单
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**改前**:`POST /v3/admin/order/{id}/traveler/add` 与 `POST /v3/admin/order/{id}/transport-plan/add` 的幂等键只拼订单 ID(`#id`),3 秒窗口内**同订单任意两次提交**都会被判定为重复请求而拦截——哪怕两次提交的是完全不同的两个人、或完全不同的两个大交通批次。定制师连续为一家人逐个录入出行人、或逐个补录到达/返程批次时,第二个请求大概率落在 3 秒窗口内,被误拦为「同一出行人/批次刚已提交,请勿重复提交」,且该提示恒为假(首次请求早已成功结束,不是「处理中」)。
|
||||
|
||||
**改后**:幂等键改为「订单 ID + 该对象的身份摘要」(出行人:姓名+证件号+出生日期+手机号;大交通批次:方向+交通类型+车次/航班号+出发时间+自驾时段+关联出行人集合,排序后取 SHA-256)。**身份任一字段不同即视为不同对象,两次提交各自成功,前端无需人为在两次提交间插入延时**;只有身份完全相同的重复提交才会在 3 秒窗口内被拦。请求体、响应体结构均未变,也未新增/删除任何字段。
|
||||
|
||||
---
|
||||
|
||||
## 二、变更接口清单
|
||||
|
||||
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|
||||
|---|------|------|------|----------|------|
|
||||
| 1 | 单个出行人新增 | POST | `/v3/admin/order/{id}/traveler/add` | 幂等键收窄 | 幂等键加入出行人身份摘要,不同人不再互相拦截 |
|
||||
| 2 | 大交通批次新增 | POST | `/v3/admin/order/{id}/transport-plan/add` | 幂等键收窄 | 幂等键加入批次身份摘要,不同批次不再互相拦截 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 1. 单个出行人新增 `POST /v3/admin/order/{id}/traveler/add`
|
||||
|
||||
**VO**: `TravelerCreateReqVO → TravelerVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台订单详情页「出行人」页签,定制师为已创建的订单逐个补录出行人档案时调用;与批量编辑接口(`/traveler/batch-edit`)并列存在,用于单个补录/追加场景,例如客户临时增加一名随行儿童。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| id | Path | Long | ✅ | - | 订单 ID |
|
||||
| name | Body | String | - | - | 姓名(可后填) |
|
||||
| gender | Body | String | - | 1=男/2=女/0=未知 | 性别 |
|
||||
| birthday | Body | LocalDate | ✅ | 不得晚于今天 | 出生日期,后端按年龄分段自动派生 `travelerType` |
|
||||
| idType | Body | String | - | 取值见数据字典 `id_card_type` | 证件类型 |
|
||||
| idNo | Body | String | - | - | 证件号(明文传,DB 加密) |
|
||||
| nationality | Body | String | - | - | 国籍(默认中国) |
|
||||
| race | Body | String | - | - | 民族(默认汉族) |
|
||||
| phone | Body | String | - | - | 出行人手机(明文传,DB 加密) |
|
||||
| emergencyContact | Body | String | - | - | 紧急联系人姓名 |
|
||||
| emergencyPhone | Body | String | - | - | 紧急联系人电话(明文传) |
|
||||
| roomGroupNo | Body | Integer | - | - | 同住分组号 |
|
||||
|
||||
#### 出参 `Result<TravelerVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Long | 出行人 ID |
|
||||
| orderId | Long | 订单 ID |
|
||||
| teamNo | String | 团号 |
|
||||
| travelerType | String | 出行人类型:ADULT/CHILD/YOUNG_CHILD/BABY |
|
||||
| travelerTypeName | String | 出行人类型中文名(字典优先,枚举 label 兜底) |
|
||||
| name | String | 姓名 |
|
||||
| gender | String | 性别:1=男/2=女/0=未知 |
|
||||
| birthday | LocalDate | 出生日期 |
|
||||
| idType | String | 证件类型 |
|
||||
| idTypeName | String | 证件类型中文名(字典优先,枚举 label 兜底) |
|
||||
| idCardMasked | String | 证件号脱敏值(前3后4,中间星号,#2894) |
|
||||
| idProvinceCode | String | 身份证省级行政区代码;非大陆证件或无法识别为 null |
|
||||
| idProvinceName | String | 身份证省级行政区名称 |
|
||||
| nativePlace | String | 所属地(省+地级市,#5642);非大陆证件或未命中为 null |
|
||||
| nationality | String | 国籍 |
|
||||
| race | String | 民族 |
|
||||
| phoneMasked | String | 出行人手机脱敏值 |
|
||||
| emergencyContact | String | 紧急联系人姓名 |
|
||||
| emergencyPhoneMasked | String | 紧急联系人电话脱敏值 |
|
||||
| roomGroupNo | Integer | 同住分组号 |
|
||||
| profileStatus | String | 资料完善状态:PENDING/COMPLETED |
|
||||
| transportPlanIds | List\<Long\> | 关联大交通批次 ID 列表 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "王小明",
|
||||
"gender": "1",
|
||||
"birthday": "2018-06-20",
|
||||
"idType": "ID_CARD",
|
||||
"idNo": "220103201806201234",
|
||||
"nationality": "中国",
|
||||
"race": "汉族",
|
||||
"phone": "13812342046",
|
||||
"emergencyContact": "王大明",
|
||||
"emergencyPhone": "13988888888",
|
||||
"roomGroupNo": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": 70123456789012,
|
||||
"orderId": 60123456789012,
|
||||
"teamNo": "26-0480",
|
||||
"travelerType": "ADULT",
|
||||
"travelerTypeName": "成人",
|
||||
"name": "张三",
|
||||
"gender": "1",
|
||||
"birthday": "1985-08-12",
|
||||
"idType": "ID_CARD",
|
||||
"idTypeName": "身份证",
|
||||
"idCardMasked": "220***********1234",
|
||||
"idProvinceCode": "22",
|
||||
"idProvinceName": "吉林省",
|
||||
"nativePlace": "内蒙古呼伦贝尔市",
|
||||
"nationality": "中国",
|
||||
"race": "汉族",
|
||||
"phoneMasked": "138****2046",
|
||||
"emergencyContact": "李四",
|
||||
"emergencyPhoneMasked": "139****8888",
|
||||
"roomGroupNo": 1,
|
||||
"profileStatus": "COMPLETED",
|
||||
"transportPlanIds": []
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口是单条创建接口,成功路径必返回完整的单个对象,不存在「空列表」场景。`travelerTypeName` / `idTypeName` 的中文名解析是「字典优先,枚举 label 兜底」——字典服务不可用时回落到枚举自带中文 label,不会返回 `null`(该兜底策略是既有行为,非本次改动)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100502,
|
||||
"message": "同一出行人刚已提交,请勿重复提交",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 3 秒幂等窗口内,仅当出行人身份(姓名+证件号+出生日期+手机号)与前一次提交**完全相同**时才会被拦为 `code=100502`;姓名/证件号/出生日期/手机号任一不同即视为不同的人,两次提交各自成功,前端无需人为在两次提交间插入延时。
|
||||
- 幂等身份摘要取 SHA-256(64 位小写 hex),不改变请求体字段本身;前端提交的仍是原始明文字段。
|
||||
- 同订单另有 30 秒 `@Lock4j` 互斥锁(本次未变),用于防止与批量编辑接口(`/traveler/batch-edit`)并发写导致人数计数错乱;锁只管互斥,幂等键只管拦「同一次提交的重放」,两者不可互相替代。
|
||||
- 成功响应 `code` 固定为 **200**(非 0);若前端用 `code === 0` 判断成功,会把这次成功误判为失败。
|
||||
- 未登录 → 网关层拦截,返回 401 信封;`birthday` 缺失 → HTTP 200 + `code: 400`(`@NotNull`/`@PastOrPresent` 校验失败)。
|
||||
|
||||
---
|
||||
|
||||
### 2. 大交通批次新增 `POST /v3/admin/order/{id}/transport-plan/add`
|
||||
|
||||
**VO**: `TransportPlanReqVO → TransportPlanVO`
|
||||
|
||||
#### 使用场景
|
||||
|
||||
管理后台订单详情页「大交通」页签,定制师为订单逐个新增到达/返程批次时调用(每个批次至少关联 1 名出行人);与批量替换接口(`/transport-plan/batch`)并列存在,用于「多批到达/返程」场景下逐批补录。
|
||||
|
||||
#### 入参
|
||||
|
||||
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|
||||
|------|------|------|------|------|------|
|
||||
| id | Path | Long | ✅ | - | 订单 ID |
|
||||
| direction | Body | String | ✅ | ARRIVAL / DEPARTURE | 方向 |
|
||||
| transportType | Body | String | ✅ | FLIGHT / TRAIN / SELF_DRIVE | 交通类型 |
|
||||
| transportNo | Body | String | - | SELF_DRIVE 时为空 | 航班号/车次号 |
|
||||
| carrier | Body | String | - | - | 航司/铁路公司 |
|
||||
| departStation | Body | String | - | - | 出发站 |
|
||||
| arriveStation | Body | String | - | - | 到达站 |
|
||||
| departTime | Body | LocalDateTime | - | FLIGHT/TRAIN 必填 | 出发时间 |
|
||||
| arriveTime | Body | LocalDateTime | - | FLIGHT/TRAIN 必填 | 到达时间 |
|
||||
| selfDrivePeriod | Body | String | - | MORNING/AFTERNOON/EVENING,仅 SELF_DRIVE | 自驾时段 |
|
||||
| selfDriveEta | Body | LocalDateTime | - | 仅 SELF_DRIVE 可选 | 自驾预计到达时间 |
|
||||
| travelerIds | Body | List\<Long\> | ✅ | 至少 1 个 | 关联出行人 ID |
|
||||
| pickupRequired | Body | Boolean | - | 新增缺省 true | 是否需要接送 |
|
||||
| pickupRemark | Body | String | - | - | 接送备注 |
|
||||
| remark | Body | String | - | ≤500 字 | 备注 |
|
||||
|
||||
#### 出参 `Result<TransportPlanVO>`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | String | 批次 ID(`Long` 按字符串序列化) |
|
||||
| orderId | String | 订单 ID(`Long` 按字符串序列化) |
|
||||
| teamNo | String | 团号 |
|
||||
| direction | String | 方向:ARRIVAL/DEPARTURE |
|
||||
| directionLabel | String | 方向中文标签 |
|
||||
| mode | String | 模式:TOGETHER/SEPARATE;单条新增固定为 TOGETHER |
|
||||
| modeLabel | String | 模式中文标签 |
|
||||
| transportType | String | 交通类型:FLIGHT/TRAIN/SELF_DRIVE |
|
||||
| transportTypeLabel | String | 交通类型中文标签 |
|
||||
| transportNo | String | 航班号/车次号 |
|
||||
| carrier | String | 航司/铁路公司 |
|
||||
| departStation | String | 出发站 |
|
||||
| arriveStation | String | 到达站 |
|
||||
| departTime | LocalDateTime | 出发时间 |
|
||||
| arriveTime | LocalDateTime | 到达时间 |
|
||||
| selfDrivePeriod | String | 自驾时段 |
|
||||
| selfDrivePeriodLabel | String | 自驾时段中文标签 |
|
||||
| selfDriveEta | LocalDateTime | 自驾预计到达时间 |
|
||||
| pickupRequired | Boolean | 是否需要接送 |
|
||||
| pickupRemark | String | 接送备注 |
|
||||
| travelers | List\<TravelerRef\> | 关联出行人(id 字符串化 + name + travelerType + travelerTypeName) |
|
||||
| remark | String | 备注 |
|
||||
| creatorType | String | 创建来源:USER/ADMIN |
|
||||
| creatorTypeName | String | 创建来源中文名 |
|
||||
| createTime | LocalDateTime | 创建时间 |
|
||||
| updateTime | LocalDateTime | 更新时间 |
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"direction": "ARRIVAL",
|
||||
"transportType": "FLIGHT",
|
||||
"transportNo": "CA1234",
|
||||
"carrier": "中国国际航空",
|
||||
"departStation": "北京首都T3",
|
||||
"arriveStation": "长春龙嘉",
|
||||
"departTime": "2026-06-01T08:30:00",
|
||||
"arriveTime": "2026-06-01T10:15:00",
|
||||
"travelerIds": [70123456789012, 70123456789013],
|
||||
"pickupRequired": true,
|
||||
"pickupRemark": "需在 T3 出口举牌接机",
|
||||
"remark": "需要接机举牌"
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"id": "80012345",
|
||||
"orderId": "60123456789012",
|
||||
"teamNo": "26-0480",
|
||||
"direction": "ARRIVAL",
|
||||
"directionLabel": "到达",
|
||||
"mode": "TOGETHER",
|
||||
"modeLabel": "一起到达",
|
||||
"transportType": "FLIGHT",
|
||||
"transportTypeLabel": "飞机",
|
||||
"transportNo": "CA1234",
|
||||
"carrier": "中国国际航空",
|
||||
"departStation": "北京首都T3",
|
||||
"arriveStation": "长春龙嘉",
|
||||
"departTime": "2026-06-01T08:30:00",
|
||||
"arriveTime": "2026-06-01T10:15:00",
|
||||
"selfDrivePeriod": null,
|
||||
"selfDrivePeriodLabel": null,
|
||||
"selfDriveEta": null,
|
||||
"pickupRequired": true,
|
||||
"pickupRemark": "需在 T3 出口举牌接机",
|
||||
"travelers": [
|
||||
{ "id": "70123456789012", "name": "张三", "travelerType": "ADULT", "travelerTypeName": "成人" }
|
||||
],
|
||||
"remark": "需要接机举牌",
|
||||
"creatorType": "ADMIN",
|
||||
"creatorTypeName": "定制师代录",
|
||||
"createTime": "2026-07-01T10:00:00",
|
||||
"updateTime": "2026-07-01T10:30:00"
|
||||
},
|
||||
"traceId": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 空数据 / 降级响应
|
||||
|
||||
本接口是单条创建接口,成功路径必返回完整的单个对象,不存在「空列表」场景。`travelers[].travelerTypeName` 同样是「字典优先,枚举 label 兜底」,字典服务不可用时回落枚举 label,不返回 `null`(既有行为,非本次改动)。
|
||||
|
||||
#### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100502,
|
||||
"message": "同一大交通批次刚已提交,请勿重复提交",
|
||||
"data": null,
|
||||
"traceId": null,
|
||||
"success": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 业务边界
|
||||
|
||||
- 3 秒幂等窗口内,仅当批次身份(方向+交通类型+车次/航班号+出发时间+自驾时段+关联出行人集合)与前一次提交**完全相同**时才会被拦为 `code=100502`;任一字段不同即视为不同批次,两次提交各自成功。
|
||||
- `travelerIds` 参与身份摘要前会先排序去重:前端把同一批 ID 换个顺序重新提交,仍会命中同一幂等键(业务上视为同一次提交的重放)。
|
||||
- SELF_DRIVE 场景 `transportNo` 为空、`departTime` 也非必填,身份摘要因此额外纳入 `transportType`/`selfDrivePeriod`/`travelerIds`,避免两批不同自驾到达被误判为同一批。
|
||||
- 同订单另有 30 秒 `@Lock4j` 互斥锁(本次未变),用于防止 admin 多端并发新增同方向 plan;锁与幂等键职责不同、不可互相替代。
|
||||
- 成功响应 `code` 固定为 **200**(非 0);若前端用 `code === 0` 判断成功,会把这次成功误判为失败。
|
||||
- 未登录 → 网关层拦截,返回 401 信封;`direction`/`transportType` 缺失或 `travelerIds` 为空 → HTTP 200 + `code: 400`。
|
||||
|
||||
---
|
||||
|
||||
## 四、契约约束与正确调用方式
|
||||
|
||||
> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。
|
||||
|
||||
### ✅ 正确 / ❌ 错误 payload 对照
|
||||
|
||||
| 场景 | 结果 |
|
||||
|------|------|
|
||||
| ✅ 同订单连续新增两个不同出行人(`name`/`idNo`/`birthday`/`phone` 任一不同) | 两次请求均返回 200,各自创建成功,无需人为延时 |
|
||||
| ✅ 同订单连续新增两个不同大交通批次(身份字段任一不同) | 两次请求均返回 200,各自创建成功 |
|
||||
| ❌ 3 秒内重复提交身份字段完全相同的出行人 | 第二次返回 `code=100502`,零写入 |
|
||||
| ❌ 3 秒内重复提交身份字段完全相同的大交通批次 | 第二次返回 `code=100502`,零写入 |
|
||||
|
||||
### 切换状态时的必要动作
|
||||
|
||||
两个接口均为单条创建接口,不涉及状态切换;调用前无需额外前置动作。
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库行为
|
||||
|
||||
| 场景 | 写入行为 |
|
||||
|------|----------|
|
||||
| 同订单连续提交两个不同出行人 | 各自插入 1 行 `order_traveler`;改动前,第二个请求会被幂等键拦截,零写入 |
|
||||
| 同订单 3 秒内重复提交同一出行人(身份完全相同) | 只有首次请求落 1 行;重复请求被拦截,零写入(改动前后一致) |
|
||||
| 同订单连续提交两个不同大交通批次 | 各自插入 1 行 `order_transport_plan` + N 行桥接表 `order_transport_plan_traveler`;改动前,第二个请求会被拦截,零写入 |
|
||||
| 同订单 3 秒内重复提交同一批次(身份完全相同) | 只有首次请求写入;重复请求被拦截,零写入(改动前后一致) |
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为
|
||||
|
||||
- 未登录 → 401(网关拦截)
|
||||
- 校验失败(`birthday` 缺失、`direction`/`transportType` 非法、`travelerIds` 为空等) → HTTP 200 + `code: 400`
|
||||
- 3 秒窗口内同身份重复提交 → HTTP 200 + `code: 100502`,零写入
|
||||
- 下游字典服务(`travelerTypeName`/`idTypeName`/`transportTypeLabel` 等中文名解析)降级 → 回落枚举自带中文 label,不返回 null、不阻断主流程(既有行为,非本次改动)
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 字段级对比
|
||||
|
||||
| 字段 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| (无) | 请求体、响应体字段结构均未变 | 同左 |
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 行为 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 幂等键组成 | `#id`(仅订单 ID) | `#id + ':' + 身份摘要`(订单 ID + SHA-256 身份摘要) |
|
||||
| 同订单连续提交两个不同对象(3 秒内) | 第二次被拦为 `code=100502`,零写入,且提示「处理中」恒为假 | 两次均成功,各自写入 1 条记录 |
|
||||
| 3 秒内重复提交同一对象(身份完全相同) | 拦截 | 拦截(无变化) |
|
||||
| 错误提示文案 | 「同一出行人刚已提交,请勿重复提交」/「同一大交通批次刚已提交,请勿重复提交」(改动前后文案一致,仅拦截范围变窄) | 同左 |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否——请求体、响应体字段结构未变,只是原本被误拦的场景(连续提交不同对象)现在能成功,属于放宽约束
|
||||
- **前端是否必须同步上线**: 否——若前端此前为规避误拦而在两次提交之间人为加了延时或做了排队逻辑,那段代码可以撤掉,但不撤也不会出错
|
||||
- **前端 workaround 清理点**: 若前端曾针对「连续录入第二个出行人/批次报 100502」做过特殊重试或提示遮蔽逻辑,现在可以移除;不清理也不影响功能,只是不再需要
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: `POST /v3/admin/order/{id}/traveler/add`、`POST /v3/admin/order/{id}/transport-plan/add` 两个端点的幂等拦截范围
|
||||
- **零影响**:
|
||||
- 出行人批量编辑接口 `/traveler/batch-edit`(幂等键仍是 `#id`,未变)
|
||||
- 大交通批次编辑/软删/批量替换接口 `/transport-plan/{planId}/edit`、`/transport-plan/{planId}/delete`、`/transport-plan/batch`(幂等键均未变)
|
||||
- 出行人软删、信息校验、补全出行信息等其余出行人/大交通端点
|
||||
- 请求体、响应体字段结构(未新增/删除/改类型任何字段)
|
||||
- 同订单 30 秒 `@Lock4j` 互斥锁行为
|
||||
|
||||
---
|
||||
|
||||
## 八、测试环境已验证
|
||||
|
||||
测试服(hl-order-service-v3,dev-v3 提交 `91a52ba46c`)实测:
|
||||
|
||||
```
|
||||
POST /v3/admin/order/{id}/traveler/add 同订单并发提交两个不同出行人(间隔 150ms) → 均 200,各自创建成功 ✓
|
||||
POST /v3/admin/order/{id}/traveler/add 同订单并发提交两次完全相同的出行人(间隔 150ms) → 先 200,后一条 code=100502 ✓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8629](https://git.1814.love:8443/wx/HL/issues/8629)
|
||||
- 关联 PR: [wx/HL#8637](https://git.1814.love:8443/wx/HL/pulls/8637)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8629](https://git.1814.love:8443/wx/HL/issues/8629)
|
||||
- **PR**: [#8637](https://git.1814.love:8443/wx/HL/pulls/8637)
|
||||
- **Merge commit**: [91a52ba46c](https://git.1814.love:8443/wx/HL/commit/91a52ba46c)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
schema: "hl-changelog/v2"
|
||||
ticket: "8630"
|
||||
title: "团期活跃子订单清零后,自动复位团级需求确认与整团用车需求"
|
||||
consumer: "admin"
|
||||
author: "wx(GIT)"
|
||||
change_type: "修复"
|
||||
backend_status: "deployed"
|
||||
gateway_status: "not_required"
|
||||
frontend_status: "pending"
|
||||
frontend_owner: ""
|
||||
frontend_ref: ""
|
||||
target_release: ""
|
||||
verified_at: ""
|
||||
status_note: "hl-order-service-v3 dev-v3 合入提交 8e00cc98bc(PR #8640,同 PR 还含 #8631 的两处纯 javadoc 订正,与本单契约无关,未在本文档中提及)。本次改动不涉及任何接口的请求/响应结构变化,只影响既有字段在特定场景下的取值。"
|
||||
updated_at: "2026-09-30"
|
||||
base: "dev-v3"
|
||||
---
|
||||
|
||||
# 团期活跃子订单清零后,自动复位团级需求确认与整团用车需求
|
||||
|
||||
> **存放目录**: 二期(order-v3/fleet)→ `changelogs-v2/2026-09/`
|
||||
>
|
||||
> **服务**: hl-order-service-v3
|
||||
> **PR**: [#8640](https://git.1814.love:8443/wx/HL/pulls/8640)
|
||||
> **Issue**: [#8630](https://git.1814.love:8443/wx/HL/issues/8630)
|
||||
> **日期**: 2026-09-30
|
||||
> **影响范围**: 团期详情页「需求确认」状态、整团用车需求状态;不涉及任何请求/响应字段结构变化
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键变化
|
||||
|
||||
**改前**:一个团期整体确认需求(`requirementConfirmed=true`)、且整团用车需求也已确认(`CONFIRMED`)之后,如果该团期名下的子订单被逐个取消,直到**最后一个活跃子订单也被取消**,系统没有任何收尾动作——`requirementConfirmed` 停留在 `true`,整团用车需求状态停留在 `CONFIRMED`。此时团期详情页、车务待办侧仍显示「需求已确认」「用车已确认」,而这个团实际上已经一户不剩,且这个不一致**不报错、不告警**,只能靠人工发现。
|
||||
|
||||
**改后**:当一个团期的活跃子订单数(`order_status != CANCELLED` 的子订单条数)由非零变为零时,系统自动做两件事:
|
||||
1. 若整团用车需求当前处于 `CONFIRMED`,自动退回 `DRAFT`(不是 `PENDING_RECONFIRM`);
|
||||
2. 若团级 `requirementConfirmed` 当前为 `true`,自动清为 `false`。
|
||||
|
||||
只要这两项里至少有一项真的发生了状态变化,就会在该团期的时间线写入一条 `BATCH_REQUIREMENT_REOPENED`(需求重开待确认)事件,`extra` 中携带 `trigger: "ALL_SUB_ORDERS_CANCELLED"`、`orderId`(触发收尾的最后一个取消子订单 ID)、`requirementConfirmedCleared`(布尔)、`groupVehicleRequirementWithdrawn`(布尔)。两项复位互相独立、各自按条件写,天然幂等;两项都无需变化时(例如本来就是 `false`/`DRAFT`)不写时间线、不产生噪声。
|
||||
|
||||
本次改动**不涉及任何请求体或响应体的字段增删/改类型**,纯粹是既有字段在「团期归零」这一新增场景下会被系统自动改写取值。
|
||||
|
||||
---
|
||||
|
||||
## 二、影响的字段与读取入口
|
||||
|
||||
以下字段的**读取路径未变**,本次改动只影响它们在「团期活跃子订单清零」这一时刻之后的取值:
|
||||
|
||||
| 字段 | 归属接口(示例) | 改前在团期归零后的取值 | 改后 |
|
||||
|------|------|------|------|
|
||||
| `requirementConfirmed` | `GET /v3/admin/order/group-batch/{groupBatchId}`(`GroupBatchDetailRespVO`)及房务看板系列 VO | 停留在归零前的最后取值(可能仍是 `true`) | 若归零前为 `true`,归零后自动变为 `false` |
|
||||
| 整团用车需求 `status` | 整团用车需求相关读端点(`GroupVehicleRequirementRespVO.status`) | 停留在归零前的最后取值(可能仍是 `CONFIRMED`) | 若归零前为 `CONFIRMED`,归零后自动变为 `DRAFT` |
|
||||
| 团期时间线 | 团期时间线读端点 | 归零无任何留痕 | 新增一条 `BATCH_REQUIREMENT_REOPENED` 事件(仅当至少一项真的被复位时才写) |
|
||||
|
||||
---
|
||||
|
||||
## 三、精确触发条件
|
||||
|
||||
- **"活跃子订单"的判据** = `order_status != CANCELLED`(含 `PENDING_PAY`、`COMPLETED` 等非取消状态均计入活跃;与既有人数计数、归团回填、对账口径一致)。**不是**看板上「免闸户不计」的统计口径——房务免闸的户在用车这一侧仍可能要车,因此不按闸门维度扣减分母。
|
||||
- **收尾时点** = 该团期的活跃子订单数由非零变为零的那一刻(子订单取消事务提交之后)。覆盖以下所有取消入口:admin 出行前取消、C 端取消、退团审批、流团逐户取消、通用 `transition()` 的 CANCEL 分支、超时自动取消——这些入口最终都汇流到同一个内部取消事件,因此逐个入口都会触发本收尾逻辑。
|
||||
- **不覆盖**的取消路径:
|
||||
- `TERMINATE`(出行中终止行程 → `COMPLETED`)是与 `CANCEL`(→ `CANCELLED`)完全不同的状态路径,不会触发本收尾——出行中终止行程不代表这个团没有人,语义上也不应该清需求确认。
|
||||
- 直接修改数据库、绕过应用层的取消不会触发(无代码路径可挂载)。
|
||||
- **用车需求只在 `CONFIRMED` 这一档被自动退回**:`DRAFT`/`PENDING_RECONFIRM` 本来就不是已确认,无需处理;`DISPATCHED`(已发车务)/`DONE`(配车完成)**不会**被自动撤回——车务可能已经接单甚至配完车,自动撤回等于单方面掀掉车务在办的工作,这属于另一个业务决策,本次不做;`CANCELLED` 是流团终态,不会走到本路径。
|
||||
|
||||
---
|
||||
|
||||
## 六、边界行为(刻意不做的部分)
|
||||
|
||||
- **`batchStatus`(团期阶段)本次不变**:一个活跃子订单数归零的团期,其 `batchStatus` 可以继续停留在任意阶段(例如 `RESOURCE_PREPARING`「资源准备中」),不会被自动置为 `CANCELLED` 或退回 `RECRUITING`——自动改阶段涉及流团审批合规性判断,留给后续工单单独定案。前端据此判断"团是否还有效"时,**不能只看 `batchStatus`**,需要结合活跃子订单数或 `requirementConfirmed`/用车需求状态的复位来综合判断。
|
||||
- **`DISPATCHED`/`DONE` 的用车需求不会被回退**:见上节"三、精确触发条件"。
|
||||
- **房务就绪标记(`hotel_ready`)本次不动**:本收尾只处理 `requirementConfirmed` 与整团用车需求两项,房务侧的就绪标记不在本次收尾范围内,两者目前不对称——这是已知缺口,不在本单范围内一并解决。
|
||||
- **不做历史数据回填**:已经处于"团期归零但需求确认/用车需求未复位"这种旧脏数据状态的历史团期,本次改动不会自动纠正,只对本次改动上线之后新发生的"归零"事件生效。
|
||||
|
||||
---
|
||||
|
||||
## 六.5、枚举 / 数据字典
|
||||
|
||||
整团用车需求状态 `GroupVehicleRequirementStatus`(本次改动涉及的部分状态,完整枚举 6 值):
|
||||
|
||||
| 值 | 中文名 | 说明 |
|
||||
|------|------|------|
|
||||
| DRAFT | 草稿 | 本次改动的复位目标 |
|
||||
| CONFIRMED | 已确认 | 本次改动的复位起点(仅此档会被自动退回) |
|
||||
| PENDING_RECONFIRM | 待重新确认 | 不受本次改动影响 |
|
||||
| DISPATCHED | 已发车务 | 不受本次改动影响(明确不回退) |
|
||||
| DONE | 配车完成 | 不受本次改动影响(明确不回退) |
|
||||
| CANCELLED | 已取消 | 不受本次改动影响 |
|
||||
|
||||
团期时间线事件类型(本次涉及):
|
||||
|
||||
| 值 | 中文名 | 说明 |
|
||||
|------|------|------|
|
||||
| BATCH_REQUIREMENT_REOPENED | 需求重开待确认 | 复用既有事件类型(此前用于"定制师在整团确认后自行改需求"场景),本次新增一种触发来源;两种来源在 `extra.trigger` 字段区分,本次新增值为 `ALL_SUB_ORDERS_CANCELLED` |
|
||||
|
||||
---
|
||||
|
||||
## 六.6、修改前后对比
|
||||
|
||||
### 行为级对比
|
||||
|
||||
| 场景 | 改前 | 改后 |
|
||||
|------|------|------|
|
||||
| 团期活跃子订单数由非零变为零,此前 `requirementConfirmed=true` | 停留 `true`,无提示 | 自动变为 `false` |
|
||||
| 团期活跃子订单数由非零变为零,此前整团用车需求 `CONFIRMED` | 停留 `CONFIRMED`,无提示 | 自动退回 `DRAFT` |
|
||||
| 团期活跃子订单数由非零变为零,此前两项均已是"未确认"状态 | 无变化 | 无变化,不写时间线(幂等,无噪声) |
|
||||
| 团期时间线 | 归零无任何记录 | 至少一项被复位时,新增一条 `BATCH_REQUIREMENT_REOPENED` 事件,`extra.trigger=ALL_SUB_ORDERS_CANCELLED` |
|
||||
|
||||
---
|
||||
|
||||
## 六.7、影响评估
|
||||
|
||||
- **是否破坏向后兼容**: 否——字段名称、类型、接口路径均未变,只是取值在新场景下会被后端自动改写
|
||||
- **前端是否必须同步上线**: 视前端现有逻辑而定——若前端曾假设"一旦确认过就不会自动变回未确认"并据此做过缓存/跳过重复请求之类的优化,需要重新核对该假设在"团期归零"场景下不再成立
|
||||
- **需要前端注意的读取口径变化**: `requirementConfirmed`、整团用车需求 `status` 在团期活跃子订单归零后可能被系统自动改写,不再只由人工操作(确认/打回)改变;`batchStatus` 不受此次自动复位联动,读取时不能用 `batchStatus` 代替对这两个字段的直接读取
|
||||
|
||||
---
|
||||
|
||||
## 七、不影响范围
|
||||
|
||||
- **仅影响**: 团期活跃子订单数归零这一时刻,`requirementConfirmed` 与整团用车需求 `CONFIRMED` 状态的自动复位
|
||||
- **零影响**:
|
||||
- 团期阶段 `batchStatus` 的取值与流转规则(见"六、边界行为")
|
||||
- 房务就绪标记 `hotel_ready`
|
||||
- 整团用车需求 `DISPATCHED`/`DONE`/`PENDING_RECONFIRM`/`CANCELLED` 四档的自动流转规则
|
||||
- 所有接口的请求体、响应体字段结构(本次零新增、零删除、零改类型)
|
||||
- 团期归零之前已经存在的历史脏数据(不做回填)
|
||||
- 受控重配窗口相关的 `BATCH_VEHICLE_REQUIREMENT_REOPENED` 事件(另一独立事件类型,与本次复用的 `BATCH_REQUIREMENT_REOPENED` 不是同一个)
|
||||
|
||||
---
|
||||
|
||||
## 十、相关文档
|
||||
|
||||
- 关联 Issue: [wx/HL#8630](https://git.1814.love:8443/wx/HL/issues/8630)
|
||||
- 关联 PR: [wx/HL#8640](https://git.1814.love:8443/wx/HL/pulls/8640)
|
||||
|
||||
## 关联 / 联系人
|
||||
|
||||
### 链接
|
||||
|
||||
- **Issue**: [#8630](https://git.1814.love:8443/wx/HL/issues/8630)
|
||||
- **PR**: [#8640](https://git.1814.love:8443/wx/HL/pulls/8640)
|
||||
- **Merge commit**: [8e00cc98bc](https://git.1814.love:8443/wx/HL/commit/8e00cc98bc)
|
||||
|
||||
### 联系人
|
||||
|
||||
- **后端负责人**: @wx
|
||||
在新工单中引用
屏蔽一个用户