十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Node.js+MySQL超市管理系统毕业设计:从数据库到接口实战

Node.js+MySQL超市管理系统毕业设计:从数据库到接口实战 简介面向计算机、软件工程等专业学生的毕业设计论文资源以Node.js超市管理系统为课题聚焦超市首页、用户管理、商品分类、购物订单、系统管理、我的信息等核心模块涵盖商品上架、订单处理、用户权限配置等典型业务场景。内容完整覆盖摘要、技术介绍、需求分析、系统设计、数据库概念模型与数据表设计、前后台功能实现等章节系统采用Node.js与Vue.js构建前端MySQL作为后台数据库可帮助读者理解B/S架构下超市管理系统的完整设计与实现路径。资源为单个docx文档约2.58MB章节层级清楚便于按目录快速定位、复制或二次编辑。已有105人学习下载适合正在筹备Web类毕业设计、需要参考完整论文框架或了解Node.jsVueMySQL前后端分离项目设计的同学可直接作为论文写作模板和系统开发思路借鉴。1. 为什么用Node.js做超市管理系统毕业设计是性价比较高的选择超市管理系统是计算机相关专业毕业设计里出现频率很高的题目它把进销存流程的常见动作都放在了一个闭环里商品建档、入库、收银结算、库存扣减、销售统计。这个题目真正的难点不在某个单一功能多么复杂而在于每个操作之间要对得上账答辩时老师顺着订单反查商品档案和库存记录不能出现断点。选择Node.js来完成这个题目最大的优势是前后端用同一门语言贯穿Express负责接口Vue页面负责交互MySQL承接数据持久化整个工程比Java全家桶轻不少适合在毕业设计周期内有效完成。Node.js的异步模型和npm生态在处理批量入库、并发收银、生成日报表这类场景时都有成熟方案代码量也更收敛。下面按数据库设计、后端接口、前端联调、测试与论文成稿四个阶段展开给出一条可以照做的技术路径。2. 超市管理系统的需求拆解与MySQL数据库建模2.1 按角色拆功能避免在需求分析里堆砌模块先把系统使用者抽象成三个角色管理员、收银员、库管员。管理员维护商品分类、商品档案、价格以及员工账号收银员在前台收银页面通过名称或条码查询商品加入购物车后选择支付方式结算库管员负责办理入库登记和库存盘点。多数毕业设计做到前两个角色就可以形成完整业务闭环库管功能可以合并进管理员模块也可以拆出来单独体现工作量。按角色反向划定功能模块表格如下模块核心功能对应数据表登录认证账号密码校验、Token签发、角色权限控制user商品分类管理分类新增修改、分类下商品数据统计category商品管理商品增删改查、条码检索、上下架、调价product库存管理入库登记、库存调整、低于安全库存提醒product、stock_log收银管理购物车、结算下单、订单查看、退货sale_order、sale_order_item统计报表日营业额、毛利、商品销量排行、低库存清单订单与商品聚合论文需求分析章节画功能模块图时可以按这张表扩展开一个模块对应一个小矩形下面标注子功能。这样做的好处是老师看图的直觉与后期数据库表设计能一一对应起来避免出现功能图画了一堆、数据库里却没有承载表的情况。2.2 核心数据表设计与建表SQL核心数据表共五张user、category、product、sale_order、sale_order_item。如果论文强调进销存过程再加入一张stock_log专门记录每次入库、出库、盘点的变动原因。设计时有两个很容易被忽视的点。第一商品表必须同时保留price零售价与cost_price成本价统计报表里的“毛利”是两者之差如果只设计零售价答辩时被问到“毛利怎么算”就答不上来。第二订单明细表里要冗余保存商品名称和下单单价不能通过商品ID实时去关联商品表因为商品可能后期调价历史订单必须保留交易发生那一刻的快照。CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; CREATE TABLE category ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT NOT NULL DEFAULT 0 COMMENT 排序权重, PRIMARY KEY (id) ) ENGINEInnoDB COMMENT商品分类表; CREATE TABLE product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, barcode VARCHAR(32) NOT NULL COMMENT 商品条码收银时扫描, name VARCHAR(100) NOT NULL COMMENT 商品名称, category_id INT UNSIGNED NOT NULL COMMENT 所属分类ID, price DECIMAL(10,2) NOT NULL COMMENT 零售价, cost_price DECIMAL(10,2) NOT NULL COMMENT 成本价用于计算毛利, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, safety_stock INT NOT NULL DEFAULT 10 COMMENT 低于该值触发预警, unit VARCHAR(10) NOT NULL DEFAULT 件, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_barcode (barcode), KEY idx_category (category_id) ) ENGINEInnoDB COMMENT商品表;商品表的barcode列加上唯一索引既保证收银扫描时通过条码能立刻命中索引也避免同一商品重复建档。stock直接放在商品表里适合论文场景如果做大规模并发销售系统要把库存拆到独立表再配预扣机制那是生产级系统的复杂度论文里不用刻意去做。订单相关的两张表CREATE TABLE sale_order ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, total_amount DECIMAL(10,2) NOT NULL COMMENT 应付总额, paid_amount DECIMAL(10,2) NOT NULL COMMENT 实付金额, pay_type TINYINT NOT NULL DEFAULT 1 COMMENT 1现金 2微信 3支付宝, cashier_id INT UNSIGNED NOT NULL COMMENT 收银员用户ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_cashier (cashier_id) ) ENGINEInnoDB COMMENT销售单主表; CREATE TABLE sale_order_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, product_name VARCHAR(100) NOT NULL COMMENT 商品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, qty INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计金额, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB COMMENT销售单明细表;订单主表只保存业务头信息明细逐行记录每件商品形成一对多关系。order_no加上唯一索引是为了防止并发情况下订单号重复前端不要把自增主键id直接泄露出去对外展示的是order_no。2.3 Node.js连接MySQL连接池、字符集与时区的配置数据库驱动选择mysql2因为从npm安装后可以直接使用Promise接口不需要再包装回调它同时支持预处理语句能规避大部分SQL注入风险。连接配置const mysql require(mysql2/promise); const pool mysql.createPool({ host: 127.0.0.1, port: 3306, user: root, password: 这里替换成实际密码, database: supermarket, waitForConnections: true, connectionLimit: 10, queueLimit: 0, charset: utf8mb4, timezone: 08:00 }); module.exports { pool };几个参数说明connectionLimit控制连接池最大连接数本地演示环境10即可如果答辩时用浏览器开多个标签页操作建议调整到20太大会拖慢数据库响应。waitForConnections设为true连接不够用时请求排队等待而不是直接抛错这样可以避免前台页面出现数据库连接失败的白屏异常。timezone: 08:00让查询结果里的DATETIME直接按北京时间返回避免Node进程与MySQL服务器时区不一致导致时间差8小时。数据库和表结构准备好后就进入后端接口实现环节。3. 用Express框架实现Node.js后端接口JWT认证与销售事务3.1 项目初始化与Windows下npm脚本执行问题后端项目单独建目录避免与前端页面混在一起。初始化命令如下mkdir supermarket-server cd supermarket-server npm init -y npm install express mysql2 jsonwebtoken cors dayjs如果电脑还没有配置Node.js环境先到官网装LTS版本安装时勾选“Add to PATH”装完用node -v、npm -v两个命令确认环境变量生效。在Windows的PowerShell里接着执行npm run dev时很容易遇到这个报错“npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本”。原因不是npm坏了而是PowerShell默认执行策略Restricted不允许运行.ps1脚本。解决办法是以当前用户身份放开执行策略Set-ExecutionPolicy -Scope CurrentUser RemoteSignedRemoteSigned允许本机创建的脚本直接运行从外部下载的脚本必须经过签名。这个设置只影响当前用户不会波及整个系统。项目骨架按功能拆文件supermarket-server/ ├── app.js ├── routes/ │ ├── auth.js │ ├── product.js │ └── order.js ├── middleware/ │ └── auth.js ├── config/ │ └── db.js └── package.json主入口app.js的内容很薄const express require(express); const cors require(cors); const app express(); app.use(cors({ origin: http://localhost:5173 })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(/api/auth, require(./routes/auth)); app.use(/api/product, require(./routes/product)); app.use(/api/order, require(./routes/order)); app.listen(3000, () { console.log(后端服务已启动http://localhost:3000); });express.json()用来解析前端传来的JSON请求体cors中间件解决浏览器跨域。这里把接口统一挂到/api前缀下前端调用时路径一致部署时也方便统一转发。3.2 登录接口与JWT令牌校验的实现登录接口是论文演示中老师第一个点开的接口。演示代码使用sha256加盐的方式保存密码生产环境会用bcrypt但对毕业设计而言前者已经可以解释清楚“密码不是明文存放”这个安全概念。// routes/auth.js const express require(express); const crypto require(crypto); const jwt require(jsonwebtoken); const { pool } require(../config/db); const router express.Router(); const SECRET_KEY your-secret-key; router.post(/login, async (req, res) { const { username, password } req.body; if (!username || !password) { return res.json({ code: 400, msg: 用户名和密码不能为空 }); } const hash crypto .createHash(sha256) .update(password doc_salt) .digest(hex); const [rows] await pool.query( SELECT id, username, role FROM user WHERE username ? AND password ? AND status 1, [username, hash] ); if (rows.length 0) { return res.json({ code: 401, msg: 用户名或密码错误 }); } const token jwt.sign( { uid: rows[0].id, role: rows[0].role }, SECRET_KEY, { expiresIn: 12h } ); res.json({ code: 0, token, user: { id: rows[0].id, username: rows[0].username, role: rows[0].role } }); }); module.exports router;代码里通过参数化查询的?占位符把用户名和密码传给SQL语句避免字符串拼接带来注入风险。查询时没有把password字段放进SELECT返回给前端的用户对象也不包含敏感信息。jwt.sign生成的token有效期12小时答辩当天提前启动服务演示过程基本不会过期。对应的认证中间件如下// middleware/auth.js const jwt require(jsonwebtoken); module.exports function (req, res, next) { const header req.headers.authorization || ; const token header.startsWith(Bearer ) ? header.slice(7) : null; if (!token) { return res.status(401).json({ code: 401, msg: 未登录请先登录 }); } try { req.session jwt.verify(token, SECRET_KEY); next(); } catch (err) { return res.status(401).json({ code: 401, msg: 登录状态已过期 }); } };中间件解析Authorization头里的Bearer token验证通过后把用户信息挂到req.session后续接口拿req.session.role做权限判断即可。注意SECRET_KEY不要写死在代码里被提交到Git仓库答辩项目可以放在环境变量中。3.3 商品接口分页查询、新增条码与权限控制商品模块是后端接口最多的地方至少实现列表查询、按条码搜索、新增、修改、删除五个接口。列表查询要支持分页和关键词模糊搜索// routes/product.js const express require(express); const auth require(../middleware/auth); const { pool } require(../config/db); const router express.Router(); router.get(/list, auth, async (req, res) { const page parseInt(req.query.page) || 1; const size parseInt(req.query.size) || 10; const keyword req.query.keyword || ; const where keyword ? WHERE p.name LIKE ? OR p.barcode LIKE ? : WHERE 11; const params keyword ? [%${keyword}%, %${keyword}%] : []; const [list] await pool.query( SELECT p.id, p.barcode, p.name, p.price, p.stock, p.status, c.name AS category_name FROM product p LEFT JOIN category c ON p.category_id c.id ${where} ORDER BY p.id DESC LIMIT ?, ?, [...params, (page - 1) * size, size] ); const [[{ total }]] await pool.query( SELECT COUNT(*) AS total FROM product p ${where}, params ); res.json({ code: 0, data: { page, size, total, list } }); });分页参数page与size都必须显式转成整数避免前端传入字符串导致排序异常。LIMIT的第一个参数是偏移量(page - 1) * size第二个参数是每页行数。关键在count查询依然带上同一个where条件不然总条数与列表结果对不上。新增商品时管理员身份需要放开权限// 简单角色校验跟在auth中间件之后使用 function requireAdmin(req, res, next) { if (req.session.role ! 1) { return res.status(403).json({ code: 403, msg: 无权限操作 }); } next(); } router.post(/save, auth, requireAdmin, async (req, res) { const { barcode, name, categoryId, price, costPrice, stock } req.body; try { await pool.query( INSERT INTO product (barcode, name, category_id, price, cost_price, stock) VALUES (?, ?, ?, ?, ?, ?), [barcode, name, categoryId, price, costPrice, stock] ); res.json({ code: 0, msg: 商品保存成功 }); } catch (err) { if (err.code ER_DUP_ENTRY) { return res.json({ code: 500, msg: 该条码已存在 }); } res.json({ code: 500, msg: 保存失败 err.message }); } });插入时依赖数据库的uk_barcode唯一索引来兜底重复条码代码里通过ER_DUP_ENTRY错误码捕获重复场景并给出明确提示。这里把requireAdmin放在auth之后先确认登录、再查角色权限顺序不能反。3.4 下单接口中用事务和行锁防超卖收银下单在答辩中很可能被现场演示“两个人同时买最后一件商品”的场景。单接口不做并发控制会出现库存扣成负数但订单还在生成的情况。使用SELECT ... FOR UPDATE行锁加事务可以稳妥解决// routes/order.js const express require(express); const auth require(../middleware/auth); const { pool } require(../config/db); const router express.Router(); router.post(/create, auth, async (req, res) { const { items, payType, paidAmount } req.body; const cashierId req.session.uid; const conn await pool.getConnection(); try { await conn.beginTransaction(); let totalAmount 0; for (const item of items) { const [rows] await conn.query( SELECT id, name, price, stock FROM product WHERE id ? FOR UPDATE, [item.productId] ); if (rows.length 0) throw new Error(商品不存在 item.productId); if (rows[0].stock item.qty) throw new Error(库存不足 rows[0].name); totalAmount Number(rows[0].price) * item.qty; await conn.query( UPDATE product SET stock stock - ? WHERE id ?, [item.qty, item.productId] ); } const orderNo SO Date.now().toString(36) Math.random().toString(36).slice(2, 6); const [result] await conn.query( INSERT INTO sale_order (order_no, total_amount, paid_amount, pay_type, cashier_id) VALUES (?, ?, ?, ?, ?), [orderNo, totalAmount, paidAmount, payType, cashierId] ); const orderId result.insertId; for (const item of items) { const [prod] await conn.query( SELECT name, price FROM product WHERE id ?, [item.productId]); await conn.query( INSERT INTO sale_order_item (order_id, product_id, product_name, price, qty, subtotal) VALUES (?, ?, ?, ?, ?, ?), [orderId, item.productId, prod[0].name, prod[0].price, item.qty, Number(prod[0].price) * item.qty] ); } await conn.commit(); res.json({ code: 0, orderNo }); } catch (err) { await conn.rollback(); res.json({ code: 500, msg: 下单失败 err.message }); } finally { conn.release(); } }); module.exports router;FOR UPDATE把查询得到的商品行在事务内锁住其他事务要修改这一行必须等待当前事务结束这样两个并发的收银请求不会同时扣减同一件商品。整个下单逻辑被beginTransaction包住循环中任一商品库存不足都会抛错随后执行rollback数据库不会留下只有订单头、没有明细的脏数据。finally里的conn.release()保证连接无论成功还是失败都会归还连接池避免连接池被耗尽。4. 前端页面实现与Node.js接口联调4.1 用Vue3Vite搭建前端项目前端项目与后端平级依赖使用Vite创建Vue3项目npm create vitelatest supermarket-web -- --template vue cd supermarket-web npm install npm install axios element-plus element-plus/icons-vue npm run dev浏览器访问http://localhost:5173看到Vite默认页即搭建成功。Vue3组件化写法便于在论文里展示“页面-接口-数据表”的对应关系比直接在HTML里写原生AJAX更清晰也符合毕业论文中“前后端分离架构”的表述。4.2 axios请求封装与401跳转前端所有请求都经过统一封装方便在请求头注入token// src/utils/http.js import axios from axios; const http axios.create({ baseURL: http://localhost:3000/api, timeout: 10000 }); http.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; }); http.interceptors.response.use( res { if (res.data.code 401) { localStorage.removeItem(token); window.location.href /login; } return res.data; }, err { return Promise.reject(err); } ); export default http;请求拦截器在每次发起请求前从localStorage取token拼成Bearer开头的认证头。响应拦截器统一检查code 401一旦token失效就清理本地存储并跳回登录页。这样页面里的业务代码不需要反复处理过期场景。4.3 收银台页面与购物车交互收银台是功能的核心用Vue的ref与computed管理商品列表和购物车script setup import { ref, computed } from vue; import http from ../utils/http; const keyword ref(); const tableData ref([]); const cart ref([]); const payType ref(1); const totalAmount computed(() cart.value.reduce((sum, item) sum item.price * item.qty, 0) ); async function searchProduct() { const res await http.get(/product/list, { params: { keyword: keyword.value, page: 1, size: 20 } }); if (res.code 0) { tableData.value res.data.list; } } function addToCart(row) { const found cart.value.find(item item.id row.id); if (found) { found.qty; } else { cart.value.push({ ...row, qty: 1 }); } } async function submitOrder() { const res await http.post(/order/create, { items: cart.value.map(item ({ productId: item.id, qty: item.qty })), payType: payType.value, paidAmount: totalAmount.value }); if (res.code 0) { cart.value []; alert(下单成功订单号 res.orderNo); } else { alert(res.msg); } } /script template div input v-modelkeyword placeholder输入商品名或条码 keyup.entersearchProduct / table tr v-forrow in tableData :keyrow.id dblclickaddToCart(row) td{{ row.barcode }}/td td{{ row.name }}/td td¥{{ row.price }}/td td库存 {{ row.stock }}/td /tr /table div div v-for(item, index) in cart :keyitem.id span{{ item.name }}/span button clickcart[index].qty---/button span{{ item.qty }}/span button clickcart[index].qty/button span¥{{ item.qty * item.price }}/span /div div应收¥{{ totalAmount }}/div button clicksubmitOrder结算/button /div /div /template加购时用...row扩展运算符把商品行全部属性拷贝进购物车对象后端下单时只需要productId和qty所以前端在提交前做一次map转换。totalAmount不维护中间变量使用computed推导任何增减数量都会自动同步页面上“应收合计”与购物车内容始终一致。4.4 跨域配置与联调中的常见问题前端5173端口与后端3000端口不同源浏览器会拦截跨域响应。解决方式是在后端入口统一加载cors中间件app.use(cors({ origin: http://localhost:5173, methods: [GET, POST, PUT, DELETE], allowedHeaders: [Content-Type, Authorization] }));allowedHeaders显式声明允许的自定义请求头Authorization头必须包含在内否则携带token的请求会被浏览器在预检阶段拦截。联调时如果某个接口提示网络错误先看浏览器Network面板404说明路径或前缀不一致403说明角色权限不够CORS报错说明需要检查后端origin配置。现象可能原因处理方法请求挂Access-Control-Allow-Origin后端未加cors中间件检查cors配置与前端origin是否匹配404 Not Found接口路径不一致核对前后端/api前缀与文件名401 Unauthorizedtoken缺失或过期重新登录检查请求头是否携带Bearer500 库存不足并发或真实库存不够检查订单事务里的行锁是否生效5. 功能测试用例、部署与毕业论文写作的对应关系5.1 从演示脚本里整理功能测试用例测试用例在论文里不是凑字数而是要让老师能顺着表看到“系统是真正跑过的”。建议从演示场景反推用例执行后在表中完整记录操作步骤、预期结果与实际结果用例编号功能模块操作步骤预期结果T01登录认证输入错误密码提示“用户名或密码错误”不返回tokenT02商品管理新增重复条码商品提示“该条码已存在”数据库无重复数据T03库存管理将库存调整为低于safety_stock仪表盘出现库存预警提示T04收银结算购物车中放入2件库存为1的商品点击结算提示库存不足订单未写入T05收银结算正常结算3件商品选支付宝支付生成订单号库存同步扣减3件明细表有3行每张功能截图旁边标注对应的用例编号论文实现章节的工作量会直观很多。答辩现场一般没有时间完整操作老师最常关注T04这类边界用例的演示效果。5.2 答辩环境的部署准备后端启动前确认MySQL服务运行然后执行npm start如果使用自定义脚本可以在package.json中配置start: node app.js。答辩前不要只在开发服务器上验证建议停掉IDE直接用命令启动一遍确认没有依赖本地绝对路径。数据库里的商品数据要提前替换成演示当天会用到的常见商品条码用真实可扫描的价格取整数方便现场口算验证。5.3 写论文时每章对应哪些内容论文正文结构固定为需求分析、系统设计、系统实现、系统测试四大部分。需求分析对应本文第2章的功能模块表与角色划分画出用例图系统设计对应数据表、E-R图以及后端接口列表系统实现部分每个模块放1到2张操作截图和适量关键代码代码不需要整段放进论文挑登录校验、事务下单、库存扣减这三段即可系统测试部分直接使用5.1的用例表。最终提交时Word的.docx与工程代码分目录保存论文里的截图要重命名为用例编号方便通篇对照。提示答辩前把页面上的提示文案、订单号格式和数据库编码统一成一套现场演示时不要出现前半段中文提示、后半段英文报错的现象这种细节往往比功能本身更影响评分。本文还有配套的精品资源点击获取
返回列表