做护肤化妆品这类高复购、强信任的生意,微信小程序商城几乎是标配。这几年我帮品牌方搭过好几套电商小程序,这次在做一个"精致护肤购物系统"的时候,前端选了 uniapp + Vue 语法写微信小程序,后端同时准备了 PHP 和 Node.js 两套可切换的实现,管理端用的还是 Vue 工程。整套系统跑下来,从技术选型、数据库设计、小程序端开发、后端接口联调,到环境搭建踩坑和上线前检查,每一步都有值得记录的东西。这篇文章就按我实际动手的顺序完整梳理一遍,打算做电商类小程序、或者正在折腾毕业设计电商系统的同学,可以直接对着抄。
1. 为什么选 uniapp + PHP/Node.js 双后端方案:先想清楚再动手
1.1 护肤商城小程序的"一套代码"执念
先说前端。化妆品商城这种项目,老板嘴上说的是"我要一个微信小程序",心里想的是"以后抖音小程序、支付宝小程序、甚至独立 App 我都要上"。如果一开始就用原生微信小程序写,后面每次扩展平台都是一次重写,成本直接翻倍。
uniapp 解决的就是这个问题。它用 Vue 语法写页面,编译到微信小程序时自动转成 WXML/WXSS,关键的业务代码(登录、购物车、下单、支付)可以做到 90% 复用。团队里本来就会 Vue 的人,基本不需要额外学小程序语法,上手成本很低。我在这次项目里用的就是 uniapp + Vue 3 的写法,页面结构、组件通信、生命周期都贴近 Vue 习惯,写起来比原生舒服太多。
另外要说一句,标题里写的是"vue+uniapp",很多人以为要用两个框架,其实不是——uniapp 本身就是基于 Vue 的,这里的 vue 指的是"用 Vue 语法开发 uniapp 应用",同时也会用到 Vuex/Pinia 做状态管理、vue-router(uniapp 里叫 pages.json 路由配置,但心智模型一致)。如果你想先跑通原型,也可以单独拿 Vue 写一个 H5 管理后台,配合小程序端展示商品和订单数据。
1.2 后端为什么准备两套:PHP 和 Node.js 不是二选一,是切换
很多同学看到"PHP_nodejs"这个写法会懵:到底用哪个?我的真实建议是:这套商城系统的定位是可切换、可交付、可演示的两套后端实现,你根据团队情况和部署环境选一套作为主力。
PHP 路线的优势是部署极其简单,虚拟主机、宝塔面板、服务器上装个 php-fpm 就能跑,ThinkPHP 或 Laravel 框架对商品、订单这种 CRUD 密集型业务非常合适。护肤商城大部分接口就是"查商品列表、查详情、下订单、查订单",这种业务 PHP 写起来快、维护容易,小团队一个人就能管住整个后端。
Node.js 路线(我用 Express 或 Koa)的优势在于高并发场景下的库存扣减、优惠券核销、购物车合并这一类逻辑,异步 IO 模型天然比 PHP 的同步模型抗压。如果你预期上线后会有秒杀、限量抢购、积分兑换这类玩法,Node.js 后端的表现会更稳。
我在项目里做的处理是:两套后端共用一个数据库、一套接口文档、一套鉴权规则。开发时默认跑 PHP 版本,需要演示 Node.js 能力时直接把 baseURL 切到 Node 服务,小程序端无感知。这种设计在交付项目、给客户演示、或者答辩的时候都很有说服力,因为它证明你不只懂一种技术栈。
1.3 明确用户角色和核心业务闭环
做商城系统最容易犯的错是一上来就写代码,结果页面、接口、表结构全是散的。我在动工前先理清了角色和闭环:
- C 端用户:注册/登录、浏览分类、搜索商品、查看详情、加购物车、下单、支付、查订单、申请售后。
- 管理端用户:商品上下架、SKU/库存管理、订单发货、售后处理、用户管理、数据统计。
- 核心闭环:登录态 -> 看商品 -> 加购 -> 下单 -> 支付 -> 商家发货 -> 确认收货 -> 复购。
护肤品的特殊性在于:同一款精华往往有 30ml、50ml 两个规格,同一款口红有多个色号,这就是典型的 SKU(库存量单位)场景。商品表存基础信息,SKU 表存"规格 + 色号 + 独立库存 + 独立价格",购物车、订单明细都必须挂 SKU ID,不能只挂商品 ID。这一步想清楚,后面所有开发都顺了。
2. 项目整体架构与数据库设计:化妆品店的商品粒度是关键
2.1 工程目录:前后端分离该怎么拆
项目交付的时候是完整的源码工程,目录结构我是这样设计的:
skincare-mall/ ├── client-uniapp/ # 小程序端 uniapp 工程 │ ├── pages/ │ │ ├── index/ # 首页 │ │ ├── category/ # 分类页 │ │ ├── cart/ # 购物车 │ │ ├── user/ # 我的 │ │ ├── goods/ # 商品详情 │ │ ├── order/ # 订单列表/确认订单 │ │ └── login/ # 登录页 │ ├── components/ # 公共组件(商品卡片、导航栏等) │ ├── utils/request.js # 封装 wx.request │ ├── store/ # Pinia 状态管理 │ └── manifest.json # 小程序配置 ├── server-php/ # PHP 后端(ThinkPHP 8) │ ├── app/ │ │ ├── controller/ # 接口控制器 │ │ ├── model/ # 数据模型 │ │ └── middleware/ # 鉴权中间件 ├── server-node/ # Node.js 后端(Express 4) │ ├── routes/ # 路由 │ ├── controllers/ # 控制器 │ ├── middleware/ # JWT 鉴权 │ └── app.js └── admin-vue/ # 管理后台 Vue3 工程有人会问,为什么把 PHP 和 Node 放在同一个仓库里?因为两套后端逻辑同源,数据库一致,放在一起方便交付、方便对照、也方便演示。平时开发我建议只在环境变量里切换后端地址,小程序端只需要改utils/request.js里的 baseURL。
2.2 核心表结构:商品、SKU、库存
护肤商城最核心的五张表,我直接给出参考结构:
users(用户表)
- id、openid、nickname、avatar、phone、gender、created_at
- openid 是微信登录唯一凭证,必须建唯一索引。
goods(商品表)
- id、title、subtitle、category_id、cover、images(JSON 数组)、detail(富文本)、status(上架/下架)、sales(销量)、created_at
- 注意:商品表里不存价格和库存,因为一个商品有多个 SKU,价格可能在活动期间浮动。
goods_sku(SKU 表)
- id、goods_id、spec_name(比如"50ml")、spec_value(比如"经典款")、price、original_price、stock、sku_code
cart(购物车表)
- id、user_id、sku_id、quantity、checked、created_at
- 加购时先查 SKU 是否下架、库存是否够,数量不能超过库存。
orders(订单表)
- id、order_sn(唯一订单号)、user_id、total_amount、pay_amount、pay_status、ship_status、refund_status、address_snapshot(JSON 快照)、created_at
order_items(订单明细表)
- id、order_id、sku_id、goods_title、spec_name、price、quantity
这里特别强调一下address_snapshot这个字段:用户在确认订单后,地址、商品名、价格都要做一份 JSON 快照存进订单里。因为用户之后可能改地址、商品可能改价改名字,订单作为交易凭证必须保持下单那一刻的原始信息。我做售后和客服对账时,全靠这个快照,不然用户说"我买的时候明明 199",后台一查商品已经调价,很难扯清楚。
2.3 订单状态机的设计
订单状态我用一个整数状态字段加一个状态文本字段管理:
| 状态值 | 含义 | 触发动作 |
|---|---|---|
| 0 | 待支付 | 下单成功,未支付 |
| 1 | 待发货 | 支付成功回调 |
| 2 | 待收货 | 商家后台点击发货 |
| 3 | 已完成 | 用户确认收货或自动收货 |
| 4 | 已取消 | 超时未支付/用户取消 |
| 5 | 退款中 | 用户发起售后 |
| 6 | 已退款 | 售后审核通过并退款 |
状态流转一定要在后端做校验,小程序端只管展示。比如:只有状态为 0 的订单才能调起支付;只有状态为 1 的订单才能发货;只有状态为 2 的订单才能确认收货。这个如果写在客户端,用户抓包改请求就能绕过逻辑,后患无穷。
3. 小程序端开发实录:从登录到下单的完整链路
3.1 微信登录获取手机号:现在不是你想调就能调
护肤商城这种电商小程序,最核心的登录流程就是"微信授权手机号登录"。这块踩坑特别多,我详细讲。
微信官方从 2023 年开始调整了接口规则:getPhoneNumber这个能力现在要求小程序必须通过认证,并且接口调用方式也变了。正确流程是:
- 用户点击"手机号快捷登录"按钮,触发
<button open-type="getPhoneNumber" @getphonenumber="onGetPhone">。 - 在
onGetPhone回调里拿到e.detail.code,这个 code 有效期很短,必须立刻传给后端。 - 后端拿着 code 调微信接口
https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=ACCESS_TOKEN,换取真实的手机号。 - 如果用户之前没注册过,就自动创建账号返回 token;如果注册过,直接返回 token 完成登录。
同时,wx.login获取的code也要传给后端调微信code2Session接口换openid。也就是说,一次登录要处理两个 code:一个换 openid(系统唯一身份),一个换手机号(用户联系方式)。我在设计接口时把两步合并成一个登录接口:
// 小程序端登录处理 async function handleLogin() { const loginRes = await uni.login(); // 用户点击按钮后拿到的手机号 code const phoneCode = phoneCodeFromBtn; const res = await request('/api/auth/phoneLogin', { method: 'POST', data: { loginCode: loginRes.code, phoneCode: phoneCode } }); uni.setStorageSync('token', res.data.token); }后端逻辑则是:先用loginCode换 openid,再用phoneCode换手机号,查 users 表,没有就插入,有就更新 nickname/avatar,最后签发 token 返回。注意 token 不要用明文手机号拼,最好是 JWT 或自定义随机串,过期时间设 7 天,小程序端每次请求都带上。
3.2 顶部导航栏高度适配:胶囊按钮和刘海屏的纠缠
热词里反复出现"微信小程序顶部导航栏高度",这个我真得单独讲,因为它看着是小问题,实际天天折磨人。
默认导航栏是微信原生渲染的,标题居中、胶囊按钮在右边。但做护肤商城这种对视觉要求高的项目,首页和商品详情页一般都要自定义导航栏(比如让背景融入顶部、放品牌 logo、做毛玻璃效果)。一旦自定义导航栏,你就得自己算状态栏高度和导航栏高度,否则各型号手机上标题要么顶到刘海,要么和胶囊按钮重叠。
标准写法是:
// 计算导航栏高度 function getNavBarHeight() { const systemInfo = uni.getSystemInfoSync(); // 胶囊按钮位置信息 const menuButton = uni.getMenuButtonBoundingClientRect(); const statusBarHeight = systemInfo.statusBarHeight; // 状态栏高度 const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height; return { statusBarHeight: statusBarHeight, navBarHeight: navBarHeight }; }这个公式的原理是:胶囊按钮垂直方向居中对齐于导航栏,所以胶囊按钮到状态栏底部的距离 * 2 + 胶囊高度约等于导航栏总高度。用这套计算,iPhone 刘海屏、安卓挖孔屏、普通屏幕都能统一自适应。拿到高度后,自定义导航栏组件占位:
<view :style="{ height: statusBarHeight + 'px' }"></view> <view :style="{ height: navBarHeight + 'px', display: 'flex', alignItems: 'center' }"> <!-- 标题和按钮 --> </view>我建议把这段封装成一个custom-nav-bar组件,全局复用。这个组件在项目里非常高频,首页、详情页、分类页、购物车页都会用到。
3.3 商品列表、详情与购物车的交互细节
商品列表页我用的是"左侧分类 + 右侧商品瀑布流"布局。左侧分类数据从/api/category/list拿,右侧商品图用mode="widthFix"自适应,避免图片变形。护肤品的商品卡片小图建议用正方形 1:1 裁切,详情页大图用 3:4 竖图,因为化妆品瓶身大多是竖长条,竖图更有质感。
购物车有几个交互点不能漏:
- 左滑删除:uniapp 里没有内置左滑组件,我用了
movable-area实现或者直接放删除按钮更省事,看你要不要极致交互。 - 全选/单选联动:底部结算栏实时计算选中商品的件数和总价,这个要在 Vue computed 里处理,不能每次打开页面才算。
- 数字加减:点击加号要即时调后端更新数量,同时本地先做乐观更新(先改 UI,请求失败再回滚),这样用户手感和数据一致性都能保证。
购物车徽标(右上角红点数字)用uni.setTabBarBadge做,每次商品数量变化后重新拉取购物车总数更新徽标。这个细节虽然小,但对电商转化率很有影响,用户能直观感受到"加购成功"。
3.4 下单流程与支付回调处理
下单流程我建议用"确认订单页 -> 提交订单 -> 调起支付 -> 支付结果页"四步。确认订单页展示商品明细、默认地址、优惠券、积分抵扣和实付金额。这里注意,所有优惠计算最好后端算完后返回给前端展示,前端不要自己算,否则活动规则一变就要重新发版。
调起支付时,后端要先调用微信支付的统一下单接口,拿到prepay_id后按规范签名,返回给小程序端paySign等 5 个参数,小程序端再调uni.requestPayment。完整写法:
// 调起微信支付 const paymentParams = await request('/api/order/pay', { method: 'POST', data: { orderSn: orderSn } }); uni.requestPayment({ provider: 'wxpay', timeStamp: paymentParams.timeStamp, nonceStr: paymentParams.nonceStr, package: paymentParams.package, signType: paymentParams.signType, paySign: paymentParams.paySign, success: (res) => { // 支付成功,跳转订单列表 uni.redirectTo({ url: '/pages/order/list?status=1' }); }, fail: (err) => { // 支付失败或取消 uni.showToast({ title: '支付未完成', icon: 'none' }); } });这里有个重要的经验:支付成功回调不能只信小程序端。前端success只能作为 UI 提示,订单状态必须以微信服务器异步通知(notify_url)为准。也就是说,后端收到支付成功通知后再把订单状态从未支付改成待发货。如果只按前端回调改状态,用户支付成功后没等回调就关掉页面,订单就会一直卡在"待支付",这是电商系统最常见的线上事故之一。我在设计里每次都把支付通知接口和订单状态更新解耦,并且通知接口要做签名校验,防止伪造回调。
4. 后端接口开发与联调:PHP 和 Node.js 的共通设计
4.1 统一返回格式和接口命名
不管用 PHP 还是 Node.js,后端接口的第一原则是:返回格式统一。我用的格式是:
{ "code": 0, "message": "success", "data": {} }其中code为 0 表示成功,非 0 为业务错误码(比如 1001 用户未登录、1002 库存不足、1003 商品已下架)。小程序端的 request.js 统一拦截:code 为 0 直接返回 data;code 非 0 弹出错误提示;HTTP 401 则清理本地 token 并跳转登录页。
接口命名建议按资源走 RESTful 风格,比如:
GET /api/goods/list商品列表GET /api/goods/detail?id=xx商品详情POST /api/cart/add加购POST /api/order/create创建订单POST /api/order/pay获取支付参数
命名统一之后,PHP 和 Node 两套后端在 controller 层几乎可以一一对应,我切换后端演示的时候,前端代码一行都不用改。
4.2 Token 鉴权与会话管理
小程序端每次请求都带着Authorization: Bearer <token>,后端中间件统一校验。PHP 端我用 ThinkPHP 的中间件机制,Node.js 端用 Express 的 middleware。
核心逻辑都一样:
// Node.js 鉴权中间件示例 function authMiddleware(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) return res.status(401).json({ code: 1001, message: '未登录' }); try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.userId = decoded.userId; next(); } catch (e) { res.status(401).json({ code: 1001, message: '登录失效' }); } }PHP 端思路完全一样,只是用hash_equals校验签名或者用 firebase/php-jwt 库解析。有两个容易踩的坑:
- token 里别放手机号、openid 这种敏感信息,放 user_id 就够了,用户资料每次从数据库查。
- 修改密码、后台封号时要能立即生效,所以 token 里通常带一个
token_version,用户表里存版本号,校验时不匹配就拒绝。微信小程序登录没有密码场景,但这个设计在做管理后台权限的时候很有用。
4.3 跨域、代理与本地联调
开发微信小程序的时候,wx.request其实不校验跨域,因为它走的是宿主环境(微信客户端),不是浏览器。所以本地联调只需要在小程序开发者工具里勾选"不校验合法域名",然后把 baseURL 指向本机局域网 IP(比如http://192.168.1.100:8080)就行。
但热词里出现了"php跨域+jsonp",这是 Vue 管理后台在浏览器里访问后端接口时遇到的问题。管理后台跑在localhost:5173,后端跑在localhost:8080,浏览器跨域请求默认被拦截。处理方式:
- PHP 端在响应头里加
Access-Control-Allow-Origin白名单。 - Node.js 端用
cors中间件配置。
// Node.js 跨域配置 app.use(cors({ origin: ['http://localhost:5173'], // 管理后台地址 credentials: true }));jsonp 是老方案,现在基本只用于兼容极端场景,我建议一律用 CORS,业务代码更干净。
本地联调还有一个隐藏问题:如果手机真机调试,必须先保证手机和电脑在同一 WiFi,且服务器防火墙允许访问对应端口。我因为外网 IP 不通、本地隧道工具不稳定,卡过好几次,后来索性直接用内网穿透工具把本地接口映射成临时公网域名,真机上就能调试,也方便给客户演示。
5. 环境搭建踩坑实录:从零装出可运行环境
新拉下来的工程要能跑起来,第一步就是环境。这部分我踩过的坑基本覆盖了热词里那一排问题,逐个说。
5.1 Node.js 安装与 npm 脚本权限问题
Windows 上装好 Node.js 后,在 PowerShell 里运行npm -v,经常直接报错:
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1, 因为在此系统上禁止运行脚本这是 PowerShell 执行策略默认限制本地脚本导致的,不是 Node 没装好。解决办法是管理员身份打开 PowerShell,执行:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned然后选 Y 确认。如果不想改全局策略,也可以绕过:在终端里运行npm.cmd -v,或者在项目目录直接用npx命令。我建议设置 RemoteSigned,因为 Node 生态里很多工具(vue、vite、eslint)都需要执行 npm 脚本,每次绕过太麻烦。
另外,安装依赖前先确认 Node 版本。uniapp 新版和 Vite 生态对 Node 版本有要求,太老(低于 16)会直接报错,我用的是 Node 18 LTS,配合 Vue 3 的工程非常稳。
5.2 PHP 环境配置与常见报错
PHP 后端我用的是 PHP 8 配合 ThinkPHP 8。热词里有两条关于 PHP 环境的经典报错:
no package 'libzip' found是 Linux 下源码编译 PHP 时装扩展报的错,需要先装libzip-dev再重新编译:
apt install libzip-dev ./configure --with-zip make && make installvcruntime140.dll 14.0 is not compatible是 Windows 下 PHP 版本和 VC++ 运行库不匹配。解决办法很简单:去微软官网装最新的 Visual C++ Redistributable(x64 版本),装完重启命令行就好。这个 90% 的 PHP 环境问题都是因为这个运行库缺失或太旧。
另外,如果你本机同时装了 PHP 和 Node,注意 80 端口可能会被 IIS 或 Apache 占用,导致php think run启动的调试服务器起不来。我一般用php think run -p 8080指定一个不冲突的端口。
5.3 Vue 工程创建与 TSConfig 解析失败
管理后台用的 Vue 3 工程,热词里那条failed to load tsconfig '@vue/tsconfig/tsconfig.web.json': tsconfig not found是典型问题。新创建的 Vue + TypeScript 工程会继承一个基础 tsconfig,如果你用的是简化版脚手架、或者 npm 依赖没装全,就会找不到@vue/tsconfig这个包。
解决办法分两步:先确认依赖里有@vue/tsconfig,没有就装:
npm install -D @vue/tsconfig装好后,如果还在报错,检查项目根目录 tsconfig 文件里extends的路径是否写对了。新版脚手架的写法一般是:
{ "extends": "@vue/tsconfig/tsconfig.web.json", "compilerOptions": { "types": ["vite/client"], "paths": { "@/*": ["src/*"] } } }这种问题本质上就是 Node 模块找不到,不要纠结,把 node_modules 删了重新npm install一遍能解决 80% 的"找不到 XX 配置"类报错。
5.4 uniapp 打包微信小程序的两大坑
uniapp 工程写完后,要运行到微信开发者工具里调试、也要上传代码到微信后台。这里有两个我几乎每次开新项目都会踩的坑:
第一个坑:工具不识别 uniapp 项目。在 HBuilderX 里点"运行到小程序模拟器"之前,必须在微信开发者工具里开启服务端口:设置 -> 安全设置 -> 服务端口 -> 开启。不开启的话 HBuilderX 永远提示"未启动微信开发者工具"。
第二个坑:manifest.json 配置不完整导致打包后丢功能。manifest.json 里的mp-weixin节点要写清楚 appid、项目名称、甚至权限声明。比如登陆获取手机号、定位店铺的时候,权限要在后台和 manifest 里都声明,否则真机调试时接口能调通但能力被微信拒绝。
打包命令可以直接用 CLI 方式:
npm run build:mp-weixin然后用微信开发者工具"导入项目",选择工程根目录下的dist/dev/mp-weixin或dist/build/mp-weixin文件夹,填上自己的小程序 AppID 就能跑。注意:微信开发者工具导入的一定是编译产物,不是 uniapp 源码,很多新手在这里搞混。
6. 上线前检查清单与运营经验
6.1 微信小程序类目与合规:护肤品的资质要求
护肤化妆品类小程序有个特殊的合规要求:涉及化妆品销售,微信审核时通常会要求提供《化妆品经营许可证》或品牌方的授权链路。个人主体小程序可以卖一些简单的生活用品,但化妆品类大概率通不过审核,建议提前准备企业主体和小程序认证。
另外,微信支付要单独申请商户号,个人主体没有微信支付商户号权限,这也是电商类小程序必须企业主体的原因。这里我特别提醒:接口调试可以先用测试号,但上线前一定要把 AppID、商户号、支付密钥这些换成正式的,并且支付回调地址必须是 HTTPS 域名。我见过好几个项目本地联调一切正常,一上线支付就失败,最后发现是小程序后台的 request 合法域名没配或者忘了配支付回调。
6.2 性能优化:图片、分包、缓存三板斧
护肤商城首页和商品详情页都是图片大户,这类项目第一屏加载速度直接决定跳出率。我上线前的优化基本围绕三点:
- 图片走 CDN,且商品图在管理后台上传时自动压缩,控制单张不超过 200KB。小程序包体有 2MB 限制,本地不能塞大图。
- 首页和分类页拆成独立分包,用户从微信扫小程序码进入某个商品详情页时,只加载对应分包,冷启动速度明显提升。
- 高频接口做缓存,比如首页banner、分类列表这种数据变化频率低的内容,后端接口加 Cache-Control 响应头,小程序端缓存 10 分钟再过期。
还有一个小细节:微信小程序每个页面会预载,但商品列表这类数据量大的接口要启用uni.showLoading加骨架屏,不然白屏时间太长用户就直接退出了。骨架屏可以用 CSS 动画简单实现,比 loading 菊花体验好一个档次。
6.3 售后与复购场景的代码支撑
护肤品类复购率高,但客诉也集中在"用了过敏""买到假货""包装破损"。系统层面我在三块做了支撑:
- 订单详情页放"申请售后"入口,用户发起后订单状态变退款中,管理后台收到售后工单,商家可以选择同意退款或拒绝并填写理由。
- 商品详情页增加"历史购买记录",这个字段在用户表和订单表里能查到,用户进入详情页时后端判断该用户是否买过同类商品,买过的话详情页展示一个"老客专属价"标签,这套系统里用会员等级字段实现。
- 优惠券系统要支持"支付后自动发券",比如买精华送一张下月可用的 30 元回购券。这个逻辑在支付异步通知里判断:订单实付金额满 300 就给用户发放一张券,券状态、有效期都存数据库,下单时校验券是否可用、是否在有效期内。
这三块做完,整个商城才真正有"运营"的味道,而不只是一个商品展示工具。
6.4 长期维护的一点心得
最后说点实在的。这套系统交付之后,我最深的体会是:电商项目 60% 的问题出在支付和库存,30% 出在环境配置,只有 10% 是真正的业务逻辑 bug。
支付回调重复通知、库存并发超卖、本地环境配不对,这些我都逐个排查过。微信支付回调不是只调一次,失败会重试多次,所以后端更新订单状态前一定要判断当前状态,避免重复发货、重复发券。库存扣减我用的方案是数据库条件更新UPDATE goods_sku SET stock = stock - 1 WHERE id = ? AND stock > 0,受影响行数为 0 就说明库存不够,直接返回失败。这个方案简单可靠,比先查后改稳得多。
环境这块,建议把项目运行所需要的 Node 版本、PHP 版本、扩展列表、启动命令全部写进 README,并且附上我上面说的这几个报错和对应解法。一个能"照着 README 一次跑通"的工程,才是真正敢交付的工程。护肤商城这套系统的所有源码结构、接口定义、数据库脚本我都按这个标准整理过,后面再接手的人(包括三个月后的我自己)都能快速上手。