Node.js电商购物商城源码实战:Express+MySQL+JWT全解析
2026/9/17 3:35:34 网站建设 项目流程

简介:基于Node.js与Express框架二次开发的电商购物商城完整源码,面向毕业设计、Node.js入门学习者及中小型Web项目开发者。项目内置数据库操作示例与详细注解,运行npm start即可在本地3000端口体验完整购物流程。整个rar压缩包约61.88MB,解压后共2000个文件,以JavaScript源码、Markdown文档、JSON配置、HTML/CSS页面及依赖扩展模块为主,前后端代码和第三方库一并包含。目前已有4500余人学习下载,适合作为毕业设计或课程项目的直接参考。除核心商城功能外,还可获得数据库脚本与操作示例、接口调用说明和目录结构注解,便于快速理清Express项目组织方式;系统自带商品浏览、登录、购物车、订单等电商常用模块,可直接对照二次修改;二次封装框架降低了搭建门槛,适合边用边学,也能直接在此基础上扩展订单、商品、用户等业务模块。

1. 拿到一份 Node.js 电商购物商城源码,先看懂它解决什么问题

毕业设计选电商购物商城系统,是这几年最稳妥的方向之一。标题里「含源码项目」意味着你拿到的不是几张页面截图加一份答辩 PPT,而是一套别人已经跑通的完整工程。对这种 .rar 解压包,最怕的不是代码读不懂,而是不知道从哪下手:Node.js 版本对不对、npm 依赖能不能装上、数据库脚本藏在哪个目录、前端是模板渲染还是前后端分离。

这套系统的技术骨架并不神秘,绝大多数毕设版本走的是同一条路:Express 承担 HTTP 层,MySQL 存用户和商品,JWT 做登录态,前端用模板引擎或静态页面完成交互。真正拉开水准差距的,是订单事务怎么保证不超卖、后台权限怎么拦截、以及答辩现场能不能三分钟把环境拉起来。

下面按「选型 → 实现 → 订单与后台 → 排错运行」四条线往下拆。新手可以照着命令逐步复现;已经写过增删改查的熟手,重点看事务和权限的写法,以及最后一节运行环境排查——那才是接手别人源码时最耗时间的部分。

2. Node.js 电商购物商城系统的技术选型:Express + MySQL 为什么是默认答案

2.1 框架选型:Express 在 Node.js 生态里赢在哪

打开一份 Node.js 电商毕设源码,package.json 里大概率是 express、mysql2、jsonwebtoken、nodemon 这一组。这背后的逻辑不是时髦,而是稳。

Express 4.x 的路由和中间件模型足够简单,一个 app.js 就能把所有接口挂起来,对「一个人写完整套系统」的场景极其友好。对比同为 Node 框架的 Koa 2,Express 的中间件是线性执行,回调风格直白,网上资料和报错记录都多;对比 NestJS,得先接受依赖注入和装饰器那套重结构,学习曲线陡很多。毕设的核心目标是「自己能向答辩老师解释每一行代码」,Express 恰好满足这一点。

还有一层现实考虑:毕业设计的数据量和并发量远到不了架构瓶颈,与其在框架炫技上花时间,不如把人力投到商品、购物车、订单这些业务线里。答辩老师翻代码时,期望看到清晰的 API 文件划分和可运行的业务闭环,而不是一堆抽象层。接手 .rar 源码时如果发现用的是 Express 4 配回调写法,不要觉得过时,这恰恰是最好盘活的组合。

2.2 数据库二选一:MySQL 与 MongoDB 的取舍

电商系统里订单、库存、金额都是强一致场景,MySQL 的事务能力是刚需。毕设里大量 .sql 脚本直接用 Navicat 导入,也是 MySQL 生态成熟的体现。打开 .rar 时先翻一眼:有 .sql 文件基本就是 MySQL;有 mongoose 模型文件则是 MongoDB。

对比维度MySQLMongoDB
事务能力强 ACID,行级锁,下单扣库存有保障4.0 起支持多文档事务,但常规写法用得少
建模方式先建表再写代码,字段固定文档结构灵活,改字段不用迁移
毕设源码占比绝大多数,sql 文件导入即可相对少,需要自己维护模型层
Node 驱动mysql2,手写 SQL 直观mongoose,ORM 写法抽象一层
适合模块订单、库存、用户日志、商品详情快照

