20 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 | 7459 | 订单取消的房务处置改走 outbox 耐久命令:取消后房务待办与需求状态改为异步产生(约 1 秒内),流团逐户各产 REFUND | admin | wx(GIT) | 修改接口 | deployed | not_required | not_required | 2026-09-15 | PR #7731(Issue #7459)已 squash 合并 dev-v3(合并提交 4a63bba2b,2026-09-15 10:44),测试服 hl-order-service-v3 部署基线 816b1b522 即已包含该提交(更晚于它),并有真实网关+SQL 联测证据(工单 7459 评论 54437、54430,2026-09-15 实测),故 backend_status 记 deployed。gateway_status=not_required:本次未新增/修改任何路径,cancel/pre-trip 与 todos 均为已有路由。frontend_status=pending:待前端确认取消后的待办轮询/刷新逻辑是否已经假设了同步可见(若之前是取消接口返回即刷新一次列表,现在需要等约 1 秒或改为短轮询/延迟刷新)。 前端 not_required(grep 实证):order-v2 详情域零处查询 order/todos,取消成功后只刷新订单详情;房务待办页为房务角色独立页面,前端无「取消成功同 tick 断言待办已生成」的同步可见假设,无需延迟刷新/短轮询改动。 | 2026-09-15 | dev-v3 |
房务待办: 订单取消的房务处置改走异步 outbox 命令
存放目录:
- 一期(v2,无 order-v3 标签的工单)→ changelogs/{YYYY-MM}/
- 二期(v3,order-v3 标签的工单)→ changelogs-v2/{YYYY-MM}/
服务: hl-order-service-v3 (端口 8086) PR: #7731 Issue: #7459 日期: 2026-09-15 影响范围: 订单取消(出行前取消、退团/解散批量取消)之后,房务待办与需求 house_status 由该订单事务同步产生改为异步产生(约 1 秒内完成);流团批量取消时按户各自独立产生 REFUND 待办
关键变化
- 本次变了什么:POST /v3/admin/order/{id}/cancel/pre-trip 取消订单成功后,房务侧的处置(判定是否已配房、生成 REFUND 待办、需求 house_status 翻为 EXCEPTION)此前是在取消这次请求的同一个数据库事务里同步完成的;本次改为取消事务提交前把一条耐久命令写入 order_fleet_command_outbox(命令类型 HOUSE_ORDER_CANCELLED),由异步处理器读取并执行。命令的判定依据是取消那一刻冻结的快照(是否已配房、active 分房行 ID 列表),不是处理器执行时刻重新查询的最新状态。
- 前端调用方以前以为的是什么:取消接口(cancel/pre-trip)返回 200 之后,立即查询房务待办列表(GET /v3/admin/order/todos)或该订单的需求详情,就能看到 REFUND 待办与 house_status=EXCEPTION;退团/解散批量取消一批子订单时,房务侧的处置是随批量取消这次请求一起完成的。
- 实际现在是什么:取消接口返回 200 之后,房务待办与需求状态是异步产生的,实测约 1 秒内完成(不是立即、也不需要用户手动重试);如果前端在取消成功的同一个事件循环里立即查询待办列表,可能看不到刚产生的 REFUND 待办,需要等一小段时间或做一次延迟刷新/短轮询。退团/解散批量取消时,每个子订单各自独立产生一条 outbox 命令、各自异步处理,逐户产生 REFUND 待办,不是整团一条。
一、背景(选填)
PR 正文:订单取消的房务处置改走 order_fleet_command_outbox 耐久命令 HOUSE_ORDER_CANCELLED(取消事务 BEFORE_COMMIT 入队,去重键 HOUSE_ORDER_CANCELLED:{orderId}),处理器按取消时冻结的快照判定,替代原 AFTER_COMMIT 实时判定;流团路径在清房前冻结快照,已配房户逐户产生房务 REFUND 待办(对应工单 7459 AC-9)。
| 维度 | 证据 |
|---|---|
| 触发点 | OrderCancelledFleetListener(原监听 OrderCancelledEvent,改为落 outbox 命令而非直接处理) |
| 处理器 | OrderFleetCommandOutboxProcessor,命中 HOUSE_ORDER_CANCELLED 类型后分派给 HouseOrderCancelledCommandService |
| 去重键 | HOUSE_ORDER_CANCELLED:{orderId},同订单重复入队/重放不会产生重复 REFUND 待办 |
| 实测时延 | 工单 7459 评论 54437:两单取消请求发出到 outbox 落终态耗时均约 1 秒,create_time 与 update_time 同秒 |
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 取消订单(出行前) | POST | /v3/admin/order/{id}/cancel/pre-trip |
副作用时序变更 | 房务待办与需求状态处置改为异步(约 1 秒内),直接响应体字段结构未变 |
| 2 | 房务待办列表 | GET | /v3/admin/order/todos |
数据可见时点变化 | 取消触发的 REFUND 待办在约 1 秒后才可查到,响应字段结构未变 |
三、接口详情
1. 取消订单(出行前) POST /v3/admin/order/{id}/cancel/pre-trip
VO: OrderCancelPreTripReqVO → Result<OrderCancelPreTripRespVO>
使用场景
管理后台在订单出行前发起取消时调用,一次性完成状态流转、退款申请发起与(本次涉及的)房务处置触发。请求体与直接响应体字段结构本次均未改动;变化在于响应返回之后,房务侧的连带处置不再与本次请求同一事务同步完成,而是异步执行。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| id | Path | Long | 是 | - | 订单 ID,本次未改 |
| cancelReason | Body | String | 是 | 非空 | 取消原因,本次未改 |
| cancelDetail | Body | String | 否 | - | 详细说明,本次未改 |
| refundMode | Body | String | 否 | POLICY(默认)/FULL_DEPOSIT/PARTIAL | 退款模式,本次未改 |
| refundAmount | Body | BigDecimal | refundMode=PARTIAL 时必填 | 大于 0 且不超过已付金额 | 部分退金额,本次未改 |
出参字段表 Result<OrderCancelPreTripRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data.refundApplicationId | String | 退款申请 ID,恒返回 null(退款单由既有 OrderCancelledEvent 异步建单,与本次改动的房务处置是两条独立的异步链路),本次未改 |
| data.refundAmount | String | 退款金额,按政策计算,本次未改 |
| data.pendingApprovalMsg | String | 审批中提示文案,本次未改 |
| data.newStatus | String | 取消后订单状态,本次未改 |
(本次改动不在这个响应体里新增字段:房务待办与 house_status 的变化通过下方端点 2 或订单/需求详情另行查询)
请求示例
{ "cancelReason": "客户临时有事无法出行", "refundMode": "POLICY" }
响应示例
以下字段结构取自源码 OrderCancelPreTripRespVO 定义,不是测试服抓包报文(真实抓包见工单 7459 评论 54437 的 DB 观测,未附原始 HTTP 报文):
{
"code": 200,
"message": "成功",
"data": {
"refundApplicationId": null,
"refundAmount": "6864.00",
"pendingApprovalMsg": null,
"newStatus": "CANCELLED"
},
"success": true
}
空数据 / 降级响应
本端点不存在成功但空数据的形态,校验通过即返回上述字段。
错误响应
错误码本次未改,举例(订单状态不允许取消):
{ "code": 100001, "message": "订单当前状态不允许取消", "data": null, "success": false }
业务边界
- 本次改动不影响本端点自身的成功/失败判定与直接响应体:取消是否成功、退款金额计算、订单状态流转均与改动前逐字节相同。
- 房务侧处置(判定是否已配房、生成 REFUND 待办、需求 house_status 翻为 EXCEPTION)改为异步:调用方拿到 200 响应后,不能假设房务侧状态已经落库,需要另外查询(见端点 2)且预期约 1 秒的延迟。
- 幂等:命令去重键为 HOUSE_ORDER_CANCELLED:{orderId},同一订单的取消只会产生一条待处理命令;即使处理失败被重试或手动重放,也不会重复生成 REFUND 待办(工单 7459 评论 54437 的 internal replay 接口实测:已处理完的订单重放返回 data=0,即扫描到 0 条待重放)。
- 退团/解散批量取消同样复用这条取消链路:批量取消 N 个子订单会产生 N 条独立的 outbox 命令,各自异步处理,互不阻塞、互不合并(工单 7459 评论 54430:两户 A、B 同批解散取消,各自独立产生/不产生 REFUND 待办,取决于各自是否已配房)。
2. 房务待办列表 GET /v3/admin/order/todos
VO: HouseTodoPageReqVO → Result<HouseTodoListRespVO>
使用场景
房务在待办页面查看自己或同事的待办。本次改动后,订单取消触发的 REFUND 待办不会在取消接口返回的同一瞬间就查得到,需要约 1 秒的异步处理时间。入参字段与响应字段结构均未改动,完整契约见另一份 changelog(15_7327_团单取消终止REFUND待办owner回落团级认领人-修改接口-管理后台.md);本文件只描述可见时点的变化。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| scope | Query | String | 否 | mine(默认)/others/all | 本次未改 |
| todoType | Query | String | 否 | 逗号分隔多选 | 本次未改,可传 REFUND 过滤 |
| orderId | Query | Long | 否 | - | 本次未改,可按订单精确查询 |
(其余入参字段本次未改,见既有契约)
出参字段表 Result<HouseTodoListRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
| data.list[] | Array | 待办列表,字段结构本次未改 |
| data.list[].todoType | String | 本次涉及 REFUND,取值域未变 |
| data.list[].ownerUserId | String | 归属房务 ID,字段本身未变;取消触发的这条记录本次起延迟约 1 秒才出现 |
(完整字段表见 15_7327_团单取消终止REFUND待办owner回落团级认领人-修改接口-管理后台.md,本文件不重复列出全部字段)
请求示例
GET /v3/admin/order/todos?scope=mine&orderId=2099707703508054018&status=RESOLVED&pageNo=1&pageSize=10
响应示例
以下取自本次会话 2026-09-15 的测试服真实抓包(账号 1001,只读 GET,命中工单 7459 评论 54437 里的取证订单,该待办因退款联动已被自动 RESOLVE,故用 status=RESOLVED 查询到):
{
"id": "2099707745790779394",
"todoType": "REFUND",
"todoTypeLabel": "退订",
"title": "客人取消订单 · 请处理酒店退订",
"status": "RESOLVED",
"orderId": "2099707703508054018",
"orderNo": "HL20260915115141720",
"ownerUserId": "1001",
"ownerName": "admin",
"createTime": "2026-09-15 11:51:52"
}
(完整字段的响应示例见 15_7327_团单取消终止REFUND待办owner回落团级认领人-修改接口-管理后台.md;本文件只强调这条记录本身:取消请求发出时刻是 11:51:51,本记录 createTime 是 11:51:52,两者相差约 1 秒,与本次改动的异步时延一致)
空数据 / 降级响应
本次改动带来的一个新形态:取消接口刚返回 200 的极短窗口内(约 1 秒以内)查询该订单的待办,可能仍是空列表,不代表处置失败,稍后重试即可查到:
{ "code": 200, "data": { "list": [], "total": 0 }, "success": true }
错误响应
本端点错误码本次未改,举例(keyword 超长,既有校验):
{ "code": 100001, "message": "keyword 最长 32 字", "data": null, "success": false }
完整错误码表见 15_7327_团单取消终止REFUND待办owner回落团级认领人-修改接口-管理后台.md。
业务边界
- 本次不改变字段结构,只改变数据出现的时点:调用方若在取消成功后立即查询本端点校验房务处置是否生效,应预留约 1 秒的等待或改为短轮询(例如间隔 500ms 重试 2-3 次),不要把「刚取消完查不到」当成失败。
- 该延迟只影响本次取消动作新产生的记录,历史已存在的待办查询行为不受影响。
- 幂等重放不会导致本端点出现重复的 REFUND 记录(见端点 1 业务边界的去重键说明)。
四、契约约束与正确调用方式(接口类必写)
本节只写后端接受拒绝 payload 的规则与调用后必须知道的取值规则,不写 UI 渲染建议。
正确与错误调用方式对照
| 场景 | 说明 |
|---|---|
| 正确:取消成功后延迟约 1 秒或短轮询再查待办列表/需求详情 | 房务处置是异步的,立即查询可能查不到 |
| 正确:把「取消接口 200」与「房务处置已完成」视为两个独立事件 | 前者是同步事实,后者是异步结果,不能用前者代表后者 |
| 错误:取消返回 200 后立即断言待办列表包含新的 REFUND 记录 | 约 1 秒内可能仍是空,不代表处置失败 |
| 错误:重复调用取消或手动重放来「确保」待办生成 | 命令按订单去重,重放不会产生第二条待办,也不会加快处理速度 |
切换状态时的必要动作
无需前端主动触发任何补偿动作。若产品体验上需要「取消成功后立即看到房务待办已生成」的即时反馈,建议前端自行做一次短轮询(如 500ms 间隔、最多 2-3 次)而不是让用户手动刷新页面。
五、数据库行为(涉及写操作时必写)
| 阶段 | 表 | 行为 |
|---|---|---|
| 取消事务内(同步) | order_fleet_command_outbox | 新增一行 HOUSE_ORDER_CANCELLED 命令,携带取消时冻结的快照(是否已配房、active 分房行 ID),初始状态待处理 |
| 取消事务内(同步) | order_main | 订单状态流转,本次未改 |
| 异步处理阶段(约 1 秒后) | order_hotel_requirement | house_status 翻为 EXCEPTION(已配房场景),status 与 claimer_id 不变,不软删 |
| 异步处理阶段(约 1 秒后) | house_todo | 已配房场景新增 1 条 REFUND 待办,dedup_key 为 REFUND-{orderId};未配房场景不新增(本身就不满足产生条件,非回归) |
| 异步处理阶段 | order_fleet_command_outbox | 该行状态翻为 SUCCEEDED(实测均在 1 秒内) |
幂等:命令与生成的 REFUND 待办均按各自的去重键(HOUSE_ORDER_CANCELLED:{orderId}、REFUND-{orderId})保证不重复;重放已处理完的命令是安全的空操作。
六、边界行为
- 未登录取消接口 → 401(网关拦截,本次未改)
- 订单状态不允许取消 → 既有错误码(本次未改)
- 取消成功但房务处置尚未完成(约 1 秒窗口内)→ 待办列表/需求详情暂时看不到新记录,不是错误,稍后可查到
- outbox 命令处理失败 → 由既有重试机制处理(超出本文件描述范围,前端无需感知);internal 重放接口不经网关,仅供后端运维排障使用
六.5、枚举 / 数据字典(接口出现枚举时必写)
本次改动不引入新枚举值。涉及的 house_status 恒为既有值 EXCEPTION,todoType 恒为既有值 REFUND,取值域均未变,仅列出作为定位上下文:
todoType(GET /v3/admin/order/todos,既有字段)
所属字段: data.list[].todoType | 类型: String
| 值 | 中文 | 说明 |
|---|---|---|
REFUND |
退订 | 本次唯一涉及的类型,取值不变,只是产生时点改为异步 |
六.6、修改前后对比(修改/删除类接口必写,新增跳过)
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
| cancel/pre-trip 请求体/响应体字段结构 | 见既有契约 | 不变 |
| GET /v3/admin/order/todos 响应字段结构 | 见既有契约 | 不变 |
行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 取消订单后房务待办/house_status 生成时点 | 与取消请求同一事务,取消返回即已完成 | 异步,约 1 秒内完成 |
| 流团批量取消(退团/解散)时房务处置粒度 | 原实时判定路径(AFTER_COMMIT) | 逐户各自独立的 outbox 命令,逐户异步处理,逐户产生 REFUND |
| 命令处理失败后的恢复方式 | 依赖原事务重试语义 | 命令持久化在 outbox 表,可通过 internal 重放接口显式重放,失败不丢 |
| 重复触发是否产生重复待办 | 依原实现而定 | 按 orderId 维度去重键幂等,明确不重复 |
六.7、影响评估(修改/删除类必写)
- 是否破坏向后兼容:否。两个端点的请求/响应字段结构均未变化,错误码未变化。真实的行为差异是时序上的(同步变异步,约 1 秒)。
- 前端是否必须同步上线:不是强制的(不同步上线不会导致接口报错),但如果前端在取消成功的回调里同步查询待办列表并据此更新 UI(例如「已生成退订待办」提示),现在可能会在约 1 秒内查询落空,建议前端补一次延迟刷新或短轮询,否则用户体验上会出现「取消成功了,但待办列表看起来没反应」的观感。
- 前端 workaround 清理点:无(此前没有相关字段可供 workaround)。
七、不影响范围(显式声明, 帮前端/QA 缩小排查面)
- 仅影响:订单取消(出行前取消及复用同一链路的退团/解散批量取消)之后,房务待办与需求 house_status 的产生时点;流团批量取消时的处置粒度(逐户而非整团)。
- 零影响:
- cancel/pre-trip 端点自身的请求校验、退款金额计算、订单状态流转结果
- GET /v3/admin/order/todos 的请求参数、响应字段结构、错误码
- 未涉及取消的其余订单流转路径(终止行程 terminate 走的是既有 MQ 事件监听路径,本次未改)
- internal 重放接口
/v3/internal/jobs/fleet-command-outbox/replay本身不经网关(既有限制,本次未改,实测经网关调用返回业务码 403「接口不可访问」)
八、测试环境已验证
真实网关 + DB 联测(工单 7459 评论 54437、54430,2026-09-15,部署基线 816b1b522,deploy-status 起跑与全程核对无漂移):
S1(已配房散客单取消):POST /v3/admin/order/2099707703508054018/cancel/pre-trip 11:51:51 → 200
→ 取消后 11:51:52(约 1 秒):order_hotel_requirement.house_status 翻为 EXCEPTION
→ house_todo 新增 1 行 REFUND(owner_user_id=1001)
→ order_fleet_command_outbox 落 1 行 HOUSE_ORDER_CANCELLED,create_time=update_time=11:51:52,status=SUCCEEDED
S2(未配房散客单取消):POST /v3/admin/order/2099707743991476225/cancel/pre-trip 11:51:51 → 200
→ 取消后 11:51:52:order_hotel_requirement 软删;house_todo 仍 0 行(未配房场景本不产生该待办,非回归)
→ outbox 同样约 1 秒内 SUCCEEDED
internal 重放接口实测(S1/S2 均已 SUCCEEDED 后重放):
POST /v3/internal/jobs/fleet-command-outbox/replay?orderId=2099707703508054018 → 200,data=0(无待重放行,未重复生成)
POST /v3/internal/jobs/fleet-command-outbox/replay?orderId=2099707743991476225 → 200,data=0
流团批量解散取消(评论 54430,gb=2099703524806823938,户 A/B):
A(已配房,had active 分房行)→ house_status=EXCEPTION,house_todo 新增 1 条 REFUND(dedup_key 唯一)
B(未配房,未付款户)→ 无 REFUND 待办(未配房场景符合预期)
两户各自独立的 outbox 命令,各自 payload 记录取消时冻结的 assignedAtCancel/activeAllocationIds 快照
网关可达性(源码 + 实测双证):/v3/internal/** 不能经网关调用,经网关请求返回业务码 403「接口不可访问」;internal 重放只能直连服务 8086 端口
(真实抓包报文的原始 JSON 未在本轮会话中重新采集,端点 1/2 的响应示例基于源码字段定义与上述 DB/时间证据组装,已在正文标注)
十、相关文档
- 关联 Issue: wx/HL#7459
- 关联 PR: wx/HL#7731
- 房务待办列表端点完整契约: changelogs-v2/2026-09/15_7327_团单取消终止REFUND待办owner回落团级认领人-修改接口-管理后台.md
关联 / 联系人
链接
联系人
- 后端负责人: @wx