
做护肤化妆品这类高复购、强信任的生意微信小程序商城几乎是标配。这几年我帮品牌方搭过好几套电商小程序这次在做一个精致护肤购物系统的时候前端选了 uniapp Vue 语法写微信小程序后端同时准备了 PHP 和 Node.js 两套可切换的实现管理端用的还是 Vue 工程。整套系统跑下来从技术选型、数据库设计、小程序端开发、后端接口联调到环境搭建踩坑和上线前检查每一步都有值得记录的东西。这篇文章就按我实际动手的顺序完整梳理一遍打算做电商类小程序、或者正在折腾毕业设计电商系统的同学可以直接对着抄。1. 为什么选 uniapp PHP/Node.js 双后端方案先想清楚再动手1.1 护肤商城小程序的一套代码执念先说前端。化妆品商城这种项目老板嘴上说的是我要一个微信小程序心里想的是以后抖音小程序、支付宝小程序、甚至独立 App 我都要上。如果一开始就用原生微信小程序写后面每次扩展平台都是一次重写成本直接翻倍。uniapp 解决的就是这个问题。它用 Vue 语法写页面编译到微信小程序时自动转成 WXML/WXSS关键的业务代码登录、购物车、下单、支付可以做到 90% 复用。团队里本来就会 Vue 的人基本不需要额外学小程序语法上手成本很低。我在这次项目里用的就是 uniapp Vue 3 的写法页面结构、组件通信、生命周期都贴近 Vue 习惯写起来比原生舒服太多。另外要说一句标题里写的是vueuniapp很多人以为要用两个框架其实不是——uniapp 本身就是基于 Vue 的这里的 vue 指的是用 Vue 语法开发 uniapp 应用同时也会用到 Vuex/Pinia 做状态管理、vue-routeruniapp 里叫 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_atopenid 是微信登录唯一凭证必须建唯一索引。goods商品表id、title、subtitle、category_id、cover、images(JSON 数组)、detail(富文本)、status(上架/下架)、sales(销量)、created_at注意商品表里不存价格和库存因为一个商品有多个 SKU价格可能在活动期间浮动。goods_skuSKU 表id、goods_id、spec_name比如50ml、spec_value比如经典款、price、original_price、stock、sku_codecart购物车表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_atorder_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-typegetPhoneNumber getphonenumberonGetPhone。在onGetPhone回调里拿到e.detail.code这个 code 有效期很短必须立刻传给后端。后端拿着 code 调微信接口https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_tokenACCESS_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拿右侧商品图用modewidthFix自适应避免图片变形。护肤品的商品卡片小图建议用正方形 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?status1 }); }, 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 直接返回 datacode 非 0 弹出错误提示HTTP 401 则清理本地 token 并跳转登录页。接口命名建议按资源走 RESTful 风格比如GET /api/goods/list商品列表GET /api/goods/detail?idxx商品详情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 Redistributablex64 版本装完重启命令行就好。这个 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 一次跑通的工程才是真正敢交付的工程。护肤商城这套系统的所有源码结构、接口定义、数据库脚本我都按这个标准整理过后面再接手的人包括三个月后的我自己都能快速上手。