# 馆内好物:架构、使用与交付 ## 产品范围 本次实现轻量的「单商品立即购买 + 微信支付 + 到馆自提」。首页进入好物列表和详情,个人中心查看商品订单;管理中心新增「商品、库存与销售订单」。 - 管理员维护标题、HTTPS 封面(复用 COS 上传)、说明、价格、排序、上下架状态。 - 各尺码、颜色按独立商品管理,在标题中标明规格;不引入购物车、物流、优惠券或复杂 SKU 系统。 - 创建商品默认库存为 0;保存后通过库存调整补货。调整数量为有符号整数,必须填写原因。 - 顾客填写取货人、手机号及数量,确认购买后微信付款。价格变化时先阻止下单,要求确认最新价格。 - 付款成功后显示提货码。管理员核对商品数量和取货人,输入顾客的 12 位提货码并确认交付。 - 管理端支持商品搜索、订单状态筛选、商品名 / 订单号 / 手机号搜索、分页、累计成交额、销量、待取货数、低库存提示。 - 售后通过详情页「联系场馆」沟通;本版不含线上退款流程。不能把已付款订单当作未付款订单取消。若后续引入商户平台退款,需配套退款对账及库存回补逻辑,不能仅修改订单状态。 ## 现有架构与接入方式 仓库为 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. 状态更新带条件,重复回调、重复取消或并发扫描不会重复计算销量、重复返还库存。核销仅允许 PAID + 正确提货码,并记录操作人/时间。 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 `