文件
hl-api-changelog/changelogs-v2/2026-09/30_8629_出行人大交通批次新增幂等键补身份段-修改接口-管理后台.md
T
API Changelog Bot和Claude Opus 5 4445618696
changelog-filename-gate / validate (push) Failing after 1s
docs(changelog): #8629 幂等键补对象身份段、#8630 团期活跃子订单清零自动复位
- #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>
2026-09-30 12:30:56 +08:00

19 KiB
原始文件 Blame 文件历史

schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer author change_type backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 8629 出行人/大交通批次单条新增:幂等键补身份段,同订单连续录入不同对象不再被误拦 admin wx(GIT) 修改接口 deployed not_required pending hl-order-service-v3 dev-v3 提交 91a52ba46c;两个端点的 @Idempotent 幂等键从「仅订单 ID」改为「订单 ID + 出行人/批次身份摘要」,请求体与响应体结构均未变。测试服验证:同订单并发提交两个不同出行人(间隔 150ms)均返回 200 各自创建成功;同订单并发提交两次完全相同的出行人(间隔 150ms)后一条返回 code=100502。 2026-09-30 dev-v3

出行人/大交通批次单条新增:幂等键补身份段,同订单连续录入不同对象不再被误拦

存放目录: 二期(order-v3/fleet)→ changelogs-v2/2026-09/

服务: hl-order-service-v3 PR: #8637 Issue: #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 列表

请求示例

{
  "name": "王小明",
  "gender": "1",
  "birthday": "2018-06-20",
  "idType": "ID_CARD",
  "idNo": "220103201806201234",
  "nationality": "中国",
  "race": "汉族",
  "phone": "13812342046",
  "emergencyContact": "王大明",
  "emergencyPhone": "13988888888",
  "roomGroupNo": 1
}

响应示例

{
  "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(该兜底策略是既有行为,非本次改动)。

错误响应

{
  "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 更新时间

请求示例

{
  "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": "需要接机举牌"
}

响应示例

{
  "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(既有行为,非本次改动)。

错误响应

{
  "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 ✓

十、相关文档

关联 / 联系人

链接

联系人

  • 后端负责人: @wx