feat: Add WonderQ-MiniAPP Public API documentation

- Introduced a comprehensive API contract for the WonderQ-MiniAPP, detailing endpoints for site configuration, product listings, and lead submissions.
- Defined data types for various entities including HeroSlide, Destination, Theme, CtaBanner, PublicProduct, and more.
- Specified request and response formats, including error handling guidelines.

chore: Update requirements to include python-multipart

- Added python-multipart dependency to requirements.txt for handling file uploads.

test: Implement API contract tests

- Created test suite for API contracts, validating serializers and endpoints for public products and leads.
- Included tests for destination and product serializers, ensuring correct data handling and validation.

test: Add configuration tests for OSS settings

- Implemented tests to verify that OSS settings are correctly loaded from environment variables.
This commit is contained in:
duanshuwen
2026-07-01 16:55:00 +08:00
parent 75f5a5a48b
commit a47b4b5dd0
14 changed files with 2210 additions and 104 deletions

353
docs/miniapp-public-api.md Normal file
View File

@@ -0,0 +1,353 @@
# WonderQ-MiniAPP Public API 对接文档
最后更新2026-06-30
本文档定义 `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` | 否 | 是否启用 |
### `Destination`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 目的地 ID |
| `name` | `string` | 是 | 目的地名称 |
| `image` | `string \| null` | 否 | 图片 URL |
| `isHot` | `boolean` | 否 | 是否热门 |
| `isActive` | `boolean` | 否 | 是否启用 |
| `aliases` | `Array<{ id: string; alias: string }>` | 否 | 搜索别名 |
### `Theme`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | 主题 ID |
| `label` | `string` | 是 | 主题名称 |
| `image` | `string` | 是 | 图片 URL |
| `targetType` | `string \| null` | 否 | 点击目标类型 |
| `targetValue` | `string \| null` | 否 | 点击目标值 |
| `isActive` | `boolean` | 否 | 是否启用 |
### `CtaBanner`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | `string` | 是 | Banner ID |
| `alt` | `string` | 是 | 图片替代文案 |
| `image` | `string` | 是 | 图片 URL |
| `targetType` | `string \| null` | 否 | 点击目标类型 |
| `targetValue` | `string \| null` | 否 | 点击目标值 |
| `isActive` | `boolean` | 否 | 是否启用 |
### `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`
用于首页轮播、目的地、主题入口和底部 CTA 配置。MiniAPP 启动时会和产品列表并行请求该接口;接口不可用或关键数组为空时,前台会回退本地静态内容。
#### 响应字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `heroSlides` | `HeroSlide[]` | 首页顶部轮播 |
| `destinations` | `Destination[]` | 首页目的地入口 |
| `themes` | `Theme[]` | 主题甄选入口 |
| `ctaBanners` | `CtaBanner[]` | 底部 CTA Banner |
| `campaigns` | `unknown[]` | 后端现有扩展字段,可保留 |
| `routeSections` | `Array<{ id: string; title: string; productIds: string[] }>` | 后端现有扩展字段,可保留 |
#### 响应示例
```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": "小七孔" }]
}
],
"themes": [],
"ctaBanners": []
}
```
### `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 会使用本地产品兜底数据。
- 产品搜索当前主要在前端执行,依赖 `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``themes``ctaBanners` 数组字段。
-`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` 规范化、必填校验、人数/预算边界和备注长度限制。