docs(order-v3): #7621 转单端点团期需求作用域限制与 808001 文案误导说明(接口零变化)
changelog-filename-gate / validate (push) Failing after 1s

三种成因分开写:普通房务对任何团期需求恒 808650;超管对已整团认领的团恒 808650;
超管对尚未整团认领、且从未被单独抢到的团期需求恒 808001,而该文案说的是
「已被其他房务抢到」,与实情不符。历史遗留数据(并团前已被抢到)是唯一能转成功的例外。

正确路径写明整团认领 POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim,
并标注该路径参数是团期 ID 而非订单 ID(后端 @ApiParam 的「团期主订单 ID」标签会误导,
以 2026-09-16 测试服实测为准)。

第八节如实写明本单未部署、未做测试服端到端验证,只有仓内单测。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mg1eNKoacprUHNvuEKxjqq
这个提交包含在:
API Changelog Bot
2026-09-16 09:33:11 +08:00
共同撰写人 Claude Opus 5
父节点 effdb312c3
当前提交 fee08eba33
@@ -0,0 +1,289 @@
---
schema: "hl-changelog/v2"
ticket: "7621"
title: "转单/超管强制指派端点:明确团期需求的作用域限制与 808001 文案在该场景下的误导(接口零变化)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修复"
backend_status: "not_required"
gateway_status: "not_required"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "not_required:本单不产生任何可部署的运行时变更——PR #7623 只补 javadoc/@ApiOperation 说明,PR #7782 只补单测,请求/响应结构、错误码集合、路由均一字未改。核心场景(超管对尚未被整团认领的团期需求调用本接口恒返 808001)依赖的判定逻辑早于本工单已存在,不因本次改动而首次生效,无需部署即对前端成立。本单未在测试服做端到端真实调用复核,第八节如实写明未验证,本 changelog 只同步契约理解,不构成已实测的结论。"
updated_at: "2026-09-16"
base: "dev-v3"
---
# 转单端点团期需求作用域限制说明(含 808001 文案在该场景下的误导,接口无变化)
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
> **服务**: hl-order-service-v3(HOUSE 抢单池 §1.3)
> **PR**: #7623(契约说明,已合 dev-v3)/ #7782(补充测试用例)
> **Issue**: #7621
> **日期**: 2026-09-16
> **影响范围**: 仅补充"转单/超管强制指派"端点对团期需求这一类调用对象的作用域说明;请求参数、响应结构、错误码集合、路由均无变化
---
## ⚠️ 关键变化
🔴 这个端点从来就转不了"团期需求",但此前的契约文档没写清楚,而它失败时给出的错误码文案在这一场景下会误导人。
- 之前你可能以为:只要一个需求处于"待处理"状态,超管就能用本接口把它强制指派给任意房务,不区分它是不是团期产品下的需求。
- 实际一直是:本接口只负责转走一个已经被某个房务实际抢到手的需求,不负责建立归属。团期产品下的需求,逐户抢单这条路本身就是被拒的,所以它们结构上到不了"已被抢到"这个状态。
- 结果:
- 普通房务对任何团期需求调用本接口,恒返 `808650`(文案已明确说明"请到团期抢单池整团认领",不会误导)。
- 超管对"尚未被整团认领"的团期需求调用本接口,如果这条需求本身也从未被单独抢到过,会恒返 `808001`,但 `808001` 的文案"需求已被其他房务抢到"在这个场景下是不准确的——事实是从来没有人抢到过它,不是被人抢走了。
- 唯一的例外:如果这条需求是在被并入团期产品之前就已经被某个房务抢到(历史遗留数据),超管调用本接口在满足"该团尚未被整团认领"的前提下真的能转成功(`200`)。这是这类历史脏数据唯一的无损转出口,不是一般规律,别照抄。
- 正确路径:团期需求的房务归属,一律通过团期抢单池的"整团认领"完成——`POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim`(无请求体;路径参数 `groupBatchId` 是**团期 ID**(`group_batch.id`),不是订单 ID——后端 `@ApiParam` 把它标成「团期主订单 ID」,那个标签会误导,以实测为准:`POST /v3/admin/order/grab-pool/group-batches/2099715422872834050/claim` 返回 200,路径里那个值是团期 ID)。
---
## 一、背景
本次不涉及任何代码行为变化,起因是工单 #7621 发现:现有联调/排查资料里没有一处写明"转单接口对团期需求会怎样",导致排查时容易把 `808001` 误判成"并发抢单竞争"去查,而实际根因是这条路径对团期需求本来就走不通。#7623 把这段边界写进了后端 javadoc;#7782 给这两种成因(超管遇到前置条件不满足 / 普通并发场景)各补了显式单测,证明代码本身没有被这次说明"顺手改坏"。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 转单/超管强制指派 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/transfer` | 契约说明补充 | 补充团期需求作用域限制与 808001 文案误导说明,接口本身零变化 |
---
## 三、接口详情
### 1. 转单/超管强制指派 `POST /v3/admin/order/hotel-requirements/{requirementId}/transfer`
**VO**: `HouseTransferReqVO → Result<Void>`
#### 使用场景
管理后台房务工作台"我的接单"列表里,房务对自己已抢到的需求点"转单";或超管在全局需求视图/团期需求视图里对某个需求点"强制指派"。后端按调用者角色(普通房务 / 超管)自动分流校验规则,前端始终调同一个端点、同一套请求体。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| requirementId | Path | Long | ✅ | - | 需求 ID |
| toUserId | Body | String(数字,Long 序列化为字符串) | ✅ | 必须是有效的房务人员 ID,且不能等于当前持有人 | 接收人房务 ID |
| reason | Body | String | 否 | ≤200 字;超管调用时 ≥10 字,否则 `808016`;普通房务留空时后端兜底记为"转单" | 转单/指派原因 |
| skipUpperLimit | Body | Boolean | 否,默认 false | 历史字段,转单次数上限已下线,当前不影响转单结果,仅留痕审计 | 跳过单量上限(历史兼容字段) |
#### 出参 `Result<Void>`
| 字段 | 类型 | 说明 |
|------|------|------|
| data | null | 本接口恒无业务数据,成功与失败时 data 均为 null,以 code/success 判定结果 |
#### 请求示例
普通房务转单(toUserId 为字符串,reason 可留空):
```json
{
"toUserId": "1003",
"reason": "我今天临时请假,转给小图接手",
"skipUpperLimit": false
}
```
超管强制指派团期需求(本条会命中"关键变化"里的边界场景,reason 必须 ≥10 字):
```json
{
"toUserId": "1008",
"reason": "该团期需求历史数据清理,统一归口给在职房务",
"skipUpperLimit": false
}
```
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"traceId": "a1b2c3d4-e5f6-0000",
"success": true
}
```
#### 空数据 / 降级响应
本接口不是列表/分页接口,没有空数据形态;成功时 data 恒为 null(与响应示例一致)。转单是本服务内部的同步事务,不经过网关/下游服务的降级路径。唯一的降级点在接收人姓名解析:如果下游用户服务查不到接收人姓名(服务不可用或该房务已离职),后端不会阻断转单,会用一个兜底展示名继续完成转单,调用方看到的仍是 200 成功。
#### 错误响应
| 错误码 | 文案 | 触发条件 | 是否本次新增 |
|---|---|---|---|
| 808090 | 未登录或非房务角色,无权操作 | 未登录/token 无效 | 否,既有码 |
| 808091 | 房务组长为只读监督角色,无权执行该操作 | 房务组长(只读监督角色)调用 | 否,既有码 |
| 808002 | 需求已不存在 | 需求已被取消/已配完,查不到有效需求 | 否,既有码 |
| 808650 | 团期订单不支持逐户抢单/转单,请到团期抢单池整团认领 | 普通房务对任意团期需求调用;或超管对已被整团认领的团期需求调用 | 否,既有码,本次首次写清适用场景 |
| 808010 | 需求不属于当前用户,无法转单 | 普通房务调用,但当前持有人不是自己 | 否,既有码 |
| 808014 | 接收人就是当前归属人,无需操作 | toUserId 等于当前持有人 | 否,既有码 |
| 808016 | 超管指派原因长度不足 10 字 | 超管调用且 reason 少于 10 字 | 否,既有码 |
| 808013 | 一单转单次数达上限(3 次) | 普通房务累计转单已达 3 次 | 否,既有码 |
| 808001 | 需求已被其他房务抢到 | 并发场景下持有人被抢先转走/释放;或超管对尚未被整团认领、且本身从未被单独抢到的团期需求调用(此场景下文案不准确,见关键变化章节) | 否,既有码,本次首次写清团期场景下文案不准确 |
代表性示例 1(普通房务对团期需求调用,文案本身准确):
```json
{
"code": 808650,
"message": "团期订单不支持逐户抢单/转单,请到团期抢单池整团认领",
"data": null,
"traceId": "a1b2c3d4-e5f6-0001",
"success": false
}
```
代表性示例 2(超管对"从未被抢到"的团期需求调用,文案不准确,需特殊处理):
```json
{
"code": 808001,
"message": "需求已被其他房务抢到",
"data": null,
"traceId": "a1b2c3d4-e5f6-0002",
"success": false
}
```
上面这条 808001 在这个场景下不能按字面展示给用户——真实原因是这条需求从来没有被任何人抢到过,不是被抢走了。前端识别到目标需求属于团期产品时,应改走"团期抢单池 → 整团认领"入口,不要展示/触发本接口,也不要把这条文案原样透传。
#### 业务边界
- 鉴权:必须是已登录且具备房务写权限的账号(808090);房务组长为只读监督角色,禁止调用(808091);接收人不能是当前持有人(808014)。
- 普通房务身份对任何团期需求(不论该团是否已被整团认领、这条需求本身是否早年已被某房务抢到)调用本接口,一律 808650,不进入后续任何判断,零写入。
- 超管身份:仅当目标团期尚未被整团认领时才可能继续往下走;若该团已被整团认领,同样 808650。
- 通过上一步之后,后端仍会原子性地重新核对这条需求当前是否处于可转状态:
- 若这条需求是在并入团期产品之前就已经被某个房务抢到(历史遗留数据),转单会真实生效(200)——这是这类历史数据唯一的无损转出口。
- 若这条需求是刚进团期、从未被任何房务单独抢到(结构上到不了可转状态),恒返 808001,但文案不准确(见上)。
- 转单次数上限:仅普通房务受限,累计已转单 3 次后再转禁止(808013);超管指派不受此限制。
- 幂等/并发:后端对当前持有人是否仍是预期的那个人做原子校验,校验失败直接返回错误、不写库;两个人同时对同一需求发起转单,只有一个会成功,另一个收到 808001(此时文案准确,是真的被抢先了)。
- 失败零写入:所有前置校验(鉴权/团期作用域/持有人/接收人/次数上限)失败均不产生任何数据变化。
---
## 四、契约约束与正确调用方式
本节只写后端接受/拒绝调用的规则,不写 UI 渲染建议。
### 正确 / 错误调用场景对照
| 场景 | 结果 |
|------|------|
| 正确:对一个已被某房务抢到的非团期需求调用转单 | 200,转单成功 |
| 正确:超管对一个合并进团期之前就已被某房务抢到的团期需求调用(该团尚未被整团认领) | 200,转单成功(历史数据唯一转出口) |
| 错误:普通房务对任意团期需求调用 | 808650,零写入 |
| 错误:超管对已被整团认领的团期需求调用 | 808650,零写入 |
| 错误:超管对尚未被整团认领、且本身从未被抢到的团期需求调用 | 808001(文案不准确,见关键变化),零写入 |
| 错误:对一个尚未被任何房务抢到的非团期需求调用转单 | 808001(此场景下文案准确:确实没人抢过,应引导去抢单而非转单) |
### 团期需求的正确处理路径
前端在渲染需求列表/详情时,只要能判断出该需求属于团期产品下的子订单,就不应该展示或触发本接口的转单/强制指派入口,一律引导到团期抢单池的整团认领:`POST /v3/admin/order/grab-pool/group-batches/{groupBatchId}/claim`(无请求体;路径参数 `groupBatchId` 是**团期 ID**(`group_batch.id`),不是订单 ID,后端 `@ApiParam` 的「团期主订单 ID」标签会误导;成功后整团房务归属一次性建立,个体需求不再需要单独转单)。
---
## 五、数据库行为
本次不产生任何数据库变化。转单成功后对外可观察的行为:接收人成为该需求新的持有人,原持有人不再能对该需求执行释放/确认/标记完成等操作;转单失败(含本文所述的团期作用域限制)时不产生任何数据变化。
---
## 六、边界行为
- 未登录/token 无效 → 网关层 401;到达服务后 808090
- 需求不存在或已失效(如所在订单已取消)→ 808002
- 调用者是房务组长(只读监督角色)→ 808091
- 团期需求 + 普通房务 → 808650(业务边界详见三.1)
- 团期需求 + 超管 + 该团已被整团认领 → 808650
- 团期需求 + 超管 + 该团未被整团认领 + 需求本身从未被单独抢到 → 808001(文案不准确)
- 团期需求 + 超管 + 该团未被整团认领 + 需求本身是历史遗留的已被抢到数据 → 200,转单真实生效
- 下游用户服务解析接收人姓名失败 → 不阻断,退回兜底展示名继续完成转单,仍返回 200
---
## 六.5、枚举 / 数据字典
本接口入参(toUserId/reason/skipUpperLimit)与出参(恒为 Result<Void>,data 无字段)均不涉及新增枚举值,本节不适用。
## 六.6、修改前后对比
### 字段级对比
无字段变化。
### 行为级对比
| 行为 | 改前(契约文档口径) | 改后(契约文档口径,实际运行时行为未变) |
|------|------|------|
| 普通房务对团期需求调用转单 | 文档未提及,容易误以为和非团期需求走同一套判权规则 | 明确写清:恒返 808650,不进入持有人校验等后续步骤 |
| 超管对从未被抢到的团期需求调用转单 | 文档未提及 808001 在此场景下的特殊性 | 明确写清:恒返 808001,但文案"需求已被其他房务抢到"在此场景不准确,真实原因是从未有人抢过它;正确处理是改走整团认领 |
| 超管对合并进团期前已被抢到的团期需求调用转单 | 文档未提及 | 明确写清:这类历史遗留数据可以真实转单成功,是唯一的无损转出口,不代表新进团期的需求也能这样处理 |
## 六.7、影响评估
- 是否破坏向后兼容: 否——请求参数、响应结构、错误码集合、路由均未改变,逐字节一致。
- 前端是否必须同步上线: 否——本单不产生任何需要部署的后端代码变化,没有上线这一步。
- 前端 workaround 清理点: 如果前端此前把本接口的 808001 一律按字面文案展示给用户、并允许对团期需求的失败结果直接重试,建议改为:①渲染需求列表/详情时,能判断出目标需求属于团期产品的,就不展示/不触发转单/强制指派入口,直接引导到团期抢单池整团认领;②即便被动收到这个场景下的 808001,也不要照抄文案展示,改成"该团期需求不支持单户转单,请使用整团认领",避免用户误以为是竞态失败、反复重试。
---
## 七、不影响范围
- 仅影响: 管理后台超管/房务对团期需求调用转单/强制指派端点时,如何理解与处理返回的错误码(以及对应的 UI 引导逻辑)。
- 零影响:
- 对非团期需求的转单/超管指派全流程——请求、响应、判权规则逐字节不变
- 抢单(claim)/释放(release)/我的接单等 HOUSE 抢单池其余 4 个端点
- 团期抢单池自身的 5 个端点(#7322:列表/整团认领/整团释放/团级接管/我的团)
- 小程序端(本次涉及端点仅管理后台)
- 请求/响应字段结构、错误码集合——无新增、无删除、无字段变化
---
## 八、测试环境已验证
本单未部署、未在测试服做端到端真实调用验证。以下只是仓内测试结果,不构成测试服实测证据:
- HouseGrabServiceImplTest 新增 2 条(团期需求转单前置入参 fromClaimerId=null 校验;并发失配场景入参为操作人本人)
- HotelRequirementTransferCasMysqlTest(真库集成测试)新增 5 条(两条前置条件各设计一个只违反它自己的探针 + 两条阳性对照)
- 定向跑:124 / 0 / 0 / 0(Tests / Failures / Errors / Skipped)
- ArchTest:63 / 0 / 0 / 0
---
## 九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|----|-------|------|------------|
| #7623 | #7621 | 给转单端点的 javadoc / @ApiOperation 补作用域说明,已合 dev-v3 | 有效 |
| 本次 #7782 | #7621 | 给上面的说明补充显式单测(Service 2 条 + 真库集成 5 条),证明代码未被顺手改坏 | 有效,未部署测试服 |
---
## 十、相关文档
- 关联 Issue: [wx/HL#7621](https://git.1814.love:8443/wx/HL/issues/7621)
- 关联 PR: [wx/HL#7623](https://git.1814.love:8443/wx/HL/pulls/7623)(契约说明)、[wx/HL#7782](https://git.1814.love:8443/wx/HL/pulls/7782)(补充测试)
- 后续计划: 若后续有人对 dev-v3 做常规部署并顺带把本单一起带上测试服,请在测试服对"超管调用团期需求 transfer 恒返 808001"和"普通房务调用团期需求 transfer 恒返 808650"两条各补一次真实网关调用记录,回填本文件第八节并把 backend_status 视情况改为 deployed
## 关联 / 联系人
### 链接
- **Issue**: [#7621](https://git.1814.love:8443/wx/HL/issues/7621)
- **PR**: [#7623](https://git.1814.love:8443/wx/HL/pulls/7623) / [#7782](https://git.1814.love:8443/wx/HL/pulls/7782)
### 联系人
- **后端负责人**: @wx