diff --git a/%SystemDrive%/ProgramData/SogouInput/Components/Picface/Cloud/sgim_picface_cloud.bin b/%SystemDrive%/ProgramData/SogouInput/Components/Picface/Cloud/sgim_picface_cloud.bin deleted file mode 100644 index 306921d..0000000 Binary files a/%SystemDrive%/ProgramData/SogouInput/Components/Picface/Cloud/sgim_picface_cloud.bin and /dev/null differ diff --git a/%SystemDrive%/ProgramData/SogouInput/Components/Picface/Cloud/sgim_picface_cloud_bak.bin b/%SystemDrive%/ProgramData/SogouInput/Components/Picface/Cloud/sgim_picface_cloud_bak.bin deleted file mode 100644 index 306921d..0000000 Binary files a/%SystemDrive%/ProgramData/SogouInput/Components/Picface/Cloud/sgim_picface_cloud_bak.bin and /dev/null differ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 15d9729..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,114 +0,0 @@ -# AGENTS.md - -## 全局协作规则 - -- 默认全程使用中文回答;除非用户明确要求,不切换语言。 -- 回答保持简洁直接,避免无效铺垫和空话。 -- 需求不清晰时先提问确认;不要自行猜测并执行有风险操作。 -- 默认只读优先。创建、修改、删除文件必须有用户明确授权。 -- 严格按当前需求工作,不额外加功能、不扩大改动范围。 -- 识别到密钥、Token、真实环境变量、账号密码、隐私配置时,禁止展示、复述或输出。 -- 不读取、输出或提交 `.env`、`.env.local` 等真实环境文件。 - -## 项目定位 - -`WonderQ-Admin` 是独立的 WonderQ 后端 API 服务,当前技术栈为 Python + FastAPI + SQLAlchemy 2 + Alembic + PostgreSQL + JWT + Pydantic。项目为 H5 前台提供 Public API,为后台管理端提供 Admin API,并通过 Docker Compose 部署 API、PostgreSQL 和 Redis。 - -## 当前目录结构 - -```text -WonderQ-Admin/ -├─ app/ -│ ├─ main.py # FastAPI 应用入口、CORS、错误处理、路由注册 -│ ├─ config.py # 环境变量配置 -│ ├─ database.py # SQLAlchemy engine/session/Base -│ ├─ models.py # ORM 模型,兼容原 Prisma 表结构 -│ ├─ schemas.py # Pydantic 请求校验 -│ ├─ auth.py # JWT 与后台鉴权 -│ ├─ serializers.py # SQLAlchemy 对象响应序列化 -│ ├─ content.py # Python seed 内容源 -│ ├─ seed.py # 初始化/重置数据命令 -│ └─ routers/ -│ ├─ public.py # H5 Public API -│ ├─ admin.py # 后台 Admin API -│ └─ shared.py # 路由共享查询 -├─ alembic/ # 数据库迁移 baseline -├─ data/generated-products.json -├─ tests/ # 单元测试和接口冒烟测试 -├─ Dockerfile # API 镜像 -├─ docker-compose.yml # api/postgres/redis 编排 -├─ requirements.txt # Python 依赖 -├─ pyproject.toml # Python 项目元数据与 pytest 配置 -├─ .env.example # 环境变量模板 -└─ README.md # 启动、部署、迁移说明 -``` - -## 启动方式 - -本地开发: - -```bash -python -m venv .venv -.venv\Scripts\activate -pip install -r requirements.txt -docker compose up -d postgres redis -alembic upgrade head -python -m app.seed -uvicorn app.main:app --host 0.0.0.0 --port 4000 --reload -``` - -手动重启 4001 端口服务:先停止当前监听进程及其 worker,再手动运行 API。 - -```powershell -$port = 4001 -$listenerPids = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | - Select-Object -ExpandProperty OwningProcess -Unique -foreach ($listenerPid in $listenerPids) { - Get-CimInstance Win32_Process | - Where-Object { $_.ParentProcessId -eq $listenerPid } | - ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } - Stop-Process -Id $listenerPid -Force -ErrorAction SilentlyContinue -} -uvicorn app.main:app --host 0.0.0.0 --port 4001 --reload -``` - - -Docker 全量启动: - -```bash -docker compose up --build -``` - -健康检查:`http://localhost:4000/health`。 - -## 测试流程 - -- 修改 Python 代码后运行 `pytest`。 -- 修改数据库模型或迁移后运行 `alembic upgrade head`,并在空库验证 `python -m app.seed`。 -- 保留已有 PostgreSQL 数据时,先备份,再使用 `alembic stamp head` 标记 baseline。 -- Docker 相关变更后运行 `docker compose up --build` 并检查 `/health`。 - -## 开发准则 - -- 优先保持现有 API 路径和响应结构兼容,不主动重设计接口。 -- 外部输入必须通过 Pydantic schema 校验。 -- 数据库访问统一通过 `app/database.py` 提供的 Session。 -- 表名和字段名需要兼容原 Prisma 生成的 mixed-case PostgreSQL 结构。 -- Admin API 默认需要 `require_admin`,登录接口除外。 -- 后台数据变更继续记录 `AuditLog`。 -- 真实密钥只从环境变量读取,禁止写入源码、测试或文档。 -- `python -m app.seed` 会重置内容数据,生产环境使用前必须明确确认。 - -## 锁定核心文件 - -未经用户明确授权禁止修改: - -- `.env`、`.env.local`、生产环境变量和任何密钥配置。 -- `app/models.py`、`alembic/versions/*`:数据库结构和迁移。 -- `app/auth.py`:后台鉴权逻辑。 -- `app/routers/admin.py`、`app/routers/public.py`:核心 API 行为。 -- `app/seed.py`、`app/content.py`、`data/generated-products.json`:初始化内容和迁移数据源。 -- `Dockerfile`、`docker-compose.yml`:部署入口。 -- `requirements.txt`、`pyproject.toml`:依赖和测试配置。 - -如确需修改上述文件,先说明原因、影响范围、验证方式,并等待用户确认。 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 071fbda..0000000 --- a/docs/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# WonderQ-Admin 文档 - -本目录存放后端 API 相关文档。 - -- `backend/README.md`:当前 Python + FastAPI 后端运行说明。 -- `admin-ui-api-requirements.md`:后台管理前端当前必需的 Admin API 对接需求与字段契约。 -- `miniapp-public-api.md`:WonderQ-MiniAPP 前台对接 Public API 契约。 -- `admin-backend-plan.md`:早期后台建设规划,保留作业务范围和阶段规划参考;其中技术栈建议已被当前 Python 迁移方案取代。 - -后台管理前端文档位于 `D:\www\znkj\WonderQ-Admin-UI\docs`。 diff --git a/docs/admin-backend-plan.md b/docs/admin-backend-plan.md deleted file mode 100644 index a548afd..0000000 --- a/docs/admin-backend-plan.md +++ /dev/null @@ -1,36 +0,0 @@ -# WonderQ-Admin 后端规划摘要 - -## 当前定位 - -`WonderQ-Admin` 是 WonderQ 的独立后端 API 服务,当前技术栈已调整为 Python + FastAPI + SQLAlchemy 2 + Alembic + PostgreSQL,通过 Docker Compose 部署 API、PostgreSQL 和 Redis。 - -## 核心目标 - -- 为 H5 前台提供稳定的 Public API。 -- 为后台管理端提供 JWT 鉴权的 Admin API。 -- 保留现有 PostgreSQL 数据和主要接口路径,降低前端联调成本。 -- 使用 Alembic 管理后续数据库迁移,已有数据库通过 `alembic stamp head` 接入 baseline。 - -## 当前业务模块 - -- 首页配置:轮播、目的地、主题卡片、底部 CTA、发布版本。 -- 线路产品:列表、详情、图片、详情区块、状态和排序。 -- 目的地:目的地基础信息、别名、热门状态。 -- 活动专题:活动和产品关联。 -- 线索:前台提交、后台列表、状态流转。 -- 媒体:媒体资源登记。 -- 审计:后台关键变更写入 `AuditLog`。 - -## 近期优先级 - -1. 保持 Public/Admin API 与现有前端调用兼容。 -2. 补充更多 PostgreSQL 集成测试,覆盖迁移、seed 和核心接口。 -3. 建立生产迁移流程:备份、`alembic stamp head`、后续增量迁移。 -4. 按实际业务继续扩展权限、订单、客户和消息模块。 - -## 安全与部署原则 - -- 生产环境必须替换 `JWT_SECRET`,禁止使用示例值。 -- 不提交 `.env`、日志、数据库备份和任何真实密钥。 -- `python -m app.seed` 会重置内容数据,生产环境执行前必须明确确认。 -- Docker Compose 适合本地和单机部署;生产可按相同环境变量拆分到托管数据库或容器平台。 diff --git a/docs/admin-ui-api-requirements.md b/docs/admin-ui-api-requirements.md deleted file mode 100644 index 88513fd..0000000 --- a/docs/admin-ui-api-requirements.md +++ /dev/null @@ -1,651 +0,0 @@ -# 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; - 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; - 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`、`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 -``` - -请求体: - -```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` - -`hotelGroups` 对应首页“特色酒店”模块。该模块已按“特价优惠”的数据配置方式调整,运营可以新增、编辑、删除、排序酒店卡片,并维护标题、描述、价格、标签、封面图和前台展示状态。它只维护首页酒店卡片,不绑定商品本体,也不读取线路商品关联。 - -```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 -``` - -字段规则: -- 新增请求至少包含 `title`,可包含 `description`、`image`、`coverImage`、`priceAmount`、`priceUnit`、`tags`、`status`、`isActive`、`sortOrder`。 -- 更新请求可包含 `title`、`description`、`image`、`coverImage`、`priceAmount`、`priceUnit`、`tags`、`status`、`isActive`、`sortOrder`。 -- `coverImage` 是酒店封面图主字段;为兼容旧前端,后端同时保留 `image`。当请求只传其中一个字段时,后端应同步另一个字段。 -- `priceAmount` 为价格数值,`priceUnit` 为价格单位文案,默认建议为 `起/晚`。 -- `tags` 最多 3 个,保存时去掉空标签。 -- `status` 只允许 `draft`、`published`;管理端“前台启用”开关会同步提交 `status` 与 `isActive`。Public API 只返回 `status="published"` 且 `isActive=true` 的酒店卡片。 -- 删除只删除首页酒店卡片配置,不删除素材库资源;删除后后端重新整理剩余项 `sortOrder`。 -- `PATCH /reorder` 的 `itemIds` 必须完整覆盖当前 `hotelGroups` 全部配置项 ID,不能缺失、重复或包含未知 ID。 - -#### 万趣用车 `vehicleOptions` - -`vehicleOptions` 对应首页“万趣用车”模块,继续作为普通首页内容卡片维护,不复用酒店价格和标签字段。 - -```http -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` 必须完整覆盖当前 `vehicleOptions` 全部配置项 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;媒体库接口暂不属于本需求范围。 - diff --git a/docs/backend/README.md b/docs/backend/README.md deleted file mode 100644 index a3467c8..0000000 --- a/docs/backend/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# 后台 API 服务 - -`WonderQ-Admin` 当前是独立的 Python + FastAPI API 服务,使用 PostgreSQL 保存业务数据,通过 Docker Compose 启动 `api`、`postgres` 和 `redis`。 - -## 本地 API 启动 - -```bash -python -m venv .venv -.venv\Scripts\activate -pip install -r requirements.txt -docker compose up -d postgres redis -alembic upgrade head -python -m app.seed -uvicorn app.main:app --host 0.0.0.0 --port 4000 --reload -``` - -健康检查:`http://localhost:4000/health` - -## 前端联调 - -后台管理前端位于 `D:\www\znkj\WonderQ-Admin-UI`。如需连接本服务,在前端 `.env` 或环境变量中配置: - -```text -VITE_API_BASE_URL="http://localhost:4000" -``` diff --git a/docs/miniapp-public-api.md b/docs/miniapp-public-api.md deleted file mode 100644 index 81809ff..0000000 --- a/docs/miniapp-public-api.md +++ /dev/null @@ -1,521 +0,0 @@ -# WonderQ-MiniAPP Public API 对接文档 - -最后更新:2026-07-02 - -本文档定义 `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` | 否 | 是否启用 | -| `sortOrder` | `number` | 否 | 后台排序值,Public API 按该字段升序输出 | - -### `Destination` - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `string` | 是 | 目的地 ID | -| `name` | `string` | 是 | 目的地名称 | -| `image` | `string \| null` | 否 | 图片 URL | -| `isHot` | `boolean` | 否 | 是否热门 | -| `isActive` | `boolean` | 否 | 是否启用 | -| `sortOrder` | `number` | 否 | 后台排序值,Public API 按该字段升序输出 | -| `aliases` | `Array<{ id: string; alias: string }>` | 否 | 搜索别名 | - -### `Theme` - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `string` | 是 | 主题 ID | -| `label` | `string` | 是 | 主题名称 | -| `image` | `string` | 是 | 主题图片 URL | -| `targetType` | `string \| null` | 否 | 点击目标类型 | -| `targetValue` | `string \| null` | 否 | 点击目标值 | -| `isActive` | `boolean` | 否 | 是否启用 | -| `sortOrder` | `number` | 否 | 后台排序值,Public API 按该字段升序输出 | - -### `CtaBanner` - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `string` | 是 | 服务卡片 ID | -| `alt` | `string` | 是 | 服务标题,展示在“更多服务”卡片上 | -| `image` | `string` | 是 | 服务卡片背景图 URL | -| `targetType` | `string \| null` | 否 | 点击目标类型 | -| `targetValue` | `string \| null` | 否 | 点击目标值 | -| `isActive` | `boolean` | 否 | 是否启用 | -| `sortOrder` | `number` | 否 | 后台排序值,Public API 按该字段升序输出 | - -### `Campaign` - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `string` | 是 | 活动 ID | -| `slug` | `string` | 是 | 活动标识 | -| `title` | `string` | 是 | 活动标题 | -| `description` | `string \| null` | 否 | 活动描述 | -| `coverImage` | `string \| null` | 否 | 活动封面图 | -| `priceAmount` | `number \| null` | 否 | 参考起价,单位按 `priceUnit` 展示 | -| `priceUnit` | `string \| null` | 否 | 价格单位文案,默认 `起/人` | -| `tags` | `string[]` | 否 | 活动卡片标签,最多 3 个 | -| `status` | `string` | 是 | 活动状态;Public API 只返回 `published` | -| `startsAt` | `string \| null` | 否 | 活动开始时间 | -| `endsAt` | `string \| null` | 否 | 活动结束时间 | - -### `RouteSection` - -`RouteSection` 用于描述首页“精选线路”下的动态运营分组。后台可按任务新增、编辑、删除和排序分组;MiniAPP 不应依赖固定分组 ID,只按接口返回的分组顺序和 `productIds` 渲染。 - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `string` | 是 | 后端生成的分组 ID,客户端只用于列表 key 和商品关联,不作为固定业务枚举 | -| `title` | `string` | 是 | 分组标题 | -| `subtitle` | `string \| null` | 否 | 分组副文案 | -| `productIds` | `string[]` | 是 | 该分组包含的产品 ID;产品详情来自 `/api/public/products.items` | -| `isActive` | `boolean` | 否 | 是否启用;Public API 通常只返回启用分组 | -| `sortOrder` | `number` | 否 | 后台展示顺序;Public API 按该字段升序输出 | - -Public API 输出规则: -- `GET /api/public/site-config` 只返回启用的 `routeSections`。 -- `routeSections[].productIds` 只包含已发布商品 ID;未发布、归档或不存在的商品不得出现在 Public 响应中。 -- 动态分组按后台 `sortOrder` 升序返回,`productIds` 的顺序就是用户侧商品卡展示顺序。 -- MiniAPP 会按 `productIds` 匹配 `/api/public/products.items[].id`;接口缺失、`routeSections` 为空或没有可匹配商品时回退 `src/content.ts` 的本地精选线路兜底内容。 - -### `HomeCard` - -`HomeCard` 用于首页“万趣用车”等普通内容卡片模块。MiniAPP 只消费卡片展示字段,不在这些模块里读取商品本体或线路商品关联。 - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `string` | 是 | 后端生成的卡片 ID | -| `title` | `string` | 是 | 卡片标题 | -| `description` | `string \| null` | 否 | 卡片描述 | -| `image` | `string \| null` | 否 | 卡片封面图 URL;为空时客户端可使用本地兜底图 | -| `isActive` | `boolean` | 否 | 是否启用;Public API 通常只返回启用卡片 | -| `sortOrder` | `number` | 否 | 后台展示顺序;Public API 按该字段升序输出 | - -### `HotelCard` - -`HotelCard` 用于首页“特色酒店”模块。该模块按管理端“特价优惠”同类配置方式维护标题、描述、价格、标签、封面图和发布状态,但仍然只代表首页酒店展示卡片,不绑定商品本体。 - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| `id` | `string` | 是 | 后端生成的酒店卡片 ID | -| `title` | `string` | 是 | 酒店卡片标题 | -| `description` | `string \| null` | 否 | 酒店卡片描述 | -| `image` | `string \| null` | 否 | 兼容旧字段;后端会与 `coverImage` 保持一致 | -| `coverImage` | `string \| null` | 否 | 酒店封面图主字段;MiniAPP 优先使用该字段 | -| `priceAmount` | `number \| null` | 否 | 价格数值,前端可按页面需要展示 | -| `priceUnit` | `string \| null` | 否 | 价格单位文案,例如 `起/晚` | -| `tags` | `string[]` | 否 | 酒店标签,最多 3 个 | -| `status` | `"draft" \| "published"` | 否 | 发布状态;Public API 只返回 `published` | -| `isActive` | `boolean` | 否 | 是否启用;Public API 只返回启用项 | -| `sortOrder` | `number` | 否 | 后台展示顺序;Public API 按该字段升序输出 | - -Public API 输出规则: -- `GET /api/public/site-config` 只返回 `status="published"` 且 `isActive=true` 的 `hotelGroups`。 -- `GET /api/public/site-config` 只返回启用的 `vehicleOptions`。 -- 两个数组按后台 `sortOrder` 升序返回。 -- `hotelGroups` 字段缺失、数组为空或没有可用图片时,MiniAPP 使用 `src/content.ts` 的本地特色酒店内容兜底;酒店图片优先取 `coverImage`,再取 `image`。 -- `vehicleOptions` 字段缺失、数组为空或图片为空时,MiniAPP 使用 `src/content.ts` 的本地万趣用车内容兜底。 - -### `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` | 否 | 图集 | -| `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` - -用于首页轮播、目的地、主题入口和更多服务配置。MiniAPP 启动时会和产品列表并行请求该接口;接口不可用或关键数组为空时,前台会回退本地静态内容。 - -#### 响应字段 - -| 字段 | 类型 | 说明 | -| --- | --- | --- | -| `heroSlides` | `HeroSlide[]` | 首页顶部轮播 | -| `destinations` | `Destination[]` | 首页目的地入口 | -| `map` | `Array<{ id: string; image: string; isActive?: boolean }>` | 贵州地图图片;MiniAPP 当前消费 `map[0].image` | -| `themes` | `Theme[]` | 主题甄选入口 | -| `ctaBanners` | `CtaBanner[]` | “更多服务”卡片配置 | -| `campaigns` | `Campaign[]` | 活动元信息,可用于“特价优惠”入口;当前不包含活动产品结果列表 | -| `routeSections` | `RouteSection[]` | “精选线路”子分组定义 | -| `hotelGroups` | `HotelCard[]` | “特色酒店”卡片配置,只返回 `published` 且启用项 | -| `vehicleOptions` | `HomeCard[]` | “万趣用车”卡片配置,只返回启用项 | - -#### 首页模块数据归属 - -| 首页模块 | 当前接口归属 | 说明 | -| --- | --- | --- | -| 特价优惠 | `site-config.campaigns` + `/api/public/products` | 当前 Public API 只返回活动元信息,不直接返回“特价优惠结果列表”。MiniAPP 若要展示活动线路,可按活动标题、标签或后续扩展的活动产品关联从 `/api/public/products` 中筛选。 | -| 精选线路 | `site-config.routeSections` + `/api/public/products` | `routeSections` 返回动态分组与 `productIds`;具体产品卡片数据由 `/api/public/products.items` 提供。后台可按任务新增、删除、停用和排序分组,用户侧不假设固定三组。 | -| 更多服务 | `site-config.ctaBanners` | 返回启用服务卡片,按后台排序展示;无有效配置时回退本地 `bottomCtas` 内容。 | -| 特色酒店 | `site-config.hotelGroups` | 返回 `published` 且启用的酒店卡片,按后台排序展示;MiniAPP 优先消费 `coverImage`,无有效配置时回退本地内容。 | -| 万趣用车 | `site-config.vehicleOptions` | 返回启用用车卡片,按后台排序展示;无有效配置时回退本地内容。 | - -#### 响应示例 - -```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": "小七孔" }] - } - ], - "map": [ - { - "id": "map-1", - "image": "/assets/guizhou/guizhou-map.jpg", - "isActive": true - } - ], - "themes": [], - "ctaBanners": [], - "campaigns": [ - { - "id": "campaign-1", - "slug": "classic-deal", - "title": "经典打卡特惠", - "description": "经典首游活动", - "coverImage": "/assets/guizhou/libo-xiaoqikong.jpg", - "priceAmount": 162500, - "priceUnit": "起/人", - "tags": ["臻藏旅位", "赛事庆典"], - "status": "published", - "startsAt": null, - "endsAt": null - } - ], - "routeSections": [ - { - "id": "route-section-001", - "title": "经典人文打卡线路", - "subtitle": "黄果树、荔波小七孔、千户苗寨、镇远古城、梵净山一次串联", - "productIds": ["8a6e7c4f-0000-4000-9000-000000000001"], - "isActive": true - }, - { - "id": "route-section-002", - "title": "极限山野户外野咖线路", - "subtitle": "溶洞、峡谷、漂流、峰林骑行和山野咖啡组合", - "productIds": [], - "isActive": true - }, - { - "id": "route-section-003", - "title": "人文+户外综合混搭线路", - "subtitle": "非遗村寨、古城夜游、自然轻探险和精品住宿同程安排", - "productIds": [], - "isActive": true - } - ], - "hotelGroups": [ - { - "id": "hotel-group-001", - "title": "经典酒店", - "description": "城市接驳、景区度假和温泉休整,适合首游贵州的小包团动线。", - "image": "/assets/guizhou/bailian-hot-spring.jpg", - "coverImage": "/assets/guizhou/bailian-hot-spring.jpg", - "priceAmount": 68000, - "priceUnit": "起/晚", - "tags": ["温泉", "亲子"], - "status": "published", - "isActive": true, - "sortOrder": 0 - } - ], - "vehicleOptions": [ - { - "id": "vehicle-option-001", - "title": "5座舒适用车", - "description": "适合2-4人家庭或好友小团,城市接送、景区穿梭更灵活。", - "image": "/assets/guizhou/jiaxiu-tower.jpg", - "isActive": true, - "sortOrder": 0 - } - ] -} -``` - -### `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 会使用本地产品兜底数据。 -- “精选线路”由 `site-config.routeSections` 定义动态分组标题、副文案和商品 ID 顺序,由 `/api/public/products.items` 提供产品详情;客户端不依赖固定分组 ID 或固定三组数量。 -- `routeSections` 缺失、为空或无法匹配到有效商品时,MiniAPP 使用 `src/content.ts` 的本地精选线路内容回退。 -- `ctaBanners` 缺失或为空时,MiniAPP 使用 `src/content.ts` 的本地 `bottomCtas` 内容回退。 -- “特色酒店”和“万趣用车”分别由 `site-config.hotelGroups`、`site-config.vehicleOptions` 提供;特色酒店优先使用 `coverImage`,字段缺失、数组为空或图片为空时使用本地内容兜底。 -- “特价优惠”当前没有独立 Public 结果列表字段;`site-config.campaigns` 只提供活动元信息,活动线路需通过产品标签/关键词筛选或后续扩展活动产品关联字段。 -- 产品搜索当前主要在前端执行,依赖 `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`、`map`、`themes`、`ctaBanners`、`campaigns`、`routeSections`、`hotelGroups`、`vehicleOptions` 数组字段。 -- 为 `GET /api/public/site-config` 验证 `routeSections` 表达“精选线路”子分组;接口返回当前已配置且启用的分组,未配置时返回空数组并由 MiniAPP 本地内容兜底。 -- 为 `GET /api/public/site-config` 验证 `routeSections` 只返回启用分组,且 `productIds` 不包含未发布商品。 -- 为 `GET /api/public/site-config` 验证 `hotelGroups` 只返回 `published` 且启用卡片,包含 `coverImage`、价格和标签字段;验证 `vehicleOptions` 只返回启用卡片,并按 `sortOrder` 升序。 -- 为 `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` 规范化、必填校验、人数/预算边界和备注长度限制。