From 9df9dbb442c0c96f7064316f2edb8a15c40355b7 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Sun, 20 Sep 2026 14:43:16 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#8016=20=E5=9B=A2=E6=9C=9F?= =?UTF-8?q?=E5=80=99=E9=80=89=E9=85=92=E5=BA=97=20roomCategory=20=E5=85=A5?= =?UTF-8?q?=E5=8F=82=E7=9C=9F=E6=AD=A3=E5=8F=82=E4=B8=8E=E6=88=BF=E5=9E=8B?= =?UTF-8?q?=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工单 #8016 追加范围,PR #8022 合并提交 7b713305e,测试服 order-v3 已部署并网关取证。 契约零增删字段,仅行为变化:matchedRoomTypeId 现在只落在同 roomCategory 的房型上, matchedRoomTypeLabel 恒为房型真实中文名(不再回显入参 code),新增置灰文案「所选房型今日无房」, todayAvailable 口径不变(全房型合计)。 前端行动项(mmg):第二个房型下拉需按第一个下拉选定的大类过滤,数据早已在 roomTypes[].roomCategory(工单 #4204),用现有字段即可实现,无需等后端。 Co-Authored-By: Claude Opus 5 (1M context) --- ..._团期候选房型大类过滤-修改接口-管理后台.md | 352 ++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 changelogs-v2/2026-09/20_8016_团期候选房型大类过滤-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/20_8016_团期候选房型大类过滤-修改接口-管理后台.md b/changelogs-v2/2026-09/20_8016_团期候选房型大类过滤-修改接口-管理后台.md new file mode 100644 index 00000000..2a3718d3 --- /dev/null +++ b/changelogs-v2/2026-09/20_8016_团期候选房型大类过滤-修改接口-管理后台.md @@ -0,0 +1,352 @@ +--- +schema: "hl-changelog/v2" +ticket: "8016" +title: "团期候选酒店:roomCategory 入参从「仅展示标签」到「真正参与房型过滤」(1 个接口,契约零增删字段)" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-20" +status_note: "酒店候选查询 GET /v3/admin/hotel-candidates 契约零增删字段,只改行为:roomCategory 入参此前完全不参与房型过滤,只当展示标签回显(matchedRoomTypeLabel 甚至直接赋成入参 code 本身,掩盖错配);现在 roomCategory 非空白时 matchedRoomTypeId 只能落在同大类房型上(忽略大小写去空白),matchedRoomTypeLabel 恒为房型真实中文名,新增置灰文案「所选房型今日无房」(与既有三支互斥,仅在 availFreshness=fresh 且 todayAvailable>0 时出现)。todayAvailable 口径不变仍是全房型合计。roomCategory 不传/空白时行为逐字一致。PR #8022 squash 合并 dev-v3,合并提交 7b713305e;测试服 hl-order-service-v3 已部署该提交(deploy-status 核验 BEHIND 0,is-ancestor 退出码 0),gateway 未重新部署因未涉及路由改动。测试服网关实测订单 2100856430239121409,四组(不传/DELUXE/STANDARD/PARENT_CHILD)各 21 候选,回显缺陷计数 0,todayAvailable 逐 hotelId 比对 A vs B/C 均 0/21 不一致,D 组 15 个 todayAvailable>0 候选置灰原因逐一核对均为所选房型今日无房。🔴 前端行动项:第二个房型下拉需按第一个下拉选定大类过滤,数据早已在 roomTypes[].roomCategory 里(工单 #4204 加的),前端用现有字段即可实现,本次未新增任何字段,无需等后端。" +updated_at: "2026-09-20" +base: "dev-v3" +--- + +# order-v3: 酒店候选 roomCategory 入参从「仅展示标签」到「真正参与房型过滤」 + +> **存放目录**: `changelogs-v2/{YYYY-MM}/`(管理后台,二期 order-v3) +> +> **服务**: hl-order-service-v3 (端口 8086) +> **PR**: [#8022](https://git.1814.love:8443/wx/HL/pulls/8022) +> **Issue**: [#8016](https://git.1814.love:8443/wx/HL/issues/8016) +> **日期**: 2026-09-20 +> **影响范围**: 管理后台订单详情「选择酒店」弹窗的候选酒店匹配与快速配房 + +--- + +## ⚠️ 关键变化 + +1. **`roomCategory` 入参终于会过滤房型了**:改前它只是回显标签,不参与任何匹配;改后 `matchedRoomTypeId` 只会落在与 `roomCategory` 相同大类的房型上。 +2. **`matchedRoomTypeLabel` 改为房型真实中文名**:改前它被赋值为入参 `roomCategory` 本身(一个大写 code,如 `DELUXE`),会把「选中的其实不是所要大类」的错误掩盖掉;改后恒为真实房型名(如「豪华大床房」)。 +3. **新增置灰文案「所选房型今日无房」**:与既有「未核房,请先核房」「核房数据过期,请先核房」「今日无房」三支互斥,那三支一字未改。 +4. **契约零增删字段**:本次不新增、不删除任何请求/响应字段,`roomCategory` 不传或传空白时行为与改前逐字一致。 +5. **🔴 前端仍需自己动手**:第二个房型下拉需要按第一个下拉选定的大类过滤;后端提供的数据(`roomTypes[].roomCategory`)在工单 #4204 就已经给了,本次没有新增字段,前端可以直接用现有响应字段实现,无需后端再改接口。详见「九、前端需要做的事」。 + +--- + +## 一、背景 + +wx 2026-09-20 反馈:「选择酒店」弹窗里第一个下拉选了「豪华房」,第二个房型下拉仍然列出大床房、标间。经核查,问题分两部分: + +- **后端部分(本文档范围)**:`roomCategory` 入参此前完全不参与 matched 房型的选取,只是被当成展示标签回显,导致 `matchedRoomTypeId` 可能指向错误大类的房型、`quickPickEnabled` 对错房型开了「快速配房」绿灯,且 `matchedRoomTypeLabel` 回显的是入参 code 而非真实房型名,掩盖了这个错配。 +- **前端部分(不在本文档范围,需 mmg 自行处理)**:第二个房型下拉本身没有按第一个下拉的选择做过滤,详见「九、前端需要做的事」。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 酒店候选查询 | GET | `/v3/admin/hotel-candidates` | 修改 | `roomCategory` 入参真正参与 matched 房型过滤;`matchedRoomTypeLabel` 改为回显真实房型中文名;新增置灰文案「所选房型今日无房」;契约零增删字段 | + +--- + +## 三、接口详情 + +### 1. 酒店候选查询 `GET /v3/admin/hotel-candidates` + +**VO**:`HotelCandidateQueryReqVO → HotelCandidateRespVO` + +#### 使用场景 + +管理后台订单详情页「选择酒店」弹窗:定制师/房务在弹窗里选定「房型大类」(如豪华房、标间)后,弹窗按此大类过滤候选酒店,并展示每个候选是否可「快速配房」(即该酒店是否有该大类的够数房型)。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Query | Long | ✅ | - | 订单 ID | +| dayNumber | Query | Integer | - | ≥1 | 第几天(从 1 开始,用于推算 stayDate = departDate + dayNumber - 1) | +| stayDate | Query | LocalDate(`yyyy-MM-dd`) | - | - | 入住日期(直接指定优先于 dayNumber 推算) | +| city | Query | String | - | - | 城市代码 | +| keyword | Query | String | - | - | 关键词,非空时突破单城限定,跨城/省按关键词搜酒店 | +| limit | Query | Integer | - | 1~50,默认 30 | 返回候选条数上限 | +| roomCategory | Query | String | - | 字典 `room_category` 的 value(大写 code) | **本次生效**:非空白时 matched 只匹配同大类房型 | +| roomCount | Query | Integer | - | ≥1 | 需要的房间数,不传退化为 available>0 的旧行为 | +| preferredHotelId | Query | Long | - | - | 定制师指定的优先酒店 ID | +| requirementId | Query | Long | - | - | 用房需求 ID(传入后 days JSON 里所有 hotelId 作为定制师指定) | + +⚠️ `roomCategory` 在 Swagger 上的 `example` 字段写的是 `standard_double`,**那是过期垃圾**,既不是字典 code 也不是中文,不要照抄。真实取值见「六.5、枚举 / 数据字典」。 + +#### 出参字段表 + +`candidates[]` 元素(仅列本次涉及/相关字段,完整字段清单见源码 `HotelCandidateRespVO.Candidate`): + +| 字段 | 类型 | 说明 | +|------|------|------| +| matchedRoomTypeId | Long(字符串) | 匹配的房型 ID;**本次生效**:`roomCategory` 非空白时只能落在同大类房型上,为空则为 null | +| matchedRoomTypeAvailable | Integer | 匹配房型的今日可用数 | +| matchedRoomTypeLabel | String | **本次改变取值口径**:恒为房型真实中文名,不再回显入参 roomCategory | +| quickPickEnabled | Boolean | 是否支持快速配房 | +| quickPickDisabledReason | String | 置灰原因;**本次新增一支**「所选房型今日无房」 | +| todayAvailable | Integer | 今日可用房数(全房型合计,不按 roomCategory 过滤,口径不变) | +| roomTypes[].roomCategory | String | 该房型的真实大类 code(字典 room_category 的 value);早于本次已存在(工单 #4204) | + +#### 请求示例 + +```http +GET /v3/admin/hotel-candidates?orderId=2100856430239121409&stayDate=2026-10-08&dayNumber=1&roomCategory=DELUXE +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "stayDate": "2026-10-08", + "city": "hailar", + "productType": "GROUP", + "candidates": [ + { + "hotelId": "200001", + "hotelName": "海拉尔假日酒店", + "roomTypes": [ + { + "roomTypeId": "300001", + "name": "豪华大床房", + "roomCategory": "DELUXE", + "available": 7 + } + ], + "todayAvailable": 12, + "availFreshness": "fresh", + "matchedRoomTypeId": "300001", + "matchedRoomTypeAvailable": 7, + "matchedRoomTypeLabel": "豪华大床房", + "quickPickEnabled": true, + "quickPickDisabledReason": null + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +所选大类没有够数房型时(实测组 D:`roomCategory=PARENT_CHILD`,库内无此大类房型): + +```json +{ + "code": 200, + "message": "成功", + "data": { + "candidates": [ + { + "hotelId": "200002", + "todayAvailable": 15, + "availFreshness": "fresh", + "matchedRoomTypeId": null, + "matchedRoomTypeAvailable": null, + "matchedRoomTypeLabel": null, + "quickPickEnabled": false, + "quickPickDisabledReason": "所选房型今日无房" + } + ] + }, + "success": true +} +``` + +#### 错误响应 + +本接口无专属错误码;业务失败(下游降级、无候选等)一律 HTTP 200 返回空 `candidates` 或置灰字段,不抛错误码。`orderId` 缺失时走参数校验的全局异常处理: + +```json +{ + "code": 400, + "message": "orderId 不能为空", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- `roomCategory` 比较**忽略大小写、去首尾空白**(容忍存量脏数据);房型自身 `roomCategory` 为 `null`(resource 侧漏配分类)时该房型**不**参与 matched,宁可置灰让人工核房,也不拿分类未知的房型冒充所选大类。 +- `roomCategory` 不传或传空白字符串时,matched 选取逻辑与改前**逐字一致**(不限大类)。 +- `todayAvailable` 恒为**全房型合计**,不受 `roomCategory` 过滤影响;前端可用「`matchedRoomTypeId` 为空但 `todayAvailable>0`」判断「这家酒店有房、只是所选大类没有」,据此提示换大类而非换酒店。 +- 「所选房型今日无房」仅在 `availFreshness=fresh` 且 `todayAvailable>0` 时出现,与「未核房,请先核房」「核房数据过期,请先核房」「今日无房」三支互斥,不会误报未核房的候选。 +- CORE / CUSTOM 产品类型行为无变化:过滤逻辑对三种产品类型统一生效,但只有传了 `roomCategory` 时才会产生差异。 + +--- + +## 四、契约约束与正确调用方式 + +> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。 + +### ✅ 正确 / ❌ 错误取值对照 + +| 场景 | roomCategory 取值 | 结果 | +|------|---------|------| +| ✅ 过滤豪华房 | `DELUXE` | matched 只从大类=DELUXE 的房型中选 | +| ✅ 过滤标间 | `STANDARD` | matched 只从大类=STANDARD 的房型中选 | +| ✅ 不限大类 | 不传 / 空字符串 | 行为与改前一致,不过滤 | +| ❌ 传中文 | `豪华房` | 不会匹配到任何房型(房型侧存的是大写 code,不做中文翻译),退化为「所选房型今日无房」或「今日无房」 | +| ❌ 抄 Swagger example | `standard_double` | 同上,不会匹配到任何房型;该 example 已过期,不能照抄 | + +### 切换大类时的必要动作 + +前端切换第一个下拉(房型大类)后,需重新发起 `GET /v3/admin/hotel-candidates` 请求并带上新的 `roomCategory`;后端不做「记忆上次大类」之类的会话状态,每次请求都是独立判定。 + +--- + +## 五、数据库行为 + +本接口是纯查询接口,不写库。`roomCategory` 参与的是内存里对 resource 端批量返回的 `roomTypes` 做过滤/匹配,不落任何表。 + +--- + +## 六、边界行为 + +- 未登录 → 401(网关拦截) +- `orderId` 缺失/非法 → 400(参数校验) +- 下游 resource 服务降级 → 候选 `todayAvailable`/`matchedRoomTypeAvailable` 为 `null`,`quickPickEnabled=false`,不 500 不阻断弹窗 +- 老数据兼容 → 无影响,本次不改表结构,存量候选数据无关 + +--- + +## 六.5、枚举 / 数据字典 + +### roomCategory(字典 `room_category`) + +**所属字段**: `HotelCandidateQueryReqVO.roomCategory`(入参)/ `HotelCandidateRespVO.Candidate.RoomTypeOption.roomCategory`(出参) | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `STANDARD` | 标间 | | +| `SINGLE` | 单人间 | | +| `TWIN` | 双床房 | | +| `QUEEN` | 大床房 | | +| `DELUXE` | 豪华房 | | +| `KING` | 豪华大床 | | +| `SUITE` | 套房 | | +| `FAMILY` | 家庭房 | | +| `YURT` | 蒙古包 | | +| `SPECIAL` | 特色房 | | +| `PARENT_CHILD` | 亲子房 | | + +### quickPickDisabledReason(文案枚举,非字典表) + +**所属字段**: `HotelCandidateRespVO.Candidate.quickPickDisabledReason` | **类型**: `String` + +| 值 | 触发条件 | +|----|------| +| `未核房,请先核房` | `matchedRoomTypeAvailable == null` 或 `availFreshness=never_checked` | +| `核房数据过期,请先核房` | `availFreshness=stale` | +| `今日无房` | 已核房但全房型都无余量 | +| `所选房型今日无房`(**本次新增**) | `roomCategory` 非空白 + `matchedRoomTypeAvailable==null` + `availFreshness=fresh` + `todayAvailable>0`,即整家酒店有房、只是所选大类没有 | + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| `matchedRoomTypeId` | 可能落在与 `roomCategory` 不同大类的房型上 | 只能落在与 `roomCategory` 相同大类的房型上(忽略大小写、去空白比较) | +| `matchedRoomTypeAvailable` | 同上(可能对应错房型) | 同上(改为对应正确大类的房型,或为 `null`) | +| `matchedRoomTypeLabel` | 恒为入参 `roomCategory` 本身(如 `DELUXE`) | 恒为房型真实中文名(如「豪华大床房」) | +| `quickPickEnabled` | 可能对错大类房型开放「快速配房」 | 只对匹配大类的够数房型开放 | +| `quickPickDisabledReason` | 三支枚举 | 四支枚举,新增「所选房型今日无房」 | +| `todayAvailable` | 全房型合计 | 不变,仍是全房型合计 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 传 `roomCategory=DELUXE` 但该酒店只有标间够数 | 仍可能 matched 到标间,`quickPickEnabled=true`,label 显示 `DELUXE` | matched 为 `null`,`quickPickEnabled=false`,置灰原因「所选房型今日无房」 | +| 不传 `roomCategory` | 不限大类匹配 | 不限大类匹配(逐字一致) | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 是(局部)——只影响「传了 `roomCategory` 且该酒店没有对应大类够数房型」这一种此前被错误 matched 的场景;不传 `roomCategory` 的调用方零影响。 +- **前端是否必须同步上线**: 否(后端这次修的是数据准确性,不改字段,前端不改代码也不会报错)/ 是(前端另需改造第二个房型下拉的过滤逻辑,见「九、前端需要做的事」,但那是独立于本次后端契约的前端本地问题)。 +- **前端 workaround 清理点**: 若前端此前对 `matchedRoomTypeLabel` 里出现英文大写 code(而非中文)做过特殊兼容/正则清洗,现在可以撤掉——该字段恒为真实中文名。 + +--- + +## 七、不影响范围 + +- **仅影响**: 传了非空白 `roomCategory` 参数的候选查询请求 +- **零影响**: + - `roomCategory` 不传或传空白的调用(行为逐字一致) + - `todayAvailable` 的取值口径(仍是全房型合计) + - 请求/响应字段的增删(本次零增删) + - 错误码(本接口本无专属错误码,本次未新增) + - 「未核房,请先核房」「核房数据过期,请先核房」「今日无房」三支既有置灰文案的触发条件 + +--- + +## 八、测试环境已验证 + +测试服网关实测,订单 `2100856430239121409`(团期),`stayDate=2026-10-08`,`dayNumber=1`,每组 21 个候选,四组响应 body `code` 均为 200: + +| 组 | 入参 | 结果 | +|---|---|---| +| A | 不带 `roomCategory`(基线) | 15 个候选有 matched | +| B | `roomCategory=DELUXE` | 2 个候选有 matched,两者 matched 房型在自己 `roomTypes[]` 中的 `roomCategory` 均为 `DELUXE`,label 为「豪华大床房」 | +| C | `roomCategory=STANDARD` | 12 个候选有 matched,全部 `roomCategory=STANDARD`,label 为中文房型名(普通标间/标准间/商务标间/精品标间等) | +| D | `roomCategory=PARENT_CHILD`(库内无此大类房型) | 0 个候选有 matched;15 个 `todayAvailable>0` 的候选 `quickPickDisabledReason` 逐一核对均恰为「所选房型今日无房」 | + +三条对照: + +- `matchedRoomTypeLabel == 入参 code` 的计数在 B、C 两组均为 **0**(回显缺陷确认已修); +- `todayAvailable` 逐 hotelId 比对 A vs B、A vs C 均 **0/21 不一致**(大类过滤未影响全房型合计); +- 阳性对照成立(B、C 两组 matched 非 null 的候选数分别为 2 和 12,**非全 null**,所以「无错误大类混入」不是空断言)。**如实说明**:B 组样本量偏薄(2/21),C 组(12/21)是更强的主证据。 + +部署核实:`deploy-status` 报 `hl-order-service-v3` 跑 `dev-v3 @ 7b713305e`,`BEHIND 0/N`,`git merge-base --is-ancestor` 退出码 0。`hl-gateway` 未重新部署——本改动不涉及 gateway 路由。 + +--- + +## 九、前端需要做的事(🔴 mmg 行动项) + +wx 2026-09-20 反馈:「选择酒店」弹窗里第一个下拉选了「豪华房」,第二个房型下拉仍然列出大床房、标间。 + +**这一条后端改不了,需要前端改**:第二个房型下拉应当按第一个下拉选定的大类过滤。 + +所需数据后端**早已提供且本次未变**——响应里每个候选的 `roomTypes[]` 逐项都带 `roomCategory`(真实大类 code,工单 #4204 加的,原意就是「供前端从候选真实房型生成房型下拉」)。**前端用现有数据即可完成过滤,无需后端再改接口、也无需新增任何字段。** + +前端还应同步的两点: + +- 若界面上有回显「匹配房型」,注意 `matchedRoomTypeLabel` 现在是**真实房型中文名**而非大类名,展示文案可能需要微调; +- 新增置灰文案「所选房型今日无房」需要正常展示(它与「今日无房」语义不同:前者是**换个大类可能有房**)。 + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#8016](https://git.1814.love:8443/wx/HL/issues/8016) +- 关联 PR: [wx/HL#8022](https://git.1814.love:8443/wx/HL/pulls/8022) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#8016](https://git.1814.love:8443/wx/HL/issues/8016) +- **PR**: [#8022](https://git.1814.love:8443/wx/HL/pulls/8022) +- **Merge commit**: [7b713305e](https://git.1814.love:8443/wx/HL/commit/7b713305e) + +### 联系人 + +- **后端负责人**: @wx