9.8 KiB
馆内好物:架构、使用与交付
产品范围
本次实现轻量的「单商品立即购买 + 微信支付 + 到馆自提」。首页进入好物列表和详情,个人中心查看商品订单;管理中心新增「商品、库存与销售订单」。
- 管理员维护标题、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 为已付款订单的累计件数。
- 下单以用户行锁串行限制每人最多 5 笔待付款订单,以
(userId, requestId)保证幂等。 - 事务内读取商品,验证上架及顾客看到的单价,用条件更新
stock >= quantity、商品version原子预占库存。订单保存当时的标题、封面、单价和联系人快照。 - 每笔最多 99 件,订单有效期 15 分钟。统一下单使用同一个商户订单号和明确的微信
time_expire。 - 客户端支付成功不直接更新订单。服务端查询微信,核对商户、应用、订单号、金额、币种、openid 和微信交易号,才能从 PENDING 更新为 PAID。
- 商品回调独立使用
/api/shop/wx-notify,只接受 AES-GCM 解密后的资源,再查单确认,不依赖旧会员卡回调的验签占位实现。成功返回 HTTP 204,绕过统一 API 包装。 - 定时任务每 30 秒按最近对账时间分批扫描待付款订单,支持漏回调恢复;用户也可刷新支付状态。失败会保留库存并重试。
- 取消订单先查单、关单,关单成功后才原子释放库存;若先查到支付成功则转为待发货。关单失败绝不提前释放。
- 微信尚无订单时,可能存在正在途中的预支付请求;过期后再等待 60 秒才释放,避免“取消释放后又支付”。因此用户从未成功调起支付时,取消可能提示等待,最晚由超时任务回收(微信服务异常期间会继续保留并重试)。
- 状态更新带条件,重复回调、重复取消或并发扫描不会重复计算销量、重复返还库存。发货完成仅允许已付款订单,并记录操作人/时间。
- 管理员保存商品和库存均校验版本;发生竞争时提示刷新。库存调整记录最近 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 已按用户授权完成生产数据库迁移和服务端部署。以下为后续发布参考流程;本次执行结果见文末。
- 备份数据库,先在测试数据库验证迁移
20260916090000_studio_shop。此迁移只新增 3 张表及关系索引,不改写已有会员数据。 - 在 server 包运行
pnpm exec prisma migrate deploy和pnpm prisma:generate。 - 根目录运行
pnpm build:shared、pnpm build:server,部署并重启后端。 - 沿用微信支付配置和 COS 配置,无新增必填环境变量。确保
API_BASE_URL指向服务域名、外网能访问/api/shop/wx-notify,调度进程正常运行。 - 构建发布小程序
pnpm build:app;新页面仍调用既有线上 API 域名,必须先上线后端。 - 管理员创建真实商品、上传封面、补货、上架。没有硬编码测试商品写入数据库。
- 在微信真机执行一笔小额购买:取消支付后续付、查看服务端支付确认、管理员直接发货完成、超时关单及库存回补。退款不在本版本内。
验证与限制
- 共享包、服务端构建;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 查单、关单接口、官方小程序下单及有效期说明。
生产部署记录(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 为主;新表为增量结构,应保留,避免删除上线后产生的数据。数据库全量恢复仅用于灾难恢复,不应在正常版本回滚时直接覆盖。