把需求整理成一段话:我想拿一个现成的 Express 毕设项目改造成自己的,需要能跑通、针对性讲解核心代码、讲解验证逻辑。下面这个项目正好符合。所以直接写一篇实操向源码解析。
1. 项目概览与选题定位
1.1 这是一个什么样的毕设项目
express线上花店是一个典型的 B2C 电商型毕业设计,后端基于 Node.js 的 Express 框架,前端没有引入重型框架,而是使用模板引擎配合原生 JavaScript 完成页面渲染和交互。项目覆盖了电商系统最核心的几条链路:用户注册登录、花束/盆栽分类展示、商品详情、购物车、下单结算,以及后台的订单与商品管理。
整个项目可以用一句话概括:一个麻雀虽小但五脏俱全的线上花店系统。它非常适合作为 Web 开发方向的毕业设计,原因有三个,第一,电商题材贴近生活,答辩时老师容易理解业务逻辑,不太需要花大量时间解释“这个系统到底在做什么”;第二,用 Express 做后端,代码量比 Java SSH/Spring Boot 那套少很多,单人短期内可以完整写完;第三,功能模块边界清晰,用户端和管理端天然分离,论文章节也好拆分。
我在拿到这套源码的第一时间就把整个目录结构完整过了一遍,它的分层方式对新手很友好,没有过度设计,只是老老实实地把“路由”、“控制器”、“数据库连接”拆开。这种结构可能不如企业级项目那么讲究,但对于毕设来说,恰恰是优点——好讲、好改、好理解。
如果你正在犹豫“用 Node.js 做毕设会不会显得技术含量低”,我个人的看法是:技术栈的争议远没有“系统是否完整、功能是否闭环、你是否真的理解代码”重要。Express 做电商系统的思路和 Java Web 是一致的,无非是请求进来、处理业务、读写数据库、返回结果,只要你能把这套讲清楚,完全站得住脚。
1.2 这套系统解决的几个核心问题
从业务角度拆,这套花店系统主要解决三类问题。
第一类是“用户怎么逛店买东西”。线下花店可以到店选购,线上就得提供商品列表、商品详情、加入购物车、结算下单这些能力。项目里的商品列表支持分类筛选和分页,详情页有图片、价格、库存、描述信息,购物车和订单模块构成了完整的交易闭环。
第二类是“店主怎么管理商品和订单”。后台管理系统里,店主可以新增花束商品、修改价格库存、上下架商品,还能查看用户提交的订单,更新订单状态——比如把“待付款”改成“已发货”,或者标记“已完成”。这里面的核心逻辑是:前台用户产生的数据,后台管理者能看得到、改得动。
第三类是“用户身份怎么识别”。因为是完整的电商系统,必须处理注册登录,否则购物车和订单就分不清是谁的。项目用express-session做会话保持,前端登录后,后端会在 Session 里标记userId,后续所有购物车和订单接口都依赖这个 ID 来区分数据归属。
这三个问题对应了电商系统的地基,扫码看看这套代码时,建议先带着这三个问题去看,很多文件就不再是零散的,而是一条完整的业务线。
1.3 技术栈选型的“为什么”
先看后端。Express 是 Node.js 生态里最经典、资料最多、上手成本最低的 Web 框架。它的中间件机制非常灵活,路由写法直观,适合快速搭建 REST API 或者服务端渲染页面。在这个项目里,Express 负责接收所有 HTTP 请求,按路由分发到对应的处理函数,再配合模板引擎把数据渲染成 HTML 返回给浏览器。
数据库用的是 MySQL。为什么不用 MongoDB?虽然 MongoDB 在 Node.js 圈也很流行,但大多数高校的数据结构、数据库原理课程教的是关系型数据库,MySQL 的LEFT JOIN、事务、外键约束这些概念和电商订单模型天然匹配。比如“一笔订单包含多个订单项”,这在 MongoDB 里要嵌套文档,在 MySQL 里拆成orders和order_items两张表做关联,理论上更容易让评审老师接受。
前端技术栈是Bootstrap + jQuery + EJS 模板引擎。这里多说一句,很多同学看到“不用 Vue 不用 React”就觉得项目老旧,但毕设场景下,服务端渲染有不可替代的优势。EJS 可以直接在 HTML 模板里写<% %>输出后端变量,不用解决跨域问题,也不用前后端联调,一个人开发时效率是最高的。这个项目选择EJS而不是Art-template,也是考虑到 EJS 是 Express 官方生态里最标准的方案,查资料的人多,遇到问题更容易搜到答案。
整个系统的技术栈可以列成一张表:
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 运行环境 | Node.js + Express 4.x | 后端服务与路由分发 |
| 数据库 | MySQL 5.7+ / 8.0 | 关系型存储核心数据 |
| 模板引擎 | EJS | 服务端渲染页面 |
| 前端样式 | Bootstrap 4 + 原生 JS | 响应式布局与交互 |
| 会话管理 | express-session | 登录态保持 |
| 文件上传 | multer | 处理商品图片 |
| 密码加密 | bcryptjs | 用户密码哈希存储 |
这套组合的另一个好处是部署简单。Node.js 项目只要有npm就能拉依赖,后端和前端在同一个进程里,不需要额外配 Nginx 做静态资源反向代理(虽然生产环境推荐配,但不配也能跑),部署到云服务器上的心智负担小很多。
2. 核心功能与数据库设计详解
2.1 功能模块梳理
我习惯把整个系统按角色拆成前台和后台两块,这样看代码的时候思路会非常清晰。
前台用户端(普通访客/已登录用户):
- 用户注册、登录、退出。注册时校验用户名是否重复,密码经过
bcryptjs加密后入库。 - 首页轮播图和推荐花束展示。轮播图是静态数据,推荐花束从数据库查最新上架的商品。
- 商品列表页。支持按分类筛选,支持分页,每页默认 12 条。
- 商品详情页。展示商品大图、价格、库存、加购按钮。
- 购物车。用户可以加购、修改数量、删除商品、全选结算。
- 订单确认页。从购物车选中的商品生成订单,填写收货人信息,提交后生成订单并扣减库存。
后台管理端(管理员角色):
- 管理员登录。
- 商品管理:商品列表、新增商品、编辑商品、删除(逻辑删除或物理删除,项目里用的是物理删除,如果你要做成毕设建议改成逻辑删除,增加一个
status字段)。 - 分类管理:添加、编辑、删除花束分类。
- 订单管理:查看所有订单,按状态筛选,修改订单状态。
- 用户管理:查看用户列表,虽然一般不会在毕设里实现封禁操作,但列表展示是有的。
这些功能看起来多,其实落到表结构上就几张核心表的事。数据模型规划得合理,后面的代码写起来就顺。
2.2 数据库表结构拆解
为了还原整个系统的数据基础,我当时把建表 SQL 全部梳理了一遍。核心表一共有 7 张,如果算上管理员表是 8 张。
用户表users:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 用户ID |
| username | VARCHAR(50) 唯一 | 登录名 |
| password | VARCHAR(255) | bcrypt 哈希后的密码 |
| phone | VARCHAR(20) | 手机号码 |
| VARCHAR(100) | 邮箱,可空 | |
| avatar | VARCHAR(255) | 头像路径,可空 |
| create_time | DATETIME | 注册时间 |
这里要特别提醒:password字段在 MySQL 中一定要给足长度。很多新手喜欢用VARCHAR(50),但 bcrypt 生成的哈希值通常长 60 个字符,50 根本存不下,插入时会直接报错。如果你拿到源码后自己重新建库,这个坑大概率会遇到。
商品分类表categories:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 分类ID |
| name | VARCHAR(50) | 分类名称,如“鲜花”“绿植” |
| sort | INT | 排序权重,数字越小越靠前 |
商品表products:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 商品ID |
| category_id | INT | 外键关联 categories |
| name | VARCHAR(100) | 商品名称 |
| description | TEXT | 商品描述 |
| price | DECIMAL(10,2) | 价格,单位元 |
| stock | INT | 库存数量 |
| image | VARCHAR(255) | 商品主图路径 |
| status | TINYINT | 上下架状态,1 上架 0 下架 |
| create_time | DATETIME | 创建时间 |
购物车表cart_items:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 记录ID |
| user_id | INT | 外键关联 users |
| product_id | INT | 外键关联 products |
| quantity | INT | 购买数量 |
| checked | TINYINT | 是否勾选,1 勾选 |
| add_time | DATETIME | 加入时间 |
关于checked这个字段,我看到很多毕设项目是用一个单独的接口在前端控制“全选”,但真正的数据状态还是得落到数据库。cart_items 表里加checked字段以后,提交订单时只要一条 SQL 就能查出“当前用户勾选的所有购物车项”,后续处理非常方便。
订单表orders:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 订单ID |
| order_no | VARCHAR(32) | 订单编号,唯一 |
| user_id | INT | 下单用户 |
| receiver_name | VARCHAR(50) | 收货人姓名 |
| receiver_phone | VARCHAR(20) | 收货人电话 |
| receiver_address | VARCHAR(255) | 收货地址 |
| total_amount | DECIMAL(10,2) | 订单总金额 |
| status | TINYINT | 0待付款 1已付款 2已发货 3已完成 4已取消 |
| create_time | DATETIME | 下单时间 |
订单项表order_items:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT 自增主键 | 项ID |
| order_id | INT | 外键关联 orders |
| product_id | INT | 商品ID |
| product_name | VARCHAR(100) | 下单时商品名称快照 |
| product_image | VARCHAR(255) | 下单时图片快照 |
| price | DECIMAL(10,2) | 下单时单价 |
| quantity | INT | 购买数量 |
| subtotal | DECIMAL(10,2) | 小计金额 |
这里有一个非常实用的设计点:为什么order_items要冗余product_name和product_image,而不是直接 JOINproducts表?因为商品信息是可能变的,比如店长改了价格、改了下架,如果订单项只存product_id,用户查看历史订单时,商品名称和价格可能已经对不上了。冗余一份快照数据,订单才算真正稳定。
2.3 表之间怎么关联
理解这几张表之间的关系,对整个项目的理解非常有帮助:
products.category_id→categories.id,商品属于一个分类,一个分类下有多个商品。cart_items.user_id→users.id,购物车记录属于某个用户。cart_items.product_id→products.id,购物车里的商品对应商品表。orders.user_id→users.id,订单归属用户。order_items.order_id→orders.id,一个订单包含多个订单项。order_items.product_id→products.id,订单项对应商品。
下单这个动作背后的数据库操作,其实是三步:第一步查出勾选的购物车记录,关联商品表拿到实时价格;第二步计算总金额,插入一条orders记录,再批量插入order_items记录;第三步删除购物车中已结算的记录。这三步必须在一个事务里完成,否则可能出现“订单生成了但购物车没清空”或者“库存扣了但订单没创建成功”的脏数据问题。源码中如果已经用connection.beginTransaction()包住了这三步,那说明写这个项目的人确实考虑到了数据一致性。
3. Express 后端代码是怎么组织的
3.1 目录结构逐层解析
拿到源码后先看目录,这个项目的目录结构大致是这个样子,我按自己的理解标注了作用:
. ├── app.js // Express 入口文件,中间件注册、路由挂载 ├── package.json // 依赖清单 ├── config │ └── db.js // 数据库连接配置 ├── routes │ ├── index.js // 前台页面路由 │ ├── user.js // 注册、登录、退出、个人信息 │ ├── cart.js // 购物车相关接口 │ ├── order.js // 下单、订单列表、订单详情 │ └── admin.js // 后台管理路由,含子路由 ├── controllers // 控制器层,处理具体业务逻辑 │ ├── userController.js │ ├── productController.js │ ├── cartController.js │ └── orderController.js ├── models // 数据模型层,封装 SQL 操作 │ ├── user.js │ ├── product.js │ ├── cart.js │ └── order.js ├── views // EJS 模板 │ ├── index.ejs │ ├── login.ejs │ ├── register.ejs │ ├── product_list.ejs │ ├── product_detail.ejs │ ├── cart.ejs │ ├── order_confirm.ejs │ └── admin │ ├── admin_login.ejs │ ├── product_manage.ejs │ └── order_manage.ejs ├── public // 静态资源 │ ├── css │ ├── js │ └── uploads // 上传的图片存这里 └── utils └── response.js // 统一响应工具,比如 res.ok() 包装我提一个看法,这个结构非常适合毕设,因为路由-控制器-模型三层分离,每一层职责单一。路由层只做分发,控制器层接收请求参数、调模型方法、组织响应,模型层只负责 SQL 和数据的存取。答辩被问到“你的项目分层思路是什么”,直接照这条线讲就行,一点都不会乱。
3.2 入口文件 app.js 做了什么
Express 项目最核心的启动文件就是app.js,它的作用是把所有零件拼装起来。主要做几件事:
第一,引入依赖,创建 Express 实例。
const express = require('express'); const session = require('express-session'); const path = require('path'); const app = express();第二,注册中间件。中间件是 Express 的核心概念,可以理解成请求流水的“关卡”,每个请求会按注册顺序依次经过这些关卡。
// 解析 POST 表单数据 app.use(express.urlencoded({ extended: true })); app.use(express.json()); // 托管静态资源 app.use(express.static(path.join(__dirname, 'public'))); // Session 配置 app.use(session({ secret: 'flower_shop_secret_key', resave: false, saveUninitialized: true, cookie: { maxAge: 1000 * 60 * 60 * 24 } }));第三,设置模板引擎。指定views目录和 EJS 模板后缀。
app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs');第四,挂载路由。把不同业务模块的路由对象挂载到指定前缀下。
app.use('/', indexRouter); app.use('/user', userRouter); app.use('/cart', cartRouter); app.use('/order', orderRouter); app.use('/admin', adminRouter);第五,错误处理和监听端口。
app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('服务器内部错误'); }); app.listen(3000, () => { console.log('花店服务已启动: http://localhost:3000'); });这些代码看着简单,但里面有一个知识点特别容易被毕设答辩老师追问——express.urlencoded和express.json到底有什么作用。简单来说,前端提交普通表单时,Content-Type是application/x-www-form-urlencoded,必须用urlencoded才能把 body 数据解析成对象;而前后端通过 AJAX 发 JSON 时,需要json中间件才能把它解析到req.body上。两个都注册,任何一种提交方式都不会有问题。
3.3 Session 登录态的实现
Session 是电商系统的地基,没登录就不能下单。这个项目的 Session 用法很标准:
用户登录成功后,在控制器里执行req.session.userId = user.id; req.session.username = user.username;。
退出登录时,先req.session.destroy(),再重定向到首页。
判断登录态的中间件写在路由层,比如:
// routes/order.js router.use((req, res, next) => { if (!req.session.userId) { return res.redirect('/user/login'); } next(); });这个中间件的作用是:访问订单相关的任何页面或接口之前,先检查 Session 里有没有userId,没有就踢回登录页。实际操作中,我发现很多同学容易把这段逻辑写在每个接口里,造成大量重复代码。这个项目把它作为路由级的中间件统一处理,是很成熟的做法。答辩时如果被问“如何保护订单接口不被未登录用户访问”,你可以直接指出这段代码,并解释“中间件在路由进入控制器之前拦截请求”,老师对这个回答通常比较认可。
还有一个关于 Session 配置的细节值得讲一讲:saveUninitialized: true的意义是,即使本次访问没有写入任何 Session 数据,也强制创建一个 Session 会话并种下 Cookie。这样有个好处,是用户访问商品页后加入购物车,每次都能拿到同一个 Session ID,不会因为服务端没有主动保存而丢失会话。
3.4 商品列表和分页查询的实现
商品列表页是第一屏,也是答辩时高频提问点。这个项目大概率会用到“分页条件查询”和“模糊搜索”,我这里讲一下标准实现思路。
前端请求链接可能是这样的:/list?categoryId=2&page=1&keyword=玫瑰。
控制器收到参数后,先解析参数,再调模型方法查询:
const getList = async (req, res) => { let { categoryId, page = 1, keyword = '' } = req.query; page = parseInt(page) || 1; const PAGE_SIZE = 12; const offset = (page - 1) * PAGE_SIZE; // 构建 WHERE 条件 let where = ' WHERE status = 1 '; let params = []; if (categoryId) { where += ' AND category_id = ? '; params.push(categoryId); } if (keyword) { where += ' AND name LIKE ? '; params.push(`%${keyword}%`); } const list = await productModel.getList(where, params, offset, PAGE_SIZE); const total = await productModel.getCount(where, params); const totalPages = Math.ceil(total / PAGE_SIZE); res.render('product_list', { list, pagination: { page, totalPages, total }, categoryId, keyword }); };这里用?占位符传参,而不是直接拼接 SQL 字符串,是为了防止 SQL 注入。这个点一定要在答辩时主动提出来——老师问到安全性时,回答“所有 SQL 参数都是预编译占位符方式传入,不直接拼接字符串”,分直接就有了。
3.5 下单事务怎么写
下单接口是整个项目里逻辑最重的一段代码,源码中只要有“事务”就基本在这一块。标准的执行顺序:
- 查出当前用户勾选的购物车记录,确保记录非空。
- 遍历购物车记录,关联商品表,拿到最新价格和库存。
- 循环加总金额,生成一个订单号。
- 插入
orders表,拿到生成的orderId。 - 循环插入
order_items表,同时扣减products表库存。 - 删除已结算的购物车记录。
防止部分成功部分失败,全程必须包裹在事务里:
const connection = await db.getConnection(); try { await connection.beginTransaction(); // 上面 2-5 步的 SQL 全部用 connection.execute() await connection.commit(); } catch (err) { await connection.rollback(); throw err; } finally { connection.release(); }这里我建议大家读代码的时候,重点看下单接口有没有“扣库存前校验库存是否足够”的判断。正常逻辑应该是:
if (product.stock < quantity) { throw new Error(`商品 ${product.name} 库存不足`); }这个校验必须有,否则用户直接改前端数量提交,也能买超过库存的商品,这在答辩演示时如果被老师用极端数据试一下,特别容易翻车。我看到的这个源码版本里是做了兼容的,但你在复现项目时一定要亲手测一遍库存不足的场景。
3.6 后台文件上传的实现
商品图片上传是电商后台必须有的功能。这个项目用的是multer,配置大概长这样:
const multer = require('multer'); const path = require('path'); const storage = multer.diskStorage({ destination: path.join(__dirname, '../public/uploads'), filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, Date.now() + '-' + Math.round(Math.random() * 1e9) + ext); } }); const upload = multer({ storage, limits: { fileSize: 2 * 1024 * 1024 } // 限制 2MB });上传接口里就可以用upload.single('image')接收前端传过来的image文件字段。文件名用“时间戳+随机数”拼出来,目的就是防止重名覆盖。
一个容易被忽视的坑:上传后的图片地址要能通过浏览器直接访问,public目录必须在express.static托管范围内。如果你把上传目录放到public外面,图片路径会 404。因此源码里destination指向public/uploads正是正确做法。
4. 页面渲染与交互流程分析
4.1 EJS 模板是怎么工作的
EJS 的工作方式很好理解,在 HTML 结构基础上嵌入<% %>标签,服务端在响应时把变量填充进去,再整段吐出 HTML。拿商品列表页举例,核心代码大致是这样:
<% list.forEach(function(item) { %> <div class="col-lg-3 col-md-4 col-sm-6 mb-4"> <div class="card h-100"> <img src="<%= item.image %>" class="card-img-top" alt="<%= item.name %>"> <div class="card-body"> <h5 class="card-title"><%= item.name %></h5> <p class="card-text price">¥<%= item.price %></p> <a href="/product/detail/<%= item.id %>" class="btn btn-sm btn-outline-primary">查看详情</a> </div> </div> </div> <% }) %>这种写法对比前后端分离有什么优势,我个人理解是这样的:省掉了一堆 API 联调和跨域配置,一个文件从数据到页面全打通。对于只有一个开发者、没有专业前端配合的毕设项目,效率提升非常明显。
4.2 购物车的交互链路
购物车页面是一个典型的“服务端渲染+局部 AJAX”混合模式。初次进入页面时,路由渲染完整 HTML。之后的“修改数量”“删除商品”“勾选商品”都是通过 AJAX 发请求到后端,成功后前端只改一个数值或移除一个 DOM 节点,而不是整页刷新。
用一个常见场景来说明:用户点击“+”按钮加数量,前端的 JS 会发送 POST 请求到/cart/update,带上cartItemId和新的quantity,后端更新数据库后返回{ success: true },前端收到结果后把页面上的数量和合计金额做局部更新。
这种交互方式在实践中体验不错,不用每次点击都重新加载整个页面,而且写起来也不算复杂。唯一要注意的是,前端不能过度信任自己的“乐观更新”——也就是说,点击“+”后不应该立刻把价格改掉,一定要等后端返回成功再更新页面。原因很简单,如果库存不够,后端会返回失败,前端如果先改了,数字缩回去会显得很业余。
4.3 订单确认页的数据来源
订单确认页和购物车页面直接相关。用户勾选了若干商品,点击“去结算”,页面会展示所有勾选商品的明细、总金额,以及要填写的收货人表单。
这个页面在实现上有两种常见方式。一种是完全依赖前端状态,把购物车勾选数据通过 localStorage 或者 URL 参数传递;另一种是后端根据 user_id 和 checked=1 实时查询。这个项目用的是后者,也就是每次访问订单确认页时,后端都会重新从数据库里查一遍当前用户勾选的购物车项。这种做法更稳,刷新页面也不会丢数据,推荐保留原逻辑,不要改成前端传参。
提交订单时,表单需要提交收货人姓名、电话、地址三要素,后端只接受 POST 请求,且会校验这些字段是否为空。如果你要改造成自己的毕设,建议在前后端都加校验,后端校验是底线,前端校验只是体验优化。
4.4 后台管理的表格页面
后台管理页面在毕设里通常是最“稀疏平常”的部分,无非是一张表格加上增删改查的按钮。但正因为如此,反而容易忽视验证逻辑。
商品管理页,新增和编辑共用一个弹窗表单,图片选择走的是文件上传。要注意的是,编辑商品时要能正确回显原来的图片,这要求表单在新增和编辑两种模式下,对product.id的处理必须区分开。项目中新增时没有id,编辑时必须带上id,控制器通过id是否存在来决定是 insert 还是 update。
订单管理页,最重要的操作是“修改状态”。建议你在复现时用下拉框选择当前状态,点击“更新”提交。因为订单状态不是随便填的,应该限定在合法的状态枚举里。后端即使只做前端枚举校验,也要做服务端校验,防止有人直接构造请求把订单状态改成一个非法值。
5. 本地运行与部署实操记录
5.1 环境准备清单
拿到源码后,第一步不是写代码,而是先把环境准备好。你需要安装的东西有:
- Node.js 12 以上版本,推荐 14/16/18 LTS。版本不要选太老的,Express 4.x 在高版本 Node 上兼容性更好。
- MySQL 5.7 或 8.0,提供数据库服务。
- Navicat 或者其他 MySQL 客户端工具(也可以用命令行)。
- npm 包管理器,装 Node.js 时自带。
检查是否安装成功,打开终端输入:
node -v npm -v如果都有版本号,说明 Node 环境 OK。MySQL 那边,需要记住一个能登录的账号密码,后面配置要用。
5.2 初始化数据库
源码里一般会附一个.sql文件,文件名大概叫flower_shop.sql,或者就是database.sql。里面含建库、建表、插入初始数据的全部 SQL。我建议大家在 MySQL 里手动建一个独立的数据库,然后导入 SQL:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS flower_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p flower_shop < flower_shop.sql这里有一个细节容易被坑:建表的时候务必将字符集设置成utf8mb4。utf8mb4支持中文和四字节的 Emoji 字符,而老版本的utf8在某些 MySQL 配置下存中文没问题,但遇到特殊符号会报错。这个项目初始 SQL 里如果写的是utf8mb4,你可以照搬;如果老项目用的是utf8,我建议自行改成utf8mb4再导入。
导入完成后,可以顺手执行几条 SQL 验证一下数据是否存在:
USE flower_shop; SELECT COUNT(*) FROM users; SELECT COUNT(*) FROM products; SELECT COUNT(*) FROM categories;正常返回非 0 的数量,说明初始数据没问题。然后找一个测试账号登录用,初始 SQL 通常会自带一个admin管理员账号和一个普通测试用户。如果没有,注册入口也可以自己注册。
5.3 修改数据库配置并启动
数据库导入之后,打开config/db.js或者.env后缀的配置文件(取决于具体源码,有的把配置写在config/config.js里),根据你的 MySQL 账号密码修改:
module.exports = { host: 'localhost', port: 3306, user: 'root', password: '你的MySQL密码', database: 'flower_shop' };这里我强调一下,千万不要把密码写错,或者用默认的占位符123456。很多同学反映“启动后登录不了”,十有八九是密码对不上。
配置文件改好之后,在项目根目录执行:
npm installnpm install会依据package.json里的声明把 Express、mysql2、express-session、ejs、multer、bcryptjs 等依赖全部装到node_modules目录。装完以后,执行:
npm start正常情况下,终端会打印“花店服务已启动,访问 http://localhost:3000”,然后在浏览器里访问http://localhost:3000,就能看到花店首页了。
5.4 部署到服务器上的建议
毕设演示通常是在本地进行,但如果有线上演示需求,还是建议部署到一台云服务器。Express 项目部署的思路和所有 Node 服务一致:
本地准备node_modules依赖,可以直接在服务器上执行npm install --production,也可以把本地的node_modules直接打包上传。前者更规范,推荐用前者。
服务器需要安装 Node.js 和 MySQL。如果服务器上已经有一个空白的 MySQL,需要把本地的flower_shop.sql导进去。这里要注意,云服务器上的 MySQL 默认可能只允许本机连接,如果你的项目连的是云数据库,配置里host要填数据库的公网地址或者内网地址,具体看你的云服务商怎么设置。
真正跑服务时,不建议直接用node app.js,因为 Ctrl+C 一关,进程就没了。可以用进程守护工具,比如pm2:
npm install -g pm2 pm2 start app.js --name flower-shop pm2 save pm2 startup然后通过http://你的服务器IP:3000访问。如果希望用 80 端口,可以按我前面说的,在服务器上装一层 Nginx 做反向代理:
server { listen 80; server_name your_domain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这一步选做,毕设演示不需要这么复杂,但如果你的论文里写了“系统已部署上线”,这层代理会让答辩老师觉得你的部署思路是完整的。
6. 常见报错与排查速查表
这部分是我在实操过程中比较想给新手看的地方。下面的问题几乎是被问烂了的,我把现象和原因按表格整理出来。
| 现象 | 大概率原因 | 解决办法 |
|---|---|---|
npm install报 ERESOLVE 依赖树冲突 | npm 版本过高与旧依赖不兼容 | 用npm install --legacy-peer-deps或升级项目依赖 |
| 启动后首页能看,但登录报“Cannot POST /user/login” | 路由没挂载或用错请求方法 | 检查表单method="post"与路由router.post是否一致 |
| 注册提示密码不能为空,但已经填了 | body 解析中间件没注册 | 确认app.use(express.urlencoded({ extended:true }))在路由挂载之前 |
图片上传报MulterError: File too large | 超过limits.fileSize限制 | 调大上传限制或者压缩图片 |
| 后台新增商品后,图片 404 | 上传路径和静态目录不一致 | 确保uploads目录在public下,并且访问路径对应 |
| 添加购物车报 500,SQL 里有 errno 1452 | 外键约束失败,product_id 不存在 | 检查商品是否被误删,或外键关联是否完整 |
| 刷新“/admin/order”能访问,但管理员列表为空 | 权限中间件没生效或登录 Session 丢失 | 确认后台登录后req.session.isAdmin正确设置 |
| 输入中文商品名后,页面乱码 | 数据库或页面字符集不是 utf8mb4 | 建库表统一 utf8mb4,HTML meta 也声明 UTF-8 |
npm start后提示Error: Cannot find module 'mysql2' | 依赖没装全或被误删 | 重新npm install,或手动npm install mysql2 |
除了表格里的问题,我再补充几个实操中容易忽略的细节。
端口被占用。如果你的 3000 端口已经被其他进程占用,app.listen(3000)会报EADDRINUSE。解决办法是把端口改成 3001 或者用lsof -i:3000找到占用进程处理掉。
Session 固定在登录后会失效。有些同学改了代码,把express-session的cookie配置改成{ maxAge: 1000 * 60 },也就是一分钟过期。本地测试时登录完马上操作没问题,但几秒后轻微刷新就发现登录状态没了。排查时先看 Cookie 的过期时间,确认自己的配置是不是被自己改坏了。
商品列表页第一次访问很慢。这个多数是因为本地电脑第一次加载 Bootstrap 的 CDN 资源,网络不佳。如果项目里引的是 CDN,建议下载下来放到public目录,本地部署后就不会有这个问题。
删库跑路的操作别乱试。有些同学为了测试方便,直接执行了DROP TABLE products;,然后又想恢复数据,结果只能重新导 SQL。建议在测试环境操作,不要看一条执行一条,步骤记录下来,未来写论文章节时也有依据。
7. 自己改造扩展的几个方向
源码拿来直接用当然可以,但毕业设计通常要求“要有自己的东西”。哪怕只是改几个地方,也能让课题显得更有个人工作量。我列出几个适合在这个花店项目上做的升级,难度从低到高。
方向一:增加“鲜花速递”配送时间选择。在订单确认页加一个“期望送达时间”字段,orders 表加一列delivery_time,下单时校验时间不早于当前时间。这个功能有生活场景,业务逻辑简单,前后端改动不大,答辩时能讲清楚。
方向二:增加商品库存预警。后台订单管理页顶部加一个统计卡片,显示“库存不足 10 件的商品数”。实现方法就是一条COUNT(*) FROM products WHERE stock < 10,五分钟能写完,但论文里可以写“基于库存数据的多维度分析”,在系统功能上是个亮点。
方向三:接入支付宝沙箱支付。在“待付款”订单处,对接支付平台的沙箱环境,支付成功后回调修改订单状态。这个方向工作量较大,但如果你的毕业设计偏工程实践,很加分。改动思路:引入支付 SDK,在订单详情页生成支付二维码,轮询查询支付结果,成功则调用后端改状态接口。
方向四:前端升级成 Vue3 + 前后端分离。Express 原样保留成纯后端 API,前端用 Vite 初始化 Vue 项目,通过 Axios 请求数据。这个方向适合前端基础比较好的同学,工作量适中,效果和新颖度直接拉满。但要做好心理准备,联通“购物车”、“登录态”和“路由权限”这一步,比想象中麻烦一点,特别是要处理 Token 的存取和请求拦截器。
方向五:引入 Redis 做 Session 持久化。默认express-session的内存存储是开发用的,服务重启后 Session 会丢。你把 Session 存储换成 Redis,登录状态重启后还能保持。这个点写进论文里,可以展示“有状态服务在生产环境的高可用方案”的理解深度。不过本地需要多装一个 Redis,环境配置麻烦一些,建议优先在服务器上测试。
8. 拿到源码后从头到尾的检查清单
我自己不管是下载别人的源码还是团队内部交接代码,都会按一条固定清单过一遍,免得东看一坨西看一坨。这里把清单整理出来,你拿到这套花店源码后也可以按这个顺序盘它。
- [ ] 1. 先跑
npm install,确认依赖完整。 - [ ] 2. 导入 SQL,确认所有表建好、有初始数据。
- [ ] 3. 修改数据库配置,启动服务,首页能开。
- [ ] 4. 注册一个新用户,验证用户名重复时报错。
- [ ] 5. 登录,验证没有登录的情况下直接访问
/cart会被重定向。 - [ ] 6. 逛商品列表,测试分类筛选和分页。
- [ ] 7. 商品详情页加入购物车,连续加多个不同商品。
- [ ] 8. 修改购物车数量,验证库存不够时的后端报错。
- [ ] 9. 勾选部分商品,提交订单,验证订单能生成、库存能扣减。
- [ ] 10. 进入后台,用管理员账号登录,新增一个商品,上传一张图片。
- [ ] 11. 修改商品价格和库存,刷新前台确认数据同步。
- [ ] 12. 更新订单状态,确认前后台展示一致。
按这一整套流程走下来,正常耗费一个下午。但这一下午能换来对整个项目的全局掌控。很多同学答辩翻车不是因为代码写不出来,而是“系统是自己跑起来的,但完全讲不清测试路径”。拿着清单一步步验证,每一个页面背后对应哪条路由、哪个控制器、哪个 SQL,基本都心里有数了。
我个人认为,毕业设计最怕的不是代码差,而是中途连源码本身都跑不起来。那才是真的灾难现场。先把这套源码盘透,再决定怎么改自己的业务逻辑,才是效率最高的路径。如果你能把“从导入 SQL 到后台发布商品”的完整流程走通,答辩时已经比大多数“只会播放 PPT”的同学强很多了。