mysql2 的 promise 写法让await能贯穿全程,配合连接池使用,比老式 mysql 模块的回调地狱干净得多。如果源码里用的是 lowdb 或 jsonfile 直接读写 JSON 当数据库,那是纯演示级实现,答辩时容易被追问数据持久化和并发问题,建议迁移到 MySQL。

2.3 从 .rar 到工程:目录结构先认清楚

解压后不要急着npm install,先把目录结构在脑子里过一遍。典型结构长这样:

shop-server/ ├── app.js # Express 入口,挂载路由和中间件 ├── .env # 数据库密码、JWT 密钥配置 ├── package.json ├── sql/ │ └── shop.sql # 建库建表脚本,Navicat 直接导入 ├── routes/ │ ├── user.js # 注册登录 │ ├── product.js # 商品列表、详情 │ ├── cart.js # 购物车 │ └── order.js # 订单 ├── middleware/ │ └── auth.js # JWT 校验、管理员校验 └── public/ # 前端静态页面或打包后的 Vue 文件

提示:先看 sql 目录再决定怎么启动。没有 .sql 文件的「源码」,大概率连数据库脚本都没给全,接手成本会翻倍。

routes 和 middleware 是阅读优先级最高的地方,业务逻辑全在这里。public 里如果是打包后的前端文件,说明项目是「后端接口 + 静态页面」模式,启动后端后直接访问http://localhost:3000即可;如果 routes 里返回的是 HTML 片段,则是 Express 自带的模板渲染,两者启动方式一致,但调试入口不同,前者看接口数据,后者直接改页面。

3. 在 Express 里落代码:商品查询、JWT 登录与购物车接口实现

3.1 初始化项目与连接池参数设置

先搭建最小可运行骨架。依赖就三样:express 提供 Web 服务,mysql2 负责数据库,jsonwebtoken 处理登录态。

// server.js —— Express 4 最小骨架 const express = require('express'); const mysql = require('mysql2/promise'); const app = express(); app.use(express.json()); // 解析 JSON 请求体 // 连接池:不是每次请求都新建连接 const pool = mysql.createPool({ host: 'localhost', port: 3306, user: 'root', password: '123456', database: 'shop', waitForConnections: true, connectionLimit: 10, dateStrings: true // 日期以字符串返回,避免时区偏移 }); // 健康检查接口,排错时最先打这个 app.get('/api/health', async (req, res) => { const [rows] = await pool.query('SELECT 1'); res.json({ ok: true, db: rows.length === 1 }); }); app.listen(3000, () => console.log('shop server running at 3000'));

连接池的connectionLimit设为 10 足够应付毕设演示,设太大反而浪费 MySQL 连接数;waitForConnections: true保证池满时请求排队而不是直接报错。数据库连接串里的 password 不该硬编码,应该从 .env 读取,但很多源码为了让学生少配一步直接写死,接手时记得改成自己的密码。启动后先访问/api/health,这一步通了,后面的接口问题就不会甩锅给数据库连接。

3.2 JWT 登录注册:token 的生成与校验

登录态用 JWT 是 Node.js 电商系统的主流做法。服务端签发 token,客户端存下来每次请求带上,无需在内存里维护 session。密码一律用 bcryptjs 做哈希,明文存库是答辩时的扣分点。

