--- schema: "hl-changelog/v2" ticket: "7509" title: "酒店按景区口径接入供应商关系" consumer: "admin" author: "lc(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "64573356" target_release: "" verified_at: "2026-09-11" status_note: "后端已部署并经 Gateway 验证;待前端展示酒店列表供应商全称并接入酒店供应商关系操作。前端已交付(hl-admin 64573356):HOTEL 加入供应商关系免变更原因集合(设置/改绑/解绑),395062 订单门禁走兜底 message 透后端原文。" updated_at: "2026-09-11" base: "dev-v3" --- # 酒店管理:按景区口径接入供应商关系 > **服务**: hl-resource-service(8082) > **PR**: #7552 > **Issue**: #7509 > **日期**: 2026-09-11 > **影响范围**: 管理端酒店列表、酒店供应商查询/设置/改绑/解绑 ## ⚠️ 关键变化 - 酒店列表每条记录固定返回 `supplierFullName`;无当前供应商时为 `null`。 - 酒店使用通用资源供应商关系接口,路径参数固定传 `resourceModule=HOTEL`;供应商类型固定为 `HOTEL`,`requiredTypeCode` 可省略。 - 酒店首次绑定、改绑和解绑都可以省略 `changeReason`;此前设置、改绑缺原因返回 `400 changeReason不能为空`。 - 真正改绑或解绑前会检查订单:酒店被未结束订单排房、住宿需求点名或团期分房时返回 `395062`;产品里仅列为备选的酒店不拦截。 ## 一、背景 酒店管理此前列表不含供应商全称,关系操作要求填写原因,改绑、解绑不检查订单。本次按景区、餐厅、游玩项目现行口径冻结酒店所需的展示字段、调用参数、原因规则和订单状态边界。 ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|---|---|---|---|---| | 1 | 酒店列表 | GET | `/admin/hotel/items` | 响应字段新增 | 每条记录固定返回 `supplierFullName` | | 2 | 查询酒店当前供应商 | GET | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/view` | 调用契约补充 | 酒店传 `resourceModule=HOTEL` | | 3 | 设置或改绑酒店供应商 | PUT | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/update` | 请求与行为修改 | 可省略原因;真正改绑增加未结束订单门禁 | | 4 | 解绑酒店供应商 | POST | `/admin/supplier/resource-relations/{resourceModule}/{resourceId}/unbind` | 行为修改 | 可省略原因;增加未结束订单门禁 | ## 三、接口详情 ### 1. 酒店列表 `GET /admin/hotel/items` **VO**: `HotelQueryRequest / PageResult` #### 使用场景 酒店管理列表初始化、翻页、筛选或刷新时调用;列表“供应商”列直接读取 `supplierFullName`。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `keyword` | Query | String | 否 | 最长 100 字 | 名称/地址模糊搜索 | | `cityKeyword` | Query | String | 否 | 最长 100 字 | 城市名、省份、地址模糊搜索 | | `hotelType` | Query | String | 否 | 字典 `hotel_type` | 住宿类型 | | `city` | Query | String | 否 | - | 城市 | | `district` | Query | String | 否 | 最长 32 字 | 区/县精确匹配 | | `starLevel` | Query | String | 否 | 字典 `hotel_star_level` | 星级 | | `status` | Query | Integer | 否 | `0` 下架,`1` 上架 | 状态筛选 | | `tagId` | Query | Long | 否 | - | 单个标签 | | `tagIds` | Query | String | 否 | 逗号分隔 | 多个标签 | | `sortBy` | Query | String | 否 | 最长 50 字 | 排序字段 | | `sortDir` | Query | String | 否 | `asc` 或 `desc` | 排序方向 | | `page` | Query | Integer | 否 | 最小 1,默认 1 | 页码 | | `pageSize` | Query | Integer | 否 | 1~100,默认 20 | 每页条数 | 以上入参均为既有参数,本次不变。 #### 出参 `Result>` | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 业务结果 | | `data.total` / `page` / `pageSize` | Long / Integer / Integer | 分页信息 | | `data.records[].hotelId` | String | 酒店 ID | | `data.records[].name` | String | 酒店名称 | | `data.records[].supplierFullName` | String 或 null | 当前有效供应商的法定全称;未关联或供应商已删除时为 `null`,字段始终存在 | | `data.records[]` 其他字段 | Object | 原酒店列表字段保持不变 | #### 请求示例 无请求体: ```http GET /admin/hotel/items?page=1&pageSize=20 HTTP/1.1 Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "total": 2, "page": 1, "pageSize": 20, "records": [ {"hotelId": "2029000000000000101", "name": "示例酒店 A", "status": 1, "supplierFullName": "示例酒店供应商有限公司"}, {"hotelId": "2029000000000000102", "name": "示例酒店 B", "status": 1, "supplierFullName": null} ] } } ``` #### 空数据 / 降级响应 ```json {"code":200,"message":"成功","success":true,"data":{"total":0,"page":1,"pageSize":20,"records":[]}} ``` #### 错误响应 ```json {"code":401,"message":"缺少有效的 Authorization 头","success":false,"data":null} ``` #### 业务边界 - 本次不新增筛选、排序或分页规则;`supplierFullName` 只用于展示,不是供应商名称快照。 - 无当前有效关系时必须按 `null` 处理,不要根据字段是否存在分支。小程序酒店列表不返回该字段。 ### 2. 查询酒店当前供应商 `GET /admin/supplier/resource-relations/{resourceModule}/{resourceId}/view` **VO**: `SupplierResourceRelationRespVO` #### 使用场景 打开酒店供应商弹窗或在写操作后刷新当前关系时调用。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `resourceModule` | Path | String | 是 | 酒店固定为 `HOTEL` | 资源模块 | | `resourceId` | Path | String | 是 | 正整数 | 酒店 ID | #### 出参 `Result` | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 业务结果 | | `data.relationId` | String | 当前关系 ID | | `data.supplierId` / `supplierNo` / `supplierName` | String | 当前供应商 ID、编号和全称 | | `data.resourceModule` / `moduleName` | String | `HOTEL` / 酒店管理 | | `data.resourceId` / `resourceName` | String | 酒店 ID 和名称 | | `data.requiredTypeCode` / `requiredTypeName` | String | `HOTEL` / 酒店管理 | | `data.remark` | String 或 null | 关系备注 | | `data.available` / `unavailableReasons` | Boolean / Array | 当前关系是否仍可用及不可用原因 | | `data.createTime` / `updateTime` | String | `yyyy-MM-dd HH:mm:ss`;写操作使用最新 `updateTime` | #### 请求示例 无请求体: ```http GET /admin/supplier/resource-relations/HOTEL/2029000000000000101/view HTTP/1.1 Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "relationId": "2098200000000000101", "supplierId": "2097899397319684099", "supplierNo": "SUP260001", "supplierName": "示例酒店供应商有限公司", "resourceModule": "HOTEL", "moduleName": "酒店管理", "resourceId": "2029000000000000101", "resourceName": "示例酒店 A", "requiredTypeCode": "HOTEL", "requiredTypeName": "酒店管理", "remark": null, "available": true, "unavailableReasons": [], "createTime": "2026-09-11 20:00:00", "updateTime": "2026-09-11 20:00:00" } } ``` #### 空数据 / 降级响应 当前没有有效关系时不是空成功,返回 `395038`。 #### 错误响应 ```json {"code":395038,"message":"供应商资源关联不存在","success":false,"data":null} ``` #### 业务边界 - 使用现有登录凭证及 `supplier:resource:view` 服务端权限;前端按钮可见性不能替代后端判权。 - 未关联时按 `395038` 展示“未设置供应商”;不要把它当成系统异常。 ### 3. 设置或改绑酒店供应商 `PUT /admin/supplier/resource-relations/{resourceModule}/{resourceId}/update` **VO**: `SupplierResourceReassignReqVO / SupplierResourceRelationRespVO` #### 使用场景 酒店当前无关系时首次设置供应商,或选择另一供应商后改绑。候选供应商仍用 `GET /admin/supplier/items/list?resourceModule=HOTEL&resourceId={id}` 获取(本次不变)。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `resourceModule` | Path | String | 是 | 酒店固定为 `HOTEL` | 资源模块 | | `resourceId` | Path | String | 是 | 正整数 | 酒店 ID | | `supplierId` | Body | String | 是 | 正整数 | 目标供应商 ID | | `requiredTypeCode` | Body | String | 否 | 可省略;传值只能为 `HOTEL` | 关系要求的供应商类型,由后端固定 | | `remark` | Body | String | 否 | 最长 500 字 | 关系备注 | | `expectedCurrentSupplierId` | Body | String | 改绑时是 | 与版本时间同时传或同时省略 | 当前关系的 `supplierId` | | `expectedRelationUpdateTime` | Body | String | 改绑时是 | `yyyy-MM-dd HH:mm:ss` | 当前关系的 `updateTime` | | `changeReason` | Body | String | 否 | 最长 500 字 | 首次设置和改绑均可省略;不要补默认原因 | #### 出参 `Result` | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 业务结果 | | `data.relationId` / `supplierId` / `supplierNo` / `supplierName` | String | 生效关系及供应商信息 | | `data.resourceModule` / `moduleName` / `resourceId` / `resourceName` | String | 酒店模块和资源信息 | | `data.requiredTypeCode` / `requiredTypeName` | String | 固定 `HOTEL` / 酒店管理 | | `data.remark` | String 或 null | 关系备注 | | `data.available` / `unavailableReasons` | Boolean / Array | 当前可用性 | | `data.createTime` / `updateTime` | String | 当前关系时间与后续操作版本 | #### 请求示例 首次绑定时不要传两个 `expected*` 字段,也无需传 `requiredTypeCode`、`changeReason`: ```json {"supplierId":"2097899397319684099"} ``` 改绑时两个版本字段必须来自最新关系回读: ```json {"supplierId":"2097849575707475970","expectedCurrentSupplierId":"2097899397319684099","expectedRelationUpdateTime":"2026-09-11 20:00:00"} ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "relationId": "2098200000000000102", "supplierId": "2097849575707475970", "supplierNo": "SUP260002", "supplierName": "示例新酒店供应商有限公司", "resourceModule": "HOTEL", "moduleName": "酒店管理", "resourceId": "2029000000000000101", "resourceName": "示例酒店 A", "requiredTypeCode": "HOTEL", "requiredTypeName": "酒店管理", "remark": null, "available": true, "unavailableReasons": [], "createTime": "2026-09-11 20:01:00", "updateTime": "2026-09-11 20:01:00" } } ``` #### 空数据 / 降级响应 无空成功结果;订单服务、权限或字典等必要依赖无法明确核验时返回失败且不改变关系。 #### 错误响应 ```json {"code":395062,"message":"酒店已关联订单,不能更换供应商","success":false,"data":null} ``` ```json {"code":400,"message":"requiredTypeCode与资源模块不匹配","success":false,"data":null} ``` #### 业务边界 - 当前无关系时是首次绑定:不传版本对;即使酒店已有订单也允许绑定。 - 当前有关系且目标供应商不同才是真正改绑:必须传完整、最新版本对;酒店被未结束订单排房、住宿需求点名或团期分房时返回 `395062`,原关系不变;只关联已完成、已取消订单(包括两者混合)或只在产品里列为备选时允许。 - 版本过期返回 `395014`;必要依赖无法明确判断返回 `395039`;失败不改变关系或审计。 ### 4. 解绑酒店供应商 `POST /admin/supplier/resource-relations/{resourceModule}/{resourceId}/unbind` **VO**: `SupplierResourceUnbindReqVO / Result` #### 使用场景 用户确认清除酒店当前供应商关系时调用。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | `resourceModule` | Path | String | 是 | 酒店固定为 `HOTEL` | 资源模块 | | `resourceId` | Path | String | 是 | 正整数 | 酒店 ID | | `expectedCurrentSupplierId` | Body | String | 是 | 正整数 | 当前关系的 `supplierId` | | `expectedRelationUpdateTime` | Body | String | 是 | `yyyy-MM-dd HH:mm:ss` | 当前关系的 `updateTime` | | `changeReason` | Body | String | 否 | 最长 500 字 | 可省略、`null`、空串或空白;不要补默认原因 | #### 出参 `Result` | 字段 | 类型 | 说明 | |---|---|---| | `code` / `message` / `success` | Integer / String / Boolean | 业务结果 | | `data` | null | 成功时固定为 `null` | #### 请求示例 ```json {"expectedCurrentSupplierId":"2097849575707475970","expectedRelationUpdateTime":"2026-09-11 20:01:00"} ``` #### 响应示例 ```json {"code":200,"message":"成功","success":true,"data":null} ``` #### 空数据 / 降级响应 当前无关系时返回 `395038`;必要依赖无法明确核验时返回 `395039`,均不执行解绑。 #### 错误响应 ```json {"code":395062,"message":"酒店已关联订单,不能更换供应商","success":false,"data":null} ``` #### 业务边界 - 解绑必须使用当前关系最新版本对;版本过期返回 `395014`,原关系不变。 - 酒店被未结束订单排房、住宿需求点名或团期分房时返回 `395062`;只关联已完成、已取消订单(包括两者混合)时允许解绑。 - 登录、`supplier:resource:manage` 服务端权限、数据范围、幂等、锁和审计规则保持不变;失败零写入。 ## 四、契约约束与正确调用方式 | 场景 | 正确 payload | 调用结果 | |---|---|---| | 查询当前关系 | 无请求体,`resourceModule=HOTEL` | 返回关系;无关系为 `395038` | | 首次绑定 | `{"supplierId":"22"}` | 不传版本对、类型和原因 | | 改绑 | `{"supplierId":"33","expectedCurrentSupplierId":"22","expectedRelationUpdateTime":"2026-09-11 20:00:00"}` | 两个版本字段必须成对且取最新关系值 | | 解绑 | `{"expectedCurrentSupplierId":"33","expectedRelationUpdateTime":"2026-09-11 20:01:00"}` | 可省略 `changeReason` | | 错误:传其他类型 | `{"supplierId":"33","requiredTypeCode":"SCENIC"}` | 返回 `400`,不写入 | | 错误:版本字段只传一个 | `{"supplierId":"33","expectedCurrentSupplierId":"22"}` | 返回 `400`,不写入 | 每次改绑或解绑前先回读当前关系,成功后重新刷新关系和酒店列表。业务失败可能仍为 HTTP 200,必须同时判断响应体 `code` 与 `success`。 ## 五、数据库行为 | 前端操作 | 外部可观察结果 | |---|---| | 首次绑定 | 生成一个当前有效关系,`requiredTypeCode=HOTEL`;未填原因时不虚构默认原因 | | 改绑 | 旧关系转为历史,新供应商成为唯一当前关系,并保留正常变更审计 | | 解绑 | 当前关系消失并保留正常解绑审计;酒店列表返回 `supplierFullName: null` | | 订单、版本、权限、类型或资格校验失败 | 当前关系和审计均不变化 | 本次不迁移或回填任何数据,不修改订单业务、订单数据或订单供应商快照。 ## 六、边界行为 - 未登录返回 `401`;关系查询要求查看权限,设置、改绑、解绑要求维护权限及对应数据范围。 - 酒店未绑定供应商时,列表返回 `supplierFullName: null`,关系查询返回 `395038`。 - `changeReason` 最长 500 字;省略、`null`、空串或纯空白均按未填写处理。 - 未结束订单包含除 `COMPLETED`、`CANCELLED` 以外的状态以及未知状态;新旧订单任一确认存在即返回 `395062`。 - 订单关联只认房控已排房、订单当前住宿需求点名、团期分房;产品里仅列为备选的酒店不计入。 - 无法从任一订单服务得到明确结果时返回 `395039`,不把依赖异常当成无订单。 ## 六.5、枚举 / 数据字典 ### `resourceModule` / `requiredTypeCode`(酒店供应商关系) **所属字段**: Path `resourceModule`、Body/Response `requiredTypeCode` | **类型**: `String` | 值 | 中文 | 说明 | |---|---|---| | `HOTEL` | 酒店管理 | 酒店关系固定值;`requiredTypeCode` 可省略并由后端确定 | ### 业务错误码 | 值 | 中文 | 说明 | |---|---|---| | `395062` | 酒店已关联订单,不能更换供应商 | 本次新增;真正改绑或解绑时存在未结束订单 | ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |---|---|---| | `GET /admin/hotel/items` 的 `records[].supplierFullName` | 不返回 | 每条固定返回 `String` 或 `null` | | 酒店设置/改绑的 `changeReason` | 必填 | 可省略;原有非空值继续兼容 | | 酒店解绑的 `changeReason` | 已可省略 | 保持可省略 | ### 行为级对比 | 行为 | 改前 | 改后 | |---|---|---| | 酒店列表展示供应商 | 列表无名称字段 | 直接读取 `supplierFullName` | | 酒店真正改绑、解绑 | 不检查订单 | 被未结束订单排房、点名或团期分房时返回 `395062`;其他情形放行 | | 尚未绑定供应商的酒店 | 可首次绑定(须填原因) | 可首次绑定且可省略原因,即使已有订单 | ## 六.7、影响评估 - **是否破坏向后兼容**: 否;新增可空字段、放宽可选入参与新增业务保护。 - **前端是否必须同步上线**: 是。 - **前端 workaround 清理点**: 酒店列表直接展示 `supplierFullName`;复用供应商关系弹窗并传 `HOTEL`;删除酒店设置/改绑/解绑原因弹框和必填校验;收到 `395062` 时提示原文并保持当前关系。 ## 七、不影响范围 - **仅影响**: 管理端酒店列表、酒店供应商关系操作。 - **零影响**: 订单创建、状态流转、结算、支付、退款、订单数据和订单供应商快照;小程序酒店列表;供应商候选列表。 - 景区、餐厅、游玩项目及其他资源模块的字段、类型、原因规则和订单门禁保持原契约;数据库结构、配置、Redis、MQ 和 Gateway 路由无变化。 ## 八、测试环境已验证 TEST 部署提交 `02e998fbf706e5d69cb872ece7c9034128dec6e7`(hl-order-service-v3 任务 c01e3996、hl-order-service-v2 任务 051efee5、hl-resource-service 任务 72fd67b1,均双实例健康);2026-09-11 经 Gateway 以真实 TEST 身份实测 38/38 通过: | 场景 | 结果 | |---|---| | `GET /admin/hotel/items` 每条记录含 `supplierFullName`;已绑定酒店为当前供应商全称,未绑定为 `null` | 通过 | | 未绑定酒店首次绑定、改绑、解绑均不传 `changeReason` 成功;列表字段随之变化 | 通过 | | `requiredTypeCode=SCENIC` 绑定酒店 | 返回 400 | | 已绑定且有未结束订单(房控排房)的酒店改绑、解绑 | 均返回 `395062`,原关系不变 | | 仅在未结束订单产品中列为备选的酒店改绑并改回 | 均成功 | | 尚未绑定、订单住宿需求已点名的酒店首次绑定 | 成功 | | 同一酒店在该订单未结束时改绑、解绑 | 均返回 `395062`,原关系不变 | | 订单出行前取消后(仅关联已取消订单)改绑、解绑 | 均成功 | | 验收前后真实供应商关系、V2/V3 订单摘要 | 一致 | 仅关联已完成订单、已完成与已取消混合两种情形由 V2、V3 隔离 MySQL 自动化测试证明。 **当前状态:后端已部署并验证;待前端处理。** ## 九、相关历史 PR | PR | Issue | 说明 | 是否仍有效 | |---|---|---|---| | #7358 | #7357 | 景区改绑供应商免原因 | 是 | | #7401 | #7400 | 全部资源供应商解绑免原因 | 是 | | #7493 | #7387 | 餐厅列表、原因规则与订单门禁 | 是 | | #7507 | #7500 | 游玩项目列表、原因规则与订单门禁 | 是 | | **#7552** | **#7509** | 酒店列表、原因规则与订单门禁 | **是,酒店最新契约** | ## 十、相关文档 - 关联 Issue:[#7509](https://git.1814.love:8443/wx/HL/issues/7509) - 后端 PR:[#7552](https://git.1814.love:8443/wx/HL/pulls/7552) - 合并提交:`02e998fbf706e5d69cb872ece7c9034128dec6e7`(PR #7552) ## 关联 / 联系人 ### 链接 - **Issue**: [#7509](https://git.1814.love:8443/wx/HL/issues/7509) - **PR**: [#7552](https://git.1814.love:8443/wx/HL/pulls/7552) - **Merge commit**: `02e998fbf706e5d69cb872ece7c9034128dec6e7` ### 联系人 - **后端负责人**: @lc