Files
WonderQ-Project/docs/detail-api.md
duanshuwen ca6f9397e0 feat(mini-app, docs): 新增小程序详情页,更新项目文档与规范
新增全套关联组件:DetailHero、DetailOverviewCard、DetailInfoCard、DetailMediaGallery、DetailActionBar及ConciergeContactSheet。
新增详情页数据处理工具类detailPresentation.ts,处理商品展示数据的格式化与默认值兼容。
更新项目文档:补充三个领域API补充文档引用,优化AGENTS.md中的前端代码规范与docs文档列表。
2026-08-17 23:28:07 +08:00

295 lines
11 KiB
Markdown
Raw 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`。
>
> 状态:待实现契约。本文件参考 `WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts` 定义详情页展示数据。当前 `detailPresentation.ts` 仍依赖已移除的 `Product` 类型,后端也没有详情管理接口;本契约采用与商品领域解耦的详情展示模型,不恢复 Product、ProductImage 或订单关联。
## 领域边界
详情管理只维护详情页可编辑的展示内容:
- 详情页识别键、标题眉标、出行时长和标题文案。
- 详情介绍、行程亮点、费用包含、费用不含和注意事项。
- 详情页图片画廊及图片顺序。
- 启用状态和详情列表顺序。
本接口不负责:
- 商品、商品价格、商品库存或商品详情表。
- Product、ProductImage 或任何商品外键。
- 订单、预订、收藏、评价或线索。
- 详情页底部的电话、管家联系和预订动作。
`key` 是详情展示内容自己的稳定业务键,不得设计为 Product ID 外键。详情页如何从玩法、页面入口或其他前台上下文定位 `key`,由前台导航契约另行约定。
## 与 `detailPresentation.ts` 的关系
`detailPresentation.ts` 当前是前台展示适配器,不是持久化模型:
- `eyebrow``duration``title``subtitle``intro``highlights``included``excluded``notes``gallery` 组成最终展示对象。
- 当前实现从 `Product` 的标题、摘要、标签、详情区块和图片数组推导部分字段。
- 当前实现对洞穴/探险路线生成另一组固定亮点,并使用固定费用说明和注意事项。
- 当前实现会将主图、接口图片和 fallback 图片去重后截取前 6 张。
新的 Admin API 应直接维护最终展示字段Admin UI 不应复刻这些推导逻辑,也不应依赖已移除的 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` 或省略字段。
- 失败响应沿用 Admin API 约定,包含 `message``code` 和可选的 `details`
## 数据类型
以下内容字段与 `DetailPresentation` 保持兼容,管理端补充 `id``key`、状态和审计时间:
```ts
type DetailPresentation = {
eyebrow: string;
duration: string;
title: string;
subtitle: string;
intro: string;
highlights: string[];
included: string[];
excluded: string[];
notes: string[];
gallery: string[];
};
type DetailRecord = DetailPresentation & {
id: string;
key: string;
isActive: boolean;
sortOrder: number;
createdAt: string;
updatedAt: string;
};
type DetailCreate = DetailPresentation & {
key: string;
isActive?: boolean;
sortOrder?: number;
};
type DetailPatch = Partial<DetailCreate>;
type DetailListResponse = {
details: DetailRecord[];
};
type DetailReorderRequest = {
itemIds: string[];
};
```
## 字段约束
| 字段 | 类型 | 必填 | 约束和用途 |
| --- | --- | --- | --- |
| `id` | `string` | 响应必填 | 后端生成的记录 ID仅供管理端识别记录。 |
| `key` | `string` | 是 | 详情展示稳定键;建议使用小写字母、数字和中划线,例如 `classic-panorama`。不得关联 Product 表。 |
| `eyebrow` | `string` | 是 | 详情页顶部眉标,例如“玩法推荐”。 |
| `duration` | `string` | 是 | 展示用时长例如“5天4晚”接口保存最终文案不要求前端从标题正则提取。 |
| `title` | `string` | 是 | 详情页主标题。 |
| `subtitle` | `string` | 是 | 详情页副标题或目的地说明。 |
| `intro` | `string` | 是 | “详细介绍”区域的主介绍文案。 |
| `highlights` | `string[]` | 是 | “行程亮点”列表,保留数组顺序。 |
| `included` | `string[]` | 是 | “费用包含”列表,保留数组顺序。 |
| `excluded` | `string[]` | 是 | “费用不含”列表,保留数组顺序。 |
| `notes` | `string[]` | 是 | “注意事项”列表,保留数组顺序。 |
| `gallery` | `string[]` | 是 | 详情图片 URL 列表,按展示顺序返回;建议最多 6 张以匹配当前前台逻辑。 |
| `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
{
"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"],
"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>
```
成功返回单个 `DetailRecord`。记录不存在返回 `404 DETAIL_NOT_FOUND`
### 新增详情
```http
POST /api/admin/details
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求体为 `DetailCreate`。成功返回 `201` 和新建的 `DetailRecord`;未传 `sortOrder` 时追加到当前列表末尾。
### 编辑详情
```http
PATCH /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>
Content-Type: application/json
```
请求体为 `DetailPatch`,只更新提交的字段。修改 `key` 时仍须保证全局唯一。成功返回更新后的 `DetailRecord`;记录不存在返回 `404 DETAIL_NOT_FOUND``key` 冲突返回 `409 DETAIL_KEY_EXISTS`
### 删除详情
```http
DELETE /api/admin/details/{detailId}
Authorization: Bearer <admin-jwt>
```
成功返回:
```json
{ "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
{ "items": [] }
```
其中 `items` 为更新 `sortOrder` 后的 `DetailRecord[]`。缺少 ID、包含未知 ID 或出现重复 ID 时返回 `400 DETAIL_REORDER_INVALID`
## 图片与素材
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`,按 `sortOrder` 渲染详情列表。
2. 新增和编辑表单维护 `key`、眉标、时长、标题、副标题、介绍和四组列表文案。
3. `highlights``included``excluded``notes` 使用可增删的重复字段编辑器,提交时保留数组顺序,不拼接成换行字符串。
4. 使用图片上传接口维护 `gallery`,支持新增、删除和调整图片顺序。
5. 上移或下移详情时提交完整详情 ID 列表,不直接修改本地 `sortOrder` 后假设保存成功。
6. 删除前要求二次确认;删除成功后以接口返回或重新查询的数据更新列表。
7. 处理 `401``404``409``422``5xx`,保存、上传或排序进行中禁用重复提交。
8. 详情编辑器不得出现 Product ID、ProductImage ID、库存、订单或预订字段。
建议的 Admin UI API 封装函数:
```ts
getDetails();
getDetail(detailId: string);
createDetail(input: DetailCreate);
updateDetail(detailId: string, input: DetailPatch);
deleteDetail(detailId: string);
reorderDetails(itemIds: string[]);
```
## 与前台展示模型的映射
Admin API 返回的 `DetailRecord` 可以映射为 `DetailPresentation`
```ts
const presentation: DetailPresentation = {
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,
};
```
接入时应优先使用接口已保存的最终文案和图片顺序,不再依赖 `stripTitle`、标题时长正则、洞穴路线分支或 fallbackGallery 生成同一字段。
## 后端落地边界
本契约落地时需要由 `WonderQ-Admin` 补充对应 ORM 模型、迁移、schema、路由、序列化和审计日志`WonderQ-Admin-UI` 补充 API 类型、请求封装、详情列表、编辑器、图片管理和排序交互。当前后端没有 `/api/admin/details` 路由Admin UI 没有详情管理入口,`detailPresentation.ts` 也不是可直接作为后端契约的完整类型来源。
相关文档:
- [Admin API 主契约](./admin-api-requirements.md)
- [管家管理 Admin API](./concierge-api.md)
- [详情展示适配器](../WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts)