- 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.
507 lines
12 KiB
Markdown
507 lines
12 KiB
Markdown
# WonderQ-Admin-UI Admin API 接口需求
|
||
|
||
本文档用于指导 `WonderQ-Admin` 后端按当前 `WonderQ-Admin-UI` 管理端完成 Admin API 对接。接口需求来源于前端 `src/api.ts` 与 `src/App.tsx` 的实际类型、请求封装和页面调用。
|
||
|
||
## 范围
|
||
|
||
- 本文只覆盖当前后台管理前端必需的 Admin API。
|
||
- 不包含 H5 Public API、订单、媒体库、审计日志、活动专题等后续规划能力。
|
||
- 后端现有 `GET /api/admin/me`、`GET /api/admin/media-assets` 不在当前 UI 必需范围内。
|
||
|
||
## 通用约定
|
||
|
||
- 基础路径:`/api/admin`。
|
||
- 请求与响应均使用 JSON。
|
||
- 除 `POST /api/admin/auth/login` 外,其余接口都需要后台登录态。
|
||
- 登录成功后前端会把返回的 `token` 存入本地,并在后续请求头中发送:
|
||
|
||
```http
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
- 日期时间字段返回 ISO 8601 字符串。
|
||
- ID 字段按字符串处理。
|
||
- 前端错误提示优先读取响应体的 `message` 字段;如后端沿用 FastAPI 默认 `detail`,建议同时兼容输出 `message`,避免管理端展示兜底错误。
|
||
|
||
## 当前对接状态
|
||
|
||
| 接口 | 前端依赖 | 后端现状 | 备注 |
|
||
| --- | --- | --- | --- |
|
||
| `POST /api/admin/auth/login` | 登录页 | 已覆盖 | 返回 token 和 user |
|
||
| `GET /api/admin/dashboard` | 客户端已封装 | 已覆盖 | 当前 UI 暂未展示 |
|
||
| `GET /api/admin/products` | 商品维护、结构维护 | 已覆盖 | UI 当前只传 `keyword` |
|
||
| `POST /api/admin/products` | 新建商品 | 已覆盖 | 返回完整 Product |
|
||
| `PATCH /api/admin/products/{id}` | 编辑商品 | 已覆盖 | 返回完整 Product |
|
||
| `GET /api/admin/destinations` | 商品目的地下拉、目的地页 | 已覆盖 | 需要返回别名和商品数 |
|
||
| `GET /api/admin/site-config` | 首页/目的地/活动结构维护 | 已覆盖 | 需要包含未启用内容 |
|
||
| `PATCH /api/admin/site-config/{module}/{item_id}` | 模块内容编辑 | 已覆盖 | 模块名需保持一致 |
|
||
| `GET /api/admin/leads` | 需求线索页 | 已覆盖 | UI 当前不传筛选参数 |
|
||
| `PATCH /api/admin/leads/{id}/status` | 线索状态流转 | 已覆盖 | UI 更新后会重新拉列表 |
|
||
| `POST /api/admin/publish` | 结构维护发布 | 已覆盖 | UI 使用 `title` 提示发布结果 |
|
||
| `POST /api/admin/reset-guizhou-content` | 贵州内容重置 | 已覆盖 | 高风险操作,需鉴权和审计 |
|
||
|
||
## 枚举
|
||
|
||
### ProductStatus
|
||
|
||
```ts
|
||
type ProductStatus = "draft" | "published" | "archived";
|
||
```
|
||
|
||
### LeadStatus
|
||
|
||
```ts
|
||
type LeadStatus = "new" | "assigned" | "contacted" | "planning" | "won" | "invalid";
|
||
```
|
||
|
||
### SiteModule
|
||
|
||
```ts
|
||
type SiteModule = "heroSlides" | "destinations" | "themes" | "ctaBanners";
|
||
```
|
||
|
||
## 公共数据结构
|
||
|
||
### Destination
|
||
|
||
```ts
|
||
type Destination = {
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
region?: string | null;
|
||
image?: string | null;
|
||
isHot: boolean;
|
||
isActive: boolean;
|
||
sortOrder: number;
|
||
aliases?: Array<{ id: string; alias: string }>;
|
||
_count?: { products: number };
|
||
};
|
||
```
|
||
|
||
### Product
|
||
|
||
```ts
|
||
type Product = {
|
||
id: string;
|
||
sourceId?: number | null;
|
||
title: string;
|
||
subtitle?: string | null;
|
||
priceAmount?: number | null;
|
||
priceUnit: string;
|
||
tags: string[];
|
||
coverImage?: string | null;
|
||
summary?: string | null;
|
||
images?: Array<{ id?: string; url: string; alt?: string | null; sortOrder: number }>;
|
||
detailSections?: ProductDetailSection[] | null;
|
||
status: ProductStatus;
|
||
sortWeight: number;
|
||
updatedAt: string;
|
||
destination?: Destination | null;
|
||
destinationId?: string | null;
|
||
};
|
||
```
|
||
|
||
### ProductDetailSection
|
||
|
||
```ts
|
||
type ProductDetailBlock =
|
||
| { type: "text"; text: string }
|
||
| { type: "image"; url: string; alt?: string | null };
|
||
|
||
type ProductDetailSection = {
|
||
key: string;
|
||
label: string;
|
||
title?: string | null;
|
||
blocks: ProductDetailBlock[];
|
||
};
|
||
```
|
||
|
||
### ProductInput
|
||
|
||
`POST /products` 与 `PATCH /products/{id}` 复用该结构;`PATCH` 可以只提交需要修改的字段。
|
||
|
||
```ts
|
||
type ProductInput = {
|
||
title: string;
|
||
subtitle?: string;
|
||
destinationId?: string | null;
|
||
priceAmount?: number | null;
|
||
priceUnit?: string;
|
||
tags: string[];
|
||
coverImage?: string | null;
|
||
summary?: string | null;
|
||
images?: Array<{ url: string; alt?: string | null; sortOrder: number }>;
|
||
detailSections?: ProductDetailSection[];
|
||
status: ProductStatus;
|
||
sortWeight: number;
|
||
};
|
||
```
|
||
|
||
字段处理要求:
|
||
|
||
- `title` 必填,后端至少应校验非空;当前后端 schema 为最少 2 个字符。
|
||
- `priceAmount` 可为空;不为空时应为大于等于 0 的整数。
|
||
- `priceUnit` 为空时后端默认使用 `起/人`。
|
||
- `images` 保存前按数组顺序重排 `sortOrder`。
|
||
- `detailSections` 中空 key、空 label、空 blocks 的模块不应保存为有效详情模块。
|
||
- 当 `status` 首次变为 `published` 时,后端可写入发布时间。
|
||
|
||
### Lead
|
||
|
||
```ts
|
||
type Lead = {
|
||
id: string;
|
||
destination?: string | null;
|
||
phone: string;
|
||
note?: string | null;
|
||
sourcePage?: string | null;
|
||
status: LeadStatus;
|
||
createdAt: string;
|
||
sourceProduct?: { id: string; title: string } | null;
|
||
assignedUser?: { id: string; name: string } | null;
|
||
};
|
||
```
|
||
|
||
后端可额外返回 `travelDate`、`peopleCount`、`budgetMin`、`budgetMax` 等字段,但以上字段是当前管理端展示所需的最小集合。手机号属于隐私信息,日志、错误和文档示例中不得输出真实号码。
|
||
|
||
### SiteConfig
|
||
|
||
```ts
|
||
type SiteConfig = {
|
||
heroSlides: Array<{
|
||
id: string;
|
||
title: string;
|
||
kicker?: string | null;
|
||
image: string;
|
||
targetType?: string | null;
|
||
targetValue?: string | null;
|
||
isActive: boolean;
|
||
}>;
|
||
destinations: Destination[];
|
||
themes: Array<{
|
||
id: string;
|
||
label: string;
|
||
image: string;
|
||
targetType?: string | null;
|
||
targetValue?: string | null;
|
||
isActive: boolean;
|
||
}>;
|
||
ctaBanners: Array<{
|
||
id: string;
|
||
alt: string;
|
||
image: string;
|
||
targetType: string;
|
||
targetValue?: string | null;
|
||
isActive: boolean;
|
||
}>;
|
||
};
|
||
```
|
||
|
||
### SiteItemPatch
|
||
|
||
```ts
|
||
type SiteItemPatch = {
|
||
title?: string;
|
||
kicker?: string;
|
||
name?: string;
|
||
label?: string;
|
||
alt?: string;
|
||
image?: string | null;
|
||
targetType?: string | null;
|
||
targetValue?: string | null;
|
||
isActive?: boolean;
|
||
};
|
||
```
|
||
|
||
模块字段映射:
|
||
|
||
| module | 可编辑字段 |
|
||
| --- | --- |
|
||
| `heroSlides` | `title`、`kicker`、`image`、`targetType`、`targetValue`、`isActive` |
|
||
| `destinations` | `name`、`image`、`isActive` |
|
||
| `themes` | `label`、`image`、`targetType`、`targetValue`、`isActive` |
|
||
| `ctaBanners` | `alt`、`image`、`targetType`、`targetValue`、`isActive` |
|
||
|
||
## 接口明细
|
||
|
||
### 登录
|
||
|
||
```http
|
||
POST /api/admin/auth/login
|
||
```
|
||
|
||
请求体:
|
||
|
||
```json
|
||
{
|
||
"email": "admin@example.com",
|
||
"password": "example-password"
|
||
}
|
||
```
|
||
|
||
响应体:
|
||
|
||
```ts
|
||
{
|
||
token: string;
|
||
user: {
|
||
id?: string;
|
||
name: string;
|
||
email: string;
|
||
role: string;
|
||
};
|
||
}
|
||
```
|
||
|
||
状态码要求:
|
||
|
||
- `200`:登录成功。
|
||
- `401`:账号不存在、密码错误或账号停用。
|
||
|
||
### 工作台统计
|
||
|
||
```http
|
||
GET /api/admin/dashboard
|
||
```
|
||
|
||
当前前端客户端已封装该接口,但页面暂未展示。后端保持兼容即可。
|
||
|
||
响应体:
|
||
|
||
```ts
|
||
{
|
||
stats: {
|
||
productCount: number;
|
||
publishedProductCount: number;
|
||
destinationCount: number;
|
||
newLeadCount: number;
|
||
leadCount: number;
|
||
campaignCount: number;
|
||
};
|
||
recentLeads: Lead[];
|
||
}
|
||
```
|
||
|
||
### 商品列表
|
||
|
||
```http
|
||
GET /api/admin/products?keyword=<keyword>
|
||
```
|
||
|
||
查询参数:
|
||
|
||
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `keyword` | `string` | 是 | 搜索商品标题、短标题或标签 |
|
||
| `status` | `ProductStatus` | 否 | 后端可支持状态筛选 |
|
||
| `take` | `number` | 否 | 后端当前可限制返回条数 |
|
||
|
||
响应体:
|
||
|
||
```ts
|
||
{
|
||
items: Product[];
|
||
}
|
||
```
|
||
|
||
排序建议:`sortWeight` 升序,再按 `updatedAt` 倒序。结构维护页和商品维护页都会读取该接口。
|
||
|
||
### 新建商品
|
||
|
||
```http
|
||
POST /api/admin/products
|
||
```
|
||
|
||
请求体:`ProductInput`
|
||
|
||
响应体:`Product`
|
||
|
||
状态码要求:
|
||
|
||
- `201`:创建成功。
|
||
- `422`:字段校验失败。
|
||
|
||
### 更新商品
|
||
|
||
```http
|
||
PATCH /api/admin/products/{id}
|
||
```
|
||
|
||
请求体:`Partial<ProductInput>`
|
||
|
||
响应体:`Product`
|
||
|
||
状态码要求:
|
||
|
||
- `200`:更新成功。
|
||
- `404`:商品不存在。
|
||
- `422`:字段校验失败。
|
||
|
||
当前 UI 保存商品后会使用响应体刷新编辑状态,因此后端需要返回完整 Product,而不是只返回成功标记。
|
||
|
||
### 目的地列表
|
||
|
||
```http
|
||
GET /api/admin/destinations
|
||
```
|
||
|
||
响应体:
|
||
|
||
```ts
|
||
{
|
||
items: Destination[];
|
||
}
|
||
```
|
||
|
||
要求:
|
||
|
||
- 返回所有目的地,包括未启用项,便于后台维护。
|
||
- 按 `sortOrder` 升序。
|
||
- 每个目的地包含 `aliases`。
|
||
- 每个目的地建议包含 `_count.products`,用于后台判断关联商品数量。
|
||
|
||
### 站点配置
|
||
|
||
```http
|
||
GET /api/admin/site-config
|
||
```
|
||
|
||
响应体:`SiteConfig`
|
||
|
||
要求:
|
||
|
||
- 返回 `heroSlides`、`destinations`、`themes`、`ctaBanners` 四个模块。
|
||
- Admin API 需要返回未启用内容;Public API 才按发布/启用状态过滤。
|
||
- 各模块按 `sortOrder` 升序。
|
||
|
||
### 更新站点配置项
|
||
|
||
```http
|
||
PATCH /api/admin/site-config/{module}/{item_id}
|
||
```
|
||
|
||
路径参数:
|
||
|
||
| 参数 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| `module` | `SiteModule` | 只能为 `heroSlides`、`destinations`、`themes`、`ctaBanners` |
|
||
| `item_id` | `string` | 对应模块内容项 ID |
|
||
|
||
请求体:`SiteItemPatch`
|
||
|
||
响应体:更新后的内容项对象。
|
||
|
||
状态码要求:
|
||
|
||
- `200`:更新成功。
|
||
- `400`:模块不存在。
|
||
- `404`:内容项不存在。
|
||
- `422`:字段校验失败。
|
||
|
||
当前 UI 保存后会重新调用 `GET /api/admin/site-config` 刷新页面,响应体只需保证是合法 JSON。
|
||
|
||
### 线索列表
|
||
|
||
```http
|
||
GET /api/admin/leads
|
||
```
|
||
|
||
查询参数:
|
||
|
||
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `status` | `LeadStatus` | 否 | 后端可支持状态筛选 |
|
||
| `take` | `number` | 否 | 后端当前可限制返回条数 |
|
||
|
||
响应体:
|
||
|
||
```ts
|
||
{
|
||
items: Lead[];
|
||
}
|
||
```
|
||
|
||
排序建议:`createdAt` 倒序。当前 UI 展示客户手机号、创建时间、目的地/备注、来源商品/来源页面和状态。
|
||
|
||
### 更新线索状态
|
||
|
||
```http
|
||
PATCH /api/admin/leads/{id}/status
|
||
```
|
||
|
||
请求体:
|
||
|
||
```ts
|
||
{
|
||
status: LeadStatus;
|
||
}
|
||
```
|
||
|
||
响应体:更新后的 `Lead`,至少需要包含 `id` 和 `status`。
|
||
|
||
状态码要求:
|
||
|
||
- `200`:更新成功。
|
||
- `404`:线索不存在。
|
||
- `422`:状态值非法。
|
||
|
||
当前 UI 更新后会重新调用 `GET /api/admin/leads`,因此响应体不会直接用于渲染列表。
|
||
|
||
### 发布站点配置
|
||
|
||
```http
|
||
POST /api/admin/publish
|
||
```
|
||
|
||
请求体:空 JSON 对象或无请求体均可兼容。
|
||
|
||
响应体:
|
||
|
||
```ts
|
||
{
|
||
id: string;
|
||
title: string;
|
||
publishedAt: string;
|
||
}
|
||
```
|
||
|
||
当前 UI 只读取 `title` 展示发布结果。后端可额外返回 `status`、`snapshot` 等字段。
|
||
|
||
### 重置贵州内容
|
||
|
||
```http
|
||
POST /api/admin/reset-guizhou-content
|
||
```
|
||
|
||
请求体:空 JSON 对象或无请求体均可兼容。
|
||
|
||
响应体:
|
||
|
||
```ts
|
||
{
|
||
heroSlides: number;
|
||
destinations: number;
|
||
themes: number;
|
||
ctaBanners: number;
|
||
products: number;
|
||
}
|
||
```
|
||
|
||
要求:
|
||
|
||
- 该接口会重置内容数据,必须走后台鉴权。
|
||
- 后端需要记录审计日志。
|
||
- 生产环境调用前应通过部署流程或权限控制额外确认。
|
||
|
||
## 后端实现注意事项
|
||
|
||
- Admin API 默认使用 `require_admin`,登录接口除外。
|
||
- 所有外部输入通过 Pydantic schema 校验。
|
||
- 变更类接口需要继续写入 `AuditLog`。
|
||
- 响应字段使用 camelCase,以匹配当前前端类型。
|
||
- 允许后端返回额外字段,但不要移除本文列出的前端依赖字段。
|
||
- 当前管理端不会直接上传图片,只维护图片 URL;媒体库接口暂不属于本需求范围。
|
||
|