Files
WonderQ-Project/docs/wanfa-api.md
duanshuwen e082bd2d98 feat(api): 实现三端统一的JSON API响应契约
- 新增`api_response.py`统一响应封装工具类,提供标准成功/错误响应构造方法
- 重构WonderQ-Admin全局异常处理器,将所有异常转换为标准响应格式
- 修改所有公共和管理端接口的返回逻辑,统一使用`code`(与HTTP状态码一致)、`msg`和`data`的三层结构
- 新增`api-response-contract.md`文档,定义完整的三端统一JSON响应规范
- 更新所有领域API文档,明确业务数据需位于`data`字段内,补充响应格式说明
- 为WonderQ-MiniAPP和WonderQ-Admin-UI新增响应解析逻辑和类型定义,自动完成协议校验和错误处理
- 更新所有测试用例,适配新的响应结构确保接口符合契约要求
- 新增`module-config-api.md`模块配置API文档,补充站点模块配置的接口约定
- 更新项目README文档,调整文档分类顺序将响应契约置于首位
2026-08-19 22:02:20 +08:00

350 lines
12 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.

# 玩法管理 Admin API
> 适用端:`WonderQ-Admin`、`WonderQ-Admin-UI`。
>
> 状态:已实现契约。本文件参考 `WonderQ-MiniAPP/src/pages/play/components/playData.ts` 定义玩法分类和路线的数据结构,以及管理端需要的查询、维护和排序接口。它不是 MiniAPP Public API 文档。
当前实现:`WonderQ-Admin` 通过迁移 `0015_wanfa` 创建 `WanfaCategory``WanfaRoute` 表并导入稳定初始 ID`WonderQ-Admin-UI` 已接入分类和路线的查询、新增、编辑、删除及排序操作。
所有 JSON 响应遵循 [三端统一 API 响应契约](./api-response-contract.md),成功业务对象位于 `data`,失败时 `data``null`
路线接口只维护分类和路线摘要字段。路线详情不写入 `WanfaRoute`,由独立 `DetailRecord` 通过 `docs/detail-api.md` 管理,详情记录的 `key` 等于路线 ID。首页玩法推荐和玩法页路线点击后统一跳转 `/pages/detail/index?routeId={route.id}`;无关联路线时才回退到需求页。
## 领域边界
玩法管理只维护玩法分类和路线卡片内容:
- 分类名称和展示顺序。
- 路线标题、副标题、封面、路线数量和需求关键词。
- 分类与路线的新增、编辑、删除和排序。
- 首页玩法推荐与分类的关联由首页内容域维护,本域只提供可被关联的分类和路线数据。
玩法领域不负责:
- 商品、商品详情、预订或订单。
- 目的地实体和目的地维护。
- 线索创建或线索跟进。
- Product、ProductImage 或其他已移除商品关联表。
`routeCount` 只是前台卡片展示数量,不是 Product 表的外键或实时关联统计。
## 接口清单
API 前缀为 `/api/admin`,除登录接口外均需要后台 JWT。
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/admin/wanfa/categories` | 获取全部玩法分类及其路线 |
| `POST` | `/api/admin/wanfa/categories` | 新增玩法分类 |
| `PATCH` | `/api/admin/wanfa/categories/{categoryId}` | 编辑玩法分类 |
| `DELETE` | `/api/admin/wanfa/categories/{categoryId}` | 删除玩法分类 |
| `PATCH` | `/api/admin/wanfa/categories/reorder` | 调整玩法分类顺序 |
| `POST` | `/api/admin/wanfa/categories/{categoryId}/routes` | 新增分类路线 |
| `PATCH` | `/api/admin/wanfa/categories/{categoryId}/routes/{routeId}` | 编辑分类路线 |
| `DELETE` | `/api/admin/wanfa/categories/{categoryId}/routes/{routeId}` | 删除分类路线 |
| `PATCH` | `/api/admin/wanfa/categories/{categoryId}/routes/reorder` | 调整分类内路线顺序 |
## 通用约定
- 请求和响应使用 JSON字段使用 camelCase。
- 所有管理接口需要 `Authorization: Bearer <admin-jwt>`
- 创建、编辑、删除和排序成功后写入审计日志,再提交事务。
- 变更接口返回最新变更对象;排序接口返回排序后的 `items`
- ID 由后端生成并作为非空字符串返回。初始化迁移时应优先保留 `playData.ts` 中已有的稳定 ID。
- 分类和路线的数组顺序就是管理端和前台的展示顺序,接口不要求前端依赖 `sortOrder` 字段。
- 空集合返回 `[]`,不能返回 `null` 或省略字段。
- 成功结果放入 `data`;失败返回数字 `code``msg``data: null`,可选 `errorCode``details`
- 被首页玩法推荐关联的分类不能直接删除;需先调用首页内容域的移除关联接口,否则返回 `409 WANFA_CATEGORY_RECOMMENDED`
## 数据类型
以下类型与 `playData.ts` 保持字段兼容。管理端接口不应向前台模型强制增加商品 ID、预订 ID 或数据库关联字段。
```ts
type WanfaConfig = {
categories: WanfaCategory[];
};
type WanfaCategory = {
id: string;
label: string;
routes: WanfaRoute[];
};
type WanfaRoute = {
id: string;
title: string;
subtitle: string;
image: string;
routeCount: number;
demandKeyword: string;
};
type WanfaCategoryCreate = {
label: string;
};
type WanfaCategoryPatch = {
label?: string;
};
type WanfaRouteCreate = {
title: string;
subtitle: string;
image: string;
routeCount: number;
demandKeyword: string;
};
type WanfaRoutePatch = Partial<WanfaRouteCreate>;
type WanfaReorderRequest = {
itemIds: string[];
};
```
## 字段约束
| 字段 | 类型 | 必填 | 约束和用途 |
| --- | --- | --- | --- |
| `category.id` | `string` | 响应必填 | 分类稳定标识,例如 `family-route`。创建时由后端生成。 |
| `category.label` | `string` | 是 | 左侧分类显示名称,去除首尾空白后不得为空。 |
| `category.routes` | `WanfaRoute[]` | 响应必填 | 当前分类下的路线,按展示顺序返回。 |
| `route.id` | `string` | 响应必填 | 路线稳定标识,例如 `family-water`。创建时由后端生成。 |
| `route.title` | `string` | 是 | 路线卡片标题,去除首尾空白后不得为空。 |
| `route.subtitle` | `string` | 是 | 路线卡片副标题或目的地说明。 |
| `route.image` | `string` | 是 | 可直接用于图片组件的封面 URL。 |
| `route.routeCount` | `number` | 是 | 卡片右下角展示的路线数量,服务端应校验为不小于 0 的整数。 |
| `route.demandKeyword` | `string` | 是 | 跳转需求页时可使用的预填关键词,去除首尾空白后不得为空。 |
字段最大长度应由后端 schema 统一定义,并同步到 Admin UI 表单校验;在未形成统一长度常量前,前端不能通过截断文本代替服务端校验。
## 接口详情
### 获取分类及路线
```http
GET /api/admin/wanfa/categories
Authorization: Bearer <admin-jwt>
```
成功响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"categories": [
{
"id": "family-route",
"label": "亲子路线",
"routes": [
{
"id": "family-water",
"title": "亲子玩水",
"subtitle": "贵州·轻松节奏与自然课堂",
"image": "https://example.test/assets/family-water.jpg",
"routeCount": 4,
"demandKeyword": "亲子玩水"
}
]
}
]
}
}
```
### 新增分类
```http
POST /api/admin/wanfa/categories
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求:
```json
{ "label": "亲子路线" }
```
成功返回 `201``data` 内的新分类对象,初始 `routes``[]`,并追加到分类列表末尾。
### 编辑分类
```http
PATCH /api/admin/wanfa/categories/{categoryId}
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求只允许修改分类名称:
```json
{ "label": "家庭路线" }
```
成功返回 `data` 内的更新后 `WanfaCategory`。不存在的分类返回 `404`,业务码为 `WANFA_CATEGORY_NOT_FOUND`
### 删除分类
```http
DELETE /api/admin/wanfa/categories/{categoryId}
Authorization: Bearer <admin-jwt>
```
为避免误删路线,分类仍包含路线时不得级联删除,应返回 `409`,业务码为 `WANFA_CATEGORY_NOT_EMPTY`。删除前由 Admin UI 提示先移除分类内路线。删除成功返回:
```json
{
"code": 200,
"msg": "success",
"data": { "id": "family-route" }
}
```
### 分类排序
```http
PATCH /api/admin/wanfa/categories/reorder
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求必须完整包含当前全部分类 ID不能重复
```json
{ "itemIds": ["photo-route", "family-route", "healing-route"] }
```
成功返回:
```json
{
"code": 200,
"msg": "success",
"data": { "items": [] }
}
```
其中 `items` 为排序后的 `WanfaCategory[]`。缺少 ID、包含未知 ID 或出现重复 ID 时返回 `400`,业务码为 `WANFA_CATEGORY_REORDER_INVALID`
### 新增路线
```http
POST /api/admin/wanfa/categories/{categoryId}/routes
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求:
```json
{
"title": "亲子玩水",
"subtitle": "贵州·轻松节奏与自然课堂",
"image": "https://example.test/assets/family-water.jpg",
"routeCount": 4,
"demandKeyword": "亲子玩水"
}
```
成功返回 `201``data` 内新建的 `WanfaRoute`,并追加到对应分类路线末尾。分类不存在返回 `404`,业务码为 `WANFA_CATEGORY_NOT_FOUND`
### 编辑路线
```http
PATCH /api/admin/wanfa/categories/{categoryId}/routes/{routeId}
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求体为 `WanfaRoutePatch`,只更新提交的字段。成功返回 `data` 内更新后的 `WanfaRoute`;分类或路线不存在时分别返回 `404`,业务码为 `WANFA_CATEGORY_NOT_FOUND``WANFA_ROUTE_NOT_FOUND`
### 删除路线
```http
DELETE /api/admin/wanfa/categories/{categoryId}/routes/{routeId}
Authorization: Bearer <admin-jwt>
```
成功返回 `data: { "id": "..." }`。删除后,分类内路线保持原有相对顺序。
### 分类内路线排序
```http
PATCH /api/admin/wanfa/categories/{categoryId}/routes/reorder
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求必须完整包含该分类当前全部路线 ID不能重复
```json
{ "itemIds": ["family-grassland", "family-water", "family-village"] }
```
成功返回排序后的路线:
```json
{
"code": 200,
"msg": "success",
"data": { "items": [] }
}
```
分类不存在返回 `404`,业务码为 `WANFA_CATEGORY_NOT_FOUND`;路线 ID 不完整、重复或不属于该分类时返回 `400`,业务码为 `WANFA_ROUTE_REORDER_INVALID`
## 当前本地数据映射
迁移初始数据时,分类和路线应按以下 ID 与顺序导入:
| 分类 ID | 分类名称 | 路线 ID当前顺序 |
| --- | --- | --- |
| `family-route` | 亲子路线 | `family-water``family-village``family-grassland` |
| `photo-route` | 旅拍路线 | `miao-photo``peak-photo``terrace-photo` |
| `healing-route` | 疗愈路线 | `mountain-healing``hot-spring-healing``river-healing` |
| `team-building` | 团建 | `team-challenge``team-stream``team-culture` |
| `guizhou-panorama` | 贵州全景 | `classic-panorama``mountain-panorama``wild-panorama` |
| `private-custom` | 私人定制 | `private-family``private-business``private-wild` |
图片别名的当前解析规则位于 `playData.ts``routeImageByAsset``resolveRouteImage`:已配置别名解析为完整远程 URL完整 `http` URL 直接使用,其他值按 `/assets/guizhou/{image}.jpg` 解析。Admin API 建议保存最终 URLAdmin UI 通过现有媒体上传接口获取 URL 后再提交 `image`
## Admin UI 对接要求
Admin UI 应按以下方式调用:
1. 进入玩法管理页时调用 `GET /api/admin/wanfa/categories`,以返回数组顺序渲染分类和路线。
2. 添加分类调用 `POST /categories`;编辑和删除分类分别调用对应 `PATCH``DELETE`
3. 添加、编辑和删除路线使用分类嵌套路由。
4. 上移或下移分类时提交完整分类 ID 列表;上移或下移路线时提交完整路线 ID 列表。
5. 每次变更成功后以接口返回数据更新本地状态;必要时重新请求列表,不直接拼接数据库字段。
6. 删除非空分类前展示阻止性提示,不自动级联删除路线。
7. 处理 `401``404``409``422``5xx`,并在保存中禁用重复提交。
建议的 Admin UI API 封装函数:
```ts
getWanfaCategories();
createWanfaCategory(input: WanfaCategoryCreate);
updateWanfaCategory(categoryId: string, input: WanfaCategoryPatch);
deleteWanfaCategory(categoryId: string);
reorderWanfaCategories(itemIds: string[]);
createWanfaRoute(categoryId: string, input: WanfaRouteCreate);
updateWanfaRoute(categoryId: string, routeId: string, input: WanfaRoutePatch);
deleteWanfaRoute(categoryId: string, routeId: string);
reorderWanfaRoutes(categoryId: string, itemIds: string[]);
```
## 后端落地边界
玩法数据由 `WonderQ-Admin``WanfaCategory``WanfaRoute` ORM 模型和 Admin API 负责持久化;`WonderQ-Admin-UI` 负责 API 类型、请求封装、表单、删除确认和排序交互。`playData.ts` 仅作为迁移初始数据来源,不能视为数据库或 API 数据。
相关文档:
- [Admin API 主契约](./admin-api-requirements.md)
- [页面模块配置契约](./module-config-api.md)
- [玩法本地数据](../WonderQ-MiniAPP/src/pages/play/components/playData.ts)