ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Node.js电商购物商城源码实战:Express+MySQL+JWT全解析

2026/9/17 3:35:39 拓冰建站 浏览量
Node.js电商购物商城源码实战:Express+MySQL+JWT全解析 简介基于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 2Express 的中间件是线性执行回调风格直白网上资料和报错记录都多对比 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 直观mongooseORM 写法抽象一层适合模块订单、库存、用户日志、商品详情快照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 的关键细节商品列表核心是分页和模糊搜索接口参数设计成page、pageSize、keyword三个就够用。购物车则要处理「同一商品重复加入」的情况用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要设置上限防止有人传pageSize999999把全表拉出来一般在代码里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.query。pool.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、CANCELEDPAY_DONE已支付待发货SHIPPED、REFUNDINGSHIPPED已发货COMPLETEDCANCELED已取消终态如果源码里订单状态是散落的字符串建议集中放到一个constants/orderStatus.js里导出。答辩追问「怎么防止用户支付未支付的订单、取消已支付的订单」时把上面 UPDATE 条件里的状态判断讲清楚比临场发挥强得多。5. 把源码跑起来nodejs 安装、npm 脚本权限与接口验证排错5.1 环境配置nodejs 安装与版本选择接手任何 .rar 源码第一步永远是确认 Node.js 环境而不是打开代码。先跑两条命令node -v npm -v输出里如果是v14.x、v16.x、v18.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 -vdnf module install会同时装好 node 和 npm不需要额外处理。装完后用where nodeWindows或which nodeLinux确认命令路径防止系统里同时存在多个版本导致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 install。RemoteSigned的意思是本地脚本可以运行从网上下载的脚本必须带签名这个安全级别对开发机足够。不想动执行策略的话直接用 cmdWin R 输入 cmd跑npm install也可以绕过但后续所有 npm 命令都得去 cmd 里敲。报错形态常见原因处理方式npm 无法加载文件 npm.ps1 禁止运行脚本PowerShell 执行策略限制Set-ExecutionPolicy RemoteSigned -Scope CurrentUsernode-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改代码自动重启答辩前调试会省掉大量手动重启的时间。本文还有配套的精品资源点击获取