Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
36 KiB
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 | 8666 | 产品班期改出发日联动团期与子单日期,有房务/车务占用时拒绝改期 | admin | wx(GIT) | 修改接口 | deployed | not_required | pending | 2026-10-03 | dev-v3 |
产品班期改出发日联动团期与子单日期,有房务/车务占用时拒绝改期
⚠️ 关键变化
- 改前:product-v2 改班期出发日只更新
product_schedule自己这张表;已挂在该班期上的团期(group_batch)与其子单(出发/结束日、行程day_date)保持旧日期不变,两边从此永久分叉——房务按旧日期配房、车务按旧日期派车,没有任何环节会发现或修复这个分叉。 - 改后:仅当出发日相对库里旧值发生变化时,
PUT /admin/product/item/{id}/schedule落库前先向订单域预检能否联动改期;被拒返回410206(团期已成团/已排房/已确认用车,具体原因拼进 message),预检服务不可达返回410207(fail-closed,不放行)。预检通过、本地保存成功后,后端在同一次请求内 best-effort 推送联动,把团期与全部活跃子单的三个日期、行程day_date、两级餐食日期一并平移;这一步不是前端能单独调用的接口,响应里也不会多出任何字段体现联动结果。 - 只按出发日判断是否预检:只改结束日(产品行程天数变化连带)或只改报名截止日,都不会触发预检、不会拒绝保存,这两点与改前一致。
- 新增 3 处连带行为变化,都是同一把"团期改期联动锁"或同一个车务栅栏探针改动带出来的:
- 团期子单转期
transfer-in:车务栅栏探针遇死锁(MySQL 1213)/锁等待超时(MySQL 1205),改前会被探针自身的异常处理整体吞掉、误判为"占用中"返回589576(文案把人引向一个并不存在的用车确认占用);改后探针只对锁竞争类异常原样上抛,由transfer-in新增的 catch 转译为100503,不再误报589576。 - 流团解散审批通过:批量处理子订单时,单户车务栅栏探针撞死锁/锁等待超时不再记"未处理"继续跑完其余户,而是让异常原样上抛,被审批事务既有的锁竞争转换逻辑接住,整笔回滚、返回
100503(该转换逻辑本身早于本次改动存在,本次变化的只是"探针异常不再被每户循环吞掉、能传导到这层转换";栅栏"占用中但未撞锁"的情形仍按原逻辑记"未处理")。 - 创建订单:带
productBatchId的创单与该团期当前的改期联动共用一把分布式锁(键order:gb:schedule-sync:{productBatchId},与改期联动 apply/replay 用的是同一把锁),取锁等待 5s,超时返回100503;本条目覆盖管理端POST /v3/admin/order。不带productBatchId的创单不受影响。
- 团期子单转期
- 订单时间线、团期时间线各新增若干枚举值,均通过已有的只读接口返回,接口路径与响应结构未变(见"六.5 枚举")。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 修改班期 | PUT | /admin/product/item/{id}/schedule |
新增错误码触发条件 | 出发日变化时先问订单域能否联动改期,拒绝返回 410206,预检不可达返回 410207 |
| 2 | 团期子单转期 | POST | /v3/admin/order/group-batch/{groupBatchId}/sub-order/{orderId}/transfer-in |
错误响应变化 | 车务栅栏探针撞死锁/锁等待超时,由误报 589576 改为 100503 |
| 3 | 流团解散审批通过 | POST | /v3/admin/order/group-batch/disband/{approvalId}/approve |
错误响应变化 | 单户车务栅栏探针撞死锁/锁等待超时不再记未处理,整笔回滚返回 100503 |
| 4 | 创建订单 | POST | /v3/admin/order |
新增可能的错误响应 | 带团期的创单与该团期改期联动共用锁,等锁超过 5s 返回 100503 |
三、接口详情
1. 修改班期 PUT /admin/product/item/{id}/schedule
VO: ScheduleSaveReqVO → Result<Long>
使用场景
产品运营在"产品管理 - 班期"页编辑已有班期。本次改动只影响"编辑且出发日相对库里旧值变化"这一种提交:新建班期、不改出发日的保存都不受影响。提交后,若该班期已挂团期且团期处于已成团/已排房/已确认用车等不可平移的阶段,保存会被拒绝;可平移时,保存成功后团期与子单日期会在同一次请求内由后端联动平移,前端无需也无法单独触发这一步。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| id | Path | Long | 是 | 产品须存在 | 产品 ID,后端写入请求体 productId 字段 |
| batchId | Body | Long | 否 | 修改时必传,新建不传 | 班期 ID |
| batchName | Body | String | 否 | - | 班期名称 |
| departureDate | Body | LocalDate | 是 | 非空 | 出发日期;本次改动只看这个字段相对库里旧值是否变化 |
| enrollmentDeadline | Body | LocalDate | 否 | - | 报名截止日;后端恒按"出发日−1 天"重算覆盖,入参不生效 |
| adultPrice | Body | BigDecimal | 否 | - | 成人价 |
| childPrice | Body | BigDecimal | 否 | - | 儿童价 |
| toddlerDiscount | Body | BigDecimal | 否 | - | 小童优惠额 |
| infantPrice | Body | BigDecimal | 否 | - | 幼童价 |
| singleRoomDiff | Body | BigDecimal | 是 | ≥0 | 单房差 |
| maxParticipants | Body | Integer | 否 | - | 最大参与人数(空或 0=不限) |
| maxRooms | Body | Integer | 否 | - | 总房间数(0=不限) |
| minToForm | Body | Integer | 否 | - | 最低成团户数 |
| minParticipants | Body | Integer | 否 | - | 最低成团人数 |
| manualOrderCount | Body | Integer | 否 | ≥0 | 运营手动线下占位房数 |
| manualParticipantCount | Body | Integer | 否 | ≥0 | 运营手动线下报名人数 |
| remark | Body | String | 否 | - | 备注 |
本次字段零变更,以上为完整字段表,与改前相同。
出参
| 字段 | 类型 | 说明 |
|---|---|---|
| data | Long | 班期 ID(Result<Long> 的 data),字段结构未变 |
请求示例
{
"batchId": 1780203456789012345,
"batchName": "元旦特别团",
"departureDate": "2027-01-27",
"enrollmentDeadline": "2027-01-26",
"singleRoomDiff": 200,
"maxRooms": 15,
"maxParticipants": 30
}
响应示例
出发日变化且订单域放行:
{
"code": 200,
"message": "成功",
"data": 1780203456789012345,
"traceId": null,
"success": true
}
空数据 / 降级响应
出发日未变化(只改价格/房量/备注等字段)时不触发预检,直接按原逻辑保存,返回结构与上例相同;本接口不存在"空数据"形态。
错误响应
出发日变化,但团期当前阶段不可平移(订单域阶段门/逐户资产门拒绝):
{
"code": 410206,
"message": "该班期已成团或已排房/已确认用车,不允许修改出发日期,请新建班期后逐户转期(团期已有房务计划或分房(3 行),不允许随产品班期改期)",
"data": null,
"traceId": null,
"success": false
}
出发日变化,但订单域预检不可达(Feign 异常、响应非成功或 data 为空):
{
"code": 410207,
"message": "订单服务暂不可用,无法确认该班期能否改期,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
| code | 何时出现 |
|---|---|
| 410206 | 出发日变化,订单域按阶段门/逐户资产门拒绝联动(589800~589803 之一,具体原因拼进 message 末尾括号;拿不到具体原因文案时退化为拒绝码数字) |
| 410207 | 出发日变化,订单域 Feign 调用异常、响应非成功或返回体为空,一律按不可用拒绝(fail-closed,不放行);订单域参数校验失败(589504,如 productBatchId 缺失/非法)也归在"响应非成功"之列,前端只会看到 410207,日志里出现 589504 不代表订单服务整体不可用 |
业务边界
- 只有"编辑已有班期且出发日相对库里旧值变化"才会触发预检;新建班期、不改出发日的任何保存都不受影响,与改前行为一致。
410206/410207都在本地落库之前判定,命中时班期本身零写入,保持拒绝前的值。- 结束日由产品行程天数(
tripDays)每次保存自动重算,与出发日是否变化无关;只改结束日(出发日不变)不触发预检,否则已成团班期连改价都会被拒绝。报名截止日恒为出发日前一天,随出发日派生,自身不单独判断。 - 预检通过、本地保存成功之后,团期与子单的实际日期平移由后端在同一次请求内 best-effort 执行:失败只记日志,不影响本次请求的 200 响应。前端不应把保存成功等同于团期/子单日期已同步完成,需要确认时应读团期详情或团期时间线核实实际日期(见"六.5 枚举")。
POST /admin/product/item/{id}/schedule(创建班期)与本接口共用同一个后端处理方法,是否按"编辑"处理只看请求体里batchId是否非空、与 HTTP 方法无关;调用方若误用 POST 但携带了batchId,同样会触发本次改动的预检与联动,不是只有 PUT 才会命中。- 班期状态重算若抛异常,该异常会原样上抛给调用方(本次保存请求整体失败);但此时班期本地保存与改期联动推送已经在这之前/同一收尾阶段 best-effort 执行完毕——联动可能已经生效,前端不应据"这次保存请求失败"反推"联动没有发生"。
- 车务栅栏占用(
589804)不参与预检判断,只会在预检通过、本地保存成功后的联动平移阶段触发;命中时该次平移 best-effort 失败、只记日志,不会体现在本次保存请求的响应里,也不会让保存请求本身失败。
2. 团期子单转期 POST /v3/admin/order/group-batch/{groupBatchId}/sub-order/{orderId}/transfer-in
VO: TransferSubOrderReqVO → TransferSubOrderRespVO
使用场景
房务/车务在团期控制台把某子单从一个团期转入当前团期(路径里的 groupBatchId 是转入的目标团期)。本次改动只涉及转期过程中车务栅栏探针这一步的异常处理,请求/响应字段未变。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | 是 | 目标团期须存在 | 转入的目标团期 ID |
| orderId | Path | Long | 是 | 子订单须存在且当前不在该团期 | 要转期的子订单 ID |
| fromGroupBatchId | Body | Long | 是 | 须与子订单当前所在团期一致 | 源团期 ID |
| reason | Body | String | 否 | ≤512 | 转期原因 |
| notify | Body | Boolean | 否 | 默认 true | 是否通知客户 |
出参
| 字段 | 类型 | 说明 |
|---|---|---|
| orderId | String | 子订单 ID |
| teamNo | String | 团号 |
| fromBatchNo | String | 源团期编号 |
| toBatchNo | String | 目标团期编号 |
| oldOrderAmount | BigDecimal | 转期前订单金额 |
| newOrderAmount | BigDecimal | 转期后按目标团期重算的订单金额 |
| paidAmount | BigDecimal | 已付金额 |
| newBalanceDue | BigDecimal | 转期后应补差额 |
| warnings | List<String> |
告警文案 |
请求示例
{
"fromGroupBatchId": 2106001234567890123,
"reason": "客户要求换期",
"notify": true
}
响应示例
{
"code": 200,
"message": "成功",
"data": {
"orderId": "1900000000000000456",
"teamNo": "27-0128",
"fromBatchNo": "GB20270120001",
"toBatchNo": "GB20270127002",
"oldOrderAmount": 6600.00,
"newOrderAmount": 6800.00,
"paidAmount": 2000.00,
"newBalanceDue": 4800.00,
"warnings": []
},
"traceId": null,
"success": true
}
空数据 / 降级响应
本接口无列表/分页语义,不存在空数据形态;warnings 为空数组表示转期过程无需特别提示,是正常情况,不是降级。
错误响应
车务栅栏探针遇死锁或锁等待超时(本次新增的触发路径):
{
"code": 100503,
"message": "资源被占用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
业务边界
- 该子订单的用车需求正在最终确认占用期内(非锁竞争、单纯占用中)仍返回原有的
589576「该子订单的用车需求正在最终确认中,请等确认完成或释放占用后再转期」,不受本次影响。 100503只在车务栅栏探针这一步真的撞上数据库死锁(MySQL 1213)或锁等待超时(MySQL 1205)时出现,属可重试错误;建议前端按"资源被占用,请稍后重试"提示,并允许用户直接重新提交,不需要额外的状态回滚操作。- 改前同样的锁竞争场景不会报错,而是被探针自身的异常处理吞掉、误判为"占用中"返回
589576(文案「等确认完成或释放占用后再转期」会把人引向一个并不存在的用车确认占用);改后锁竞争类异常原样上抛并转译为100503,不再误报589576。若前端曾对589576做过"提示稍后手动重试"之外的特殊处理,需要确认该处理在锁竞争场景下(现为100503)是否仍然合适,两者文案与含义不同,不应合并成同一套处理分支。
3. 流团解散审批通过 POST /v3/admin/order/group-batch/disband/{approvalId}/approve
VO: ApproveDisbandReqVO → DisbandApprovalRespVO
使用场景
团期管理员对一条流团解散申请执行"通过"操作。通过后批量处理该团期下所有子订单的解散(含用车需求取消)。本次改动只涉及其中某一户子订单车务栅栏探针撞锁时的异常处理路径,不改请求/响应字段。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| approvalId | Path | Long | 是 | 审批单须存在且可审批 | 解散审批单 ID |
| remark | Body | String | 否 | ≤512 | 审批备注 |
出参
| 字段 | 类型 | 说明 |
|---|---|---|
| approvalId | String | 审批单 ID |
| groupBatchId | String | 团期 ID |
| batchNo | String | 团期编号 |
| batchName | String | 团期名称 |
| batchLabel | String | 团期展示标签 |
| approvalStatus | String | 审批状态枚举 |
| approvalStatusName | String | 审批状态中文名 |
| reason | String | 申请原因 |
| affectedOrderCount | Integer | 受影响子订单数 |
| participantCount | Integer | 受影响人数 |
| estimatedRefundAmount | BigDecimal | 预估退款金额 |
| actualRefundAmount | BigDecimal | 实际退款金额 |
| applicantId | String | 申请人 ID |
| applicantName | String | 申请人姓名 |
| ccUserNames | List<String> |
抄送人姓名 |
| approvedById | String | 审批人 ID |
| approvedByName | String | 审批人姓名 |
| approvedAt | LocalDateTime | 审批时间 |
| approveRemark | String | 审批备注 |
| createTime | LocalDateTime | 申请时间 |
| canApprove | Boolean | 当前是否可审批 |
请求示例
{
"remark": "核实无误,予以通过"
}
响应示例
{
"code": 200,
"message": "成功",
"data": {
"approvalId": "2106100000000000111",
"groupBatchId": "2106001234567890123",
"batchNo": "GB20270120001",
"batchName": "元旦团",
"batchLabel": "元旦团 · 2027-01-20",
"approvalStatus": "APPROVED",
"approvalStatusName": "已通过",
"reason": "成团人数不足",
"affectedOrderCount": 5,
"participantCount": 11,
"estimatedRefundAmount": 28600.00,
"actualRefundAmount": 28600.00,
"applicantId": "50001234567890",
"applicantName": "李四",
"ccUserNames": [],
"approvedById": "50009876543210",
"approvedByName": "王五",
"approvedAt": "2027-01-18T10:20:00",
"approveRemark": "核实无误,予以通过",
"createTime": "2027-01-17T09:00:00",
"canApprove": false
},
"traceId": null,
"success": true
}
空数据 / 降级响应
本接口不存在空数据形态;审批单不存在或不可审批时走原有错误响应(本次未改)。
错误响应
批量处理子订单时,某户车务栅栏探针撞上数据库死锁或锁等待超时(本次新增的触发路径):
{
"code": 100503,
"message": "资源被占用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
业务边界
- 改前:批量解散逐户处理时,若某户车务栅栏探针抛出异常(含死锁/锁等待超时),该户记入"未处理"清单、不中断其余户,整单仍可能返回 200。
- 改后:探针遇死锁(MySQL 1213)或锁等待超时(MySQL 1205)会原样向上抛出,不再被当成"该户处理失败"吞掉;批量处理与审批通过共享同一个数据库事务,异常会导致整个审批通过操作回滚,整单返回
100503,所有户的解散都不生效,需要重新发起"通过"。 - 该子订单用车需求正在最终确认占用期内(非锁竞争、单纯占用中)仍走原有的"未处理"记录逻辑,不触发整单回滚,这一点未变。
100503可重试:锁竞争是瞬时状态,重新提交"通过"通常可以成功。
4. 创建订单 POST /v3/admin/order
VO: OrderCreateReqVO → OrderCreateRespVO
使用场景
管理端新建订单(含团期订单与自由出团订单)。本次改动只影响带 productBatchId 的团期订单:创单与该团期当前正在进行的改期联动共享同一把锁,若锁被占用会短暂等待而非立即报错。不带 productBatchId 的订单创建不受影响。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| productId | Body | Long | 是 | - | 产品 ID |
| tierSeq | Body | Integer | 是 | - | 档位序号 |
| departureDate | Body | LocalDate | 是 | - | 出发日期 |
| adultCount | Body | Integer | 是 | ≥1 | 成人数 |
| childCount | Body | Integer | 否 | ≥0,默认 0 | 儿童数(6-12岁) |
| youngChildCount | Body | Integer | 否 | ≥0,默认 0 | 幼童数(2-5岁,占座不占床) |
| babyCount | Body | Integer | 否 | ≥0,默认 0 | 婴儿数(0-1岁,不占座不占床) |
| customerName | Body | String | 是 | - | 客户姓名 |
| customerPhone | Body | String | 是 | 手机号格式 ^1[3-9]\d{9}$ |
客户手机 |
| customerRemark | Body | String | 否 | ≤500 | 客户备注 |
| createSource | Body | String | 否 | ≤20,不传默认 CONSULTANT |
创建来源枚举 |
| productBatchId | Body | Long | 否 | GROUP 产品必传,自由出团为空 | 产品侧团期 batchId;本次改动唯一相关字段,非空时创单会与该团期改期联动共用锁 |
| roomCount | Body | Integer | 否 | ≥1 | 房间数 |
| tags | Body | List<String> |
否 | - | 订单标签名列表 |
| sharerOpenid | Body | String | 否 | - | 分享人 openid |
| customizerId | Body | Long | 否 | - | 分享归因 customizerId |
本次字段零变更,以上为完整字段表,与改前相同。
出参
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 订单主键 |
| orderNo | String | 订单号 |
| teamNo | String | 团号 |
| orderStatus | String | 粗状态枚举(创单后 = PENDING_PAY) |
| orderStatusName | String | 粗状态中文名 |
| flowStatus | String | 细状态枚举值 |
| flowStatusName | String | 细状态中文名 |
| flowStep | Integer | 线性 6 步当前步序号 |
| flowStepTotal | Integer | 线性 6 步总步数 |
| flowStepName | String | 当前步中文名 |
| consultantId | String | 实际绑定的定制师 ID |
| consultantSource | String | 定制师来源 |
| tags | List<String> |
系统自动打的标签 |
| createdAt | LocalDateTime | 创单时间 |
| productName | String | 产品名称 |
| tierName | String | 档位名 |
| groupBatchName | String | 创单响应不回填,恒为 null(该字段只在订单列表行里赋值),判团/展示不要读它 |
| departureDate | LocalDate | 出发日 |
| returnDate | LocalDate | 返团日 |
| totalAmount | String | 订单总价 |
| depositAmount | String | 建议定金金额 |
| depositRatio | Integer | 定金比例百分比 |
| depositMode | String | 定金计算模式(FIXED/RATIO/FULL) |
| paymentMode | String | 支付模式(DEPOSIT/FULL) |
| expiryMinutes | Integer | 支付时限分钟数 |
| payUrl | String | 支付页绝对 URL |
| customerName | String | 客户姓名(回显) |
| groupBatchId | String | 运营团期 ID;非空=团订单,判团唯一字段 |
| productBatchId | String | 团期产品排期 ID,仅供溯源,不参与判团 |
| groupOrder | Boolean | 是否团订单(= groupBatchId 非空的派生值) |
请求示例
{
"productId": 30001234567890,
"tierSeq": 1,
"departureDate": "2027-01-27",
"adultCount": 2,
"customerName": "张三",
"customerPhone": "13800002046",
"productBatchId": 80001234567890123
}
响应示例
{
"code": 200,
"message": "成功",
"data": {
"id": "60123456789015",
"orderNo": "HL20270127143025001",
"teamNo": "27-0128",
"orderStatus": "PENDING_PAY",
"orderStatusName": "待支付",
"flowStatus": "AWAITING_PAY",
"flowStatusName": "待支付",
"flowStep": 0,
"flowStepTotal": 6,
"flowStepName": "待支付",
"tags": [],
"createdAt": "2027-01-18T14:30:25",
"productName": "元旦亲子营",
"tierName": "经典档",
"groupBatchName": null,
"departureDate": "2027-01-27",
"returnDate": "2027-02-01",
"totalAmount": "6800.00",
"depositAmount": "2000.00",
"depositRatio": null,
"depositMode": "FIXED",
"paymentMode": "DEPOSIT",
"expiryMinutes": 1440,
"payUrl": "https://pay.hulalv.com/pay/HL20270127143025001",
"customerName": "张三",
"groupBatchId": "70123456789012345",
"productBatchId": "80001234567890123",
"groupOrder": true
},
"traceId": null,
"success": true
}
空数据 / 降级响应
本接口不存在空数据/降级形态,创建失败一律走错误响应。
错误响应
带团期创单时,与该团期的改期联动撞锁、等待 5s 仍未拿到锁(本次新增的触发路径):
{
"code": 100503,
"message": "资源被占用,请稍后重试",
"data": null,
"traceId": null,
"success": false
}
业务边界
- 只有
productBatchId非空的创单才会与改期联动共用锁;自由出团(productBatchId为空)不受影响,不会因为别的团期正在改期而变慢或报错。 - 锁是 Redis 分布式锁(键
order:gb:schedule-sync:{productBatchId}),与改期联动 apply/replay 用的是同一把锁、同一键空间;取锁等待上限 5s,正常情况下一次改期联动的持锁时间远小于此(apply 在锁内平移整团子单,测试服 12 个样本 apply 段 41~688ms),100503只在极端并发窗口出现,属可重试错误。
四、契约约束与正确调用方式
PUT /admin/product/item/{id}/schedule只有"编辑已有班期 + 出发日相对库里旧值变化"才会触发410206/410207;前端不需要自行预判是否会触发,直接提交、按错误码分支处理即可。- 收到
410206时,message已经把具体拒绝原因(成团状态/房务计划行数/用车需求/排程子订单)拼进括号内,可直接原样展示,不需要再调用别的接口获取详情。 - 收到
410207时按"订单服务暂不可用"提示;这是本地保存前的拒绝,班期本身零写入,可直接引导用户重新提交保存。 100503(四个接口共用同一个错误码)统一按"资源被占用,请稍后重试"处理:允许用户直接重新提交原请求,不需要任何额外的状态清理或回滚操作。- 创建订单时,若明确是团期订单(填了
productBatchId),建议前端对100503与其他失败做区分提示(如"该团期正在改期,请稍后重试"),不要和参数校验类错误混在一起展示。
五、数据库行为
PUT /admin/product/item/{id}/schedule命中410206/410207时:班期本身零写入,保持拒绝前的值;订单域的团期、子单、房间计划、用车需求均不受影响。PUT /admin/product/item/{id}/schedule预检通过、本地保存成功且联动平移成功时:团期出发日/结束日/报名截止日、全部活跃子单的出发/结束日、行程day_date,以及团期与子单两级的餐食日期会一并更新;子单自身的行程天数(trip_days/trip_nights)不参与本次联动,改前改后均不受影响。只要有户被平移或团期出发日本身变化,团期的"需求已确认"标志会被重新打开(复用既有事件BATCH_REQUIREMENT_REOPENED,仅在真实发生 1→0 翻转时落时间线)、"配房完成"标志会被置回未完成;仅结束日变化、或日期已对齐只差餐食日期的"仅对齐餐食"命中,都不会触发这两个标志重置。联动写事务失败(含锁竞争)不会部分写入——要么该团期全部平移,要么该团期一个字段都不变。- 逐户用车需求随子单日期平移重基准,只处理
TRAVEL(行程用车)类需求;TRANSFER(接送机)类需求不受联动影响,不会跟着日期平移。 transfer-in/ 流团解散审批通过命中100503时:本次请求涉及的写入全部回滚,不产生部分写入。- 创建订单命中
100503时:订单主体与相关子表零写入。
六、边界行为
- 改期联动推送是后端在同一次请求内 best-effort 执行的:失败只记日志,不影响本次保存请求的 200 响应。前端不应把保存成功等同于团期/子单日期已同步完成,需要确认时应读团期详情或团期时间线核实实际日期。
100503在四个接口里都是"可重试"语义:锁等待超时或数据库死锁都是瞬时状态,不代表请求本身非法。transfer-in/流团解散审批里,车务栅栏"正在最终确认占用中但未撞锁"的情形,仍分别返回原有的589576(转期)或记为"未处理"(解散审批),不受本次改动影响。
六.5 枚举
订单时间线新增事件类型(OrderLogEventType)
所属字段:GET /v3/admin/order/{id}/status-log 响应数组元素的 eventType(机器码)/ title(中文标签) | 类型:String
| 值 | 中文 | 说明 |
|---|---|---|
GROUP_BATCH_RESCHEDULE |
团期改期 | 产品班期出发日联动改期成功平移了该订单的日期/行程/餐食时写入;内容示例:"出发日 2027-01-26→2027-01-27,返程日 2027-01-31→2027-02-01,餐食日期随之平移 3 条(产品班期改期联动)"。操作人归属:请求携带操作人信息(产品侧保存班期时实际触发该次保存的管理员)且非运维重推时记为该管理员;运维重推或缺少操作人信息时记为系统(SYSTEM);运维重推时内容末尾的括号变为"(产品班期改期联动,运维重推)"。 |
团期时间线新增事件类型(GroupBatchLogEventType)
所属字段:GET /v3/admin/order/group-batch/{groupBatchId}/status-logs 响应数组元素的 eventType(机器码)/ eventTypeName(中文标签,历史数据可能为 null,见下方业务边界) | 类型:String
| 值 | 中文 | 说明 |
|---|---|---|
BATCH_SCHEDULE_SYNCED |
班期改期联动 | 团期与子单联动平移成功时写入;内容示例:"产品班期改期联动:出发日 2027-01-26→2027-01-27,结束日 2027-01-31→2027-02-01,报名截止 2027-01-25→2027-01-26;联动子单 3 户;餐食日期平移 团期 1 条、子单 0 条"。团期与子单日期已一致、只差餐食日期未对齐时同样写本事件(不是另开新事件),内容改为例如"产品班期改期联动:团期与子单日期已对齐,仅团期餐食日期对齐到出发日 2027-01-27 共 2 条";若连餐食也已对齐则是幂等命中,不写任何时间线。 |
BATCH_SCHEDULE_SYNC_REJECTED |
班期改期联动被拒 | 产品已改期但团期当时不在可联动阶段(589800~589804 之一)时写入,独立事务落库,不随业务异常一起回滚;内容示例:"产品班期改期联动被拒:团期当前状态为「已成团」,已成团,不允许随产品班期改期" |
BATCH_SCHEDULE_DRIFT_DETECTED |
班期日期漂移 | 对账发现团期行日期与产品实时日期/活跃子单出发日/房间计划日期不一致时写入(只读检测,不改数据);只差返程日不写本事件,仅打日志。由每日定频对账任务驱动(每天 03:30,与房控对账 03:00 错峰),内容与同事件最新一条记录完全相同时跳过写入,不重复刷屏。 |
以上三个事件的 BATCH_SCHEDULE_SYNCED/BATCH_SCHEDULE_SYNC_REJECTED 两类运维重推(非前端可触发)内容前缀都会多一句"(运维重推)"。需求重开事件 BATCH_REQUIREMENT_REOPENED(枚举值本身非本次新增)新增一个触发来源:改期联动平移了至少一户或团期出发日本身变化时会复用该事件把团期"需求已确认"标志重新打开,extra.resourceType 为 SCHEDULE_SYNC,可用于区分定制师手动修改需求(原有来源)与改期联动触发(本次新增来源)。
以上两个接口均为既有只读接口,路径与响应结构未变,仅新增可能出现的枚举值。
业务边界
GroupBatchStatusLogItemVO.eventTypeName遇到历史数据里不在当前枚举范围的值时会是null,eventType仍会原样返回机器码;前端应直接使用后端给的eventTypeName/title,不要自行维护一套编码→文案映射表,新增枚举值不需要前端改代码即可正常显示。- 与团期时间线不同,订单时间线(
OrderLogEventType)遇到不在枚举范围内的历史eventType时,title会兜底返回原始机器码字符串而不是null——两个时间线接口对"未知事件"的兜底方式不同,前端渲染逻辑不能直接复用同一套判空逻辑。
六.6、修改前后对比
字段级对比
本次四个接口请求/响应字段均零变更,只是新增了触发条件或新增了可能返回的错误码。
行为级对比
| 接口 | 改前 | 改后 |
|---|---|---|
| 修改班期(出发日变化) | 只改 product_schedule,已挂团期/子单日期永久不同步 |
落库前先问订单域能否联动;可以则保存后 best-effort 联动平移团期+子单+餐食日期,不可以则拒绝(410206/410207),班期零写入 |
| 团期子单转期 | 车务栅栏探针撞死锁/锁等待超时 → 被探针自身吞掉、误判为占用中,返回 589576 |
同样场景 → 原样上抛转译为 100503,可重试,不再误报 589576 |
| 流团解散审批通过 | 单户车务栅栏探针撞锁 → 记"未处理",继续处理其余户,整单可能 200 | 单户撞死锁/锁等待超时 → 异常原样上抛,整单回滚,返回 100503 |
| 创建订单(带团期) | 与改期联动无互斥,可能拿到改期中途的旧日期 | 与改期联动共用锁,等锁 5s,超时返回 100503 |
六.7、影响评估
- 是否破坏向后兼容:否。四个接口请求字段均未变;
410206/410207是此前不会触发的全新拒绝路径。100503对transfer-in/创建订单两个接口是全新可达路径(改前分别误报589576、或根本没有任何锁竞争风险);对流团解散审批通过,100503所用的锁竞争转换逻辑本身早于本次改动存在,本次新增的只是"单户探针异常不再被吞掉、能传导到这层转换"这条此前不可达的路径。正常放行路径的请求/响应结构均不变。 - 前端是否必须同步上线:否。不处理新增错误码时,命中场景会退化为展示原始
message文案(通用错误提示兜底),不会白屏或崩溃;但改前这两个接口撞锁时分别表现为transfer-in返回589576(引导用户"等用车确认完成")、流团解散审批通过记"未处理"并可能返回成功,若前端曾针对这两种旧表现做过特殊处理,建议补上对100503的识别并给出"稍后重试"引导。 - 前端 workaround 清理点:无。
七、不影响范围
- 不带
productBatchId的创单(自由出团)完全不受影响。 - 班期保存时只改出发日之外的字段(价格、房量、备注等),或新建班期,不触发预检,行为与改前相同。
transfer-in除车务栅栏撞锁这一条路径外的其他错误码(589575/589576/589577/转期前置守卫等)未变。- 流团解散审批里车务栅栏"占用中但未撞锁"的情形(返回"未处理"而非异常)未变。
- 房务配房、分房、入住确认等接口未改。
- 无数据库结构变更(零 DDL/Flyway),无网关路由变更。
八、测试环境已验证
测试服已部署合并提交 fad5d7814b(product-v2 与 order-v3),以下均经网关真实调用,并按接口回读与库内数据判定:
- 改期被拒(410206):四类不允许改期的团期各取一例,用
PUT /admin/product/item/{productId}/schedule改departureDate,均返回410206,message内嵌订单域原因:已成团(589800)、已有房务计划/分房(589801)、已有团级正式用车需求(589802)、子单已提交酒店需求(589803,文案点名到具体订单号)。拒绝后班期、团期、子单日期全部核对未变。 - 改期放行:招募中、尚无排程的团期改出发日后,团期详情的
departDate/endDate/enrollDeadline等于新值;活跃子单出发/返程日按同一位移平移,trip_days不变;子单逐日行程day_date全量核对 17 单 53 行,零例外;未派单的 TRAVEL 用车需求已重版,TRANSFER 不动;团期餐食meal_date随之平移(样本平移 5 天)。 - 房务汇总:改期后房务需求汇总的入住日随之平移(样本 05-15/16 → 05-17/18),无
dateShifted=true、无 outOfRange 户,hotelReady=false。 - 不触发联动:只改单房差、或产品天数变化只带动
endDate变化的保存,均不走预检与联动、不被拒。 - 订单服务不可用(410207):订单服务不可达时保存返回
410207,班期不落库。 - 审计留痕:被联动的每个子单各有一条「团期改期」时间线,含出发日/返程日的旧值与新值,操作人为发起保存的管理员(非 SYSTEM);团期有一条改期联动成功记录。
- 并发创单:同一团期 10 个
POST /v3/admin/order并发创单,期间穿插一次改期:10 单全部成功,改期成功,所有子单出发日与团期一致(零分叉)。本轮没有撞出100503(锁竞争窗口很窄)。 - 耗时:测试服 12 个改期样本的 apply 段(锁内平移整团)为 41~688ms。
覆盖边界:transfer-in 与流团解散审批通过的 100503 路径(撞死锁/锁等待超时)在测试服无法稳定构造,本轮未在测试服复现,由单测与集成测试覆盖。
十、相关文档
- 产品域改期预检/联动走的是订单域内部接口(非前端可见,仅供理解链路):
POST /v3/internal/group-batch/schedule-sync/{productBatchId}/precheck、POST /v3/internal/group-batch/schedule-sync/{productBatchId}/apply、POST /v3/internal/group-batch/schedule-sync/{productBatchId}/replay(运维重推,无请求体,锁内读 product 实时日期后执行 apply,时间线操作人记为 SYSTEM)。三者均为/v3/internal/**,仅供 Feign 内部调用,不经网关,前端不可达。 100503为全仓统一的锁竞争可重试错误码,语义与既往 changelog「Lock4j 抢锁失败统一返 100503」一致。
关联 / 联系人
链接
联系人
- 后端负责人: @wx