Files
WonderQ-Admin/docs/miniapp-public-api.md
duanshuwen 1aac3c64b2 feat(hotelGroups): add offer display fields and related logic
- Add new database columns (coverImage, priceAmount, priceUnit, tags, status) to HotelGroup model
- Create hotel_group_dict serializer to handle image/coverImage synchronization and tag formatting
- Update site config endpoints to use the new serializer and filter published hotel groups
- Add Alembic migration for the new database schema changes
- Update test fixtures and API contract tests for the new fields
- Revise public and admin API documentation to document the new hotel group fields and usage rules
2026-07-03 22:44:57 +08:00

522 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WonderQ-MiniAPP Public API 对接文档
最后更新2026-07-02
本文档定义 `WonderQ-MiniAPP` 前台 H5/小程序对接 `WonderQ-Admin` 后端所需的 Public API 契约。当前 MiniAPP 主动调用站点配置、产品列表和线索提交 3 个接口;后端已存在的健康检查、产品详情和目的地列表接口建议继续保留,供后续前台按需接入。
## 基础约定
- 基础地址由 MiniAPP 环境变量 `VITE_API_BASE_URL` 控制;为空时前台按同源 `/api` 请求。
- Public API 不要求前台登录认证。
- 请求和响应均使用 JSON字符集为 UTF-8。
- 图片字段应返回可被 H5 和微信小程序访问的 URL现有前台兼容 `/assets/...` 形式。
- 列表字段建议返回空数组,不建议返回 `null`MiniAPP 对站点配置和产品列表有本地兜底内容。
- 错误响应需提供可展示信息,兼容 `{ "message": "..." }` 或 FastAPI 默认 `{ "detail": "..." }`。不要暴露内部异常、真实环境变量、Token、JWT secret、客服链接或企业 ID。
## 数据类型
### `HeroSlide`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 轮播图 ID |
| `title` | `string` | 是 | 主标题 |
| `kicker` | `string \| null` | 否 | 辅助短文案 |
| `image` | `string` | 是 | 图片 URL |
| `targetType` | `string \| null` | 否 | 点击目标类型 |
| `targetValue` | `string \| null` | 否 | 点击目标值 |
| `isActive` | `boolean` | 否 | 是否启用 |
| `sortOrder` | `number` | 否 | 后台排序值Public API 按该字段升序输出 |
### `Destination`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 目的地 ID |
| `name` | `string` | 是 | 目的地名称 |
| `image` | `string \| null` | 否 | 图片 URL |
| `isHot` | `boolean` | 否 | 是否热门 |
| `isActive` | `boolean` | 否 | 是否启用 |
| `sortOrder` | `number` | 否 | 后台排序值Public API 按该字段升序输出 |
| `aliases` | `Array<{ id: string; alias: string }>` | 否 | 搜索别名 |
### `Theme`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 主题 ID |
| `label` | `string` | 是 | 主题名称 |
| `image` | `string` | 是 | 主题图片 URL |
| `targetType` | `string \| null` | 否 | 点击目标类型 |
| `targetValue` | `string \| null` | 否 | 点击目标值 |
| `isActive` | `boolean` | 否 | 是否启用 |
| `sortOrder` | `number` | 否 | 后台排序值Public API 按该字段升序输出 |
### `CtaBanner`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 服务卡片 ID |
| `alt` | `string` | 是 | 服务标题,展示在“更多服务”卡片上 |
| `image` | `string` | 是 | 服务卡片背景图 URL |
| `targetType` | `string \| null` | 否 | 点击目标类型 |
| `targetValue` | `string \| null` | 否 | 点击目标值 |
| `isActive` | `boolean` | 否 | 是否启用 |
| `sortOrder` | `number` | 否 | 后台排序值Public API 按该字段升序输出 |
### `Campaign`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 活动 ID |
| `slug` | `string` | 是 | 活动标识 |
| `title` | `string` | 是 | 活动标题 |
| `description` | `string \| null` | 否 | 活动描述 |
| `coverImage` | `string \| null` | 否 | 活动封面图 |
| `priceAmount` | `number \| null` | 否 | 参考起价,单位按 `priceUnit` 展示 |
| `priceUnit` | `string \| null` | 否 | 价格单位文案,默认 `起/人` |
| `tags` | `string[]` | 否 | 活动卡片标签,最多 3 个 |
| `status` | `string` | 是 | 活动状态Public API 只返回 `published` |
| `startsAt` | `string \| null` | 否 | 活动开始时间 |
| `endsAt` | `string \| null` | 否 | 活动结束时间 |
### `RouteSection`
`RouteSection` 用于描述首页“精选线路”下的动态运营分组。后台可按任务新增、编辑、删除和排序分组MiniAPP 不应依赖固定分组 ID只按接口返回的分组顺序和 `productIds` 渲染。
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 后端生成的分组 ID客户端只用于列表 key 和商品关联,不作为固定业务枚举 |
| `title` | `string` | 是 | 分组标题 |
| `subtitle` | `string \| null` | 否 | 分组副文案 |
| `productIds` | `string[]` | 是 | 该分组包含的产品 ID产品详情来自 `/api/public/products.items` |
| `isActive` | `boolean` | 否 | 是否启用Public API 通常只返回启用分组 |
| `sortOrder` | `number` | 否 | 后台展示顺序Public API 按该字段升序输出 |
Public API 输出规则:
- `GET /api/public/site-config` 只返回启用的 `routeSections`
- `routeSections[].productIds` 只包含已发布商品 ID未发布、归档或不存在的商品不得出现在 Public 响应中。
- 动态分组按后台 `sortOrder` 升序返回,`productIds` 的顺序就是用户侧商品卡展示顺序。
- MiniAPP 会按 `productIds` 匹配 `/api/public/products.items[].id`;接口缺失、`routeSections` 为空或没有可匹配商品时回退 `src/content.ts` 的本地精选线路兜底内容。
### `HomeCard`
`HomeCard` 用于首页“万趣用车”等普通内容卡片模块。MiniAPP 只消费卡片展示字段,不在这些模块里读取商品本体或线路商品关联。
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 后端生成的卡片 ID |
| `title` | `string` | 是 | 卡片标题 |
| `description` | `string \| null` | 否 | 卡片描述 |
| `image` | `string \| null` | 否 | 卡片封面图 URL为空时客户端可使用本地兜底图 |
| `isActive` | `boolean` | 否 | 是否启用Public API 通常只返回启用卡片 |
| `sortOrder` | `number` | 否 | 后台展示顺序Public API 按该字段升序输出 |
### `HotelCard`
`HotelCard` 用于首页“特色酒店”模块。该模块按管理端“特价优惠”同类配置方式维护标题、描述、价格、标签、封面图和发布状态,但仍然只代表首页酒店展示卡片,不绑定商品本体。
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 后端生成的酒店卡片 ID |
| `title` | `string` | 是 | 酒店卡片标题 |
| `description` | `string \| null` | 否 | 酒店卡片描述 |
| `image` | `string \| null` | 否 | 兼容旧字段;后端会与 `coverImage` 保持一致 |
| `coverImage` | `string \| null` | 否 | 酒店封面图主字段MiniAPP 优先使用该字段 |
| `priceAmount` | `number \| null` | 否 | 价格数值,前端可按页面需要展示 |
| `priceUnit` | `string \| null` | 否 | 价格单位文案,例如 `起/晚` |
| `tags` | `string[]` | 否 | 酒店标签,最多 3 个 |
| `status` | `"draft" \| "published"` | 否 | 发布状态Public API 只返回 `published` |
| `isActive` | `boolean` | 否 | 是否启用Public API 只返回启用项 |
| `sortOrder` | `number` | 否 | 后台展示顺序Public API 按该字段升序输出 |
Public API 输出规则:
- `GET /api/public/site-config` 只返回 `status="published"``isActive=true``hotelGroups`
- `GET /api/public/site-config` 只返回启用的 `vehicleOptions`
- 两个数组按后台 `sortOrder` 升序返回。
- `hotelGroups` 字段缺失、数组为空或没有可用图片时MiniAPP 使用 `src/content.ts` 的本地特色酒店内容兜底;酒店图片优先取 `coverImage`,再取 `image`
- `vehicleOptions` 字段缺失、数组为空或图片为空时MiniAPP 使用 `src/content.ts` 的本地万趣用车内容兜底。
### `PublicProduct`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 后端产品 UUID |
| `sourceId` | `number \| null` | 否 | 历史采集产品 ID前台可用于兼容旧数据 |
| `title` | `string` | 是 | 产品标题 |
| `subtitle` | `string \| null` | 否 | 副标题 |
| `destination` | `{ id: string; name: string } \| null` | 否 | 目的地信息 |
| `priceAmount` | `number \| null` | 否 | 参考起价,单位按后端内容约定 |
| `priceUnit` | `string \| null` | 否 | 价格单位文案 |
| `tags` | `string[]` | 否 | 标签列表 |
| `coverImage` | `string \| null` | 否 | 封面图 URL |
| `summary` | `string \| null` | 否 | 摘要 |
| `images` | `Array<ProductImage>` | 否 | 图集 |
| `detailSections` | `ProductDetailSection[] \| null` | 否 | 产品详情分区 |
| `status` | `string` | 否 | 产品状态,前台主要消费 `published` 内容 |
### `ProductImage`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 否 | 图片 ID |
| `url` | `string` | 是 | 图片 URL |
| `alt` | `string \| null` | 否 | 图片说明 |
| `sortOrder` | `number` | 是 | 排序值 |
### `ProductDetailSection`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `key` | `string` | 是 | 分区 key例如 `overview``itinerary``service``notice``price``manager` |
| `label` | `string` | 是 | Tab 展示文案 |
| `title` | `string \| null` | 否 | 分区标题 |
| `blocks` | `ProductDetailBlock[]` | 是 | 内容块 |
`ProductDetailBlock` 支持两种结构:
```json
{ "type": "text", "text": "文本内容" }
```
```json
{ "type": "image", "url": "/assets/example.jpg", "alt": "图片说明" }
```
## 接口清单
### `GET /health`
用于服务健康检查。
#### 响应示例
```json
{
"ok": true,
"service": "miniapp-api"
}
```
### `GET /api/public/site-config`
用于首页轮播、目的地、主题入口和更多服务配置。MiniAPP 启动时会和产品列表并行请求该接口;接口不可用或关键数组为空时,前台会回退本地静态内容。
#### 响应字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `heroSlides` | `HeroSlide[]` | 首页顶部轮播 |
| `destinations` | `Destination[]` | 首页目的地入口 |
| `map` | `Array<{ id: string; image: string; isActive?: boolean }>` | 贵州地图图片MiniAPP 当前消费 `map[0].image` |
| `themes` | `Theme[]` | 主题甄选入口 |
| `ctaBanners` | `CtaBanner[]` | “更多服务”卡片配置 |
| `campaigns` | `Campaign[]` | 活动元信息,可用于“特价优惠”入口;当前不包含活动产品结果列表 |
| `routeSections` | `RouteSection[]` | “精选线路”子分组定义 |
| `hotelGroups` | `HotelCard[]` | “特色酒店”卡片配置,只返回 `published` 且启用项 |
| `vehicleOptions` | `HomeCard[]` | “万趣用车”卡片配置,只返回启用项 |
#### 首页模块数据归属
| 首页模块 | 当前接口归属 | 说明 |
| --- | --- | --- |
| 特价优惠 | `site-config.campaigns` + `/api/public/products` | 当前 Public API 只返回活动元信息不直接返回“特价优惠结果列表”。MiniAPP 若要展示活动线路,可按活动标题、标签或后续扩展的活动产品关联从 `/api/public/products` 中筛选。 |
| 精选线路 | `site-config.routeSections` + `/api/public/products` | `routeSections` 返回动态分组与 `productIds`;具体产品卡片数据由 `/api/public/products.items` 提供。后台可按任务新增、删除、停用和排序分组,用户侧不假设固定三组。 |
| 更多服务 | `site-config.ctaBanners` | 返回启用服务卡片,按后台排序展示;无有效配置时回退本地 `bottomCtas` 内容。 |
| 特色酒店 | `site-config.hotelGroups` | 返回 `published` 且启用的酒店卡片按后台排序展示MiniAPP 优先消费 `coverImage`,无有效配置时回退本地内容。 |
| 万趣用车 | `site-config.vehicleOptions` | 返回启用用车卡片,按后台排序展示;无有效配置时回退本地内容。 |
#### 响应示例
```json
{
"heroSlides": [
{
"id": "hero-1",
"title": "贵州小包团定制",
"kicker": "万趣,你的小包团首选",
"image": "/assets/guizhou/libo-xiaoqikong.jpg",
"targetType": "search",
"targetValue": "贵州",
"isActive": true
}
],
"destinations": [
{
"id": "dest-1",
"name": "荔波小七孔",
"image": "/assets/guizhou/libo-xiaoqikong.jpg",
"isHot": true,
"isActive": true,
"aliases": [{ "id": "alias-1", "alias": "小七孔" }]
}
],
"map": [
{
"id": "map-1",
"image": "/assets/guizhou/guizhou-map.jpg",
"isActive": true
}
],
"themes": [],
"ctaBanners": [],
"campaigns": [
{
"id": "campaign-1",
"slug": "classic-deal",
"title": "经典打卡特惠",
"description": "经典首游活动",
"coverImage": "/assets/guizhou/libo-xiaoqikong.jpg",
"priceAmount": 162500,
"priceUnit": "起/人",
"tags": ["臻藏旅位", "赛事庆典"],
"status": "published",
"startsAt": null,
"endsAt": null
}
],
"routeSections": [
{
"id": "route-section-001",
"title": "经典人文打卡线路",
"subtitle": "黄果树、荔波小七孔、千户苗寨、镇远古城、梵净山一次串联",
"productIds": ["8a6e7c4f-0000-4000-9000-000000000001"],
"isActive": true
},
{
"id": "route-section-002",
"title": "极限山野户外野咖线路",
"subtitle": "溶洞、峡谷、漂流、峰林骑行和山野咖啡组合",
"productIds": [],
"isActive": true
},
{
"id": "route-section-003",
"title": "人文+户外综合混搭线路",
"subtitle": "非遗村寨、古城夜游、自然轻探险和精品住宿同程安排",
"productIds": [],
"isActive": true
}
],
"hotelGroups": [
{
"id": "hotel-group-001",
"title": "经典酒店",
"description": "城市接驳、景区度假和温泉休整,适合首游贵州的小包团动线。",
"image": "/assets/guizhou/bailian-hot-spring.jpg",
"coverImage": "/assets/guizhou/bailian-hot-spring.jpg",
"priceAmount": 68000,
"priceUnit": "起/晚",
"tags": ["温泉", "亲子"],
"status": "published",
"isActive": true,
"sortOrder": 0
}
],
"vehicleOptions": [
{
"id": "vehicle-option-001",
"title": "5座舒适用车",
"description": "适合2-4人家庭或好友小团城市接送、景区穿梭更灵活。",
"image": "/assets/guizhou/jiaxiu-tower.jpg",
"isActive": true,
"sortOrder": 0
}
]
}
```
### `GET /api/public/products`
用于首页产品分区、搜索结果、活动页、目的地页、详情推荐和预订入口。当前 MiniAPP 一次拉取列表后在前端做搜索、筛选和推荐。
#### Query 参数
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `keyword` | `string` | 否 | 无 | 关键词搜索,建议匹配产品标题、副标题、标签、目的地名称和目的地别名 |
| `destinationId` | `string` | 否 | 无 | 按目的地 ID 筛选 |
| `status` | `string` | 否 | `published` | 产品状态 |
| `take` | `number` | 否 | `48` | 返回数量,后端当前限制 1-100 |
#### 响应字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `items` | `PublicProduct[]` | 产品列表 |
#### 响应示例
```json
{
"items": [
{
"id": "8a6e7c4f-0000-4000-9000-000000000001",
"sourceId": 101,
"title": "黄果树瀑布小包团",
"subtitle": "错峰入园,私家车接送",
"destination": { "id": "dest-anshun", "name": "黄果树" },
"priceAmount": 398000,
"priceUnit": "起/人",
"tags": ["贵州", "黄果树", "小包团"],
"coverImage": "/assets/guizhou/huangguoshu.jpg",
"summary": "适合首次到贵州的经典线路。",
"images": [
{
"id": "img-1",
"url": "/assets/guizhou/huangguoshu.jpg",
"alt": "黄果树瀑布",
"sortOrder": 0
}
],
"detailSections": [
{
"key": "overview",
"label": "行程概述",
"title": "小包团专属概览",
"blocks": [{ "type": "text", "text": "按同行人、预算和体力强度重排行程。" }]
}
],
"status": "published"
}
]
}
```
### `GET /api/public/products/{product_id}`
后端已存在,建议保留给 MiniAPP 后续详情页按需拉取。当前 MiniAPP 主要通过产品列表缓存进入详情。
#### Path 参数
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `product_id` | `string` | 产品 UUID如果传入纯数字后端按 `sourceId` 查询 |
#### 成功响应
返回单个 `PublicProduct`
#### 异常响应
| 状态码 | 说明 |
| --- | --- |
| `404` | 产品不存在 |
示例:
```json
{
"detail": "线路不存在"
}
```
### `GET /api/public/destinations`
后端已存在,建议保留给 MiniAPP 后续目的地页独立拉取。当前 MiniAPP 首页目的地来自 `site-config.destinations`
#### 响应示例
```json
{
"items": [
{
"id": "dest-libo",
"name": "荔波小七孔",
"slug": "libo-xiaoqikong",
"region": "黔南",
"image": "/assets/guizhou/libo-xiaoqikong.jpg",
"isHot": true,
"sortOrder": 0,
"isActive": true,
"aliases": [{ "id": "alias-1", "alias": "小七孔" }]
}
]
}
```
### `POST /api/public/leads`
用于首页快速定制、搜索页快速定制、需求页和预订咨询页提交线索。
#### 请求字段
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `phone` | `string` | 是 | 联系方式。前台输入文案为“手机号 / 微信号”,后端当前会去除首尾空白并压缩连续空格 |
| `destination` | `string` | 否 | 目的地或玩法 |
| `travelDate` | `string` | 否 | 出行日期MiniAPP 当前传 `YYYY-MM-DD` 字符串 |
| `peopleCount` | `number` | 否 | 出行人数,需大于 0 |
| `budgetMin` | `number` | 否 | 最低预算,需大于等于 0 |
| `budgetMax` | `number` | 否 | 最高预算,需大于等于 0 |
| `note` | `string` | 否 | 补充说明,后端当前限制最长 1000 字符 |
| `sourcePage` | `string` | 否 | 来源页面 |
| `sourceProductId` | `string` | 否 | 来源产品 UUID |
#### `sourcePage` 当前取值
| 值 | 来源 |
| --- | --- |
| `home_inline` | 首页快速定制入口 |
| `search_inline` | 搜索结果页快速定制入口 |
| `demand_page` | 提交需求页 |
| `product_consult` | 产品预订咨询页 |
#### 请求示例
```json
{
"destination": "荔波小七孔",
"phone": "187 8617 4929",
"travelDate": "2027-01-01",
"peopleCount": 2,
"note": "咨询线路:黄果树瀑布小包团;方案偏好:经典人文",
"sourcePage": "product_consult",
"sourceProductId": "8a6e7c4f-0000-4000-9000-000000000001"
}
```
#### 成功响应
状态码:`201`
```json
{
"id": "lead-uuid",
"status": "new"
}
```
#### 常见异常
| 状态码 | 场景 |
| --- | --- |
| `422` | 请求体校验失败,例如 `phone` 为空、`peopleCount` 小于等于 0、`note` 超长 |
| `500` | 服务端异常,响应不得暴露内部细节 |
## MiniAPP 当前依赖说明
- `site-config``products` 会在应用启动时并行请求任一请求失败时MiniAPP 会回退到本地静态内容。
- `products.items` 为空时MiniAPP 会使用本地产品兜底数据。
- “精选线路”由 `site-config.routeSections` 定义动态分组标题、副文案和商品 ID 顺序,由 `/api/public/products.items` 提供产品详情;客户端不依赖固定分组 ID 或固定三组数量。
- `routeSections` 缺失、为空或无法匹配到有效商品时MiniAPP 使用 `src/content.ts` 的本地精选线路内容回退。
- `ctaBanners` 缺失或为空时MiniAPP 使用 `src/content.ts` 的本地 `bottomCtas` 内容回退。
- “特色酒店”和“万趣用车”分别由 `site-config.hotelGroups``site-config.vehicleOptions` 提供;特色酒店优先使用 `coverImage`,字段缺失、数组为空或图片为空时使用本地内容兜底。
- “特价优惠”当前没有独立 Public 结果列表字段;`site-config.campaigns` 只提供活动元信息,活动线路需通过产品标签/关键词筛选或后续扩展活动产品关联字段。
- 产品搜索当前主要在前端执行,依赖 `title``tags``destination.name``summary`
- 产品详情页当前使用已加载的产品列表数据;后续可改为进入详情页时请求 `GET /api/public/products/{product_id}`
- 收藏、浏览历史和最近咨询记录由 MiniAPP 本地存储处理,不需要后端接口。
- 企业微信客服由 MiniAPP 环境变量控制,不属于 `WonderQ-Admin` Public API文档和接口不得写入真实链接或企业 ID。
## 后端验证建议
-`GET /health` 增加或保留健康检查测试。
-`GET /api/public/site-config` 验证返回 JSON 包含 `heroSlides``destinations``map``themes``ctaBanners``campaigns``routeSections``hotelGroups``vehicleOptions` 数组字段。
-`GET /api/public/site-config` 验证 `routeSections` 表达“精选线路”子分组;接口返回当前已配置且启用的分组,未配置时返回空数组并由 MiniAPP 本地内容兜底。
-`GET /api/public/site-config` 验证 `routeSections` 只返回启用分组,且 `productIds` 不包含未发布商品。
-`GET /api/public/site-config` 验证 `hotelGroups` 只返回 `published` 且启用卡片,包含 `coverImage`、价格和标签字段;验证 `vehicleOptions` 只返回启用卡片,并按 `sortOrder` 升序。
-`GET /api/public/products` 验证响应结构为 `{ items: [...] }`,并覆盖 `keyword``destinationId``status``take` 参数。
-`GET /api/public/products/{product_id}` 验证 UUID、数字 `sourceId` 和 404 场景。
-`GET /api/public/destinations` 验证只返回启用目的地及别名字段。
-`POST /api/public/leads` 验证成功创建、`phone` 规范化、必填校验、人数/预算边界和备注长度限制。