做这套系统之前,我先自己复盘了一下为什么要选“vue + nodejs”这个组合。市面上运动用品商城类的开源项目不少,但大部分是单页展示,或者后端只给一个简陋的管理接口,真正能跑通“前台购物、用户论坛、商家管理”整条链路的完整方案却不多。如果你也是准备做课程设计、个人作品集,或者给某个体育社团做内部商城,这套系统踩过的坑和最终的落地方案应该能帮你少走很多弯路。它的核心价值在于:一个前端(Vue)处理所有交互,一个后端(Node.js)提供统一API,外加论坛模块和商家后台,前后端职责边界清楚,中小型项目完全够用。
我先说明一点:下面所有内容都是基于“模拟项目X”这个虚构环境,里面的人名、机构名都是代称,纯粹是技术思路分享。整个系统我拆成了前台商城、买家用户端、商家管理、公共论坛四块,技术栈也很常规:Vue 2 + Vue Router + Vuex + Element UI 负责页面,Node.js + Express 提供接口,数据库用 MySQL,流转层用 Sequelize。下面把从需求梳理到部署上线的全过程写出来,重点不是贴代码,而是讲清楚每一步为什么这么设计。
1. 先给系统画个像:目标用户、核心角色和功能边界
1.1 这套系统到底解决什么问题
很多初学者拿到“商城系统”这类题目,第一反应就是把淘宝京东搬过来,恨不得做秒杀、直播、推荐算法。但真实场景里,一个小型体育馆、一支校园运动队、或者某个运动品牌代理商,需要的不过是一个能展示商品、能下单支付(哪怕是线下转账)、能管理库存、能跟用户有个交流出口的门户。做需求分析的时候,我把原始需求拆成了下面这个逻辑:
- 前台用户:按分类浏览运动鞋服、器材、营养补剂,搜索商品,加入购物车,提交订单,查看个人订单状态,还能在论坛发帖交流运动心得。
- 商家管理员:维护商品资料、上下架、管理库存、处理订单发货、回复论坛举报。
- 系统游客:可以看首页、看商品详情、看论坛帖子,但只有登录后才有权限下单和发帖。
这个边界非常重要。因为一旦把“所有用户都能管理一切”这种需求放进来,权限模型就会爆炸,后期每个接口都要判断身份,代码维护成本直线上升。我最终定下来的角色只有两种:普通用户(USER)和商家管理员(ADMIN),论坛版主功能并进管理员角色里,不再单独拆表。
1.2 功能模块清单与页面规划
梳理完角色,我给每个角色开了功能清单,这样写代码的时候不会东一榔头西一棒槌。你可以直接拿来当需求文档用:
| 模块 | 子功能 | 使用角色 |
|---|---|---|
| 用户认证 | 注册、登录、忘记密码(走后端发邮件重置,模拟环境用的控制台读码) | 全部 |
| 商品模块 | 分类筛选、关键字搜索、商品详情、库存展示 | 前台用户 |
| 购物车/订单 | 加入购物车、修改数量、提交订单、扣减库存、生成订单快照 | 前台用户 |
| 用户中心 | 个人资料、订单列表、订单详情、物流状态(模拟) | 前台用户 |
| 商家后台 | 商品管理、分类管理、库存预警、订单发货、数据统计 | 商家管理员 |
| 论坛模块 | 发帖、回帖、帖子列表、帖子详情、个人帖子管理 | 全部登录用户,管理员可删帖 |
| 公共部分 | 首页轮播、快捷导航、站点公告 | 游客/全部 |
页面规划我放在了前端章节再细讲,这里只说一个原则:页面数量不是越多越好,而是每个功能入口不超过两步可达。比如“用户下单”这个动作,路径是“商品列表 -> 商品详情 -> 确认订单 -> 支付成功”,中间不插任何无关的推荐位。
1.3 开发顺序建议
这个系统如果按模块一头扎进去写,很容易写着写着前端调不通、后端也不知道该返什么字段。我的建议是后端接口先行,但接口顺序有讲究:
- 先做用户认证(注册/登录),因为后面所有接口都要带 token。
- 再做分类和商品接口,哪怕先用假数据,也要把列表和详情的返回结构定下来。
- 然后是购物车和订单,这是最核心的业务流,需要和库存紧密配合。
- 论坛模块独立开发,和商城业务没有强耦合,可以穿插着做。
- 最后才是后台统计和批量操作,因为前面的数据不跑起来,统计是空的。
2. 技术选型背后的真实理由:Vue + Node.js + MySQL
2.1 为什么前端用 Vue 而不是其他框架
这个问题在每个技术评审里几乎都会被问。我的回答有两个维度。第一是上手成本,Vue 的模板语法和响应式数据绑定比手动操作 DOM 直观太多,项目小组里有基础一般的同学也能快速进入状态;第二是生态完整度,Element UI 提供现成的表格、表单、弹窗、分页组件,后台管理页面基本是拼组件,比从零画样式快好几倍。
另外,Vue 适合商城这种“页面状态多、嵌套层级深”的场景。商品列表页要保留筛选条件跳详情,详情页要带参数跳购物车确认页,这些跨页面的数据传递用 Vuex 管起来非常顺手。相比小程序那一套,Vue 的 H5 页面对接 Node.js 接口没有任何跨域限制问题(后端配好 CORS 就行),调试也方便。
2.2 为什么后端选择 Node.js + Express
选 Node.js 做后端,最大的理由是前后端语言统一,JSON 数据流转几乎不用做类型转换,开发效率高。Express 虽然看起来很“轻”,但搭配 Sequelize 操作 MySQL 后,写 CRUD 接口的逻辑非常直白,适合中小型系统。更重要的一点是,Node.js 的异步非阻塞模型处理高并发的商品浏览请求表现稳定,就算订单接口频率不高,普通商业项目也完全够用。
如果你考虑生产环境,Node.js 可以配合 PM2 做多进程负载,Nginx 反向代理静态资源,部署上也比 Java 那一套省内存。我用某云服务器 2核4G 的配置实测,同时跑 Vue 构建产物和 Node 服务端,另加 MySQL,内存占用还能控制在 60% 以内。
2.3 数据库选择的个人经验
项目一开始有人提议用 MongoDB,理由是文档型数据方便存商品规格参数。我最后还是选了 MySQL 关系型数据库。原因很简单:商城系统里订单和库存是强事务场景,订单记录必须和商品 SKU、用户 ID 保持一致性,用关系数据库的外键和事务才能把数据维护住。MongoDB 在“用户画像”这类场景更合适,用在订单系统上有点心有余而力不足。
如果你设定的商品规格比较复杂,我的折中方案是:在商品表中保留一个specsJSON 字段(MySQL 5.7 以上支持 JSON 类型),既有关系型查询的速度,又兼顾了规格参数的灵活性,前端序列化后直接渲染。这个设计我后面还会提到。
3. 数据库建模:从表结构设计看业务逻辑
3.1 用户、商品、订单的核心表设计
先上最核心的六张表结构,我自己在数据库客户端翻来覆去改了好几遍,最后才定下来。不要小看这一步,表结构一旦确定,后端接口基本就是对着字段填值,前端只要按接口契约写页面。
用户表users:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增 | 主键 |
| username | VARCHAR(50) | 登录名,唯一 |
| password | VARCHAR(100) | 加密后的密码(用 bcryptjs) |
| nickname | VARCHAR(50) | 昵称 |
| avatar | VARCHAR(255) | 头像地址 |
| phone | VARCHAR(20) | 手机号 |
| role | TINYINT | 0普通用户,1商家管理员 |
| created_at | DATETIME | 注册时间 |
商品表products:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增 | 主键 |
| category_id | INT | 关联分类表 |
| name | VARCHAR(100) | 商品名称 |
| subtitle | VARCHAR(255) | 卖点副标题 |
| cover | VARCHAR(255) | 封面图URL |
| images | TEXT | 多图URL,JSON数组 |
| price | DECIMAL(10,2) | 当前售价 |
| original_price | DECIMAL(10,2) | 原价(用于展示划线价) |
| stock | INT | 总库存 |
| sales_count | INT | 累计销量 |
| specs | JSON | 规格参数,如颜色/尺码 |
| status | TINYINT | 1上架,0下架 |
| description | TEXT | 富文本详情 |
| created_at | DATETIME | 创建时间 |
订单表用做主表和明细表的两层结构,这样既方便看整单金额,也方便后续做退货退款:
orders:id、order_no(唯一订单号)、user_id、total_amount、pay_amount、status(0待付款/1待发货/2待收货/3已完成/4已取消)、address_snapshot(JSON,收货地址快照)、created_at。order_items:id、order_id、product_id、product_name(冗余字段,防止商品改名后订单显示错乱)、product_image、price、quantity、subtotal。
我特意把商品名称和价格冗余存进订单明细,这是很多初学者最容易忽略的坑。商城商品随时可能改价、改名,如果订单只存商品 ID,用户历史订单就会跟着变动,这种数据关联在生产环境里是大忌。
3.2 购物车、地址、论坛帖子的表结构设计
购物车表我没有单独建“购物车项”和“购物车”两层,直接一个cart_items表搞定:
id、user_id、product_id、quantity、selected(是否勾选,用于批量结算)、created_at。
这样做的好处是,前端购物车页面的布局和表结构一一对应,每次操作加购、改数量、删项都是直接改这一条记录,不需要再维护“一个购物车对象下有多个 item”的复杂关系。
收货地址表addresses:id、user_id、receiver_name、receiver_phone、province、city、district、detail、is_default(TINYINT 0/1)。因为订单提交时要把地址快照存进订单表,所以地址表本身不用关联订单,删改都不影响历史订单数据。
论坛模块的表比较简单,但要注意一点,我做了帖子表和回复表两层,而不是把回复 JSON 塞进帖子表。forum_posts:id、user_id、title、content(TEXT)、category(吐槽/测评/求推荐/公告)、view_count、like_count、reply_count、status(1正常,0隐藏)、created_at。forum_replies:id、post_id、user_id、content、created_at。两个表在查询时做一次 JOIN 或两次独立查询,对系统压力不大。
3.3 为什么我坚持用外键,但只加索引不加约束
现在很多“性能至上”的文章会建议完全不用外键,把关联关系都交给业务代码控制。我在这个项目里的做法是折中:表之间的关联字段都建索引,比如orders.user_id、order_items.order_id、forum_replies.post_id,但是不建物理外键约束。
原因有两个。第一,物理外键在删除用户时会被数据库拦截,业务代码必须先删订单、删除购物车才能删用户,顺序写错就会报错,调试成本高;第二,商城场景下商品 ID 和分类 ID 有可能因为历史数据清理而失效,如果外键强制校验,后端接口会频繁收到完整性错误。用“逻辑外键”加索引,查询性能有保障,删除顺序由后端事务控制,灵活度更高。
我在设计的时候还特意把cart_items里加了一个selected字段,看起来冗余,但实际体验中很有用。用户可能勾选了五件商品,清空购物车只删其中两件的勾选状态,有这个字段就不用再临时判断哪些是勾选的。
4. 后端接口设计:认证、商品、购物车与订单状态机
4.1 JWT 认证与权限中间件的实现思路
用户登录成功后,后端签发一个 JWT token,有效期设为 7 天。每次请求时前端把 token 放在请求头Authorization里,后端用中间件解析。为了区分普通用户和管理员,中间件里除了校验 token 有效性,还要检查req.user.role。
实现上我用了一个简单但好扩展的中间件:
// middlewares/auth.js const jwt = require('jsonwebtoken'); module.exports = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ code: 401, message: '未登录' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (err) { return res.status(401).json({ code: 401, message: '登录已过期' }); } };管理员的中间件再包一层:
const requireAdmin = (req, res, next) => { if (req.user.role !== 1) { return res.status(403).json({ code: 403, message: '无权限操作' }); } next(); };这里有个容易被忽略的细节:JWT 一旦签发,是无法在服务端强制注销的,除非引入 refresh token 和黑名单机制。这个项目规模不大,我用了最直接的方式——前端在退出时删掉 localStorage 里的 token,后端中间件继续校验有效性。如果以后要做“封号”功能,建议在用户表加一个status字段,在认证中间件里同步校验。
4.2 商品列表、搜索、分类筛选的接口设计
商品接口我做了三个返回结构:列表页的“短列表”(不返回 description 和 images 多图),详情页的“完整结构”(含所有图片和富文本),以及后台管理用的表格数据(多返回 status、stock,但不下发 description)。三个接口字段不同,前端就不用在一个接口里做大量数据裁剪。
一个值得分享的分页设计是:列表接口参数里,我用page和pageSize,返回值统一为:
{ "list": [], "total": 100, "page": 1, "pageSize": 10 }这样前端分页组件可以直接映射,不需要再自己算偏移量。搜索和分类筛选都通过 SQL 的where条件叠加实现:
const whereClause = {}; if (keyword) { whereClause.name = { [Op.like]: `%${keyword}%` }; } if (categoryId) { whereClause.category_id = categoryId; } if (status) { whereClause.status = status; } const products = await Product.findAndCountAll({ where: whereClause, limit: pageSize, offset: (page - 1) * pageSize, order: [['created_at', 'DESC']], });如果在真实项目中遇到频繁搜索,可能要上全文索引或者搜索引擎,但目前这个量级 LIKE 查询完全没问题,索引加上后响应时间都在 100ms 以内。
4.3 购物车操作与订单提交的事务处理
订单提交是整个系统里最需要严谨的部分。用户从购物车勾选商品后提交,后端要做五件事:
- 查出购物车中勾选且当前处于上架状态的商品。
- 校验每个商品的库存是否满足购买数量。
- 计算订单总额(后端必须重新计算金额,不能信任前端传过来的总价)。
- 创建订单主表记录和订单明细记录。
- 扣减商品库存,累加销量,清空对应购物车项。
整个过程必须在一个数据库事务里完成。我用 Sequelize 的transaction实现:
const t = await sequelize.transaction(); try { // 循环处理商品时,用 rowLock 锁定商品行 for (const item of cartItems) { const product = await Product.findByPk(item.product_id, { transaction: t, lock: t.LOCK.UPDATE }); if (product.stock < item.quantity) { throw new Error(`商品 ${product.name} 库存不足`); } product.stock -= item.quantity; product.sales_count += item.quantity; await product.save({ transaction: t }); } // 创建订单主表和明细 const order = await Order.create({ ... }, { transaction: t }); await OrderItem.bulkCreate(orderItems, { transaction: t }); // 清空购物车 await CartItem.destroy({ where: { user_id, selected: true }, transaction: t }); await t.commit(); } catch (error) { await t.rollback(); return res.status(400).json({ message: error.message }); }这里最核心的是lock: t.LOCK.UPDATE,也就是行级锁。如果不加锁,两个用户同时买最后一件商品时,数据库会同时读出库存为 1,各自扣减成 0,结果可能出现两笔订单都成功但库存变成负数。加了行锁后,第二个事务必须等第一个事务提交后才能读剩余库存,天然避免了超卖。这一块是并发控制的关键,我在后续测试里专门验证过。
订单状态我维护了一套简单的状态机:待付款 -> 待发货 -> 待收货 -> 已完成,可取消的节点是“待付款”和“待发货”(待发货取消需管理员操作)。从状态机的角度看,每次状态变更都在后端做一个校验函数,保证状态流转不会跳步。
4.4 论坛主题帖与回复帖的接口逻辑
论坛接口比商城接口简单得多,但有个地方必须注意:帖子列表页需要显示“最新回复时间”和“回复数量”,如果每次展示都要COUNT(*)查一遍回复表,数据量上来后会拖慢列表接口。我的做法是,在forum_posts表里冗余reply_count字段,发回复时在事务里同时更新这个字段。
// 发回复时 const reply = await ForumReply.create({ post_id, user_id, content }, { transaction: t }); await ForumPost.increment('reply_count', { by: 1, where: { id: post_id }, transaction: t }); await t.commit();帖子列表按更新时间排序,这里的“更新时间”并不是created_at,而是帖子表里一个last_replied_at字段,每次回复时同步更新。这样列表页一次查询帖子表就能拿到所有排序和计数信息,无需 JOIN 回复表。
帖子详情页的数据组装可以拆两步:先查帖子主表,再查回复列表分页,两段小的独立查询,比一次 JOIN 后再拆两层数据要直观很多。
5. 前端实现:路由守卫、状态管理和核心页面的细节
5.1 Vue 项目结构怎么组织才不乱
我用 Vue CLI 创建项目后,src 目录分成了六个子目录,这套结构本来是给一个略大的项目用的,但用在中小型商城上也没有负担:
api/:按模块封装接口请求,比如product.js、order.js、forum.js、user.js。assets/:全局样式和图片资源。components/:通用组件,如分页、富文本编辑器、图片上传等。router/:路由配置,包含每个路由的 meta 信息(是否需要登录、是否仅管理员)。store/:Vuex 状态管理,分user、cart、app三个模块。views/:页面组件,按模块再分子目录,如home/、product/、cart/、order/、forum/、admin/。
这里要特别强调api/层的封装。后端返回的 JSON 通常有一个固定结构:{ code, message, data }。我在request.js里做了统一响应拦截,只要code !== 0就自动弹出错误提示,页面代码里只关心data部分。这样写业务逻辑会非常干净。
5.2 路由守卫:用户权限和页面访问控制
路由设计里有一个很关键但经常被破防的点:前端只在“点击菜单”时判断用户角色是不够的。用户可能手动在地址栏输入/admin/products,绕过菜单直接访问管理页面。所以我在全局前置守卫里统一做校验:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }); return; } if (to.meta.requiresAdmin) { const user = store.state.user.userInfo; if (!user || user.role !== 1) { next({ path: '/403' }); return; } } next(); });这里的requiresAdmin标记只加在后台路由上,普通商品页和论坛首页不需要。登录后跳回redirect参数指向的页面,这个小处理能极大提升体验,用户原本想看的商品详情页不会被强制回首页。
5.3 状态管理:购物车角标和用户信息怎么同步
购物车角标可能是全站最容易出状态不同步的地方。用户在商品详情页点“加入购物车”,此时没有任何组件点击事件让导航栏刷新。我的处理方式是:购物车数量存进 Vuex,并且只由cart模块统一管理,任何页面调用“加入购物车”或“删除购物车项”的 action 后,都会重新拉取一次购物车总数并提交 mutation 更新导航栏角标。
// store/modules/cart.js async fetchCartCount({ commit }) { const res = await getCartList(); const total = res.data.reduce((sum, item) => sum + item.quantity, 0); commit('SET_CART_COUNT', total); }这样虽然多了一次请求,但可以避免在多个页面里手动改角标。同样,用户登录后在刷新页面时会重新请求/user/info接口,把用户信息和角色恢复进 Vuex,避免刷新后角色丢失导致后台无法访问。
5.4 商品详情页与购物车结算页的实现细节
商品详情页我关注三件事:图片轮播、规格选择、加入购物车的反馈。规格选择这块,后端在specsJSON 字段里返回了颜色和尺码数组,前端用两个按钮组渲染,选择时把所有选中项拼成一个规格字符串,例如“黑色/42”,传给购物车接口存进specs_text字段。订单明细里也冗余存这个规格说明,用户下单后能看到自己买的是什么配色什么尺码。
购物车结算页的关键是“金额实时计算”。我习惯把购物车列表里的数据全放 Vuex 里,页面任何改动(勾选、数量增减)都直接调 action 更新后重新计算selectedTotal。页面展示总金额,提交订单时再把计算好的总价一起发给后端,但后端会忽略前端价格重新计算,所以这里前端价格只是预览作用。
5.5 商家后台的表格页面怎么做才能又省事又直观
后台管理页我大量使用了 Element UI 的el-table+el-dialog组合。商品列表页用表格展示商品名称、分类、价格、库存、状态和操作按钮;新增或编辑商品时,弹窗表单里用动态数组添加规格参数。这里我可以分享一个表单设计的坑:Element UI 的el-form里动态表单项,绑定字段必须是数组的下标形式,例如specsList[index].name,否则增删条目时校验规则会串号。表单恢复时,要把对象转成数组再映射进规格列表,提交时再把数组转回 JSON 对象,两端的数据结构要保持一致。
库存预警是后台比较贴心的功能。列表查询时如果stock <= lowStockThreshold,我就在表格行上渲染一个红色小标签提示,并在顶部统计“库存紧张商品数”。这个小功能不需要额外接口,查询时加一个where条件即可,但能明显提升后台的可用性。
5.6 论坛页面的富文本渲染和敏感内容处理
论坛发帖我用的富文本编辑器是 wangEditor,它生成的 HTML 存储在数据库里。这里有一个安全提醒:用户输入的 HTML 如果直接渲染到页面上,很容易造成 XSS 攻击。我在前端渲染帖子内容时没有直接v-html,而是先把 HTML 字符串经过白名单过滤,只保留 p、img、a、strong 等常用标签和属性,再渲染。由于篇幅原因,我这里不展开具体过滤代码,但所有做论坛功能的开发者一定要把这个安全防线补上。
另外,帖子列表页为了性能,只展示纯文本摘要,而不是完整 HTML。做法是把富文本内容里的标签全部剥掉,截取前 80 个字符作为摘要。这个小处理在数据量大时能明显降低列表页渲染压力。
6. 联调与排错:那些藏在细节里的“坑”
6.1 本地开发跨域问题:代理配好,别让请求被拦截
前端开发服务器默认跑在 8080 端口,Node 后端默认 3000 端口,直接调用接口必然出现跨域问题。我在vue.config.js里配置了 devServer 代理,把所有/api前缀的请求转发到后端:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' }, }, }, }, };这样前端代码里所有接口请求都写/api/products,后端实际收到的是/products。上线后 Nginx 也做同样的反向代理,把/api转发给 Node 服务,前端代码不用改一行。这个习惯特别重要,如果前端写全域名,上线换个 IP 就得全局替换。
6.2 订单并发扣库存的测试方法
本地开发时,我用两个浏览器窗口同时登录同一个测试账号,在同一个商品只剩 1 件库存的情况下,两边几乎同时点击提交订单。最初没有行锁时,果然出现了两条订单都成功的现象,数据库库存变成负数。加行锁后再测,第二个请求会等第一个事务提交,然后读到库存为 0,正常返回“库存不足”。测试并发问题不要只依赖单元测试,一定要用真实接口模拟多请求并发,才能暴露数据竞争。
如果你觉得手动开两个浏览器窗口不够严谨,可以在接口测试工具里写一个脚本,同时发两个请求来模拟。我用的方法是临时写一个 Node 脚本,用两个并发 Promise 同时请求同一个下单接口,观察返回状态。
6.3 图片上传:本地文件和服务器存储的矛盾
图片上传是商城系统的常见痛点。我在项目里用了本地磁盘存储的方案:上传接口把文件保存到public/upload/目录,数据库存路径/upload/xxx.jpg,前端通过域名拼接访问。这个方案在开发环境没问题,但部署到生产环境时要注意两点:
- 后端服务重启或者多进程部署时,上传目录要统一,不然文件会分散到不同进程的目录里。
- Nginx 需要配置
/upload静态文件路径,或者干脆把上传目录挂载到云存储对象服务上。
如果只是课程设计,本地存储完全够用。但如果真的想上线运营,我建议尽早把上传模块改成对象云存储,把文件传完后返回的 URL 直接存库,这样服务器重启和扩容都不会丢文件。
6.4 权限校验被前端绕过的场景
我测试时发现一个很典型的问题:虽然在路由守卫里做了角色判断,但管理员后台的接口如果只在auth中间件下面,没有挂requireAdmin,任何登录用户都能调用删除商品接口,只不过页面端看不到入口。所以“后端接口权限校验”和“前端页面权限控制”是两回事,必须同时做。我在所有后台操作接口上都挂了auth+requireAdmin两层中间件,并且写了一个小工具函数,测试时批量扫描所有以/admin开头的接口,确认它们都带上了权限校验,这才放心。
6.5 状态不同步的典型体验问题
下单成功后,购物车角标如果不刷新,用户会以为系统卡住了。我采取的策略是:在提交订单成功回调里直接调用fetchCartCount,把角标清零;论坛发帖成功后跳转回列表页,并重新拉取列表。这些刷新动作看起来是小事,但整个体验的细腻程度往往就体现在这里。
7. 部署上线:从本地到云服务器的完整步骤
7.1 前端构建、后端启动和数据库导入
部署我用的是最低成本的方案:一台 Linux 云服务器,上面装好 Node.js 18、MySQL 8.0、Nginx 1.24。前端构建流程是:
npm run build # 把 dist 目录里的文件上传到服务器 /var/www/mall后端则是把整个server目录上传到服务器,安装依赖后启动:
npm install --production pm2 start app.js --name mall-server数据库方面,我在本地用mysqldump导出 SQL,再导入服务器 MySQL。需要特别注意的是,导入前要改好数据库连接配置里的主机地址、用户名和密码,并把.env文件里的JWT_SECRET换成一个全新的随机字符串。
7.2 Nginx 配置:一个域名同时服务前端和后端
Nginx 的配置要点是:前端静态文件路径指向dist,所有/api请求反向代理到 Node 服务,图片上传目录单独映射。示例配置如下:
server { listen 80; server_name your-domain.com; root /var/www/mall; index index.html; location / { try_files $uri $uri/ /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 /upload/ { alias /var/www/mall/upload/; } }这里最需要注意try_files $uri $uri/ /index.html,它解决了 Vue Router 的 history 模式下,刷新子页面(比如/product/1)时 404 的问题。如果不写这句,从首页点进商品详情没问题,直接刷新详情页就是白屏。
7.3 数据库备份和日志管理
商城系统一旦跑起来,数据库备份就变成头等大事。我用 crontab 每天凌晨三点执行一个备份脚本,把 MySQL 数据库导出成 SQL 文件,保留最近七天的备份,压缩后放到备份目录:
0 3 * * * mysqldump -u root -p**** mall > /backup/mall_$(date +\%Y\%m\%d).sql后端日志方面,PM2 自带日志管理,默认输出到~/.pm2/logs/,我会定期用pm2 flush清理日志,防止磁盘占满。这些都是“系统上线之后才想起来”的事,但提前配好能省下半夜处理故障的精力。
7.4 给这套系统加功能的话,我会从哪里下手
如果后续想扩展,我的优先级排序是:第一,接入真实支付,比如当面付或微信 Native 支付,订单状态机里补上“已支付”回调逻辑;第二,把商品图片和上传文件迁到云存储,解决服务器重启丢文件问题;第三,增加订单超时自动取消的定时任务,下单后 30 分钟未支付自动关闭并回滚库存。这三个方向都是业务刚需,也是后续面试或项目介绍里最能体现思考深度的点。
最后聊一点个人体会。做完这套 vue + nodejs 商城论坛系统之后,我最大的感觉不是“我会写 Vue 了”,而是知道了从零到一搭建一个前后端分离系统,最花时间的往往不是写代码,而是把表结构、接口字段、权限模型和状态流转想清楚。尤其“订单并发扣库存”这个坑,如果没有真的去压测,永远不会知道行锁的重要性。希望这份完整复盘能帮你少走几步弯路,如果你照着这个思路做,大概率能在很短的时间里跑通一套可以拿得出手的完整系统。