最近在做一个基于Node.js和Vue的云上新鲜水果超市商城系统,内部代号g0a71。这个项目不是一个什么重量级电商平台,而是面向中小型生鲜商家的一条线上化方案。从用户端的水果浏览、购物车,到管理端的订单处理、商品上下架,再到云服务器部署,整条链路都走了一遍。过程中踩了不少坑,尤其是Node.js环境配置和npm的PowerShell权限问题。今天这篇文章我就把项目的整体拆解、核心功能实现、环境搭建和部署上线一次性讲清楚,给打算用Node.js和Vue做电商类项目的朋友做一份参考。
1. 项目概述与业务定位
1.1 为什么做这样一个“云上水果超市”
水果这个品类和数码产品、服装完全不一样。最容易变质的商品之一就是水果,库存放在那里不管,每天都有损耗,实体店半径通常只有两三公里,靠自然流量很难撑起大生意。把水果超市搬到“云上”,本质上是让销售范围从门店周边扩展到整个城区,用户线上下单,商家统一配货甚至配送到家。这样做最直接的价值是降低损耗、提升周转效率。
我之前帮朋友设计过一个小程序生鲜商城,当时对生鲜行业的配送和库存印象很深。后来决定用Node.js和Vue自己从零做一个完整的Web版水果商城,原因很简单:想让一套代码同时覆盖用户端和后台管理端,并且把前后端分离的流程走通。这个项目虽然叫“云上新鲜水果超市”,但业务并不复杂,核心是几件事:卖水果、管订单、控库存、做营销。难的是把这些流程通过代码稳定地串起来。
这个项目适合谁参考?如果你是一个想练手全栈的学生,或者准备转行做开发的初学者,又或者是一名独立开发者,想给线下水果店做一套线上商城,那这篇文章的路子基本够用了。Node.js负责提供API接口,Vue负责页面展示和交互,云服务器负责最终上线,这是一个非常典型的现代前后端分离项目。
1.2 核心需求拆解与功能清单
做项目最忌讳上来就写代码,需求不清会导致后面反复返工。我一开始就把用户端和管理端拆开,列了一张角色功能清单。
| 角色 | 核心功能 |
|---|---|
| 普通用户 | 注册登录、浏览商品、分类筛选、关键词搜索、商品详情、加购、下单、模拟支付、订单查询、确认收货、商品评价 |
| 管理员 | 商品上架/下架、库存调整、分类管理、订单发货、订单状态修改、用户列表、销售数据查看 |
用户端重点是购物体验要流畅。水果商城有很多分类场景,比如时令水果、进口水果、水果礼盒,还有应季水果的推荐位。所以商品模块不能只是一个列表,还需要支持分类、价格排序、销量排序、搜索。购物车要能增删改查,下单时要有收货地址,订单状态要清晰可追踪。
管理端重点是控制库存和处理订单。水果是生鲜品,库存数量直接关系到损耗,系统必须保证库存不能超额售卖。另外还有一个隐藏需求:订单状态必须有序流转,不能从待支付直接跳到已完成,中间要经过配货、配送这些环节。所以我在设计的时候加了一个订单状态机,用枚举值锁死合法状态转换。
这个需求清单看起来功能不少,但拆到后端接口其实也就三四十个。前端页面大概十个左右,用户端覆盖首页、商品列表、详情、购物车、订单结算、个人中心;管理端覆盖商品管理、订单管理、数据看板。整体工程量完全在一个人可以控制的范围内,这也是选型Node.js和Vue的一个重要原因。
2. 技术选型与架构设计
2.1 为什么选 Node.js + Vue 这套组合
技术选型没有绝对的对错,关键看团队和场景。我现在带领的这个项目,本身是前端团队主导,所以选Node.js是顺理成章的,所有人都写JavaScript,前后端通用一套语言和数据结构思维,沟通成本为零。前端用Vue,也不只是因为生态成熟,更因为组件化开发非常适合商城这类重复结构很多的系统。
Node.js自身适合处理I/O密集型请求。电商场景里很多操作,例如打开商品详情、加载图片、提交订单,本质上是高频率、轻计算量的I/O操作,Node.js的事件循环机制在这种情况下表现很好。再加上Express中间件生态非常成熟,写一个RESTful接口非常快。后端如果选Spring Boot也不是不行,但对于一个小型商城项目来说,Java本身的部署成本和团队学习成本会直接把开发周期拉长。
Vue这边,我最看重的是它的渐进式特点。项目初期可以用Vue Router加快单页应用,后续如果要做复杂状态管理,就引入Pinia。生态里还有Element Plus、Vant这些现成的UI库,后台管理页面几乎就是拖组件。对比React,Vue单文件组件的模板写法对新手更友好,上手速度确实快很多。
2.2 整体架构与数据流设计
整体架构我用文字描述一下,方便理解。用户通过浏览器访问域名,Nginx接收请求。所有前端静态资源,也就是Vue工程npm run build之后生成的dist目录,直接交给Nginx托管。浏览器向/api开头的路径发送请求,Nginx将这些请求反向代理到后端Node.js服务,Node.js服务再连接MySQL数据库和Redis缓存。图片文件统一放到云存储OSS上,浏览器通过直链访问。
项目目录结构上也做了前后端分离:
fruit-shop/ ├── server/ # Node.js 后端 │ ├── app.js │ ├── config/ │ ├── routes/ │ ├── controllers/ │ ├── models/ │ └── middlewares/ ├── web/ # Vue 前端 │ ├── src/ │ │ ├── api/ │ │ ├── router/ │ │ ├── stores/ │ │ ├── views/ │ │ └── components/ │ └── package.json └── docs/后端采用MVC的分层方式,routes定义路由,controllers处理业务逻辑,models负责数据库映射。前端按模块划分目录,api目录统一管理所有请求,stores管理全局状态。接口统一使用/api/v1前缀,比如登录接口是/api/v1/auth/login,商品列表是/api/v1/goods/list。返回格式固定在{ code, message, data }三层,前端axios拦截器统一处理,不用每个页面各自判断错误码。
数据库设计我是花了心思的。核心表包括用户表、商品分类表、商品表、购物车表、订单表、订单明细表和评价表。商品表中有几个字段特别关键:stock库存、sales销量、price价格、status上下架状态。订单表和订单明细表是主从结构,一个订单对应多个商品,这样方便统计每个商品卖了多少。这个模型设计看起来简单,但发货、退款、库存扣减都能在上面展开。
3. 核心功能模块实现
3.1 用户登录与JWT鉴权
用户模块是所有系统的入口,方式我选的是JWT。之所以不用Session,是因为前后端分离以后后端服务可能多实例部署,Session共享比较麻烦,JWT天然无状态,便于扩展。
注册的密码处理绝对不能明文存储。我在后端用bcrypt库对密码做加盐哈希,比对的时候用bcrypt.compare()判断。登录成功以后签发JWT,payload里只放userId和role,过期时间设置为24小时。服务端有一个鉴权中间件,读取请求头里的Authorization: Bearer <token>,校验签名成功以后把用户信息放进req.user,后续接口通过req.user直接拿当前用户。
// 登录接口核心逻辑 router.post('/auth/login', async (req, res) => { const { username, password } = req.body; const user = await User.findOne({ where: { username } }); if (!user || !bcrypt.compareSync(password, user.password)) { return res.json({ code: 400, message: '用户名或密码错误' }); } const token = jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '24h' } ); res.json({ code: 200, data: { token, userInfo: { username, role } } }); });前端Vue这边,登录页拿到token以后存到localStorage,axios请求拦截器在每个请求头上加上token。如果接口返回401,说明token过期或者非法,就清除本地身份信息并跳回登录页。这里有一个小提示:把token存localStorage在安全上有XSS风险,但对于内部小项目来说这是最方便的做法,如果追求更安全,可以存内存里,刷新页面时再通过一个刷新接口续签,项目复杂度会高一些。
管理员权限是另外处理的。我写了一个requireAdmin中间件,在鉴权中间件之后判断req.user.role是否为admin,不是就返回403。这样商品管理、订单发货这些接口就只对管理员开放,前端也会根据角色隐藏管理入口。
3.2 商品模块与图片上传
商品模块是整个商城的信息基础。列表接口我支持了分页、分类ID、关键词、价格排序和销量排序。查询用Sequelize的where和order动态拼接,避免自己拼SQL语句。
图片上传是最容易忽略实际问题的环节。开发环境可以用multer把图片存到本地public/uploads目录,但上线以后图片一定要放云存储OSS。原因很简单,应用服务器磁盘有限,图片多了会影响磁盘空间,而且云存储自带CDN加速,全国访问都会快很多。我的做法是后端接收上传的图片,然后调用云存储SDK转存到OSS桶,再把返回的URL保存到数据库,浏览器直接用这个URL访问图片。
商品列表页因为图片多,前端全部加了懒加载。Vue里用自定义指令或者现成的懒加载库都能实现,这个优化很重要,不然几百个商品在一个页面上,页面会非常卡。商品详情页则用了Swiper轮播展示多张图片,这些细节在真实商城里都是用户直接体验到的部分。
库存处理是商品模块里最容易出错的地方。我之前踩过并发超卖的问题:后台把库存改成0了还在卖。后来把扣库存操作改成一条原子SQL:
UPDATE goods SET stock = stock - 1, sales = sales + 1 WHERE id = ? AND stock > 0执行以后判断受影响行数,如果为0就说明库存不足,下单失败。这样做的好处是扣库存和检查库存一次性完成,没有“先查再改”的中间窗口,从根上避免了超卖。
3.3 购物车与订单流转
购物车我选择了服务端存储,而不是只存在浏览器本地。这样用户在手机和电脑之间切换时购物车数据不会丢失。购物车表结构比较简单:user_id、goods_id、goods_num、selected,加购和修改数量都是标准的增删改查。
订单流转是整系统的核心,我把它设计成了状态机。状态枚举有这些:pending待支付、paid待配货、shipping配送中、completed已完成、cancelled已取消、refunding退货中。每个状态都有合法的下一个状态,不允许跳变。比如待支付只能去已支付或取消,已支付只能去配送中,配送中才能到已完成。
创建订单涉及两个数据表,事务必须处理好。用户从购物车把选中的商品提交过来,后端先生成订单主记录,再循环生成订单明细,同时扣减库存。这三步必须放到同一个数据库事务里,否则会出现订单创建了但库存没扣,或者库存扣了但订单失败。我用的Sequelize事务:
const transaction = await sequelize.transaction(); try { const order = await Order.create({ ... }, { transaction }); for (const item of items) { await OrderItem.create({ ... }, { transaction }); await Goods.decrement({ stock: item.num }, { where: { id: item.goodsId }, transaction }); } await transaction.commit(); } catch (e) { await transaction.rollback(); }订单金额计算要格外小心。我在后端重新计算了一次商品总价,而不是直接信任前端传来的金额,防止有人篡改请求数据。订单号我用时间戳加随机数生成,并且加唯一索引,保证并发时不会重复。支付环节由于是演示项目,我做了模拟支付接口,调用后直接把状态从待支付改成已支付,真实生产环境可以对接微信支付或支付宝,逻辑都是一样的,只是在支付回调里更新状态。
4. 开发环境搭建与高频报错
4.1 Node.js 安装与环境变量配置
很多人在环境这步就会被卡住。Node.js安装建议去官网下LTS版本,LTS是长期维护版,稳定可靠。安装时一路Next即可,注意安装目录不要有中文和空格。安装完以后,在命令行输入node -v和npm -v能显示版本号就是成功。
有些用户下载的是免安装版,解压后还需要手动配置环境变量。我的做法是新建一个系统变量NODE_HOME,指向Node.js的解压目录,然后在Path变量里追加%NODE_HOME%。当前窗口不生效,需要重新打开命令行。如果出现npm 不是内部或外部命令,多半就是环境变量没配置正确。
npm用官方源在国内速度慢,一定要设置镜像源:
npm config set registry https://registry.npmmirror.com设置完以后npm config get registry检查一下。更方便的做法是安装pnpm,npm install -g pnpm,安装依赖会快很多,还能节省磁盘空间。我后面创建Vue项目用的就是Vite,Vite对Node.js版本要求比较高一点,尽量用16.0以上。
初学者学Node.js不要一开始就去啃Express,应该先把这个顺序理清楚:JS基础语法,模块系统,npm包管理,http模块,再上手Express框架。有了这个基础,后面写接口就不会觉得都是“魔法”。
4.2 前端 Vue 环境初始化与 npm.ps1 报错
创建Vue项目,我用的Vite模板:
npm create vite@latest web -- --template vue cd web npm install这里要说一个高频坑,Windows用户在命令行执行npm命令的时候,很可能会看到这样的报错:
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本这是因为Windows PowerShell的脚本执行策略默认是Restricted,不允许运行.ps1脚本。解决办法有两个,第一个是以管理员身份打开PowerShell,执行:
Set-ExecutionPolicy RemoteSigned然后输入Y确认。第二个方法是改用CMD或者GitBash来运行npm命令,绕开PowerShell的限制。如果不想动系统全局策略,也可以在PowerShell里临时绕过:
PowerShell -ExecutionPolicy Bypass这个报错太常出现了,我在不止一台机器上遇到过,每次都先检查执行策略。Vue项目初始化以后,还需要安装Vue Router和Pinia:
npm install vue-router pinia element-plus axiosElement Plus在后台管理页面上非常好用,表格、表单、弹窗都有现成组件。开发时后端服务和前端Vite默认不在同一个端口,所以跨域问题也需要处理。我在vite.config.js里配置了devServer的proxy,把/api请求代理到本机3000端口:
export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } });这样开发环境就不会有CORS报错,生产环境因为Nginx做了同源,也没有跨域问题。Vue调试建议装Vue Devtools插件,可以实时看组件树和Pinia状态,排查数据流问题会方便很多。Vue Router如果是history模式,还要注意部署时后端Nginx要配合配置,后面我会讲到。
5. 云上部署与项目复盘
5.1 云服务器部署流程
项目开发完不能只停留在本地,上线部署才是完整闭环。我用的是云厂商的一台Linux服务器,2核4G配置,跑这个项目绰绰有余。部署流程大致是:先安装Node.js和PM2,然后把前后端代码拉下来,分别安装依赖,构建前端,最后配置Nginx。
后端启动,我推荐PM2,它自带进程守护、日志管理和负载均衡:
pm2 start server/app.js --name fruit-shop没有安装PM2的话,先执行npm install -g pm2。PM2的好处是进程意外退出会自动重启,服务器重启也能通过pm2 startup设置开机自启。
前端构建:
cd web npm run build构建完以后,dist目录里的静态文件复制到Nginx的工作目录,Nginx配置大致长这样:
server { listen 80; server_name yourdomain.com; root /var/www/fruit-shop/web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }location /里边的try_files非常关键,如果少了,Vue Router的history模式在刷新非首页路由时会直接404。location /api/负责把接口请求转发到后端Node服务。数据库建议用云数据库MySQL,自带备份和安全组,比在服务器上自己装省心很多。云存储OSS存放商品图片,部署配置完成后,跑一遍全流程测试,注册、登录、加购、下单、发货,都要验证通过。
5.2 遇到的一些问题与排查技巧
开发过程中踩过不少坑,我按频率高低整理了一份排查清单。
第一个高频问题是Nginx配置完成后刷新页面404。原因就是上面说的history路由没有配置try_files。解决办法是在配置里加上那三行,让前端路由匹配不到文件时统一返回index.html,由Vue Router自己去解析路径。
第二个问题是接口测试正确,前端调用却报跨域。开发环境的跨域用了proxy解决,生产环境如果后端接口和前端静态资源不在同一个域名,也需要处理。最好的办法是保持同源,也就是Nginx同时托管前端资源并代理API。如果确实要分开域名,那后端就要开启CORS并设置Access-Control-Allow-Origin。
第三个问题是后端接口参数校验不够严谨,导致一些脏数据进入数据库。比如商品价格被提交成负数,用户手机号格式不对。后来我引入Joi做参数校验,每个接口在进入业务逻辑前先校验请求体,不符合规定的直接返回参数错误。这个小改造省了很多后面查数据的麻烦。
第四个问题是MySQL连接数被打满,应用不时报ETIMEDOUT。原因是最初创建数据库连接池时没有限制最大连接数,请求一多连接就被耗尽。改成固定连接池以后,问题消失。下面是Sequelize连接池的推荐配置:
new Sequelize(database, username, password, { dialect: 'mysql', pool: { max: 10, min: 0, idle: 30000 } });另外在云服务器部署时,第一次上线经常出现接口能通,但是浏览器访问还是白屏,一定要先查看Vue的构建日志,确认dist目录是不是真的生成了,以及Nginx的root路径是不是指向了正确的目录。这些小问题排查起来不难,但第一次做会找很久,经验就在于先看Nginx错误日志和前端构建日志,不要瞎猜。
5.3 做这个项目的几点心得
这个项目做完,我个人的体会是,Node.js和Vue的组合做中小型商城确实舒服,但前提是要有工程规范。我早期写接口时命名混乱,有的叫getGoodsList,有的叫listGoods,前端还要配合记忆。后端接口务必统一命名规则,例如GET /goods、POST /orders、PUT /orders/:id/status,按资源划分,语义清晰,前端可以少走很多弯路。
数据库表结构也要提前设计。我在评审时把用户、商品、订单的主外键关系画成简单的关联图,确认无误后才开始写代码。中间因为购物车和订单关联关系不清晰,调整过一次表结构,成本不低。一定要在开发前把表字段、枚举值、状态流转都列出来,落到文档上。
还有一个心得:不要忽略测试。整个项目手工测试了好几轮,特别是订单状态乱序操作,比如用户下单后强制刷新、后台重复发货,这些边界情况如果不写状态机,系统肯定会出问题。我在状态转换处加了合法性校验,不符合流程的操作一律拒绝。
如果后续还要扩展,我会做这几件事:一是用Redis缓存热点商品数据,降低数据库压力;二是接入微信小程序端,复用现有接口;三是增加限时秒杀功能,进一步压测Node.js在高并发下的表现。这个项目本身还有很多可以打磨的地方,但核心链路已经是完整闭环,对整个开发流程的价值非常大。