93 lines
9.8 KiB
Markdown
93 lines
9.8 KiB
Markdown
# 馆内好物:架构、使用与交付
|
||
|
||
## 产品范围
|
||
|
||
本次实现轻量的「单商品立即购买 + 微信支付 + 到馆自提」。首页进入好物列表和详情,个人中心查看商品订单;管理中心新增「商品、库存与销售订单」。
|
||
|
||
- 管理员维护标题、HTTPS 封面(复用 COS 上传)、说明、价格、排序、上下架状态。
|
||
- 各尺码、颜色按独立商品管理,在标题中标明规格;不引入购物车、物流、优惠券或复杂 SKU 系统。
|
||
- 创建商品默认库存为 0;保存后通过库存调整补货。调整数量为有符号整数,必须填写原因。
|
||
- 顾客填写取货人、手机号及数量,确认购买后微信付款。价格变化时先阻止下单,要求确认最新价格。
|
||
- 付款成功后订单进入待发货。管理员核对商品数量和取货人后,直接确认发货并完成订单。
|
||
- 管理端支持商品搜索、订单状态筛选、商品名 / 订单号 / 手机号搜索、分页、累计成交额、销量、待发货数、低库存提示。
|
||
- 售后通过详情页「联系场馆」沟通;本版不含线上退款流程。不能把已付款订单当作未付款订单取消。若后续引入商户平台退款,需配套退款对账及库存回补逻辑,不能仅修改订单状态。
|
||
|
||
## 现有架构与接入方式
|
||
|
||
仓库为 pnpm workspace:`app`(Vue 3、Pinia、uni-app)、`server`(NestJS、Prisma/MySQL)、`shared`(共享类型)。现有会员卡订单与开卡、续卡、邀请赠课、身体画像关联,因此实物商品使用独立模型和模块,不进入会员卡发放链路。
|
||
|
||
- 服务端:`src/shop/`,经 `AppModule` 注册。复用 `PrismaService`、JWT、`RolesGuard`、微信支付签名/解密请求能力、现有调度框架。
|
||
- 数据:新增 `Product`、`ProductOrder`、`ProductStockChange`;订单关联现有 `User`。
|
||
- 前端:`pages/shop` 独立分包,首页 `ProductShop` 展示精选商品;用户及管理员复用 `ShopOrders` 组件;商品管理仍在 admin 分包。
|
||
- 共享类型:`types/shop.ts`。所有金额按整数分传输,界面按元展示。
|
||
- 视觉:延续首页奶油白、暖棕、鼠尾草绿,用大封面、留白、衬线标题和清晰的价格层级呈现馆内精选。
|
||
- 技能:使用 frontend-design 指导界面;服务端遵循仓库既有 NestJS/Prisma、DTO 校验、权限、事务和 Jest 约定。当前技能目录没有专门的 NestJS 后端技能,没有引入无关鉴权框架。
|
||
|
||
## 库存和支付规则
|
||
|
||
`Product.stock` 表示**当前可售库存**,已经排除待付款订单预占量。`sold` 为已付款订单的累计件数。
|
||
|
||
1. 下单以用户行锁串行限制每人最多 5 笔待付款订单,以 `(userId, requestId)` 保证幂等。
|
||
2. 事务内读取商品,验证上架及顾客看到的单价,用条件更新 `stock >= quantity`、商品 `version` 原子预占库存。订单保存当时的标题、封面、单价和联系人快照。
|
||
3. 每笔最多 99 件,订单有效期 15 分钟。统一下单使用同一个商户订单号和明确的微信 `time_expire`。
|
||
4. 客户端支付成功不直接更新订单。服务端查询微信,核对商户、应用、订单号、金额、币种、openid 和微信交易号,才能从 PENDING 更新为 PAID。
|
||
5. 商品回调独立使用 `/api/shop/wx-notify`,只接受 AES-GCM 解密后的资源,再查单确认,不依赖旧会员卡回调的验签占位实现。成功返回 HTTP 204,绕过统一 API 包装。
|
||
6. 定时任务每 30 秒按最近对账时间分批扫描待付款订单,支持漏回调恢复;用户也可刷新支付状态。失败会保留库存并重试。
|
||
7. 取消订单先查单、关单,关单成功后才原子释放库存;若先查到支付成功则转为待发货。关单失败绝不提前释放。
|
||
8. 微信尚无订单时,可能存在正在途中的预支付请求;过期后再等待 60 秒才释放,避免“取消释放后又支付”。因此用户从未成功调起支付时,取消可能提示等待,最晚由超时任务回收(微信服务异常期间会继续保留并重试)。
|
||
9. 状态更新带条件,重复回调、重复取消或并发扫描不会重复计算销量、重复返还库存。发货完成仅允许已付款订单,并记录操作人/时间。
|
||
10. 管理员保存商品和库存均校验版本;发生竞争时提示刷新。库存调整记录最近 50 条人工调整原因及操作人;系统预占/释放由订单本身追溯。
|
||
|
||
状态:`PENDING → PAID → COMPLETED`;`PENDING → CANCELLED`。商品可下架但不删除,保证历史订单有效。
|
||
|
||
## 接口
|
||
|
||
公开:`GET /shop/products`、`GET /shop/products/:id`。
|
||
|
||
登录用户:`POST /shop/orders`、`GET /shop/orders`、`GET /shop/orders/:id`,以及 `POST /shop/orders/:id/pay|sync|cancel`。所有读写均按 JWT 用户限定订单归属。
|
||
|
||
管理员:`/admin/shop/summary`、`/admin/shop/products`(GET/POST)、`/admin/shop/products/:id`(GET/PUT)、`/admin/shop/products/:id/stock`(GET/POST)、`/admin/shop/orders`(GET)、`/admin/shop/orders/:id/complete`(POST)。控制器统一要求 ADMIN。
|
||
|
||
分页默认 20,最大 50;金额、数量、手机号、封面 URL、状态筛选及分页都使用 DTO 校验。客户端不能指定订单总价或购买用户。
|
||
|
||
## 上线顺序
|
||
|
||
2026-09-16 已按用户授权完成生产数据库迁移和服务端部署。以下为后续发布参考流程;本次执行结果见文末。
|
||
|
||
1. 备份数据库,先在测试数据库验证迁移 `20260916090000_studio_shop`。此迁移只新增 3 张表及关系索引,不改写已有会员数据。
|
||
2. 在 server 包运行 `pnpm exec prisma migrate deploy` 和 `pnpm prisma:generate`。
|
||
3. 根目录运行 `pnpm build:shared`、`pnpm build:server`,部署并重启后端。
|
||
4. 沿用微信支付配置和 COS 配置,无新增必填环境变量。确保 `API_BASE_URL` 指向服务域名、外网能访问 `/api/shop/wx-notify`,调度进程正常运行。
|
||
5. 构建发布小程序 `pnpm build:app`;新页面仍调用既有线上 API 域名,必须先上线后端。
|
||
6. 管理员创建真实商品、上传封面、补货、上架。没有硬编码测试商品写入数据库。
|
||
7. 在微信真机执行一笔小额购买:取消支付后续付、查看服务端支付确认、管理员直接发货完成、超时关单及库存回补。退款不在本版本内。
|
||
|
||
## 验证与限制
|
||
|
||
- 共享包、服务端构建;uni-app 类型检查;微信小程序构建;Prisma schema validate;Git diff 空白检查。
|
||
- Jest 覆盖价格变更、越权归属、负数/小数/非法输入、请求幂等、最后库存竞争、支付金额及付款人核对、重复回调、关单失败、过期释放、版本冲突、发货完成状态和微信 GET 签名规范。
|
||
- 全量 Jest 覆盖率运行:26 个套件、418 项测试通过(新增 60 项测试);并发测试基于条件更新行为模拟,不能替代真实 MySQL 多连接并发验证。
|
||
- 用本地独立测试数据在 390px 手机尺寸 H5 预览检查列表、详情、管理汇总、编辑页面。测试图片为标注 PREVIEW 的占位插画,未上传 COS、未接触线上商品。
|
||
- 已完成生产数据库迁移、服务端部署和线上接口冒烟验证。尚未执行真实微信扣款、COS 实际上传或微信真机验收;小程序新版仍需发布。
|
||
- 仓库 `pnpm lint` 目前失败于 `eslint: command not found`;package.json 没有安装 ESLint,仓库也没有提供 ESLint 配置。此项不代表 lint 已通过。
|
||
- 构建有既有 Browserslist 数据过期、Sass legacy API 警告;商城共享样式已改为纯 CSS,通过 Vue `<style scoped src="…/shop.css">` 引入,避免 Sass `@import` 弃用警告及 uni-app 全局样式注入对 `@use` 顺序的影响。
|
||
|
||
微信协议参考:[官方 JSAPI 查单、关单接口](https://github.com/wechatpay-apiv3/wechatpay-go/blob/main/docs/payments/jsapi/JsapiApi.md)、[官方小程序下单及有效期说明](https://pay.wechatpay.cn/doc/v3/merchant/4012791897)。
|
||
|
||
## 生产部署记录(2026-09-16)
|
||
|
||
- 服务目录:`/usr/local/web/mp-pilates-server`;共享包:`/usr/local/web/mp-pilates-shared`。
|
||
- 使用服务器现有 Node 22.16.0、PM2 和 Prisma 5.22.0。仓库旧部署脚本写死的 Node 22.17.1 路径不适用于当前服务器,本次未使用该脚本。
|
||
- 迁移:`20260916090000_studio_shop` 成功;全部 10 个迁移完成。新三表可通过 Prisma Client 正常访问,初始均为 0 条。
|
||
- 发布包 SHA-256:`7eeea42587cf2e188e337619a9a4ea0b88bbe97fe3857e733e39b4e65fe3f1fb`。
|
||
- 备份目录:`/usr/local/web/releases/mp-pilates-shop-20260916/`(服务器本机受限访问)。
|
||
- `database-before.sql.gz`:迁移前完整数据库逻辑备份,已检查 gzip 完整性。
|
||
- `previous-build.tgz`:旧服务、共享包、Prisma schema/迁移和生成的 Prisma Client。
|
||
- `live-dist-before` / `live-shared-before`:切换前运行产物。
|
||
- `release.tgz` / `stage`:本次发布包和暂存文件。
|
||
- 保留线上环境配置、支付证书和已有依赖。PM2 已重启并保存进程配置,健康检查及商品列表成功。
|
||
- 公网验证:健康、商品列表、场馆信息均 200;非法分页 400;未登录商品订单/管理接口 401。
|
||
- 服务器本机验证:管理员商品/订单/汇总接口 200;普通会员管理接口 403;用户商品订单 200;旧会员卡列表 200;无效支付回调 400。权限验证使用 30 秒有效、仅用于只读冒烟检查的测试身份,不操作真实会员账号。
|
||
- 未创建演示商品、测试订单或发起真实扣款。小程序新版发布后,管理员即可从新入口维护真实商品。
|
||
- 回滚以恢复旧运行产物和对应生成 Client、重启 PM2 为主;新表为增量结构,应保留,避免删除上线后产生的数据。数据库全量恢复仅用于灾难恢复,不应在正常版本回滚时直接覆盖。
|