Files
WonderQ-Project/docs/detail-api.md
duanshuwen feec6ab80f feat(wanfa): 新增路线详情参考价格配置及展示功能
- 新增数据库迁移脚本,为详情表添加四个价格相关字段
- 完善后端schema校验、序列化逻辑,增加价格相关的数据清理与验证规则
- 新增价格配置校验逻辑,设置起售价时必须选择对应的计价单位
- 在Admin UI详情编辑器中新增价格配置模块,支持配置起售价、计价单位、适用人数范围和价格说明
- 更新小程序端详情页面,支持展示配置的参考价格信息
- 补充相关测试用例,更新API文档与类型定义
2026-08-27 22:02:51 +08:00

381 lines
15 KiB
Markdown
Raw Permalink 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-Vue`。
>
> 状态:已实现契约。本文件定义独立路线详情展示模型,供 `WonderQ-Admin`、`WonderQ-Admin-UI-Vue` 和 `WonderQ-MiniAPP` 三端联调使用。详情不恢复 Product、ProductImage 或订单关联。
## 领域边界
详情管理只维护详情页可编辑的展示内容:
- 详情页识别键、标题眉标、出行时长和标题文案。
- 详情介绍、行程亮点、费用包含、费用不含和注意事项。
- 详情页图片画廊及图片顺序。
- 详情页参考价格:价格起始值、单位、适用人数范围和价格说明。
- 详情页可选的联系管家顾问 ID顾问资料仍由管家领域维护。
- 启用状态和详情列表顺序。
本接口不负责:
- 商品、商品价格、商品库存或商品详情表。
- 订单或预订价格计算;本接口中的价格仅用于路线详情展示。
- Product、ProductImage 或任何商品外键。
- 订单、预订、收藏、评价或线索。
- 管家顾问的头像、二维码、服务详情和管家 CRUD详情只保存顾问 ID。
`key` 固定使用玩法路线 ID即服务端生成的 `WanfaRoute.id` UUID`DetailRecord` 不建立数据库外键。详情页通过 `/pages/detail/index?routeId={key}` 定位内容;详情记录删除不会跨领域级联删除路线或其他数据。
所有 JSON 响应遵循 [三端统一 API 响应契约](./api-response-contract.md),成功业务对象位于 `data`,失败时 `data``null`
## 与 `detailPresentation.ts` 的关系
`detailPresentation.ts` 是前台展示适配器,不是持久化模型:
- `eyebrow``duration``title``subtitle``intro``highlights``included``excluded``notes``gallery` 组成最终展示对象。
- `priceStartingValue``priceUnit``pricePeopleRange``priceDescription` 组成可选的参考价格展示对象。
- 当前实现优先消费 Public API 返回的最终展示字段。
- 接口失败、字段不完整或详情未配置时MiniAPP 按路线 ID 使用本地网络图片和模拟文案兜底。
新的 Admin API 应直接维护最终展示字段;`WonderQ-Admin-UI-Vue` 不应复刻这些推导逻辑,也不应依赖已移除的 Product 字段。
## 接口清单
API 前缀为 `/api/admin`,除登录接口外均需要后台 JWT。
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/admin/details` | 获取全部详情展示配置 |
| `GET` | `/api/admin/details/{detailId}` | 获取单个详情展示配置 |
| `POST` | `/api/admin/details` | 新增详情展示配置 |
| `PATCH` | `/api/admin/details/{detailId}` | 编辑详情展示配置 |
| `DELETE` | `/api/admin/details/{detailId}` | 删除详情展示配置 |
| `PATCH` | `/api/admin/details/reorder` | 调整详情展示配置顺序 |
## 通用约定
- 请求和响应使用 JSON字段使用 camelCase。
- 所有管理接口需要 `Authorization: Bearer <admin-jwt>`
- `GET /details` 返回启用和停用的全部配置,按 `sortOrder` 升序返回。
- 创建、编辑、删除和排序成功后写入审计日志,再提交事务。
- 创建和编辑返回最新详情对象;排序接口返回排序后的 `items`
- ID 由后端生成并作为非空字符串返回;`key` 由调用方提供并保持稳定。
- 空列表返回 `[]`,不能返回 `null` 或省略字段。
- 成功结果放入 `data`;失败返回数字 `code``msg``data: null`,可选 `errorCode``details`
## 数据类型
以下内容字段与 `DetailPresentation` 保持兼容,管理端补充 `id``key`、状态和审计时间:
```ts
type DetailPresentation = {
key: string;
eyebrow: string;
duration: string;
title: string;
subtitle: string;
intro: string;
highlights: string[];
included: string[];
excluded: string[];
notes: string[];
gallery: string[];
priceStartingValue: number | null;
priceUnit: "person" | "day" | "group" | null;
pricePeopleRange: string;
priceDescription: string;
};
type DetailRecord = DetailPresentation & {
id: string;
key: string;
conciergeAdvisorId: string | null;
isActive: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
};
type PublicDetail = Omit<DetailPresentation, "key"> & {
key: string;
conciergeAdvisor: PublicConciergeAdvisor | null;
};
type PublicConciergeAdvisor = {
avatar: string;
name: string;
role: string;
details: Array<{ icon: string; label: string }>;
qrImage: string;
};
type DetailCreate = Omit<DetailPresentation, "priceStartingValue" | "priceUnit" | "pricePeopleRange" | "priceDescription"> & {
key: string;
priceStartingValue?: number | null;
priceUnit?: "person" | "day" | "group" | null;
pricePeopleRange?: string | null;
priceDescription?: string | null;
conciergeAdvisorId?: string | null;
isActive?: boolean;
sortOrder?: number;
};
type DetailPatch = Partial<DetailCreate>;
type DetailListResponse = {
details: DetailRecord[];
};
type DetailReorderRequest = {
itemIds: string[];
};
```
## 字段约束
| 字段 | 类型 | 必填 | 约束和用途 |
| --- | --- | --- | --- |
| `id` | `string` | 响应必填 | 后端生成的记录 ID仅供管理端识别记录。 |
| `key` | `string` | 是 | 玩法路线 ID例如 `family-water`;必须唯一,不建立数据库外键。 |
| `eyebrow` | `string` | 是 | 详情页顶部眉标,例如“玩法推荐”。 |
| `duration` | `string` | 是 | 展示用时长例如“5天4晚”接口保存最终文案不要求前端从标题正则提取。 |
| `title` | `string` | 是 | 详情页主标题。 |
| `subtitle` | `string` | 是 | 详情页副标题或目的地说明。 |
| `intro` | `string` | 是 | “详细介绍”区域的主介绍文案。 |
| `highlights` | `string[]` | 是 | “行程亮点”列表,保留数组顺序。 |
| `included` | `string[]` | 是 | “费用包含”列表,保留数组顺序。 |
| `excluded` | `string[]` | 是 | “费用不含”列表,保留数组顺序。 |
| `notes` | `string[]` | 是 | “注意事项”列表,保留数组顺序。 |
| `gallery` | `string[]` | 是 | 详情图片 URL 列表,按展示顺序返回;建议最多 6 张以匹配当前前台逻辑。 |
| `priceStartingValue` | `number \| null` | 否 | 参考起始值,单位为万元;未配置时为 `null`,不参与订单或报价计算。 |
| `priceUnit` | `"person" \| "day" \| "group" \| null` | 否 | 价格展示单位,分别对应“人”“天”“团”;未配置价格时为 `null`。 |
| `pricePeopleRange` | `string` | 否 | 适用人数范围例如“2-4人”仅用于展示。 |
| `priceDescription` | `string` | 否 | 价格补充说明,例如“价格以最终确认方案为准”。 |
| `conciergeAdvisorId` | `string \| null` | 否 | 关联的管家顾问 ID不建立数据库外键空字符串保存为 `null`。 |
| `isActive` | `boolean` | 响应必填 | 是否进入已发布前台内容,创建默认 `true`。 |
| `sortOrder` | `number` | 响应必填 | 非负整数,数值越小越靠前;创建时未传则追加到末尾。 |
| `createdAt` | `string` | 响应必填 | ISO 8601 创建时间。 |
| `updatedAt` | `string` | 响应必填 | ISO 8601 最后更新时间。 |
服务端应校验 `key` 唯一、文本字段去除首尾空白后不为空、数组元素为非空字符串、`gallery` 为 URL 列表、`sortOrder` 为非负整数。具体文本最大长度由后端 schema 统一定义,并同步到 Admin UI 表单校验。
## 接口详情
### 获取详情列表
```http
GET /api/admin/details
Authorization: Bearer <admin-jwt>
```
成功响应:
```json
{
"code": 200,
"msg": "success",
"data": {
"details": [
{
"id": "detail-001",
"key": "classic-panorama",
"eyebrow": "玩法推荐",
"duration": "5天4晚",
"title": "经典贵州全景",
"subtitle": "贵州·瀑布、苗寨、古城与山地风光",
"intro": "沿着贵州山地的自然纹理深入探索。",
"highlights": ["核心景观串联", "小团出行,按同行人节奏调整"],
"included": ["行程内用车与接送服务"],
"excluded": ["往返大交通及个人消费"],
"notes": ["贵州多山多雨,请准备防滑鞋和轻便雨具。"],
"gallery": ["https://example.test/assets/detail-01.jpg"],
"priceStartingValue": 1.68,
"priceUnit": "person",
"pricePeopleRange": "2-4人",
"priceDescription": "价格以最终确认方案为准。",
"isActive": true,
"sortOrder": 0,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
]
}
}
```
### 获取单个详情
```http
GET /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>
```
成功返回 `data` 内的单个 `DetailRecord`。记录不存在返回 `404`,业务码为 `DETAIL_NOT_FOUND`
### 新增详情
```http
POST /api/admin/details
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求体为 `DetailCreate`。成功返回 `201``data` 内新建的 `DetailRecord`;未传 `sortOrder` 时追加到当前列表末尾。`conciergeAdvisorId` 传空字符串时归一化为 `null`,传入不存在的顾问 ID 返回 `422 DETAIL_CONCIERGE_ADVISOR_INVALID`
### 编辑详情
```http
PATCH /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求体为 `DetailPatch`,只更新提交的字段。修改 `key` 时仍须保证全局唯一。成功返回 `data` 内更新后的 `DetailRecord`;记录不存在返回 `404`,业务码为 `DETAIL_NOT_FOUND``key` 冲突返回 `409`,业务码为 `DETAIL_KEY_EXISTS`
### 删除详情
```http
DELETE /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>
```
成功返回:
```json
{
"code": 200,
"msg": "success",
"data": { "id": "detail-001" }
}
```
删除后应重新规范化剩余记录的 `sortOrder`,从 `0` 开始连续编号。详情记录没有 Product、订单或线索外键因此删除不触发跨领域级联操作。
### 调整详情顺序
```http
PATCH /api/admin/details/reorder
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求必须完整包含当前全部详情 ID不能重复
```json
{ "itemIds": ["detail-002", "detail-001"] }
```
成功返回:
```json
{
"code": 200,
"msg": "success",
"data": { "items": [] }
}
```
其中 `items` 为更新 `sortOrder` 后的 `DetailRecord[]`。缺少 ID、包含未知 ID 或出现重复 ID 时返回 `400 DETAIL_REORDER_INVALID`
## MiniAPP Public API
### 获取路线详情
```http
GET /api/public/details/{key}
```
无需鉴权。`key``WanfaRoute.id`,例如 `family-water`。接口只返回启用且已配置的展示字段,不返回管理端 ID、状态、排序和审计时间
```ts
type PublicDetail = {
key: string;
eyebrow: string;
duration: string;
title: string;
subtitle: string;
intro: string;
highlights: string[];
included: string[];
excluded: string[];
notes: string[];
gallery: string[];
conciergeAdvisor: PublicConciergeAdvisor | null;
};
```
不存在、停用或未配置详情时返回 `404`。详情未配置顾问、顾问不存在或顾问已停用时,详情仍正常返回,但 `conciergeAdvisor``null`。MiniAPP 请求失败或字段不完整时,按 `key` 查找本地 fallbackfallback 不伪造管家数据,找不到 fallback 时展示未找到和重试状态。详情页仅在返回有效顾问时展示“联系管家”入口。
## 图片与素材
Admin UI 使用现有素材上传接口获取图片 URL
```http
POST /api/admin/media-assets/upload
```
建议详情页图片使用 `group=detail`,将返回的 `url` 按用户排列顺序写入 `gallery`。接口只保存图片 URL不创建 ProductImage 表或商品图片关联。
当前 `detailPresentation.ts` 的 fallback 图片属于前台兜底逻辑。Admin API 正式接入后Admin UI 不应把 fallback 图片自动写入数据库;应由运营人员明确上传和排序详情图片。
## Admin UI 对接要求
Admin UI 应按以下方式调用:
1. 玩法页加载时同时调用玩法分类、`GET /api/admin/details``GET /api/admin/concierge/advisors`;详情编辑器嵌入现有路线编辑抽屉,不新增侧边菜单。
2. 新增和编辑表单维护眉标、时长、标题、副标题、介绍、价格配置和四组列表文案;保存路线时以已保存的路线 ID 作为详情 `key`
3. `highlights``included``excluded``notes` 使用可增删的重复字段编辑器,提交时保留数组顺序,不拼接成换行字符串。
4. 使用图片上传接口维护 `gallery`,支持新增、删除和调整图片顺序。
5. 在详情图片后维护价格起始值、价格单位、适用人数范围和价格说明;价格未配置时提交空值,不能写入前台固定价格。
6. 上移或下移详情时提交完整详情 ID 列表,不直接修改本地 `sortOrder` 后假设保存成功。
7. 删除前要求二次确认;删除成功后以接口返回或重新查询的数据更新列表。
8. 处理 `401``404``409``422``5xx`,保存、上传或排序进行中禁用重复提交。
9. 详情编辑器不得出现 Product ID、ProductImage ID、库存、订单或预订字段联系管家使用顾问 ID 选择器;详情保存失败时明确提示路线摘要已保存、详情需要重试。
建议的 Admin UI API 封装函数:
```ts
getDetails();
getDetail(detailId: string);
createDetail(input: DetailCreate);
updateDetail(detailId: string, input: DetailPatch);
deleteDetail(detailId: string);
reorderDetails(itemIds: string[]);
```
## 与前台展示模型的映射
Public API 返回的 `PublicDetail` 可以直接映射为 MiniAPP 的 `DetailPresentation`Admin API 返回的 `DetailRecord` 只包含 `conciergeAdvisorId`,不复制管家资料:
```ts
const presentation: DetailPresentation = {
key: record.key,
eyebrow: record.eyebrow,
duration: record.duration,
title: record.title,
subtitle: record.subtitle,
intro: record.intro,
highlights: record.highlights,
included: record.included,
excluded: record.excluded,
notes: record.notes,
gallery: record.gallery,
priceStartingValue: publicDetail.priceStartingValue,
priceUnit: publicDetail.priceUnit,
pricePeopleRange: publicDetail.pricePeopleRange,
priceDescription: publicDetail.priceDescription,
conciergeAdvisor: publicDetail.conciergeAdvisor ?? null,
};
```
接入时应优先使用接口已保存的最终文案和图片顺序;本地 fallback 只用于接口失败、字段不完整或详情未配置的前台容错。
## 后端落地边界
当前实现由 `WonderQ-Admin``DetailRecord` 模型、Admin/Public 路由和审计日志提供能力;由 `WonderQ-Admin-UI-Vue` 在玩法路线编辑抽屉中维护详情及顾问 ID`WonderQ-MiniAPP` 调用 Public 详情接口并按路线 ID fallback。顾问资料始终从管家领域实时读取不复制到详情表。
相关文档:
- [Admin API 主契约](./admin-api-requirements.md)
- [管家管理 Admin API](./concierge-api.md)
- [详情展示适配器](../WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts)