diff --git a/changelogs-v2/2026-10/02_8659_房务价格日历与库存口径统一-修改接口-管理后台.md b/changelogs-v2/2026-10/02_8659_房务价格日历与库存口径统一-修改接口-管理后台.md new file mode 100644 index 00000000..cc3944cb --- /dev/null +++ b/changelogs-v2/2026-10/02_8659_房务价格日历与库存口径统一-修改接口-管理后台.md @@ -0,0 +1,520 @@ +--- +schema: "hl-changelog/v2" +ticket: "8659" +title: "房务价格日历与库存口径统一:候选页与控房表增加日历状态,扣减拒绝按原因分三码" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "" +updated_at: "2026-10-02" +base: "dev-v3" +--- + +# 房务配房接口调整:价格日历与库存口径统一、扣减失败分码 + +> **存放目录**: 二期 → `changelogs-v2/2026-10/` +> +> **服务**: hl-order-service-v3、hl-resource-service +> **Issue**: #8659 +> **日期**: 2026-10-02 +> **影响范围**: 管理后台房务配房流程,涉及候选酒店页、控房表、配房失败反馈 + +--- + +## ⚠️ 关键变化 + +- **候选酒店页 `inventoryStatus` 扩大语义**:日历已售罄(`SOLD_OUT`)映射为 `FULL`,日历已关闭(`CLOSED`)或无日历行映射为 `CLOSED`;非 `AVAILABLE` 时 `available=0`、`unlimited=false`(即使库存数本身大于 0)。**前端无需改代码**:经 hl-ui v2.1 核实,候选弹窗(`PickHotelModal.vue`)已按 `inventoryStatus` 三值禁用非 `AVAILABLE` 选项、显示「满房」「已关」,且取值用 `pickFirst` 读取不会被 `available=0` 误判为缺省继续读旧的 `stock`。 +- **控房表新增 `calendarStatus` / `calendarStatusName`**:价格日历状态原值与中文名。纯新增字段,不读不影响现有解析;**前端需在控房表加一列展示 `calendarStatusName`**,房务才能在剩余数大于 0 时看出该格「已关闭」或「已售罄」、扣减必被拒(这是本次唯一的前端动作)。 +- **扣减失败时按原因分三种错误码**(原来统一报 808901): + - `808906`「该日该房型未配置价格日历」(新增) + - `808907`「该日该房型已关闭售卖或已售罄(状态:{0})」(新增,`{0}` 为状态中文名) + - `808901`「房型库存不足」(文案收窄,现仅表示日历可售但库存不够;旧文案与此不同,见下) + - 以上均由前端通用拦截器按 `message` 自动弹出,调用方无需按 `code` 分支即可正确展示。 + +--- + +## 一、背景 + +资源侧价格日历状态(可售 `AVAILABLE` / 已售罄 `SOLD_OUT` / 已关闭 `CLOSED`)与库存数是两个独立维度。读侧(候选页、控房表)曾只看库存、不读状态,与写侧(库存扣减按 `status=AVAILABLE` 谓词)产生口径断裂:房务在候选页看到「可订」而提交配房被拒,甚至产生"该加房量"的错误判断,实际原因是日历已关房。本次统一口径:读侧添加日历状态、扣减时按真实原因分码。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 候选酒店页 | GET | `/v3/admin/hotel-candidates` | 修改 | roomTypes[].inventoryStatus 取值语义扩大 | +| 2 | 控房表 | GET | `/v3/admin/order/house-console/room-control` | 修改 | 新增 calendarStatus / calendarStatusName | +| 3 | 逐晚提交配房 | POST | `/v3/admin/order/hotel-requirements/{requirementId}/assignments` | 修改 | 扣减拒绝返回 808906 / 808907 / 808901(原统一 808901) | + +--- + +## 三、接口详情 + +本节示例 JSON 的字段名、类型、错误码与文案逐一取自源码;ID、日期等取值为说明用的构造值。所有接口统一返回 `Result` 信封(`code` / `message` / `data` / `traceId` / `success`),示例省略 `traceId`;业务失败与入参校验失败均为 HTTP 200,靠 `code` 区分。 + +### 1. 候选酒店页 `GET /v3/admin/hotel-candidates` + +**VO**: `HotelCandidateQueryReqVO → HotelCandidateRespVO` + +#### 使用场景 + +房务/定制师为某个订单的某一晚选酒店时查看候选酒店列表及房型库存。本次改动只影响候选房型 `roomTypes[].inventoryStatus`(及联动的 `available`/`unlimited`)的取值口径,请求参数与响应结构均未变。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Query | Long | ✅ | - | 订单 ID | +| dayNumber | Query | Integer | ❌ | ≥1 | 第几天(从 1 开始,`stayDate = departDate + dayNumber - 1`),与 stayDate 二选一,直接传 stayDate 优先 | +| stayDate | Query | LocalDate | ❌ | - | 入住日期;直接指定优先于 dayNumber 推算 | +| city | Query | String | ❌ | - | 城市代码 | +| keyword | Query | String | ❌ | - | 关键词非空时突破单城限定,跨城/省按酒店名/城市/省份/地址匹配;为空维持单城行为 | +| limit | Query | Integer | ❌ | 1~50,默认 30 | 返回候选条数上限,池内/定制师优先排序后取前 N | +| roomCategory | Query | String | ❌ | 字典 room_category | 非空白时参与房型过滤,matched 房型只在同大类内选取 | +| roomCount | Query | Integer | ❌ | ≥1 | 需要的房间数;不传退化为 available>0 的旧行为 | +| preferredHotelId | Query | Long | ❌ | - | 定制师指定的优先酒店 ID | +| requirementId | Query | Long | ❌ | - | 住宿需求 ID;传入后该需求 days JSON 里所有 hotelId 作为定制师指定 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| stayDate | LocalDate | 入住日期 | +| city | String | 城市代码 | +| productType | String | 产品类型(CORE/GROUP/CUSTOM,响应专有字段,无同名入参) | +| candidates[] | List | 候选酒店列表 | +| candidates[].roomTypes[] | List | 该酒店当日真实房型列表 | +| candidates[].roomTypes[].available | Integer | **语义变化**。`unlimited=true` 时为 NULL(表不限);否则为当日可用房数。`inventoryStatus` 非 `AVAILABLE` 时恒为 `0`(即使库里 stock 列大于 0) | +| candidates[].roomTypes[].unlimited | Boolean | **语义变化**。resource 端 stock 列为 NULL 时才为 `true`;`inventoryStatus` 非 `AVAILABLE` 时恒为 `false`(工单 #8659 前,日历已关闭/售罄但 stock=NULL 的房型也会显示「不限」,现改为显示不可订) | +| candidates[].roomTypes[].inventoryStatus | String | **修改**。`AVAILABLE`=日历可售且有余量或不限库存;`FULL`=日历可售但余量为 0 或日历已售罄;`CLOSED`=未配置价格日历、日历已关闭或日历状态字典外脏值(工单 #8659)。`FULL`/`CLOSED` 时价格字段(protocolPrice/settlementPrice/basePrice)照常返回,只有库存字段归零 | + +#### 请求示例 + +```http +GET /v3/admin/hotel-candidates?orderId=100001&dayNumber=1&city=hailar&roomCount=2 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "stayDate": "2026-10-05", + "city": "hailar", + "productType": "CORE", + "candidates": [ + { + "hotelId": "200001", + "hotelName": "海拉尔假日酒店", + "roomTypes": [ + { + "roomTypeId": "300001", + "name": "豪华大床房", + "roomCategory": "KING", + "available": 7, + "unlimited": false, + "stock": 7, + "protocolPrice": "328.00", + "inventoryStatus": "AVAILABLE" + }, + { + "roomTypeId": "300002", + "name": "标准双床房", + "roomCategory": "TWIN", + "available": 0, + "unlimited": false, + "stock": 5, + "protocolPrice": "280.00", + "inventoryStatus": "CLOSED" + } + ] + } + ] + }, + "success": true +} +``` + +上例第二个房型 `stock=5`(库里仍有余量)但日历已关闭,`inventoryStatus=CLOSED`、`available` 归零——这正是本次改动要修的口径断裂:改前 `available` 会原样显示库里的 5。 + +#### 空数据 / 降级响应 + +- 该订单该日无酒店或筛选无匹配:`candidates=[]`。 +- 日历状态字典外脏值按 `CLOSED` 处理(不单列「异常」状态)。 + +#### 错误响应 + +```json +{ + "code": 400, + "message": "orderId 不能为空", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- `inventoryStatus` 判定日历状态优先于库存数:日历 `CLOSED` 或 `SOLD_OUT` 时,即使 resource 库存列数字大于 0,`available`/`unlimited` 仍归零,不能据库存字段反推是否可订。 +- `limit` 默认 30、上限 50,候选列表本身是截断后的结果,不代表该城市全部酒店。 + +### 2. 控房表 `GET /v3/admin/order/house-console/room-control` + +**VO**: `HouseRoomControlListReqVO → HouseRoomControlRespVO` + +#### 使用场景 + +房务在房务控制台查看某个城市(或某酒店)、某日期区间的库存占用详情及团组用房明细。本次新增 `calendarStatus` / `calendarStatusName` 两个字段;前端在控房表加一列展示 `calendarStatusName` 后,房务无需逐行点开即可看到日历状态。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| cityCode | Query | String | ❌ | ≤32 字 | 城市编码;与 hotelId 都不传则查全部酒店 | +| hotelId | Query | Long | ❌ | - | 酒店 ID;传了则只查这一家,优先于 cityCode | +| dateFrom | Query | LocalDate | ✅ | - | 入住夜起(含) | +| dateTo | Query | LocalDate | ✅ | 与 dateFrom 跨度 ≤62 天 | 入住夜止(含) | +| onlyWithRemain | Query | Boolean | ❌ | - | true 时只返回剩余为不限或 >0 的行 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| stockTrackingEnabled | Boolean | resource 全局库存追踪开关;false 时行照常返回、已用取实际值,但调房量会被拒(808312) | +| rows[] | List | 按(酒店, 入住夜, 房型)升序 | +| rows[].hotelId / hotelName / cityName | - | 酒店 ID / 酒店名 / 城市中文名 | +| rows[].roomTypeId / roomTypeName | - | 房型 ID / 房型名 | +| rows[].stayDate | LocalDate | 入住夜 | +| rows[].totalRooms | Integer | 控房总数 = 剩余 + 已用;NULL 表不限量 | +| rows[].usedRooms | Integer | 已用(resource stock_used;库存开关关闭时仍取实际值) | +| rows[].remainRooms | Integer | 剩余(resource stock);NULL 表不限量 | +| rows[].assignedRooms | Integer | 已分配:扣库存的配房行 + 已确认扣库存的团期计划行间数合计 | +| rows[].protocolPrice / settlementPrice | BigDecimal(字符串) | 控房价(协议价)/ 结算价 | +| rows[].calendarStatus | String | **新增**。价格日历状态原值:`AVAILABLE / SOLD_OUT / CLOSED`,字典外历史值原样返回;该格无日历行时为 NULL。非 `AVAILABLE` 时扣减必被拒,与剩余数无关(工单 #8659) | +| rows[].calendarStatusName | String | **新增**。价格日历状态中文名:可售 / 已售罄 / 已关闭;字典外值显示「状态异常(原值)」;无日历行时为 NULL | +| rows[].usages[] | List | 团组用房明细(散客配房行或团期计划行,本表只列扣库存行) | +| rows[].usages[].orderId / teamNo / batchNo | - | 订单 ID(团期计划行为 NULL)/ 团号 / 团期批次号(散客订单为 NULL) | +| rows[].usages[].roomCount / roomSource / roomSourceLabel / confirmStatusLabel | - | 用房间数 / 房源(STOCK)/ 房源标签 / 确认状态标签 | + +#### 请求示例 + +```http +GET /v3/admin/order/house-console/room-control?cityCode=hailar&dateFrom=2026-10-01&dateTo=2026-10-31 +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "stockTrackingEnabled": true, + "rows": [ + { + "hotelId": "100001", + "hotelName": "海拉尔草原酒店", + "cityName": "海拉尔", + "roomTypeId": "300001", + "roomTypeName": "豪华双床房", + "stayDate": "2026-10-01", + "totalRooms": 12, + "usedRooms": 5, + "remainRooms": 7, + "assignedRooms": 5, + "protocolPrice": "320.00", + "settlementPrice": "300.00", + "calendarStatus": "AVAILABLE", + "calendarStatusName": "可售", + "usages": [ + { + "orderId": "1930000000000000001", + "teamNo": "HL20261001A", + "batchNo": null, + "roomCount": 3, + "roomSource": "STOCK", + "roomSourceLabel": "控房", + "confirmStatusLabel": "已确认" + } + ] + }, + { + "hotelId": "100002", + "hotelName": "海拉尔雅园宾馆", + "cityName": "海拉尔", + "roomTypeId": "300005", + "roomTypeName": "标准大床房", + "stayDate": "2026-10-01", + "totalRooms": 8, + "usedRooms": 2, + "remainRooms": 6, + "assignedRooms": 3, + "protocolPrice": "280.00", + "settlementPrice": "260.00", + "calendarStatus": "CLOSED", + "calendarStatusName": "已关闭", + "usages": [] + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +- 区间内无库存数据:`rows=[]`,`stockTrackingEnabled` 仍照常返回开关实际值。 +- 该格无日历行时 `calendarStatus`/`calendarStatusName` 为 NULL(不是「状态异常」,无行与脏值是两种不同情况)。 + +#### 错误响应 + +```json +{ + "code": 400, + "message": "dateFrom 不能为空", + "data": null, + "success": false +} +``` + +日期跨度超 62 天或起止颠倒不走参数校验码,由 Manager 本地校验后返业务码 808313。 + +#### 业务边界 + +- `calendarStatus=CLOSED` 或 `SOLD_OUT` 时,`remainRooms` 数字再大也**无法扣减**(扣减 UPDATE 只认 `status=AVAILABLE`);房务需在此处调整日历或释放库存,不能指望后续配房流程放行。 +- 该表不隐藏 `remainRooms`,即使日历已关闭仍显示实际 stock,控房表目的是查看与调整库存,不把实际数字藏起来。 +- `stockTrackingEnabled=false` 时各行仍按实际值返回 `usedRooms`/`remainRooms`,但调房量会被拒(808312),不能据此误判为"关闭追踪=不限量"。 + +### 3. 逐晚提交配房 `POST /v3/admin/order/hotel-requirements/{requirementId}/assignments` + +**VO**: `AssignmentSubmitReqVO → AssignmentSubmitRespVO` + +#### 使用场景 + +房务为一个住宿需求批量提交配房方案(一次可提交多晚 × 多组房间)。本次修改:扣减失败时根据真实原因(日历无行、日历不可售、库存不足)返回不同错误码,不再统一报 808901。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| requirementId | Path | Long | ✅ | - | 住宿需求 ID | +| items | Body | List | ✅ | 非空 | 配房项列表(批量) | +| items[].dayNumber | Body | Integer | ✅ | ≥1 | 第几天(Day1=1) | +| items[].hotelId | Body | Long | ✅ | - | 酒店 ID | +| items[].roomTypeId | Body | Long | ✅ | - | 房型 ID | +| items[].roomCategory | Body | String | ✅ | 字典 room_category | 房型字典 code(如 STANDARD) | +| items[].roomCount | Body | Integer | ✅ | ≥1 | 间数 | +| items[].deductInventory | Body | Boolean | ✅ | - | 是否扣减资源酒店房型库存;true=占用系统库存,false=仅保存配房快照不扣库存 | +| items[].protoPrice | Body | BigDecimal | ❌ | ≥0 | 协议价快照;不传按所选房型当日资源协议价兜底 | +| items[].settlementPrice | Body | BigDecimal | ❌ | ≥0 | 结算价快照;不传按资源结算价兜底,再兜底协议价 | +| items[].settleType | Body | String | ❌ | cash\|sign\|company | 支付方式快照;不传取酒店资源配置 | +| items[].breakfast | Body | String | ❌ | INCLUDED/EXCLUDED/PENDING | 早餐;不传按待确认存空 | +| items[].syncProtocolPrice / syncSettlementPrice / syncSettleType | Body | Boolean | ❌ | - | 是否把本项对应快照同步写回 resource;默认 false 不同步 | +| items[].remark | Body | String | ❌ | - | 备注 | +| items[].replaceReason | Body | String | ❌ | ≤256 字 | 替换原因;仅该天已有旧行被本项替换时落库 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| successCount | Integer | 成功条数 | +| failCount | Integer | 失败条数。当前实现恒为 `0`——扣减失败走整笔拒绝(见下),不会出现"部分成功部分失败"的响应 | +| items[] | List | 每条配房结果 | +| items[].dayNumber | Integer | 第几天 | +| items[].assignmentId | Long(JSON 字符串) | 配房 ID | +| items[].arrange | String | 配房状态,取值仅 `inquiring`(询价中)/ `confirmed`(已确认) | +| items[].deductInventory | Boolean | 本次配房是否扣减了库存 | + +#### 请求示例 + +```json +{ + "items": [ + { + "dayNumber": 1, + "hotelId": 100001, + "roomTypeId": 300001, + "roomCategory": "STANDARD", + "roomCount": 2, + "deductInventory": true + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "successCount": 1, + "failCount": 0, + "items": [ + { + "dayNumber": 1, + "assignmentId": "1970000000000000001", + "arrange": "inquiring", + "deductInventory": true + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无特殊降级。批量提交中任意一项触发库存扣减失败(808906/808907/808901)会导致**整次提交被拒绝**,不落库、不产生部分成功的配房记录;需要调整后整批重新提交。 + +#### 错误响应 + +```json +{ + "code": 808906, + "message": "该日该房型未配置价格日历", + "data": null, + "success": false +} +``` + +其他错误: + +| code | message | 触发 | +|------|---------|------| +| 808907 | 该日该房型已关闭售卖或已售罄(状态:{0}) | 日历 status 为 CLOSED 或 SOLD_OUT,`{0}` 为状态中文名 | +| 808901 | 房型库存不足 | 日历 status=AVAILABLE 但剩余数不够 | +| 808090 | 未登录或非房务角色,无权操作 | 非房务角色 | +| 808116 | 订单未抢单, 请先抢单再配房 | 需求无人持有(源码原文逗号为半角) | +| 808110 | 需求不属于当前用户 | 当前用户不是持有人(超管同样拒绝) | + +#### 业务边界 + +- `deductInventory=true` 的项触发库存扣减,三种拒绝原因分别报 808906 / 808907 / 808901;批量提交中任一项被拒,整次提交失败,不做部分落库。 +- `deductInventory=false` 的项不做库存校验,正常返回 200。 +- `roomCategory` 为必填字段,传空或不传直接触发参数校验失败(`code=400`,HTTP 状态仍为 200)。 +- 防重 3 秒,同一需求 3 秒内重复提交被拦截。 + +--- + +## 四、契约约束与正确调用方式 + +**通俗说法**:配房前看候选页或控房表,若日历状态显示「已关闭」或「已售罄」,不能提交配房;提交时若被拒,先按错误码判断原因——库存不足就加库存,日历问题就调日历。 + +### 示例对比 + +| 场景 | 旧行为 | 新行为 | +|------|--------|--------| +| 日历已关闭、stock=20 | 候选页显示可订、余 20 间;提交配房返 808901「库存不足」(误导) | 候选页 inventoryStatus=CLOSED、available=0;提交配房返 808907「已关闭」(准确);控房表 calendarStatus=CLOSED 可直观查看 | +| 日历无行 | 候选页显示可订或不显示(看评分);提交被拒 808901 | 候选页 inventoryStatus=CLOSED、available=0;提交配房返 808906「未配置日历」(准确指示资源侧缺陷) | +| 日历可售、stock=0 | 候选页显示 FULL、available=0;提交被拒 808901 | 候选页显示 FULL、available=0;提交被拒 808901(同前) | + +--- + +## 五、数据库行为 + +扣减仍走库存表 UPDATE 逻辑,本次仅改返回码与日历状态展示,无 DDL 或表结构变化。 + +--- + +## 六、边界行为 + +- 错误码 808906 / 808907 / 808901 的优先级:先判日历有无(808906)→再判日历状态(808907)→最后判库存(808901)。 +- 控房表的 `remainRooms` 与日历状态 `CLOSED` 同时出现时,表示日历被关了但库存数还在,房务调整库存须先对日历解冻(不归房务接口管)。 + +--- + +## 六.5 枚举 + +### inventoryStatus(HotelCandidateRespVO.RoomTypeOption) + +**类型**: `String` + +| 值 | 条件 | 前端表现 | +|----|------|--------| +| `AVAILABLE` | 日历 status=AVAILABLE 且库存>0(或 NULL 不限) | 可订、显示余量 | +| `FULL` | 日历 status=AVAILABLE 但库存=0,或日历 status=SOLD_OUT | 满房、显示「已满」 | +| `CLOSED` | 日历 status=CLOSED 或无日历行 | 关闭、显示「不可订」 | + +### calendarStatus(HouseRoomControlRowRespVO) + +**类型**: `String` + +| 值 | 说明 | +|----|------| +| `AVAILABLE` | 可售 | +| `SOLD_OUT` | 已售罄 | +| `CLOSED` | 已关闭 | +| NULL | 无日历行 | +| 其他 | 字典外的历史脏数据(原样返回) | + +--- + +## 六.6、修改前后对比 + +| 项 | 改前 | 改后 | +|----|------|------| +| 候选页 inventoryStatus 逻辑 | 仅看库存数;SOLD_OUT→FULL、CLOSED→CLOSED(但都显示有库存或空库存) | 优先看日历状态;SOLD_OUT→FULL、CLOSED→CLOSED(且 available=0) | +| 控房表字段 | calendarStatus / calendarStatusName 无 | **新增**,展示日历原值与中文名 | +| 扣减失败错误码 | 统一 808901(三种原因混合) | 分码:808906(无日历)、808907(日历不可售)、808901(库存不足) | + +--- + +## 六.7、影响评估 + +- **前端改动只有一处:控房表加「日历状态」列**(hl-ui v2.1 实查结论): + - 候选弹窗 `src/views/housekeeper/components/PickHotelModal.vue` 已按 `inventoryStatus` 三值分支渲染禁用态与文案(`disabled: !rt.unlimited && rt.inventoryStatus !== 'AVAILABLE'`,CLOSED 显示「已关」/FULL 显示「满房」),取库存数用 `pickFirst()` 辅助函数正确处理 `available=0` 而不误判为缺省继续回退读旧字段,三值语义收紧不影响该组件现有行为。 + - 控房表新增的 `calendarStatus`/`calendarStatusName`/`usages[]` 均为纯新增字段,hl-ui v2.1 当前对该查询结果的消费代码未读取这些字段名,新增不影响现有解析;**需要新增一列展示 `calendarStatusName`**(为 NULL 时显示「—」),否则剩余数大于 0 而日历已关闭的格子在页面上看不出来。 + - 配房提交的错误码拆分(808906/808907/808901)均由 `src/api/housekeeper/assignment.js` 所在模块走通用拦截器按 `message` 弹窗展示,调用方代码未对 808xxx 做按值分支(`useHousekeeperPlacementAdjustment.js:71` 注释确认),拆分前后前端展示路径一致。 +- **触发频率变化**:`inventoryStatus=CLOSED` 的触发条件由「仅库存为 0」扩大为「日历关闭/售罄/无日历行 或 库存为 0」,该状态出现频率会提升,但因前端已走统一的三值禁用逻辑,不需要代码改动去适配。 + +--- + +## 七、不影响范围 + +- 小程序端接口不变。 +- 订单确认、支付、发票等下游流程无改动。 + +--- + +## 八、测试环境已验证 + +测试服环境,2026-09-30~10-02。 + +``` +候选页(GET /v3/admin/hotel-candidates) + 关闭日期场景:keyword 查询返回 inventoryStatus=CLOSED, available=0 ✓ + 重新开放:状态切回 AVAILABLE, available 恢复实际库存数 ✓ + +控房表(GET /v3/admin/order/house-console/room-control) + 关闭日期:calendarStatus=CLOSED, calendarStatusName=已关闭 ✓ + 重新开放:calendarStatus=AVAILABLE, calendarStatusName=可售 ✓ + +配房扣减(POST /v3/admin/order/hotel-requirements/{id}/assignments) + 提交已关闭日期的房型:返回 808907「已关闭或已售罄」✓ + 重新开放后提交:返回 200, stock 更新 ✓ +``` + +--- + +## 十、相关文档 + +- **Issue**: [#8659](https://git.1814.love:8443/wx/HL/issues/8659) +- **PR**: [#8704](https://git.1814.love:8443/wx/HL/pulls/8704) + +## 关联 / 联系人 + +**关联工单**: #8659 +**同批变更**: #8662(旧接口删除与权限补漏) +**后端负责人**: @wx diff --git a/changelogs-v2/2026-10/02_8662_旧住宿需求提交口删除-删除接口-管理后台.md b/changelogs-v2/2026-10/02_8662_旧住宿需求提交口删除-删除接口-管理后台.md new file mode 100644 index 00000000..2d5bf7b9 --- /dev/null +++ b/changelogs-v2/2026-10/02_8662_旧住宿需求提交口删除-删除接口-管理后台.md @@ -0,0 +1,242 @@ +--- +schema: "hl-changelog/v2" +ticket: "8662" +title: "删除旧住宿需求提交接口(PUT /v3/admin/order/{id}/hotel-requirement)" +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: "" +status_note: "" +updated_at: "2026-10-02" +base: "dev-v3" +--- + +# 删除旧住宿需求提交接口 + +> **存放目录**: 二期 → `changelogs-v2/2026-10/` +> +> **服务**: hl-order-service-v3 +> **Issue**: #8662 +> **日期**: 2026-10-02 +> **影响范围**: 管理后台订单住宿需求提交流程 + +--- + +## ⚠️ 关键变化 + +- 路由 `PUT /v3/admin/order/{id}/hotel-requirement` 已删除,服务端无此路由映射。 +- **替代接口**:`POST /v3/admin/order/{id}/adjustment/submit`,请求体 `{"updates":{"hotelRequirement":{days,specialTags,remark}}}`,响应 `{success}`。 +- **权限对齐**:旧接口零权限校验,任何后台账号可修改任意订单需求;新接口校验订单归属(管理员、超管、本单定制师放行,其他后台角色返回 581008;房务返回 581045)。 + +--- + +## 一、背景 + +旧接口 `PUT /v3/admin/order/{id}/hotel-requirement` 于 #4515 标注为废弃,继任者为 `POST /v3/admin/order/{id}/adjustment/submit`。源码删除说明(Controller 类 javadoc、`API-SPEC.html` §3.1)记载的旧接口缺陷: + +1. **无权限校验**:该端点不校验操作人,任何登录后台的账号都能改写任意订单的住宿需求,不要求调用者是该单定制师。 +2. **DONE_ADJUST 分支继承原认领房务**:已完成版需求再调整时(`status=DONE` → 重提),服务端按 `order_hotel_requirement` 旧行 `is_active=0` + 新行 `version+1` 落库,新行直接复制原 `claimer_*`(沿用原房控、不重新入抢单池),这一继承行为与权限校验无关,继任接口同样保留(见六.6)。 + +继任接口已在服务层加入 `OrderViewGuard.assertOrderAccessible()` 的归属校验(管理员/超管放行,本单定制师放行,其他后台角色 581008,房务管理员 581045)。`API-SPEC.html` §3.1 删除说明与 hl-ui v2.1 代码核查一致确认:管理后台视图层此前已零调用旧接口(均已改走 `adjustment/submit`),故本次删除对前端无需额外改动。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 住宿需求提交(旧) | PUT | `/v3/admin/order/{id}/hotel-requirement` | 删除 | 改用 adjustment/submit | + +--- + +## 三、接口详情 + +本接口已删除。下表记录的是**删除前**的契约,仅供前端清理调用点之用。字段名、类型、错误码逐一取自删除前源码。服务端已无该路由映射,调用不会返回本表所述的正常响应或错误码,而将返回 HTTP 404(路由不存在)。 + +### 1. 住宿需求提交(旧) `PUT /v3/admin/order/{id}/hotel-requirement` + +**VO**: `HotelRequirementReqVO → HotelRequirementRespVO`(均已删除) + +#### 使用场景 + +删除前:定制师提交或修改订单的住宿需求(酒店偏好、特殊要求、入住日期等)。现改为 `POST /v3/admin/order/{id}/adjustment/submit`。 + +#### 入参(删除前) + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| id | Path | Long | ✅ | - | 订单 ID | +| days | Body | List | ✅ | 非空、按 dayNumber 排序 | 逐晚配房需求 | +| days[].dayNumber | Body | Integer | ✅ | ≥1 | 第几晚 | +| days[].stayDate | Body | LocalDate | ✅ | - | 入住日期 | +| days[].city | Body | String | ✅ | - | 城市代码 | +| days[].customerSelfBooked | Body | Boolean | ❌ | 默认 false | 客人自订该晚酒店 | +| days[].segments | Body | List | ❌ | - | 房间需求段(非自订晚通常需 ≥1 段) | +| days[].segments[].roomCategory | Body | String | ✅ | TWIN / KING / ... | 房型分类 | +| days[].segments[].roomCount | Body | Integer | ✅ | ≥1 | 间数 | +| specialTags | Body | List | ❌ | - | 特殊标签(e.g.「协议酒店」「靠近景区」) | +| remark | Body | String | ❌ | ≤500 字 | 特殊要求备注 | + +#### 出参(删除前) `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| requirementId | Long | 需求行 ID | +| version | Integer | 版本号(首版=1) | +| status | String | 需求状态(PENDING / DONE_ADJUST 等) | + +#### 请求示例(删除前) + +```json +{ + "days": [ + { + "dayNumber": 1, + "stayDate": "2026-10-05", + "city": "hailar", + "segments": [ + { + "roomCategory": "KING", + "roomCount": 2 + } + ] + } + ], + "specialTags": ["协议酒店"], + "remark": "靠近景区" +} +``` + +#### 响应示例(删除前) + +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "1930000000000000001", + "version": 1, + "status": "PENDING" + }, + "success": true +} +``` + +#### 错误响应(删除前) + +```json +{ + "code": 400, + "message": "days 不能为空", + "data": null, + "success": false +} +``` + +#### 空数据 / 降级响应 + +接口已删除,无空数据或降级形态可约定;前端移除调用点。 + +#### 业务边界 + +- 服务端已无该路由映射,删除后返回 HTTP 404,前端不得依赖任何响应体判断,调用点一律移除。 +- 替代接口经 `OrderViewGuard.assertOrderAccessible()` 校验归属,管理员/超管/本单定制师放行,其他后台角色 581008,房务 581045。 + +--- + +## 四、契约约束与正确调用方式 + +### 迁移路径 + +| 旧接口 | 新接口 | payload 转换 | +|--------|--------|-------------| +| `PUT /v3/admin/order/{id}/hotel-requirement` | `POST /v3/admin/order/{id}/adjustment/submit` | 旧 request body 的 `days` / `specialTags` / `remark` 改为嵌套:`{"updates":{"hotelRequirement":{days,specialTags,remark}}}` | + +### 权限变化 + +| 角色 | 旧接口 | 新接口 | +|------|--------|--------| +| 本单定制师 | 200 放行 | 200 放行 | +| 其他后台定制师 | 200 放行(**缺陷**) | 581008 拒绝 | +| 房务 | 200 放行(**缺陷**) | 581045 拒绝 | +| 管理员 / 超管 | 200 放行 | 200 放行 | + +--- + +## 五、数据库行为 + +| 前端提交 | 写入位置 | 行为 | +|----------|----------|------| +| 旧接口已删除 | - | 无(服务端零路由映射) | + +--- + +## 六、边界行为 + +- 服务端已无该路由映射,调用返回 HTTP 404(`Not Found`)。 +- 调用点一律移除,无需保留兼容代码。 + +--- + +## 六.5 枚举 + +不适用(接口已删除)。 + +--- + +## 六.6、修改前后对比 + +| 项 | 改前 | 改后 | +|----|------|------| +| 路由存在 | ✅ 存在 | ❌ 已删除,返回 404 | +| 权限校验 | ❌ 无,任何账号可修改任意订单 | ✅ 按定制师归属校验,非该单定制师返回 581008 | +| DONE_ADJUST 继承行为 | 旧行 `is_active=0` + 新行 `version+1`,复制原 `claimer_*` | 行为不变——继任接口走同一套 `adjustment/submit` 事务逻辑,继承规则与权限校验是两回事,本次改动只补了权限、未改这条继承规则 | + +--- + +## 六.7、影响评估 + +- **前端无需改动**:经 hl-ui v2.1 核实,`src/api/orderV2.js` 中的 `putHotelRequirement` 函数定义仍在(标注 `@deprecated`),但全仓库内已无任何调用点(grep 零命中);`API-SPEC.html` §3.1 的删除说明同样记载"管理后台视图层已零调用(均已改走 §6.2)",两处结论一致。该函数是死代码,本次后端删除路由不会让任何现用页面失效。 +- 如需清理,可删除 `putHotelRequirement` 这一处未使用的函数定义本身,但这不影响任何现有页面的可用性,不构成阻塞项。 + +--- + +## 七、不影响范围 + +- 新接口 `POST /v3/admin/order/{id}/adjustment/submit` 保留且功能完整。 +- 房务配房流程无改动(房务走 house 域的 `HouseAssignmentAdminController`,不涉及本接口)。 +- 小程序端、H5 端接口无改动。 + +--- + +## 八、测试环境已验证 + +测试服环境,2026-09-30~10-02。 + +``` +PUT /v3/admin/order/{id}/hotel-requirement + 非 owner 定制师角色调用:HTTP 404 ✓(路由已删除,非权限拒绝) + URL 转至新接口 POST /v3/admin/order/{id}/adjustment/submit 后: + 非 owner 定制师角色:返回 581008 无权查看此订单 ✓ + 房务角色:返回 581045 房务角色无权查看订单详情,房务仅可配房 ✓ +``` + +--- + +## 十、相关文档 + +- **Issue**: [#8662](https://git.1814.love:8443/wx/HL/issues/8662) +- **PR**: [#8705](https://git.1814.love:8443/wx/HL/pulls/8705) +- **继任接口文档**: `docs/order-v3/api/API-SPEC.html` §3.1(本端点删除说明与历史存档)、§6.2(继任端点 `adjustment/submit`) + +## 关联 / 联系人 + +**关联工单**: #8662 +**同批修改**: 询房预览权限补漏 +**后端负责人**: @wx diff --git a/changelogs-v2/2026-10/02_8662_询房预览补权限-修改接口-管理后台.md b/changelogs-v2/2026-10/02_8662_询房预览补权限-修改接口-管理后台.md new file mode 100644 index 00000000..f93bc530 --- /dev/null +++ b/changelogs-v2/2026-10/02_8662_询房预览补权限-修改接口-管理后台.md @@ -0,0 +1,238 @@ +--- +schema: "hl-changelog/v2" +ticket: "8662" +title: "询房预览接口补房务读守卫" +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: "" +status_note: "" +updated_at: "2026-10-02" +base: "dev-v3" +--- + +# 询房预览接口补房务读守卫 + +> **存放目录**: 二期 → `changelogs-v2/2026-10/` +> +> **服务**: hl-order-service-v3 +> **Issue**: #8662 +> **日期**: 2026-10-02 +> **影响范围**: 管理后台房务询房话术预览功能 + +--- + +## ⚠️ 关键变化 + +- 接口 `POST /v3/admin/order/inquiry/preview` 新增房务读守卫。 +- **非房务角色**(定制师、管理员、其他后台角色)调用返回 **808090**「未登录或非房务角色,无权操作」。 +- **房务角色**(房务管理员、超管)放行,功能无改动。 + +--- + +## 一、背景 + +二期房务功能收口中,#8390 统一给 16 个旧只读端点(日历 / 房务详情 / 酒店视图 / 转单候选 / 待办 / 月度对账 / 旧抢单池 / 订单房间)挂上房务读守卫,唯独询房话术预览这一个接口漏过,导致定制师、运营等非房务角色能调通,拿到酒店联系人与微信(源码:`HouseReadGuard.java` 类 javadoc)。本次补上后,受此守卫覆盖的端点共 17 个。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 询房话术预览 | POST | `/v3/admin/order/inquiry/preview` | 修改 | 新增房务读守卫,非房务角色返回 808090 | + +--- + +## 三、接口详情 + +### 1. 询房话术预览 `POST /v3/admin/order/inquiry/preview` + +**VO**: `InquiryPreviewReqVO → InquiryPreviewRespVO` + +#### 使用场景 + +房务在配房弹窗点击「询房」时预览即将发往酒店的话术(所见即所发),确认无误后复制到企业微信。本次修改:非房务角色被拒。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| hotelId | Body | Long | ✅ | - | 酒店 ID;按此取 resource 联系人渲染文案 | +| orderId | Body | Long | ❌ | - | 订单 ID(取团号);`assignmentId` 有效时以其所属订单为准 | +| assignmentId | Body | Long | ❌ | - | 配房 ID;存在且与 `hotelId` 匹配时,日期/房型/间数/支付方式优先取该配房快照 | +| stayDate | Body | LocalDate | ❌ | - | 入住日期;`assignmentId` 命中时被快照值覆盖 | +| roomCount | Body | Integer | ❌ | ≥1 | 房间数;`assignmentId` 命中时被快照值覆盖;都缺省时按 1 间渲染 | +| roomCategory | Body | String | ❌ | 字典 room_category | 房型类别;`assignmentId` 未命中时用于查房型中文名,查不到则原样回退为传入的 code | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| messageBody | String | 渲染后的固定格式订房确认话术(模板见下) | +| contactName | String | 联系人姓名,resource 按 hotelId 带出,前端只读回显 | +| contactWechat | String | 联系人微信号,resource 按 hotelId 带出,前端只读回显 | + +话术固定模板(`InquiryMessageTemplate.SEND_BASE`,占位符按 `Map` 渲染,缺失值替换为空串): + +``` +呼籁旅行 - 订房确认书: +团号:${teamNo} +日期:${stayDate} +房型:${roomTypeName}${roomCount}间 +备注:${tags} +1.${paymentText},价格保密。 +2.${breakfastText}${invoiceText} +3.核房电话:${phone} +辛苦确认后回复 @${replyContacts} +``` + +#### 请求示例 + +```json +{ + "hotelId": 1900000001, + "orderId": 1900000000, + "stayDate": "2026-04-28", + "roomCount": 1 +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "messageBody": "呼籁旅行 - 订房确认书:\n团号:HL20261001A\n日期:4.28\n房型:待补充1间\n备注:协议酒店\n1.领队前台现付,价格保密。\n2.含早含发票\n3.核房电话:0470-8888888\n辛苦确认后回复 @王前台", + "contactName": "王前台", + "contactWechat": "hailar_holiday" + }, + "success": true +} +``` + +上例未传 `roomCategory`、也未传 `assignmentId`,`roomTypeName` 按规则取不到任何来源,渲染为空缺省文案(源码常量 `EMPTY_VALUE_TEXT`)——`${roomTypeName}${roomCount}间` 模板不插空格,故渲染结果是该空缺省文案与 `1间` 的无分隔拼接(见上方 JSON 示例的 `messageBody`),前端如需展示分隔需自行处理,后端不改模板。日期按 `M.d` 格式渲染(无补零),`2026-04-28` → `4.28`。ID、团号、酒店联系人等取值均为说明用的构造值。 + +#### 空数据 / 降级响应 + +- 酒店联系信息查询抛异常(`loadHotelExtended` 捕获全部 `RuntimeException`):静默降级,`contactName`/`contactWechat` 返回**空字符串 `""`(不是 NULL)**,`messageBody` 仍正常渲染,缺省字段分别落空值常量(`EMPTY_VALUE_TEXT`)/空标签常量(`EMPTY_TAG_TEXT`)/现付默认文案。 +- `assignmentId` 传了但查不到记录、或与 `hotelId` 不匹配:静默降级为按请求参数 + 资源数据重新生成(不报错,仅记一条 `log.warn`),不是 assignment 快照。 +- `assignmentId` 命中但与请求里的 `orderId` 不一致:忽略请求 `orderId`,改用该配房记录的真实 `orderId`(同样静默降级,仅记日志)。 +- 不落库,房务可反复调用,无状态。 + +#### 错误响应 + +```json +{ + "code": 808090, + "message": "未登录或非房务角色,无权操作", + "data": null, + "success": false +} +``` + +其他错误: + +| code | message | 触发 | +|------|---------|------| +| 400 | hotelId 不能为空 | hotelId 未传 | +| 400 | 房间数最小为 1 | roomCount 传了但 < 1 | + +#### 业务边界 + +- 只校验角色(房务管理员/超管放行),**不校验订单归属**:房务可预览任意订单的询房话术,与 #8390 覆盖的其余 16 个只读端点行为一致。 +- 该守卫只拦「有角色但非房务」;零角色账号(网关未透传 `X-Admin-Role`)按既有口径仍放行,不受本次改动影响(`HouseReadGuard.java` 类 javadoc,#7609 G-2 定案)。 +- 预览不落库,无副作用,可反复调用。 +- `contactWechat` 仅供复制,前端不做交互(不拨电话、不主动跳转)。 + +--- + +## 四、契约约束与正确调用方式 + +### 权限对照 + +| 角色 | 改前 | 改后 | 说明 | +|------|------|------|------| +| 房务管理员 | 200 放行 | 200 放行 | 无改动 | +| 超管 | 200 放行 | 200 放行 | 无改动 | +| 定制师(CUSTOMIZER) | 200 放行(缺陷) | 808090 拒绝 | **新增限制** | +| 其他后台角色(如 ADMIN) | 200 放行(缺陷) | 808090 拒绝 | **新增限制** | +| 零角色账号(网关未透传 `X-Admin-Role`) | 放行 | 放行 | 无改动(#7609 G-2 口径,本次刻意不收) | + +--- + +## 五、数据库行为 + +不落库,本接口无数据写入。 + +--- + +## 六、边界行为 + +- 错误码 808090 与所有同域房务读端点保持一致,可统一处理。 +- 权限守卫受 Nacos 开关 `group-batch.acl.enforce.house-read-role` 控制。测试服 2026-09-30~10-02 期间该开关为开启状态(实测 ADMIN/CUSTOMIZER 均返回 808090,见「八、测试环境已验证」);生产环境以当时的配置为准,前端按本文档的错误码契约接即可,无需关心开关本身的开关状态。 + +--- + +## 六.5 枚举 + +不适用(接口无新增枚举)。 + +--- + +## 六.6、修改前后对比 + +| 项 | 改前 | 改后 | +|----|------|------| +| 权限校验 | ❌ 无房务守卫,任何角色可调 | ✅ 新增房务读守卫,非房务返回 808090 | +| 功能逻辑 | 预览话术、返回联系人 | 不变(仅权限改动) | + +--- + +## 六.7、影响评估 + +- **前端无需改动**:经 hl-ui v2.1 核实,该接口的封装函数(`src/api/housekeeper/inquiry.js` 的 `previewInquiry`)仅有一处调用——`src/views/housekeeper/components/useHousekeeperInquiryCopy.js`,位于房务专属视图目录下,本就只在房务角色登录后的界面里被触达。非房务角色(定制师等)侧没有调用这个接口的代码,本次收紧权限不会让任何现有页面报错。 + +--- + +## 七、不影响范围 + +- 房务配房流程(使用此接口的场景)功能不变。 +- 小程序端、H5 端接口无改动。 +- #8390 已覆盖的其余 16 个旧只读端点本身无行为变化,本次只是把本端点补入同一套守卫。 + +--- + +## 八、测试环境已验证 + +测试服环境,2026-09-30~10-02,经网关实测,与修前基线逐字段比对(基线快照:`p50_ac3_room_manager.json`/`p50_ac3_super_admin.json`;本轮:`p6_ac3_roommanager.json`/`p6_ac3_superadmin.json`/`p6_ac3_admin.json`/`p6_ac3_consultant.json`)。 + +``` +POST /v3/admin/order/inquiry/preview + 房务管理员(ROOM_MANAGER):200,messageBody/contactName/contactWechat 与修前基线逐字节相同 ✓ + 超管(SUPER_ADMIN):200,messageBody/contactName/contactWechat 与修前基线逐字节相同 ✓ + 管理员(ADMIN):808090 未登录或非房务角色,无权操作 ✓ + 定制师(CUSTOMIZER):808090 未登录或非房务角色,无权操作 ✓ +``` + +--- + +## 十、相关文档 + +- **Issue**: [#8662](https://git.1814.love:8443/wx/HL/issues/8662) +- **PR**: [#8705](https://git.1814.love:8443/wx/HL/pulls/8705) +- **背景工单**: [#8390](https://git.1814.love:8443/wx/HL/issues/8390)(原覆盖 16 个读端点统一守卫,本次 #8662 补上第 17 个——即本端点) + +## 关联 / 联系人 + +**关联工单**: #8662 +**同批删除**: 旧住宿需求提交接口 +**后端负责人**: @wx