const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); // 注册:密码哈希后入库 app.post('/api/register', async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ message: '用户名和密码不能为空' }); } const hash = bcrypt.hashSync(password, 10); // 10 是 salt 轮数,越高越慢 await pool.query('INSERT INTO users (username, password) VALUES (?, ?)', [username, hash]); res.status(201).json({ message: '注册成功' }); }); // 登录:比对密码,签发 token app.post('/api/login', async (req, res) => { const { username, password } = req.body; const [rows] = await pool.query('SELECT * FROM users WHERE username = ?', [username]); if (!rows.length || !bcrypt.compareSync(password, rows[0].password)) { return res.status(401).json({ message: '用户名或密码错误' }); } const token = jwt.sign( { id: rows[0].id, role: rows[0].role }, process.env.JWT_SECRET || 'dev_secret', { expiresIn: '2h' } // 过期时间,毕设演示 2 小时足够 ); res.json({ token }); }); // 鉴权中间件:放在所有需要登录的接口前面 function requireAuth(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) return res.status(401).json({ message: '未登录' }); try { req.user = jwt.verify(token, process.env.JWT_SECRET || 'dev_secret'); next(); } catch (err) { res.status(401).json({ message: '登录已过期' }); } }

expiresIn的单位可以是'2h''7d'这种字符串形式,不要用纯数字,会被当作秒。role字段在登录时一并放进 token,后台管理接口后续直接用,不用每次查库。这里有个常见的坑:token 在 JWT 里只是 base64 编码,不是加密,绝对不要把密码放进jwt.sign的 payload。

3.3 商品分页与购物车:拼 SQL 的关键细节

商品列表核心是分页和模糊搜索,接口参数设计成pagepageSizekeyword三个就够用。购物车则要处理「同一商品重复加入」的情况,用ON DUPLICATE KEY UPDATE做数量累加,比先查后插少一次请求。

// 商品分页查询:LIMIT 偏移量 + 参数化查询 app.get('/api/products', async (req, res) => { const page = parseInt(req.query.page) || 1; const pageSize = parseInt(req.query.pageSize) || 10; const keyword = req.query.keyword || ''; const where = keyword ? 'WHERE name LIKE ?' : ''; const params = keyword ? [`%${keyword}%`] : []; const [rows] = await pool.query( `SELECT id, name, price, stock, cover FROM products ${where} ORDER BY id DESC LIMIT ?, ?`, [...params, (page - 1) * pageSize, pageSize] ); const [[{ total }]] = await pool.query( `SELECT COUNT(*) AS total FROM products ${where}`, params ); res.json({ list: rows, total, page, pageSize }); }); // 加入购物车:唯一键冲突时累加数量 app.post('/api/cart', requireAuth, async (req, res) => { const { productId, quantity } = req.body; await pool.query( `INSERT INTO cart (user_id, product_id, quantity) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)`, [req.user.id, productId, quantity || 1] ); res.json({ message: '已加入购物车' }); });

注意pageSize要设置上限,防止有人传pageSize=999999把全表拉出来,一般在代码里Math.min(pageSize, 50)卡一下。所有用户输入必须走参数化查询,直接字符串拼接 SQL 在答辩时被问「SQL 注入怎么防」会很被动。LIMIT ?, ?里的两个问号在 mysql2 中同样是占位符,传数字类型即可,无需加引号。

接口方法鉴权用途
/api/registerPOST用户注册,密码哈希
/api/loginPOST登录并签发 JWT
/api/productsGET商品分页 + 关键字搜索
/api/cartPOST需要 JWT商品加入购物车,数量累加

购物车表设计建议用user_id + product_id联合唯一索引,否则同样商品会插出多行,数量越加越乱。上述代码里VALUES(quantity)是 MySQL 在 INSERT 语句中引用待插入值的关键词,在其它数据库里语义不同,这点也是答辩老师爱追问的细节。

4. 订单事务、后台管理员权限与模拟支付的实现方式

4.1 下单接口:事务里怎么防止库存超卖

电商系统最关键的一段代码是下单扣库存。最简单但错误的写法是「先查库存 → 判断够不够 → 再 UPDATE 扣减」,这种「检查后操作」模式在并发请求下会超卖。正确做法是把扣减条件写进 UPDATE 语句,让数据库行锁来保证原子性,再用事务把扣库存和生成订单绑定。

