feat(catalog): add product catalog APIs

This commit is contained in:
lai_hong
2026-09-13 11:17:53 +08:00
parent 63c0013076
commit 59e398fcf9
15 changed files with 655 additions and 68 deletions
+172
View File
@@ -0,0 +1,172 @@
# R1 商品目录接口对接说明
**提供方:成员 1R1 商品目录)**
**消费者:成员 2(设计清单)、成员 3(订单)、成员 4(设计/生产数据)**
**分支:`feat/r1-catalog`**
本文档是 R1 向其他路线交付商品数据的简明使用说明。字段、路径与
[`api-contract-v1.md`](./api-contract-v1.md) 第 3 节一致;发生冲突时以 API 契约为准。
## 1. 使用边界
- 商品与分类查询都是公开接口,调用时必须传 `auth: false`,不依赖登录态。
- 消费方只能读取在售商品;下架或不存在的商品详情返回 `404`
- `productId` 是跨路线的稳定业务 ID,不能用数据库生成顺序、数组下标或展示名称替代。
- `price` 是服务端权威价格。前端可展示,**不能**作为下单金额依据。
## 2. 稳定商品 ID
| productId | 商品 |
|---|---|
| `notebook-small` | 微雕笔记本(小) |
| `notebook-large` | 微雕笔记本(大) |
| `coaster` | 铜质杯垫 |
| `penbox` | 竹制笔盒 |
| `booklamp` | 书本型灯 |
每个商品都对应一个稳定分类 ID`cat-<productId>`,例如 `cat-penbox`
## 3. 查询接口
### `GET /api/categories`
返回扁平分类数组,已按 `sort` 升序排列。
```ts
type CategoryDTO = {
id: string
name: string
parentId?: string | null
sort: number
}
```
### `GET /api/products`
查询参数均可选:
```ts
{
page?: number // 默认 1
pageSize?: number // 默认 20,最大 100
categoryId?: string
keyword?: string // 匹配名称、副标题、描述
}
```
响应 `data`
```ts
{
list: ProductDTO[]
total: number
page: number
pageSize: number
}
```
### `GET /api/products/:id`
返回一个在售商品;商品不存在或不是 `ON_SALE` 时返回 `404`
```ts
type ProductDTO = {
id: string
name: string
categoryId?: string
price: number // 元,服务端已转为 number
originalPrice?: number
leadTime: string
subtitle?: string
description?: string
story?: string
scene?: string
tags: string[]
specs: [string, string][]
tone: [number, number, number]
mask: {
shape: 'rect' | 'circle'
width: number
height: number
borderRadius?: number
}
images: string[]
iconImg?: string
status: 'ON_SALE'
sort: number
}
```
所有接口沿用统一响应包裹:`{ code: 0, message: 'ok', data }`
## 4. 前端调用方式
前端消费者统一从 R1 API 层调用,不能在页面直接拼 HTTP 请求:
```ts
import { fetchCategories, fetchProduct, fetchProducts } from '../../utils/api/product'
const { list } = await fetchProducts({ keyword: '笔记本', page: 1, pageSize: 20 })
const product = await fetchProduct('penbox')
const categories = await fetchCategories()
```
## 5. 给成员 2:设计清单
设计清单保存商品相关数据时使用:
```ts
{
productId: product.id,
productName: product.name,
unitPrice: product.price, // 仅展示/设计清单快照;不是订单结算依据
count: quantity,
designData: {
category: { id: product.id, mask: product.mask, tone: product.tone },
},
}
```
`mask``tone` 必须原样保留,供 R4 构造生产画布;不要把 `productName` 当作关联键。
## 6. 给成员 3:订单
R3 创建订单时,客户端请求只传:
```ts
{
items: [{ productId: 'penbox', quantity: 1 }]
}
```
R3 服务端必须:
1.`productId` 查询商品并确认 `status === ON_SALE`
2. 以服务端 `price` 重算订单总价;
3.`productId``name``price``quantity` 写进 `OrderItem` 快照;
4. 忽略客户端传入的单价、商品名和总金额。
## 7. 给成员 4:设计与生产
R4 若需要从商品恢复画布尺寸或主题色,使用 `ProductDTO.mask``ProductDTO.tone`
生产任务内保存的是设计快照;不要仅凭商品名称重新定位商品。
## 8. 联调前置条件
R1 合入或本地联调前,后端需要先执行数据库迁移与种子数据:
```bash
npm run prisma:migrate
npm run prisma:seed
```
最小 smoke 验收:
```text
GET /api/categories
GET /api/products?page=1&pageSize=20
GET /api/products/penbox
GET /api/products/not-on-sale-or-missing -> 404
```
+90
View File
@@ -0,0 +1,90 @@
# R1 商品目录交付说明
**提供方:成员 1R1 商品目录)**
**可使用成员:成员 2(设计清单)、成员 3(订单)、成员 4(设计与生产)**
**分支:`feat/r1-catalog`**
R1 商品目录已完成,可以对外提供商品数据。商品页面现在从后端接口读取商品信息,不再以写死在前端的商品数据作为主数据源。本说明中的验证结果用于证明商品目录已经真实可用;接口说明用于方便其他成员接入这份目录数据。
## 已完成内容
| 完成项 | 结果 |
| --- | --- |
| 后端商品数据 | 已建立商品分类及 5 个在售商品的本机数据。 |
| 商品接口 | 已提供商品列表、单个商品详情和分类列表接口。 |
| 前端商品页面 | 首页、商品列表、商品详情、立即定制页均已接入商品接口。 |
| 本机联调环境 | Docker 中的后端、数据库、Redis 已启动;微信开发者工具可正常运行前端。 |
## 已完成前后端数据通路验证
这次验证不只是确认接口能访问,而是确认后端数据变化会传到前端页面,证明前端展示的确实是后端商品数据。
| 验证步骤 | 验证结果 |
| --- | --- |
| 读取商品初始值 | `notebook-small` 的价格为 12 元,划线原价为 18 元。 |
| 修改后端数据库 | 将该商品测试价格改为 19 元,测试划线原价改为 29 元。 |
| 重新编译小程序 | 微信开发者工具重新编译后,商品卡片和详情页同步显示 19 元、29 元。 |
| 验证结论 | 前端展示随数据库和接口返回同步改变,前后端商品数据通路正常。 |
![image-20260912143719516](C:\Users\24595\AppData\Roaming\Typora\typora-user-images\image-20260912143719516.png)
![image-20260912143750067](C:\Users\24595\AppData\Roaming\Typora\typora-user-images\image-20260912143750067.png)
**结论:** R1 商品页面读取的是后端真实商品数据;后端数据变化能够传递到前端页面。因此以下接口已完成实际联调验证,可作为其他路线的商品数据来源。
## 对外使用约定
| 约定 | 说明 |
| --- | --- |
| 稳定商品 ID | 跨模块使用 `productId` 关联商品,例如 `notebook-small``coaster``penbox`。不要使用数组下标或商品名称作为关联键。 |
| 公开查询 | 商品与分类查询不依赖登录态;前端调用时使用 `auth: false`。 |
| 价格来源 | `price` 是服务端权威价格。前端可展示价格,订单金额必须由 R3 服务端重新查询并计算。 |
| 在售限制 | 商品列表与详情只返回 `ON_SALE` 商品;不存在或下架商品的详情返回 `404`。 |
## 商品接口
所有接口统一返回:`{ code: 0, message: 'ok', data }`。实际业务数据位于 `data` 中。
| 接口 | 用途 | 参数 | 验证状态 |
| --- | --- | --- | --- |
| `GET /api/categories` | 获取按 `sort` 排序的分类列表 | 无 | 已验证 |
| `GET /api/products` | 获取在售商品分页列表 | `page``pageSize``categoryId``keyword` 均可选 | 已验证 |
| `GET /api/products/:id` | 获取一个在售商品的完整详情 | `id` 为稳定商品 ID | 已验证 |
### 商品数据字段
| 字段 | 含义 |
| --- | --- |
| `id``name``categoryId``status``sort` | 商品标识、名称、分类、在售状态和稳定排序。 |
| `price``originalPrice``leadTime` | 商品价格、划线原价和制作周期。 |
| `subtitle``description``story``scene` | 商品卖点、介绍、设计理念和使用场景。 |
| `images``iconImg``tone` | 商品图片、图标与详情页视觉主题色。 |
| `tags``specs``mask` | 商品标签、规格参数和定制画布信息。 |
### 可用商品 ID
| productId | 商品名称 | 对应分类 ID |
| --- | --- | --- |
| `notebook-small` | 微雕笔记本(小) | `cat-notebook-small` |
| `notebook-large` | 微雕笔记本(大) | `cat-notebook-large` |
| `coaster` | 铜质杯垫 | `cat-coaster` |
| `penbox` | 竹制笔盒 | `cat-penbox` |
| `booklamp` | 书本型灯 | `cat-booklamp` |
## 给其他成员的使用说明
| 成员 | 可以使用的商品数据 |
| --- | --- |
| 成员 2:设计清单 | 保存 `productId``productName``price``count`;设计数据中原样保留 `mask``tone`。 |
| 成员 3:订单 | 客户端只传 `productId``quantity`;订单服务端必须重新读取 `price` 计算金额。 |
| 成员 4:设计与生产 | 使用 `mask` 确定画布尺寸,使用 `tone` 作为主题色;生产任务保存商品 ID 与设计快照。 |
## 本机联调地址
- 后端接口:`http://127.0.0.1:3090`
- Swagger 接口页面:`http://127.0.0.1:3090/docs`
- 小程序导入目录:`wechat_wc/dist`
以上地址只适用于本机模拟器联调。真机和生产环境需要替换为已配置合法域名的 HTTPS 地址。
+3 -1
View File
@@ -56,7 +56,7 @@
更新当前用户资料。请求体:`{ nickname?: string, avatar?: string }`
## 3. 商品与分类(R1,待实现
## 3. 商品与分类(R1
### GET /api/categories
@@ -75,6 +75,8 @@
仅返回 `status=ON_SALE` 的商品。
响应 `data``{ list: ProductDTO[], total: number, page: number, pageSize: number }`
### GET /api/products/:id
公开接口。返回单个在售商品;不存在返回 404。
+125
View File
@@ -0,0 +1,125 @@
# 本机容器化联调教程
这套方案的目标是:**Windows 只负责编辑源码和运行微信开发者工具;PostgreSQL、Redis、Node.js/NestJS、Prisma 都运行在 Linux 容器中。** 因此本机行为尽量贴近未来 Linux 服务器,避免把 Windows 的 Node、数据库服务或路径习惯带进部署产物。
## 先理解两种 Compose 运行方式
| 目的 | 命令 | 特性 |
| --- | --- | --- |
| 日常开发、改代码自动重载 | `docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build` | 源码挂载进 Linux 开发容器,NestJS 热重载 |
| 发布前模拟 | `docker compose up -d --build` | 使用 `docker/Dockerfile` 的多阶段生产构建,不挂载源码 |
两种方式共用 PostgreSQL、Redis、迁移文件和 `.env`;不要同时启动它们。日常使用第一种,准备交付或部署前再使用第二种。
> Windows 要运行 `node:alpine`、`postgres:alpine` 这类 Linux 镜像,Docker Desktop 底层必须使用 **WSL 2、Hyper-V 或 Docker VMM** 之一。你不需要在 WSL 里写代码或打开 Ubuntu;它只是 Docker 的 Linux 运行底座。若完全不允许这些虚拟化能力,就只能改用远程 Linux 测试机,无法在 Windows 本机运行 Linux Compose。
## 一次性安装
1. 在 BIOS/UEFI 确认开启 CPU 虚拟化(Intel VT-x / AMD-V)。
2. 以管理员身份打开 PowerShell,执行 `wsl --install`;已安装时执行 `wsl --update`,按提示重启。
3. 安装 Docker Desktop for Windows,首次启动时选择 **Use WSL 2 instead of Hyper-V**。无需在 WSL 内安装 Node、PostgreSQL 或 Redis。
4. 重启 Docker Desktop 后,在普通 PowerShell 验证:
```powershell
docker version
docker compose version
```
安装依据见 [Docker Desktop for Windows 官方文档](https://docs.docker.com/desktop/setup/install/windows-install/) 与 [WSL 2 后端说明](https://docs.docker.com/desktop/features/wsl/)。
## 启动后端联调环境
以下命令均在 `G:\wordcloud_wechat\wxmp_backend` 执行。
1. 创建只属于本机的配置文件(该文件被 Git 忽略):
```powershell
Copy-Item .env.example .env
```
2. 打开 `.env`,至少改成下列本机联调值。不要提交 `.env`,真实微信密钥也不要放入前端。
```dotenv
NODE_ENV=development
APP_PORT=3090
JWT_SECRET=dev-only-change-this-to-a-long-random-string
WX_APPID=local-dev-appid
WX_SECRET=local-dev-secret
WX_MOCK_LOGIN=1
```
`DATABASE_URL` 和 `REDIS_URL` 可保持示例值:Compose 会在容器内自动改为 `postgres`、`redis` 服务地址。
3. 首次先拉取所有公开测试镜像,再构建并启动开发环境:
```powershell
docker compose -f docker-compose.yml -f docker-compose.dev.yml pull
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
docker compose -f docker-compose.yml -f docker-compose.dev.yml ps
```
预期 `postgres`、`redis`、`app` 都是 running/healthy。查看后端实时日志:
```powershell
docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f app
```
4. 导入 R1 商品初始数据(只需首次,或想恢复演示数据时执行):
```powershell
docker compose -f docker-compose.yml -f docker-compose.dev.yml exec app npm run prisma:seed
```
5. 在浏览器检查:
- `http://127.0.0.1:3090/docs`Swagger 接口页
- `http://127.0.0.1:3090/api/products`R1 商品列表 JSON
修改 `src/` 或 `prisma/` 后,开发 Compose 的 `app` 会热重载;变更依赖或 Dockerfile 后重新执行 `up -d --build`。停止环境用 `docker compose -f docker-compose.yml -f docker-compose.dev.yml down`。这不会删除数据库数据卷;需要完全清空数据时才使用 `down -v`。
## 微信开发者工具:打开哪里、怎样看到页面
应导入 **前端项目根目录**`G:\wordcloud_wechat\wechat_wc`。
不要导入工作区总目录 `G:\wordcloud_wechat`,也不要直接导入 `dist`。原因是前端根目录的 `project.config.json` 中已经声明 `miniprogramRoot: "dist/"`;开发者工具从该根目录读取项目配置,再把 `dist` 当作实际小程序输出。
首次操作如下:
1. 在一个 PowerShell 窗口进入前端目录,安装依赖并创建本机环境文件:
```powershell
cd G:\wordcloud_wechat\wechat_wc
npm ci
Copy-Item .env.example .env
npm run dev:weapp
```
最后一条命令保持运行。Taro 会持续把源码编译到 `dist/`;它不是服务器,也不需要在 Windows 安装后端 Node。
2. 打开微信开发者工具,选择 **导入项目**,目录选择 `G:\wordcloud_wechat\wechat_wc`。若提示 AppID,使用项目已有 AppID;仅查看 UI 也可选择测试号/游客模式(以工具实际选项为准)。
3. 等待终端首次构建完成,在开发者工具点顶部 **编译**。中间的 **模拟器** 就是小程序运行画面;切换底部或右侧的 **Console** 看前端异常、**Network** 看接口请求。以后保存 `src` 中的文件,Taro 会更新 `dist`,工具会自动或在点“编译”后刷新。
4. 为本机 HTTP 接口联调,打开开发者工具的 **详情 → 本地设置**,勾选“**不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书**”。仅本机开发使用,提交/真机预览前必须关闭。
前端 `.env` 已提供:
```dotenv
TARO_APP_API_BASE_URL=http://127.0.0.1:3090
```
它会在 `npm run dev:weapp` 编译时注入;`src/utils/request.ts` 因而请求本机 Docker 后端。更换 `.env` 后必须重启该 Taro 命令。Taro 环境变量与构建调试方式可参阅 [Taro 官方文档](https://docs.taro.zone/docs/env-mode-config) 和 [调试文档](https://docs.taro.zone/docs/envs-debug)。
## 联调检查顺序
```text
浏览器 /docs、/api/products 正常
Taro 终端构建成功,dist/ 更新
微信开发者工具导入 wechat_wc 并编译
Network 中商品请求为 http://127.0.0.1:3090/api/products,状态 200
商品页展示 R1 数据
```
注意:`127.0.0.1` 仅代表运行开发者工具的这台 Windows 电脑。它适合模拟器,不适合真实手机;真机或他人联调时,应改为可访问的 HTTPS 测试域名,并在微信公众平台配置合法 request 域名。生产环境通常让 Nginx/Caddy 作为 HTTPS 入口,应用、PostgreSQL、Redis 仍留在 Docker 内部网络,不对公网暴露。