refactor: 清理废弃业务模块并更新全栈配置
- 移除后端产品、目的地、活动专题等废弃模块的数据库表与业务代码,删除冗余API接口 - 删除小程序端详情页、线路组件等冗余代码,移除搜索工具与测试用例,调整导航逻辑 - 清理管理端废弃的类型定义、编辑器与测试代码 - 更新项目文档,修正模块维护说明与接口文档内容
This commit is contained in:
@@ -1,778 +1,112 @@
|
||||
# WonderQ-Admin-UI Admin API 接口需求
|
||||
# WonderQ 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 必需范围内。
|
||||
本文档描述 `WonderQ-Admin-UI` 当前使用的 Admin API。接口负责站点内容维护、素材、发布和需求线索管理。
|
||||
|
||||
## 通用约定
|
||||
|
||||
- 基础路径:`/api/admin`。
|
||||
- 请求与响应均使用 JSON。
|
||||
- 除 `POST /api/admin/auth/login` 外,其余接口都需要后台登录态。
|
||||
- 登录成功后前端会把返回的 `token` 存入本地,并在后续请求头中发送:
|
||||
- API 前缀:`/api/admin`。
|
||||
- 除登录接口外均需 `Authorization: Bearer <admin-jwt>`。
|
||||
- JSON 请求统一使用 camelCase 字段。
|
||||
- 变更接口写入审计日志后再提交事务。
|
||||
- 失败响应统一包含 `message`、`code` 和可选 `details`。
|
||||
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
## 接口清单
|
||||
|
||||
- 日期时间字段返回 ISO 8601 字符串。
|
||||
- ID 字段按字符串处理。
|
||||
- 前端错误提示优先读取响应体的 `message` 字段;如后端沿用 FastAPI 默认 `detail`,建议同时兼容输出 `message`,避免管理端展示兜底错误。
|
||||
| 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/admin/auth/login` | 后台登录 |
|
||||
| `GET` | `/api/admin/me` | 当前后台用户 |
|
||||
| `GET` | `/api/admin/dashboard` | 工作台统计和最近线索 |
|
||||
| `GET` | `/api/admin/site-config` | 获取全部站点配置 |
|
||||
| `POST` | `/api/admin/site-config/{module}` | 新增模块项 |
|
||||
| `PATCH` | `/api/admin/site-config/{module}/{id}` | 更新模块项 |
|
||||
| `DELETE` | `/api/admin/site-config/{module}/{id}` | 删除模块项 |
|
||||
| `PATCH` | `/api/admin/site-config/{module}/reorder` | 调整排序 |
|
||||
| `GET` | `/api/admin/leads` | 线索列表 |
|
||||
| `PATCH` | `/api/admin/leads/{id}/status` | 更新线索状态 |
|
||||
| `GET` | `/api/admin/media-assets` | 素材列表 |
|
||||
| `POST` | `/api/admin/media-assets/upload` | 上传图片 |
|
||||
| `POST` | `/api/admin/reset-guizhou-content` | 重置站点内容 |
|
||||
| `POST` | `/api/admin/publish` | 发布站点快照 |
|
||||
|
||||
## 当前对接状态
|
||||
## 站点模块
|
||||
|
||||
| 接口 | 前端依赖 | 后端现状 | 备注 |
|
||||
| --- | --- | --- | --- |
|
||||
| `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` | 商品目的地下拉、目的地页 | 已覆盖 | 需要返回别名和商品数 |
|
||||
| `POST /api/admin/site-config/destinations` | 目的地页新增目的地 | 已覆盖 | 新增后返回完整 Destination |
|
||||
| `PATCH /api/admin/site-config/destinations/{id}` | 目的地页编辑目的地 | 已覆盖 | 支持名称、图片、热门、启停、排序等字段 |
|
||||
| `DELETE /api/admin/site-config/destinations/{id}` | 目的地页删除目的地 | 已覆盖 | 有商品引用时返回 `409`,不级联删除商品 |
|
||||
| `PATCH /api/admin/site-config/destinations/reorder` | 目的地页排序 | 已覆盖 | `itemIds` 必须完整且不能重复 |
|
||||
| `POST /api/admin/site-config/destinationHero` | 目的地页新增顶部主视觉 | 已覆盖 | 返回完整 DestinationHero |
|
||||
| `PATCH /api/admin/site-config/destinationHero/{id}` | 目的地页编辑顶部主视觉 | 已覆盖 | 支持标题、小标题、图片、启停、排序 |
|
||||
| `DELETE /api/admin/site-config/destinationHero/{id}` | 目的地页删除顶部主视觉 | 已覆盖 | 删除后重排同模块 `sortOrder` |
|
||||
| `PATCH /api/admin/site-config/destinationHero/reorder` | 目的地页顶部主视觉排序 | 已覆盖 | MiniAPP 使用启用列表第一项 |
|
||||
| `POST /api/admin/site-config/destinationRegions` | 目的地页新增热门区域 | 已覆盖 | 返回完整 DestinationRegion |
|
||||
| `PATCH /api/admin/site-config/destinationRegions/{id}` | 目的地页编辑热门区域 | 已覆盖 | 支持名称、搜索关键词、景点摘要、启停、排序 |
|
||||
| `DELETE /api/admin/site-config/destinationRegions/{id}` | 目的地页删除热门区域 | 已覆盖 | 删除后重排同模块 `sortOrder` |
|
||||
| `PATCH /api/admin/site-config/destinationRegions/reorder` | 目的地页热门区域排序 | 已覆盖 | `itemIds` 必须完整且不能重复 |
|
||||
| `GET /api/admin/site-config` | 首页/目的地/活动/需求页结构维护 | 已覆盖 | 需要包含未启用内容、已保存的 `routeSections`、`hotelGroups`、`vehicleOptions`、`ctaBanners`、需求页配置 |
|
||||
| `PATCH /api/admin/site-config/{module}/{item_id}` | 模块内容编辑 | 已覆盖 | 模块名需保持一致 |
|
||||
| `GET /api/admin/leads` | 需求线索页 | 已覆盖 | UI 传 `status`、`sourcePage`、`keyword`、`createdFrom`、`createdTo`、`take` |
|
||||
| `PATCH /api/admin/leads/{id}/status` | 线索状态流转 | 已覆盖 | UI 更新后会重新拉列表 |
|
||||
| `POST /api/admin/publish` | 结构维护发布 | 已覆盖 | UI 使用 `title` 提示发布结果 |
|
||||
| `POST /api/admin/reset-guizhou-content` | 贵州内容重置 | 已覆盖 | 高风险操作,需鉴权和审计 |
|
||||
|
||||
## 枚举
|
||||
|
||||
### ProductStatus
|
||||
`SiteModule` 只允许以下值:
|
||||
|
||||
```ts
|
||||
type ProductStatus = "draft" | "published" | "archived";
|
||||
type SiteModule =
|
||||
| "heroSlides"
|
||||
| "destinationHero"
|
||||
| "demandHero"
|
||||
| "demandFeatureCards"
|
||||
| "demandForm"
|
||||
| "vehicleOptions"
|
||||
```
|
||||
|
||||
### LeadStatus
|
||||
模块职责:
|
||||
|
||||
```ts
|
||||
type LeadStatus = "new" | "assigned" | "contacted" | "planning" | "won" | "invalid";
|
||||
```
|
||||
| 模块 | 主要字段 | 约束 |
|
||||
| --- | --- | --- |
|
||||
| `heroSlides` | `title`、`kicker`、`image`、`isActive`、`sortOrder` | 可新增、编辑、删除、排序 |
|
||||
| `destinationHero` | `title`、`kicker`、`image`、`isActive`、`sortOrder` | 可新增、编辑、删除、排序 |
|
||||
| `demandHero` | `title`、`kicker`、`description`、`steps`、`isActive`、`sortOrder` | 可新增、编辑、删除、排序 |
|
||||
| `demandFeatureCards` | `title`、`description`、`isActive`、`sortOrder` | 可新增、编辑、删除、排序 |
|
||||
| `demandForm` | 表单标签、占位文案、`chips`、`isActive` | 单例,不支持排序 |
|
||||
| `vehicleOptions` | `title`、`description`、`image`、`isActive`、`sortOrder` | 可新增、编辑、删除、排序 |
|
||||
|
||||
### SiteModule
|
||||
`GET /api/admin/site-config` 返回以上全部模块,包含停用内容,空模块返回 `[]`。
|
||||
|
||||
```ts
|
||||
type SiteModule = "heroSlides" | "destinations" | "destinationHero" | "destinationRegions" | "demandHero" | "demandFeatureCards" | "demandForm" | "demandRecommendations" | "map" | "themes" | "campaigns" | "routeSections" | "hotelGroups" | "vehicleOptions" | "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[];
|
||||
destinationHero: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
kicker?: string | null;
|
||||
image?: string | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}>;
|
||||
destinationRegions: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
keyword?: string | null;
|
||||
spots?: string | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}>;
|
||||
demandHero: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
kicker?: string | null;
|
||||
description?: string | null;
|
||||
steps: string[];
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}>;
|
||||
demandFeatureCards: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}>;
|
||||
demandForm: Array<{
|
||||
id: string;
|
||||
destinationLabel: string;
|
||||
destinationPlaceholder?: string | null;
|
||||
phoneLabel: string;
|
||||
phonePlaceholder?: string | null;
|
||||
noteLabel: string;
|
||||
notePlaceholder?: string | null;
|
||||
submitLabel: string;
|
||||
chips: string[];
|
||||
isActive: boolean;
|
||||
}>;
|
||||
demandRecommendations: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
productIds: string[];
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}>;
|
||||
themes: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
image: string;
|
||||
targetType?: string | null;
|
||||
targetValue?: string | null;
|
||||
isActive: boolean;
|
||||
}>;
|
||||
campaigns: Array<{
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
coverImage?: string | null;
|
||||
priceAmount?: number | null;
|
||||
priceUnit?: string | null;
|
||||
tags: string[];
|
||||
status: "draft" | "published";
|
||||
startsAt?: string | null;
|
||||
endsAt?: string | null;
|
||||
}>;
|
||||
routeSections: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
productIds: string[];
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}>;
|
||||
hotelGroups: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
image?: string | null;
|
||||
coverImage?: string | null;
|
||||
priceAmount?: number | null;
|
||||
priceUnit?: string | null;
|
||||
tags?: string[];
|
||||
status: "draft" | "published";
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}>;
|
||||
vehicleOptions: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
image?: string | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}>;
|
||||
ctaBanners: Array<{
|
||||
id: string;
|
||||
alt: string;
|
||||
image: string;
|
||||
targetType: string;
|
||||
targetValue?: string | null;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
}>;
|
||||
};
|
||||
```
|
||||
|
||||
### SiteItemPatch
|
||||
|
||||
```ts
|
||||
type SiteItemPatch = {
|
||||
title?: string;
|
||||
subtitle?: string | null;
|
||||
kicker?: string | null;
|
||||
name?: string;
|
||||
slug?: string;
|
||||
region?: string | null;
|
||||
label?: string;
|
||||
keyword?: string | null;
|
||||
spots?: string | null;
|
||||
alt?: string;
|
||||
image?: string | null;
|
||||
description?: string | null;
|
||||
coverImage?: string | null;
|
||||
priceAmount?: number | null;
|
||||
priceUnit?: string | null;
|
||||
tags?: string[];
|
||||
steps?: string[];
|
||||
destinationLabel?: string;
|
||||
destinationPlaceholder?: string | null;
|
||||
phoneLabel?: string;
|
||||
phonePlaceholder?: string | null;
|
||||
noteLabel?: string;
|
||||
notePlaceholder?: string | null;
|
||||
submitLabel?: string;
|
||||
chips?: string[];
|
||||
targetType?: string | null;
|
||||
targetValue?: string | null;
|
||||
isHot?: boolean;
|
||||
isActive?: boolean;
|
||||
sortOrder?: number;
|
||||
productIds?: string[];
|
||||
status?: "draft" | "published";
|
||||
startsAt?: string | null;
|
||||
endsAt?: string | null;
|
||||
};
|
||||
```
|
||||
|
||||
模块字段映射:
|
||||
|
||||
| module | 可编辑字段 |
|
||||
| --- | --- |
|
||||
| `heroSlides` | `title`、`kicker`、`image`、`targetType`、`targetValue`、`isActive` |
|
||||
| `destinations` | `name`、`slug`、`region`、`image`、`isHot`、`isActive`、`sortOrder` |
|
||||
| `destinationHero` | `title`、`kicker`、`image`、`isActive`、`sortOrder` |
|
||||
| `destinationRegions` | `label`、`keyword`、`spots`、`isActive`、`sortOrder` |
|
||||
| `demandHero` | `title`、`kicker`、`description`、`steps`、`isActive`、`sortOrder` |
|
||||
| `demandFeatureCards` | `title`、`description`、`isActive`、`sortOrder` |
|
||||
| `demandForm` | `destinationLabel`、`destinationPlaceholder`、`phoneLabel`、`phonePlaceholder`、`noteLabel`、`notePlaceholder`、`submitLabel`、`chips`、`isActive` |
|
||||
| `demandRecommendations` | `title`、`subtitle`、`productIds`、`isActive`、`sortOrder` |
|
||||
| `themes` | `label`、`image`、`targetType`、`targetValue`、`isActive` |
|
||||
| `campaigns` | `title`、`description`、`coverImage`、`priceAmount`、`priceUnit`、`tags`、`status` |
|
||||
| `routeSections` | `title`、`subtitle`、`productIds`、`isActive`、`sortOrder` |
|
||||
| `hotelGroups` | `title`、`description`、`image`、`coverImage`、`priceAmount`、`priceUnit`、`tags`、`status`、`isActive`、`sortOrder` |
|
||||
| `vehicleOptions` | `title`、`description`、`image`、`isActive`、`sortOrder` |
|
||||
| `ctaBanners` | `alt`(服务标题)、`image`、`targetType`、`targetValue`、`isActive`、`sortOrder` |
|
||||
|
||||
## 接口明细
|
||||
## 登录与工作台
|
||||
|
||||
### 登录
|
||||
|
||||
```http
|
||||
POST /api/admin/auth/login
|
||||
```
|
||||
|
||||
请求体:
|
||||
`POST /api/admin/auth/login` 请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "admin@example.com",
|
||||
"password": "example-password"
|
||||
}
|
||||
{ "email": "admin@example.test", "password": "<password>" }
|
||||
```
|
||||
|
||||
响应体:
|
||||
|
||||
```ts
|
||||
{
|
||||
token: string;
|
||||
user: {
|
||||
id?: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
状态码要求:
|
||||
|
||||
- `200`:登录成功。
|
||||
- `401`:账号不存在、密码错误或账号停用。
|
||||
成功响应包含 `token` 和 `{ id, email, name, role }`。
|
||||
|
||||
### 工作台统计
|
||||
|
||||
```http
|
||||
GET /api/admin/dashboard
|
||||
```
|
||||
|
||||
当前前端客户端已封装该接口,但页面暂未展示。后端保持兼容即可。
|
||||
|
||||
响应体:
|
||||
`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>
|
||||
```
|
||||
### `GET /api/admin/leads`
|
||||
|
||||
查询参数:
|
||||
Query 参数:`status`、`sourcePage`、`keyword`、`createdFrom`、`createdTo`、`take`。`take` 范围为 1-200,默认 100。关键词只搜索联系方式、目的地和备注。
|
||||
|
||||
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `keyword` | `string` | 是 | 搜索商品标题、短标题或标签 |
|
||||
| `status` | `ProductStatus` | 否 | 后端可支持状态筛选 |
|
||||
| `take` | `number` | 否 | 后端当前可限制返回条数 |
|
||||
### `PATCH /api/admin/leads/{id}/status`
|
||||
|
||||
响应体:
|
||||
|
||||
```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,而不是只返回成功标记。
|
||||
|
||||
当前管理端不提供商品物理删除接口。目的地页联调中若需要让商品从前台消失,应通过 `PATCH /api/admin/products/{id}` 将 `status` 改为 `archived` 或解除 `destinationId` 关联;不要删除商品本体。
|
||||
|
||||
### 目的地列表
|
||||
|
||||
```http
|
||||
GET /api/admin/destinations
|
||||
```
|
||||
|
||||
响应体:
|
||||
|
||||
```ts
|
||||
{
|
||||
items: Destination[];
|
||||
}
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 返回所有目的地,包括未启用项,便于后台维护。
|
||||
- 按 `sortOrder` 升序。
|
||||
- 每个目的地包含 `aliases`。
|
||||
- 每个目的地建议包含 `_count.products`,用于后台判断关联商品数量。
|
||||
|
||||
### 目的地模块 CRUD
|
||||
|
||||
目的地页维护入口位于 WonderQ-Admin-UI 侧边栏“目的地页”,固定进入 `StructurePage fixedPage="destination"`。后台模块名称与前台三个区块一一对应为“顶部主视觉”“热门区域”“省内目的地”:`destinationHero` 维护顶部主视觉,`destinationRegions` 维护热门区域快捷入口,`destinations` 维护省内目的地卡片;商品归属通过商品维护页绑定 `destinationId`。
|
||||
|
||||
顶部主视觉:
|
||||
|
||||
```http
|
||||
POST /api/admin/site-config/destinationHero
|
||||
PATCH /api/admin/site-config/destinationHero/{id}
|
||||
DELETE /api/admin/site-config/destinationHero/{id}
|
||||
PATCH /api/admin/site-config/destinationHero/reorder
|
||||
```
|
||||
|
||||
字段要求:`title` 必填;`kicker`、`image`、`isActive`、`sortOrder` 可选。创建时 `isActive` 默认 `true`,未传 `sortOrder` 时追加到模块末尾。Public API 只返回启用项,MiniAPP 按 `sortOrder` 升序取第一项渲染目的地页顶部主视觉。
|
||||
|
||||
热门区域:
|
||||
|
||||
```http
|
||||
POST /api/admin/site-config/destinationRegions
|
||||
PATCH /api/admin/site-config/destinationRegions/{id}
|
||||
DELETE /api/admin/site-config/destinationRegions/{id}
|
||||
PATCH /api/admin/site-config/destinationRegions/reorder
|
||||
```
|
||||
|
||||
字段要求:`label` 必填;`keyword`、`spots`、`isActive`、`sortOrder` 可选。`keyword` 为空时 MiniAPP 使用 `label` 作为点击搜索关键词;`spots` 用于卡片摘要展示。Public API 只返回启用项并按 `sortOrder` 升序输出。
|
||||
|
||||
新增目的地:
|
||||
|
||||
```http
|
||||
POST /api/admin/site-config/destinations
|
||||
```
|
||||
|
||||
请求体字段:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: string;
|
||||
slug?: string;
|
||||
region?: string | null;
|
||||
image?: string | null;
|
||||
isHot?: boolean;
|
||||
isActive?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- `name` 必填,后端保存前去除首尾空白,不能为空。
|
||||
- `slug` 为空时后端按 `name` 自动生成;如果传入则不能为空字符串。
|
||||
- `isHot` 默认 `false`,`isActive` 默认 `true`,`sortOrder` 未传时追加到模块末尾。
|
||||
- 响应状态码为 `201`,响应体返回完整 `Destination`。
|
||||
|
||||
更新目的地:
|
||||
|
||||
```http
|
||||
PATCH /api/admin/site-config/destinations/{id}
|
||||
```
|
||||
|
||||
请求体为上述字段的部分对象;响应体返回更新后的完整 `Destination`。
|
||||
|
||||
删除目的地:
|
||||
|
||||
```http
|
||||
DELETE /api/admin/site-config/destinations/{id}
|
||||
```
|
||||
|
||||
删除只移除目的地配置,不删除商品本体。若仍有商品引用该目的地,后端返回 `409 MODULE_CONFIG_CONFLICT`;运营应先在商品维护页将相关商品归档或解除 `destinationId` 关联。
|
||||
|
||||
调整目的地顺序:
|
||||
|
||||
```http
|
||||
PATCH /api/admin/site-config/destinations/reorder
|
||||
```
|
||||
|
||||
请求体:
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"itemIds": ["dest-a", "dest-b", "dest-c"]
|
||||
}
|
||||
{ "status": "contacted" }
|
||||
```
|
||||
|
||||
`itemIds` 必须完整包含当前全部目的地 ID,不能重复或混入其他模块 ID。后端按数组顺序重写 `sortOrder`,Public `site-config.destinations` 也按该顺序输出启用项。
|
||||
状态值:`new`、`assigned`、`contacted`、`planning`、`won`、`invalid`。
|
||||
|
||||
### 站点配置
|
||||
## 媒体与发布
|
||||
|
||||
```http
|
||||
GET /api/admin/site-config
|
||||
```
|
||||
- 图片上传字段为 multipart `file` 和 `group`。
|
||||
- 允许 JPG、PNG、WebP、GIF,单文件最大 5MB。
|
||||
- `POST /api/admin/publish` 保存当前启用内容快照并返回版本记录。
|
||||
- `POST /api/admin/reset-guizhou-content` 只重置站点内容模块,不创建已移除领域的数据。
|
||||
|
||||
响应体:`SiteConfig`
|
||||
|
||||
要求:
|
||||
|
||||
- 返回 `heroSlides`、`destinations`、`destinationHero`、`destinationRegions`、`map`、`themes`、`campaigns`、`routeSections`、`hotelGroups`、`vehicleOptions`、`ctaBanners` 十一个模块。
|
||||
- Admin API 需要返回未启用内容;Public API 才按发布/启用状态过滤。
|
||||
- 各模块按 `sortOrder` 升序。
|
||||
- `routeSections` 返回当前已保存的子分组,包含未启用分组和后台配置的全部 `productIds`;无数据时返回空数组。`hotelGroups`、`vehicleOptions` 返回全部后台卡片,包含停用项;无数据时返回空数组。
|
||||
|
||||
### 更新站点配置项
|
||||
|
||||
```http
|
||||
PATCH /api/admin/site-config/{module}/{item_id}
|
||||
```
|
||||
|
||||
路径参数:
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `module` | `SiteModule` | 只能为 `heroSlides`、`destinations`、`destinationHero`、`destinationRegions`、`demandHero`、`demandFeatureCards`、`demandForm`、`demandRecommendations`、`map`、`themes`、`campaigns`、`routeSections`、`hotelGroups`、`vehicleOptions`、`ctaBanners` |
|
||||
| `item_id` | `string` | 对应模块内容项 ID |
|
||||
|
||||
请求体:`SiteItemPatch`
|
||||
|
||||
响应体:更新后的内容项对象。
|
||||
|
||||
状态码要求:
|
||||
|
||||
- `200`:更新成功。
|
||||
- `400`:模块不存在。
|
||||
- `404`:内容项不存在。
|
||||
- `422`:字段校验失败。
|
||||
|
||||
当前 UI 保存后会重新调用 `GET /api/admin/site-config` 刷新页面,响应体只需保证是合法 JSON。
|
||||
|
||||
### 页面模块配置 CRUD
|
||||
|
||||
页面模块的新增、更新、删除、排序、专用字段、状态码和媒体上传规则,以 `module-config-api.md` 为唯一权威文档。
|
||||
|
||||
本文件只保留 Admin UI 必需的主接口视角:
|
||||
|
||||
```http
|
||||
GET /api/admin/site-config
|
||||
POST /api/admin/site-config/{module}
|
||||
PATCH /api/admin/site-config/{module}/{item_id}
|
||||
DELETE /api/admin/site-config/{module}/{item_id}
|
||||
PATCH /api/admin/site-config/{module}/reorder
|
||||
```
|
||||
|
||||
如果需要维护以下模块的专用创建、删除、排序或字段规则,请直接查看 `module-config-api.md`:
|
||||
|
||||
- `heroSlides`
|
||||
- `destinations`
|
||||
- `destinationHero`
|
||||
- `destinationRegions`
|
||||
- `map`
|
||||
- `themes`
|
||||
- `campaigns`
|
||||
- `routeSections`
|
||||
- `hotelGroups`
|
||||
- `vehicleOptions`
|
||||
- `ctaBanners`
|
||||
### 线索列表
|
||||
|
||||
```http
|
||||
GET /api/admin/leads
|
||||
```
|
||||
|
||||
查询参数:
|
||||
|
||||
| 参数 | 类型 | 当前 UI 是否使用 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `status` | `LeadStatus` | 是 | 按线索状态筛选 |
|
||||
| `sourcePage` | `string` | 是 | 按来源页面筛选,例如 `demand_page` |
|
||||
| `keyword` | `string` | 是 | 匹配联系方式、目的地、备注和来源商品标题 |
|
||||
| `createdFrom` | `YYYY-MM-DD` 或 ISO 字符串 | 是 | 按提交时间起始筛选 |
|
||||
| `createdTo` | `YYYY-MM-DD` 或 ISO 字符串 | 是 | 按提交时间截止筛选;日期格式覆盖当天 |
|
||||
| `take` | `number` | 是 | 返回条数,当前 UI 使用 200 上限 |
|
||||
|
||||
响应体:
|
||||
|
||||
```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;
|
||||
routeSections?: number;
|
||||
hotelGroups?: number;
|
||||
vehicleOptions?: number;
|
||||
products: number;
|
||||
}
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 该接口会重置内容数据,必须走后台鉴权。
|
||||
- 后端需要记录审计日志。
|
||||
- 生产环境调用前应通过部署流程或权限控制额外确认。
|
||||
|
||||
## 后端实现注意事项
|
||||
|
||||
- Admin API 默认使用 `require_admin`,登录接口除外。
|
||||
- 所有外部输入通过 Pydantic schema 校验。
|
||||
- 变更类接口需要继续写入 `AuditLog`。
|
||||
- 响应字段使用 camelCase,以匹配当前前端类型。
|
||||
- 允许后端返回额外字段,但不要移除本文列出的前端依赖字段。
|
||||
- 当前管理端不会直接上传图片,只维护图片 URL;媒体库接口暂不属于本需求范围。
|
||||
## 兼容边界
|
||||
|
||||
- 当前 Admin UI 不应调用未列出的领域接口。
|
||||
- 站点配置字段必须与 `src/api.ts` 保持一致。
|
||||
- 任何字段、模块或路径变化必须同步更新本文档和前端类型。
|
||||
|
||||
Reference in New Issue
Block a user