# 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 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` | 首页/目的地/活动结构维护 | 已覆盖 | 需要包含未启用内容、已保存的 `routeSections`、`hotelGroups`、`vehicleOptions`、`ctaBanners` | | `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" | "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[]; 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; 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; name?: string; slug?: string; region?: string | null; label?: string; alt?: string; image?: string | null; description?: string | null; coverImage?: string | null; priceAmount?: number | null; priceUnit?: string | null; tags?: 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`、`image`、`isActive` | | `themes` | `label`、`image`、`targetType`、`targetValue`、`isActive` | | `campaigns` | `title`、`description`、`coverImage`、`priceAmount`、`priceUnit`、`tags`、`status` | | `routeSections` | `title`、`subtitle`、`productIds`、`isActive`、`sortOrder` | | `hotelGroups` | `title`、`description`、`image`、`isActive`、`sortOrder` | | `vehicleOptions` | `title`、`description`、`image`、`isActive`、`sortOrder` | | `ctaBanners` | `alt`(服务标题)、`image`、`targetType`、`targetValue`、`isActive`、`sortOrder` | ## 接口明细 ### 登录 ```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= ``` 查询参数: | 参数 | 类型 | 当前 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` 响应体:`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`、`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`、`map`、`themes`、`campaigns`、`routeSections`、`hotelGroups`、`vehicleOptions`、`ctaBanners` | | `item_id` | `string` | 对应模块内容项 ID | 请求体:`SiteItemPatch` 响应体:更新后的内容项对象。 状态码要求: - `200`:更新成功。 - `400`:模块不存在。 - `404`:内容项不存在。 - `422`:字段校验失败。 当前 UI 保存后会重新调用 `GET /api/admin/site-config` 刷新页面,响应体只需保证是合法 JSON。 #### 精选线路 `routeSections` `routeSections` 是首页“精选线路”的动态运营分组配置,不再限制为固定三组。管理端通过新增、更新、删除和排序接口维护闭环;商品本体仍由 `/api/admin/products` 维护,这里只保存首页分组、标题、副文案、启用状态、排序和关联商品 ID 顺序。 ```http POST /api/admin/site-config/routeSections PATCH /api/admin/site-config/routeSections/{section_id} DELETE /api/admin/site-config/routeSections/{section_id} PATCH /api/admin/site-config/routeSections/reorder ``` 新增请求至少包含 `title`,可包含 `subtitle`、`isActive`、`sortOrder`、`productIds`;`id` 由后端生成,不再使用 `routes` / `routes-outdoor` / `routes-mix` 固定槽位。更新请求可包含 `title`、`subtitle`、`isActive`、`sortOrder`、`productIds`。`productIds` 表示该分组关联的线路商品及展示顺序。 后端约束: - `GET /api/admin/site-config` 返回全部后台分组,包含停用分组和后台配置的全部 `productIds`。 - `productIds` 中的商品必须存在,且同一请求内不能重复。 - 同一商品不能同时出现在多个精选线路分组;冲突时返回 `409 ROUTE_SECTION_PRODUCT_CONFLICT`。 - `DELETE /api/admin/site-config/routeSections/{section_id}` 只删除分组配置并解除关联,不删除商品本体;删除后后端重新整理剩余分组 `sortOrder`。 - `PATCH /api/admin/site-config/routeSections/reorder` 的 `itemIds` 必须完整覆盖当前全部分组 ID,不能缺失、重复或包含未知 ID。 - `GET /api/public/site-config` 只返回启用分组,且 `productIds` 只包含已发布商品;未发布、归档或不存在的商品不进入 Public 响应。 #### 特色酒店和万趣用车 `hotelGroups` / `vehicleOptions` `hotelGroups` 是首页“特色酒店”卡片配置,`vehicleOptions` 是首页“万趣用车”卡片配置。两者只维护首页模块卡片,不维护商品本体或商品关联。 ```http POST /api/admin/site-config/hotelGroups PATCH /api/admin/site-config/hotelGroups/{item_id} DELETE /api/admin/site-config/hotelGroups/{item_id} PATCH /api/admin/site-config/hotelGroups/reorder POST /api/admin/site-config/vehicleOptions PATCH /api/admin/site-config/vehicleOptions/{item_id} DELETE /api/admin/site-config/vehicleOptions/{item_id} PATCH /api/admin/site-config/vehicleOptions/reorder ``` 字段规则: - 新增请求至少包含 `title`,可包含 `description`、`image`、`isActive`、`sortOrder`。 - 更新请求可包含 `title`、`description`、`image`、`isActive`、`sortOrder`。 - 删除只删除首页卡片配置,不删除商品、目的地或素材库资源;删除后后端重新整理剩余项 `sortOrder`。 - `PATCH /reorder` 的 `itemIds` 必须完整覆盖当前同模块全部配置项 ID,不能缺失、重复或包含未知 ID。 - `GET /api/public/site-config` 只返回启用卡片,并按 `sortOrder` 升序;MiniAPP 在字段缺失或空数组时使用本地内容兜底。 #### 更多服务 `ctaBanners` `ctaBanners` 对应首页“更多服务”模块,维护权益、服务管家、目的地和需求入口等服务卡片。管理端按顶部轮播相同的配置方式提供新增、编辑、删除和排序;`alt` 是前台卡片标题,`image` 是卡片背景图。 ```http POST /api/admin/site-config/ctaBanners PATCH /api/admin/site-config/ctaBanners/{item_id} DELETE /api/admin/site-config/ctaBanners/{item_id} PATCH /api/admin/site-config/ctaBanners/reorder ``` 字段规则: - 新增请求至少包含 `alt`,可包含 `image`、`targetType`、`targetValue`、`isActive`、`sortOrder`。 - 更新请求可包含 `alt`、`image`、`targetType`、`targetValue`、`isActive`、`sortOrder`。 - 删除只删除首页更多服务卡片配置,不删除素材库资源;删除后后端重新整理剩余项 `sortOrder`。 - `GET /api/public/site-config` 只返回启用卡片,并按 `sortOrder` 升序。 ### 线索列表 ```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; routeSections?: number; hotelGroups?: number; vehicleOptions?: number; products: number; } ``` 要求: - 该接口会重置内容数据,必须走后台鉴权。 - 后端需要记录审计日志。 - 生产环境调用前应通过部署流程或权限控制额外确认。 ## 后端实现注意事项 - Admin API 默认使用 `require_admin`,登录接口除外。 - 所有外部输入通过 Pydantic schema 校验。 - 变更类接口需要继续写入 `AuditLog`。 - 响应字段使用 camelCase,以匹配当前前端类型。 - 允许后端返回额外字段,但不要移除本文列出的前端依赖字段。 - 当前管理端不会直接上传图片,只维护图片 URL;媒体库接口暂不属于本需求范围。