feat(hotel): 管理端区/县字段与字典接口(后端 PR #984 + #988)
- GET /admin/hotel 新增 district 查询参数(精确匹配) - 新增 GET /admin/hotel/districts?city=... 字典接口(distinct district) - GET /admin/hotel/items 返回新增 district 字段(补 HotelListVO) 前端需改:HotelPickerModal 删除 city distinct 伪造逻辑改调字典接口;资源管理酒店列表顶栏加区/县下拉。
这个提交包含在:
父节点
bd7dcf5f3b
当前提交
eb81927845
@ -0,0 +1,238 @@
|
|||||||
|
# 管理端酒店 - 区/县筛选能力(`district` 字段 + 字典接口)
|
||||||
|
|
||||||
|
- **日期**: 2026-04-20
|
||||||
|
- **PR**: [#984](https://git.1814.love:8443/wx/HL/pulls/984) (Closes #982) + [#988](https://git.1814.love:8443/wx/HL/pulls/988)(补 HotelListVO.district)
|
||||||
|
- **类型**: FEATURE + BUGFIX(补齐后端能力,修复管理端区/县筛选不可用)
|
||||||
|
- **服务**: hl-resource-service(`/admin/hotel/**`)
|
||||||
|
- **前端是否需要改动**: **需要改动**(两处:资源管理-酒店列表顶栏、产品编辑-HotelPickerModal)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、背景
|
||||||
|
|
||||||
|
管理端两处区/县筛选当前不可用:
|
||||||
|
|
||||||
|
| # | 入口 | 前端现状 |
|
||||||
|
|---|------|---------|
|
||||||
|
| 1 | 资源管理 - 酒店列表页 | 顶栏只有城市文本输入框,没有区/县下拉 |
|
||||||
|
| 2 | 产品编辑 - 添加酒店弹窗(`HotelPickerModal.vue`) | 有区/县下拉,但选项是从已加载的 hotelList 的 `city` distinct 伪造(逻辑错误:city 和 district 不是同一维度),选中后还把值当 `city` 参数传 |
|
||||||
|
|
||||||
|
根因:后端 `HotelQueryRequest` 缺 `district` 字段(即便前端传了也会被忽略),也没有字典接口给前端做下拉数据源。
|
||||||
|
|
||||||
|
本次后端补齐两件事,让前端可以正常按区/县筛选。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、变更接口
|
||||||
|
|
||||||
|
| # | 方法 | 路径 | 变更类型 |
|
||||||
|
|---|------|------|---------|
|
||||||
|
| 1 | GET | `/admin/hotel` | **新增** `district` 查询参数(可选,精确匹配) |
|
||||||
|
| 2 | GET | `/admin/hotel/districts` | **新增接口**:按 city 列出该城市下已上架酒店的 distinct 区/县列表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、接口 1:列表查询新增 `district` 参数
|
||||||
|
|
||||||
|
### 请求参数变化
|
||||||
|
|
||||||
|
| 参数 | 类型 | 之前 | 现在 | 说明 |
|
||||||
|
|------|------|------|------|------|
|
||||||
|
| `city` | String | 可选 | 可选(不变) | 城市精确匹配(模糊匹配走 `cityKeyword`) |
|
||||||
|
| `district` | String | ❌ 不存在 | **新增,可选** | 区/县精确匹配(最大 32 字符) |
|
||||||
|
| 其他参数 | — | 不变 | 不变 | `keyword` / `cityKeyword` / `hotelType` / `starLevel` / `status` / `tagId` / `tagIds` / `sortBy` / `sortDir` / `page` / `pageSize` 等全部不变 |
|
||||||
|
|
||||||
|
### 调用示例
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 原用法:只按城市筛选(兼容,不变)
|
||||||
|
GET /admin/hotel?city=海拉尔&page=1&pageSize=20
|
||||||
|
|
||||||
|
// 新用法:city + district 组合筛选
|
||||||
|
GET /admin/hotel?city=海拉尔&district=海拉尔区&page=1&pageSize=20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 兼容性
|
||||||
|
|
||||||
|
老前端不传 `district` 完全兼容,行为不变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、接口 2:新增 区/县 字典接口
|
||||||
|
|
||||||
|
### 签名
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /admin/hotel/districts?city={city}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `city` | String | **可选** | 城市精确匹配;不传则返所有已上架酒店的 distinct district |
|
||||||
|
|
||||||
|
### 响应结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"data": [
|
||||||
|
{ "value": "海拉尔区", "label": "海拉尔区" },
|
||||||
|
{ "value": "陈巴尔虎旗", "label": "陈巴尔虎旗" },
|
||||||
|
{ "value": "额尔古纳市", "label": "额尔古纳市" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`value` 和 `label` 都是区/县名称,按区/县名称升序。
|
||||||
|
|
||||||
|
### 数据来源
|
||||||
|
|
||||||
|
- 表:`hotel`
|
||||||
|
- SQL 等价:
|
||||||
|
```sql
|
||||||
|
SELECT DISTINCT district FROM hotel
|
||||||
|
WHERE status = 1
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND district IS NOT NULL
|
||||||
|
AND district != ''
|
||||||
|
[AND city = ?]
|
||||||
|
ORDER BY district ASC
|
||||||
|
```
|
||||||
|
|
||||||
|
### 过滤规则
|
||||||
|
|
||||||
|
- 只返"已上架"酒店(`status = 1`)的 district
|
||||||
|
- 已软删除的酒店不计入
|
||||||
|
- district 为 `NULL` 或空字符串的被过滤
|
||||||
|
- `city` 传空 → 跨城市聚合;`city` 传值 → 只看该城市
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、前端改造建议
|
||||||
|
|
||||||
|
### 场景 A:产品编辑 - HotelPickerModal.vue
|
||||||
|
|
||||||
|
**改动点**:
|
||||||
|
1. 删除 L66-69 从 `hotelList` 里 distinct `city` 伪造 `areaOptions` 的代码
|
||||||
|
2. 改为调 `GET /admin/hotel/districts?city=xxx` 拉取下拉数据(建议在选中 city 时触发)
|
||||||
|
3. 选中区/县后,查询参数名从 `city` 改为 **`district`**(`city` 由 city 选择器单独负责)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 区/县选项加载(city 变化时触发)
|
||||||
|
async function loadAreaOptions(city) {
|
||||||
|
if (!city) { areaOptions.value = []; return }
|
||||||
|
const res = await request({
|
||||||
|
url: '/admin/hotel/districts',
|
||||||
|
method: 'GET',
|
||||||
|
data: { city }
|
||||||
|
})
|
||||||
|
areaOptions.value = res.data // [{ value, label }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询酒店(修正参数名)
|
||||||
|
const params = {
|
||||||
|
city: cityFilter.value, // 城市
|
||||||
|
district: areaFilter.value, // 🆕 区/县,之前错用 city
|
||||||
|
keyword: keyword.value,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 B:资源管理 - 酒店列表页
|
||||||
|
|
||||||
|
**改动点**:
|
||||||
|
1. 顶栏 `city` 输入框右侧加"区/县"级联下拉
|
||||||
|
2. city 改变时 reset 区/县并重新拉字典
|
||||||
|
3. 查询参数同步传 `district`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、列表响应新增 district 字段
|
||||||
|
|
||||||
|
`GET /admin/hotel/items` 的 records 每一项新增 `district` 字段,前端表格可直接展示区/县列。其他字段保持不变。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hotelId": "3001000000000000001",
|
||||||
|
"name": "XXX 酒店",
|
||||||
|
"city": "呼伦贝尔市",
|
||||||
|
"district": "海拉尔区", // 🆕 新增
|
||||||
|
"hotelType": "HOTEL",
|
||||||
|
"starLevel": "FIVE_STAR",
|
||||||
|
// ... 其他字段不变
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 七、响应 VO
|
||||||
|
|
||||||
|
### `HotelDistrictOptionVO`
|
||||||
|
|
||||||
|
```java
|
||||||
|
{
|
||||||
|
"value": "海拉尔区", // 区/县名称(与 label 相同)
|
||||||
|
"label": "海拉尔区" // 区/县名称
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
简单键值对,便于直接丢到 Element UI / Ant Design 的下拉组件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、不兼容变更
|
||||||
|
|
||||||
|
**无**。列表接口只是新增可选参数,字典接口是全新路径,老前端零影响。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、回归验证(已在测试环境实测)
|
||||||
|
|
||||||
|
- ✅ `/admin/hotel/districts` 无 city → 返 4 个值
|
||||||
|
- ✅ `/admin/hotel/districts?city=呼伦贝尔市` → 返 3 个值(city 精确匹配生效)
|
||||||
|
- ✅ `/admin/hotel/items` records 37/39 含 district 字段(2 个脏数据是 null 属正常)
|
||||||
|
- ✅ `/admin/hotel/items?district=海拉尔区` → 8 条过滤生效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、自行验证
|
||||||
|
|
||||||
|
测试环境部署完成后,用 admin token 验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 取 token
|
||||||
|
TOKEN=$(curl -s "https://api.test.1814.love/admin/auth/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username":"test_admin","password":"Test@2026"}' | jq -r .data.token)
|
||||||
|
|
||||||
|
# 1. 字典接口(带 city)
|
||||||
|
curl "https://api.test.1814.love/admin/hotel/districts?city=海拉尔" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq .
|
||||||
|
|
||||||
|
# 2. 字典接口(不带 city,跨城市聚合)
|
||||||
|
curl "https://api.test.1814.love/admin/hotel/districts" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data | length'
|
||||||
|
|
||||||
|
# 3. 列表接口带 district
|
||||||
|
curl "https://api.test.1814.love/admin/hotel?city=海拉尔&district=海拉尔区&page=1&pageSize=5" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data.records[] | {name, city, district}'
|
||||||
|
|
||||||
|
# 4. 列表接口老用法(不带 district,兼容验证)
|
||||||
|
curl "https://api.test.1814.love/admin/hotel?city=海拉尔&page=1&pageSize=5" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data.total'
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期**:
|
||||||
|
- 场景 1:返回该城市下已上架酒店的 distinct district 列表
|
||||||
|
- 场景 2:返回所有城市聚合后的 distinct district
|
||||||
|
- 场景 3:返回该城市该区的酒店
|
||||||
|
- 场景 4:行为与改动前完全一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、不涉及的
|
||||||
|
|
||||||
|
- **小程序端(`/internal/mp/hotel/**`)不变**:本次只改管理端,小程序端若后续要区/县筛选走独立 PR
|
||||||
|
- **DB schema 不变**:`hotel.district VARCHAR(32)` 本来就有
|
||||||
|
- **字典维护页不需要新建**:数据源是 distinct 查实际酒店数据,不是独立字典表
|
||||||
正在加载...
x
在新工单中引用
屏蔽一个用户