app.post('/api/orders', requireAuth, async (req, res) => { const { items } = req.body; // items: [{ productId, quantity, price }] const conn = await pool.getConnection(); try { await conn.beginTransaction(); let total = 0; for (const item of items) { // 关键:把库存判断写进 UPDATE 条件 const [result] = await conn.query( 'UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?', [item.quantity, item.productId, item.quantity] ); if (result.affectedRows !== 1) { await conn.rollback(); return res.status(400).json({ message: `商品 ${item.productId} 库存不足` }); } total += item.quantity * item.price; } const [order] = await conn.query( 'INSERT INTO orders (user_id, total, status) VALUES (?, ?, ?)', [req.user.id, total, 'PAY_WAIT'] ); for (const item of items) { await conn.query( 'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)', [order.insertId, item.productId, item.quantity, item.price] ); } await conn.commit(); res.status(201).json({ orderId: order.insertId, total }); } catch (err) { await conn.rollback(); next(err); } finally { conn.release(); // 连接一定要归还连接池 } });

UPDATE ... WHERE stock >= ?是关键一行:数据库行锁会让并发请求排队执行,第二个请求进来时库存已经被扣减,affectedRows返回 0,于是走回滚分支。这样既不需要悲观锁SELECT ... FOR UPDATE,也不会超卖。rollback之后return很重要,否则函数会继续往下走 commit,导致同一个请求既回滚又提交。

注意:事务操作必须用pool.getConnection()拿到的独立连接conn,而不是pool.querypool.query每次从池里随机取连接,会把事务拆散。这是新手接手源码时最容易忽略的结构性问题。

4.2 后台管理接口:用角色中间件做权限拦截

后台管理和前台用户共用同一套用户表,靠role字段区分。不要在前台代码里判断角色,正确的做法是写一个requireAdmin中间件,挂在所有管理接口前。和requireAuth组合使用,先验证登录,再验证管理员身份。

// middleware/auth.js 中的管理员校验 function requireAdmin(req, res, next) { // requireAuth 先跑,req.user 已经被填入 if (!req.user || req.user.role !== 'admin') { return res.status(403).json({ message: '无管理员权限' }); } next(); } // 商品管理接口:新增商品,删除商品,只能管理员操作 app.post('/api/admin/products', requireAuth, requireAdmin, async (req, res) => { const { name, price, stock, cover } = req.body; if (!name || price == null) { return res.status(400).json({ message: '商品名和价格必填' }); } await pool.query( 'INSERT INTO products (name, price, stock, cover) VALUES (?, ?, ?, ?)', [name, price, stock || 0, cover || ''] ); res.status(201).json({ message: '商品已添加' }); });

401(未登录)和 403(无权限)要区分开,这是答辩时容易被追问的 HTTP 语义。中间件的挂载顺序决定执行顺序,如果只写requireAdmin而漏了requireAuthreq.user是 undefined,权限判断直接失效。default admin 账号通常在 sql 脚本里预先插入,密码是明文123456,首次启动后要提醒用户改掉。

4.3 模拟支付:订单状态机的简洁实现

支付接入真实网关在毕设里不现实,也没有必要。最常见的做法是「模拟支付」:下单后订单处于待支付状态,前端提供一个「模拟支付」按钮,调用支付接口把状态置为已支付。这足以把订单流程闭环演示完整。

// 模拟支付:只有 PAY_WAIT 状态才能流转到 PAY_DONE app.post('/api/orders/:id/pay', requireAuth, async (req, res) => { const [result] = await pool.query( `UPDATE orders SET status = 'PAY_DONE', pay_time = NOW() WHERE id = ? AND user_id = ? AND status = 'PAY_WAIT'`, [req.params.id, req.user.id] ); if (result.affectedRows !== 1) { return res.status(400).json({ message: '订单状态不允许支付' }); } res.json({ message: '支付成功' }); });

状态机的核心约束在 UPDATE 的条件里:只有PAY_WAIT状态才能被支付接口更新,这就是「状态校验下沉到数据库」的好处,哪怕有人跳过前端直接调接口,也破坏不了状态流转。订单状态设计的段位,体现在有没有把枚举统一管理起来。

状态值含义可流转到
PAY_WAIT已下单,待支付PAY_DONE、CANCELED
PAY_DONE已支付,待发货SHIPPED、REFUNDING
SHIPPED已发货COMPLETED
CANCELED已取消终态

如果源码里订单状态是散落的字符串,建议集中放到一个constants/orderStatus.js里导出。答辩追问「怎么防止用户支付未支付的订单、取消已支付的订单」时,把上面 UPDATE 条件里的状态判断讲清楚,比临场发挥强得多。

5. 把源码跑起来:nodejs 安装、npm 脚本权限与接口验证排错

5.1 环境配置:nodejs 安装与版本选择

接手任何 .rar 源码,第一步永远是确认 Node.js 环境,而不是打开代码。先跑两条命令:

node -v npm -v

输出里如果是v14.xv16.xv18.x,跑 Express 4 项目最稳妥;如果是v20+v22+,部分老依赖可能给出 engine 警告,一般不影响启动。版本跨度太大时,建议直接装一个 Node 16 或 Node 18 长期支持版本。Windows 用户在 nodejs 官网下载 LTS 安装包,全默认下一步即可。Linux 服务器上,CentOS 8 这类系统用 dnf 的模块化安装:

dnf module list nodejs dnf module install nodejs:18 -y node -v

dnf module install会同时装好 node 和 npm,不需要额外处理。装完后用where node(Windows)或which node(Linux)确认命令路径,防止系统里同时存在多个版本导致node -v和实际运行环境不一致。

5.2 npm 安装依赖报错的处理清单

npm install阶段是报错重灾区,其中出现频率最高的是这条:

npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1, 因为在此系统上禁止运行脚本。

这是 PowerShell 执行策略默认禁止运行 .ps1 脚本导致的,和项目本身无关。解决办法是让 npm 的脚本获得执行许可,在 PowerShell 里执行:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

执行后选Y确认,重新打开终端再跑npm installRemoteSigned的意思是本地脚本可以运行,从网上下载的脚本必须带签名,这个安全级别对开发机足够。不想动执行策略的话,直接用 cmd(Win + R 输入 cmd)跑npm install也可以绕过,但后续所有 npm 命令都得去 cmd 里敲。

报错形态常见原因处理方式
npm 无法加载文件 npm.ps1 禁止运行脚本PowerShell 执行策略限制Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
node-gyp / msbuild 相关报错原生模块需要本机编译工具链装 Visual Studio Build Tools 的 C++ 负载,或换纯 JS 替代包
ERESOLVE unable to resolve dependency tree依赖版本互相冲突npm install --legacy-peer-deps

--legacy-peer-deps跳过 peer 依赖的严格校验,在老项目里几乎是万能钥匙。如果npm install卡在下载阶段不动,多半是网络到官方源不稳定,切到国内镜像:

npm config set registry https://registry.npmmirror.com npm config get registry

改完 registry 后重跑 install,下载速度会有明显提升。注意这只是镜像源配置,不涉及任何其它网络操作,属于常规开发环境优化。

5.3 启动、看日志、验证接口三板斧

依赖装好后,确认 .env 或 config 里的数据库连接信息,先用 Navicat 或命令行导入 sql 建表脚本,再启动项目:

npm run dev

看到shop server running at 3000之类的输出就说明启动成功。端口被占用是常见问题,Windows 下排查并杀掉占用进程:

netstat -ano | findstr :3000 taskkill /PID 进程号 /F

-ano里的o是显示进程 PID,找到 LISTENING 状态的 PID 再强杀。改端口也行,在 app.js 里把listen的端口换掉,前端页面里的请求地址也要同步改。接着验证整个业务流程:

# 1. 登录获取 token curl -X POST http://localhost:3000/api/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 2. 带 token 创建订单(把 TOKEN 换成上一步返回值) curl -X POST http://localhost:3000/api/orders \ -H "Content-Type: application/json" \ -H "Authorization: Bearer TOKEN" \ -d '{"items":[{"productId":1,"quantity":1,"price":99}]}'

一条 curl 命令能同时验证接口可达性、路由匹配、鉴权中间件和数据库连接四个环节。登录接口返回 401 就去看用户名密码;订单接口返回 400 就去查库存数量。养成「先 curl 后看页面」的习惯,接手源码的调试效率会翻倍。最后把node server.js换成nodemon server.js,改代码自动重启,答辩前调试会省掉大量手动重启的时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询