--- schema: "hl-changelog/v2" ticket: "7459" title: "订单取消的房务处置改走 outbox 耐久命令:取消后房务待办与需求状态改为异步产生(约 1 秒内),流团逐户各产 REFUND" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "not_required" frontend_status: "not_required" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "2026-09-15" status_note: "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 断言待办已生成」的同步可见假设,无需延迟刷新/短轮询改动。" updated_at: "2026-09-15" base: "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` #### 使用场景 管理后台在订单出行前发起取消时调用,一次性完成状态流转、退款申请发起与(本次涉及的)房务处置触发。请求体与直接响应体字段结构本次均未改动;变化在于响应返回之后,房务侧的连带处置不再与本次请求同一事务同步完成,而是异步执行。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | id | Path | Long | 是 | - | 订单 ID,本次未改 | | cancelReason | Body | String | 是 | 非空 | 取消原因,本次未改 | | cancelDetail | Body | String | 否 | - | 详细说明,本次未改 | | refundMode | Body | String | 否 | POLICY(默认)/FULL_DEPOSIT/PARTIAL | 退款模式,本次未改 | | refundAmount | Body | BigDecimal | refundMode=PARTIAL 时必填 | 大于 0 且不超过已付金额 | 部分退金额,本次未改 | #### 出参字段表 `Result` | 字段 | 类型 | 说明 | |------|------|------| | data.refundApplicationId | String | 退款申请 ID,恒返回 null(退款单由既有 OrderCancelledEvent 异步建单,与本次改动的房务处置是两条独立的异步链路),本次未改 | | data.refundAmount | String | 退款金额,按政策计算,本次未改 | | data.pendingApprovalMsg | String | 审批中提示文案,本次未改 | | data.newStatus | String | 取消后订单状态,本次未改 | (本次改动不在这个响应体里新增字段:房务待办与 house_status 的变化通过下方端点 2 或订单/需求详情另行查询) #### 请求示例 ```json { "cancelReason": "客户临时有事无法出行", "refundMode": "POLICY" } ``` #### 响应示例 以下字段结构取自源码 OrderCancelPreTripRespVO 定义,不是测试服抓包报文(真实抓包见工单 7459 评论 54437 的 DB 观测,未附原始 HTTP 报文): ```json { "code": 200, "message": "成功", "data": { "refundApplicationId": null, "refundAmount": "6864.00", "pendingApprovalMsg": null, "newStatus": "CANCELLED" }, "success": true } ``` #### 空数据 / 降级响应 本端点不存在成功但空数据的形态,校验通过即返回上述字段。 #### 错误响应 错误码本次未改,举例(订单状态不允许取消): ```json { "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` #### 使用场景 房务在待办页面查看自己或同事的待办。本次改动后,订单取消触发的 REFUND 待办不会在取消接口返回的同一瞬间就查得到,需要约 1 秒的异步处理时间。入参字段与响应字段结构均未改动,完整契约见另一份 changelog(15_7327_团单取消终止REFUND待办owner回落团级认领人-修改接口-管理后台.md);本文件只描述可见时点的变化。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | scope | Query | String | 否 | mine(默认)/others/all | 本次未改 | | todoType | Query | String | 否 | 逗号分隔多选 | 本次未改,可传 REFUND 过滤 | | orderId | Query | Long | 否 | - | 本次未改,可按订单精确查询 | (其余入参字段本次未改,见既有契约) #### 出参字段表 `Result` | 字段 | 类型 | 说明 | |------|------|------| | data.list[] | Array | 待办列表,字段结构本次未改 | | data.list[].todoType | String | 本次涉及 REFUND,取值域未变 | | data.list[].ownerUserId | String | 归属房务 ID,字段本身未变;取消触发的这条记录本次起延迟约 1 秒才出现 | (完整字段表见 15_7327_团单取消终止REFUND待办owner回落团级认领人-修改接口-管理后台.md,本文件不重复列出全部字段) #### 请求示例 ```http 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 查询到): ```json { "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 秒以内)查询该订单的待办,可能仍是空列表,不代表处置失败,稍后重试即可查到: ```json { "code": 200, "data": { "list": [], "total": 0 }, "success": true } ``` #### 错误响应 本端点错误码本次未改,举例(keyword 超长,既有校验): ```json { "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 ## 关联 / 联系人 ### 链接 - Issue: [#7459](https://git.1814.love:8443/wx/HL/issues/7459) - PR: [#7731](https://git.1814.love:8443/wx/HL/pulls/7731) - Merge commit: [4a63bba2b](https://git.1814.love:8443/wx/HL/commit/4a63bba2b9e90e624bee0eb8c7e2992377ac23a8) ### 联系人 - 后端负责人: @wx