本文还有配套的精品资源,点击获取
简介:直接可用的超市订单管理后台系统,前端用Vue 2.x搭配Element-UI实现响应式界面,集成Axios统一请求;后端基于Node.js和Express构建,划分清晰的routes、services、dao、models等标准模块,支持用户登录、商品增删改查、订单状态流转、库存实时查询、多角色权限控制(管理员/员工);数据库采用MongoDB,提供初始化数据文件和schema说明;项目结构明确分为client(前台)、server(主服务)、vue-api-server(可选独立API服务)三个核心目录,所有接口已完成前后端联调;配套详细README.md,涵盖本地启动步骤(Windows/macOS/Linux均适配)、环境依赖安装(npm一键)、端口配置、MongoDB连接设置及常见问题排查;无需改造即可运行,适合课程设计、毕设开发或全栈入门练习,WebStorm友好,package.统一管理依赖。
1. 项目概述:这不是一个“玩具系统”,而是一套能直接进超市仓库跑起来的订单后台
你有没有在课程设计里写过“用户登录”“商品列表”“订单提交”这种功能?写完之后发现——界面是静态的,数据是mock的,权限是写死的,连库存扣减都靠console.log模拟?我带过十几届计算机专业学生做毕设,八成卡在“前后端联调不通”“MongoDB连不上”“Element-UI表单校验不生效”这三道坎上。这套超市订单后台,就是我去年帮本地三家社区生鲜店落地真实业务时,从零搭起的最小可行系统(MVP),后来抽离出通用逻辑,打磨成你现在看到的版本。它不是教学Demo,而是我在凌晨两点帮店主查漏单、改库存、导出日报时真正在用的工具。
核心关键词——Vue2、Node.js、MongoDB、超市订单、权限管理——每一个都不是摆设。Vue2选型不是因为“老”,而是因为大量中小超市IT预算有限,运维人员只会用Chrome打开网页,不装插件、不配环境;Element-UI不是图省事,它的表格排序、分页、表单验证、弹窗提示,和超市员工“点两下就能改价格”的操作习惯完全匹配;MongoDB不用MySQL,是因为商品属性千差万别——冻品要填保质期天数,粮油要填规格单位,生鲜要填批次号,关系型数据库硬建字段会把schema搞成一团乱麻;权限管理不是只做个菜单隐藏,而是从接口层就拦截:普通员工点“删除商品”按钮,前端连请求都不会发出去;管理员导出全部订单,后端会自动过滤掉已作废单据,且生成的Excel里敏感字段(如手机号)默认脱敏。
它适合谁?如果你是大三学生,正为《Web开发实训》课焦头烂额,这套代码能让你三天内交出可演示、可截图、可答辩的完整系统;如果你是自学全栈的新手,它比官方文档更实在——router.js里每个路由为什么加meta字段,services/orderService.js里状态机怎么防重复提交,dao/inventoryDao.js里库存扣减为什么必须用findAndModify而不是先查再改,全都写在注释里;如果你是小团队技术负责人,想快速给门店配个轻量后台,它的一键部署脚本(deploy.sh / deploy.bat)能帮你3分钟内把服务跑在阿里云ECS或本地树莓派上。没有“理论上可行”,只有“我昨天刚在客户现场重启过服务”。
2. 整体架构设计与分层逻辑:为什么是这三个目录,而不是一个大杂烩?
2.1 三端分离的真实意图:client、server、vue-api-server不是为了炫技
看到目录结构里有client、server、vue-api-server三个并列主目录,很多人第一反应是:“是不是微服务?”不是。这是针对超市场景做的务实分层,每一层解决一个具体痛点:
client(Vue前端):专注交互体验。所有页面路由、组件状态、Element-UI样式、Axios拦截器(统一处理token过期跳转登录页)都在这里。它不碰任何业务逻辑,比如“下单时检查库存是否充足”这个判断,前端只负责把商品ID和数量传给后端,绝不自己查一遍MongoDB再决定能不能点“提交”。为什么?因为网络延迟、缓存失效、并发修改会让前端校验变成幻觉——你看到库存还有5件,但实际已被其他收银员秒光。这个原则贯穿始终:前端只负责“呈现”和“触发”,绝不承担“决策”责任。server(Express主服务):承载核心业务流。用户认证、商品CRUD、订单创建与状态变更(待付款→已发货→已完成)、库存增减、报表统计,全在这里实现。它采用标准的分层结构:routes/定义HTTP入口(如POST /api/orders),services/封装业务规则(如orderService.createOrder()里包含库存预占、积分计算、短信通知触发),dao/专注数据访问(inventoryDao.decreaseStock()确保原子性),models/定义MongoDB Schema(注意:ProductSchema里stock字段类型是Number而非String,避免后续聚合查询报错)。这种分层不是教条,而是为了应对超市老板随时提出的“加个功能”:上周要加“临期商品预警”,我只改了services/inventoryService.js里的一个方法,其他层完全不动。vue-api-server(独立API服务模块):解决历史遗留问题。很多老店已有旧系统(比如用PHP写的进销存),但新需求(如微信小程序下单)需要API。这个模块就是专门剥离出来的纯接口层,它复用server里的services和dao,但路由前缀是/api/v2/,响应格式强制JSON,且自带CORS白名单配置(只允许https://shop.xxx.com和https://mini.xxx.com调用)。它不渲染页面,不处理session,就是一个干净的RESTful网关。你完全可以把它部署在另一台服务器上,和主后台物理隔离——这才是生产环境该有的弹性。
提示:三个目录共用同一套
config.js,但通过NODE_ENV环境变量区分配置。开发时client调用http://localhost:3000/api/,上线后client的vue.config.js里devServer.proxy会指向Nginx反向代理,而vue-api-server则直接监听8081端口。这种设计让前端工程师改接口地址时,不用动后端一行代码。
2.2 权限管理不是“菜单开关”,而是四层过滤网
超市权限管理最常犯的错,是只做前端路由守卫(router.beforeEach)和菜单显示控制。这套系统用了四层防护,缺一不可:
前端路由守卫层:
router.js中每个路由配置meta: { roles: ['admin'] },beforeEach钩子检查store.state.user.roles,无权限直接next('/403')。但这只是第一道门,防君子不防小人——懂F12的人删掉v-if="hasRole('admin')"就能看到按钮。前端按钮级控制层:所有敏感操作按钮(如“删除商品”“导出全部订单”)都包裹
<el-button v-if="hasPermission('product:delete')">。hasPermission方法读取store.state.user.permissions数组,这个数组来自登录后后端返回的权限码列表(如['user:list','product:add','order:export'])。按钮不显示,用户连点击机会都没有。后端接口鉴权层:
server/middleware/auth.js是全局中间件,解析Header里的Authorizationtoken,验证签名并提取用户ID和角色。关键来了——它不只验证token有效性,还把用户权限列表挂载到req.user.permissions上,供后续路由使用。后端业务逻辑层:这才是真正的铁闸。以
DELETE /api/products/:id为例,routes/product.js里:javascript router.delete('/:id', auth, (req, res) => { // 第四层:即使token有效,也检查权限码 if (!req.user.permissions.includes('product:delete')) { return res.status(403).json({ code: 403, msg: '无权限删除商品' }) } productService.deleteProduct(req.params.id, req.user.userId) .then(() => res.json({ code: 200 })) .catch(err => res.status(500).json({ code: 500, msg: err.message })) })
注意:productService.deleteProduct()方法内部还会校验操作者是否是该商品的创建者(防止员工误删他人录入的商品),这就是第五重校验——业务规则校验。权限管理不是开关,而是嵌套的洋葱模型。
2.3 MongoDB设计哲学:拒绝“一张表打天下”,拥抱文档天然灵活性
超市数据最大的特点是属性爆炸。大米要填“品牌、产地、规格(5kg/袋)、保质期(12个月)”,而活鱼要填“供应商、捕捞日期、暂养池编号、检疫报告编号”。如果强行用MySQL,要么建几十个空字段(浪费空间、查询慢),要么用EAV模型(Entity-Attribute-Value,复杂到维护不住)。MongoDB的文档结构完美匹配:
// models/Product.js const ProductSchema = new Schema({ name: { type: String, required: true }, // 商品名称(必填) category: { type: String, enum: ['fresh', 'dry', 'frozen', 'daily'], required: true }, // 分类(枚举约束) sku: { type: String, unique: true }, // 商品编码(唯一索引) price: { type: Number, min: 0 }, // 售价(数值+范围校验) stock: { type: Number, default: 0 }, // 库存(默认0,避免null) // 动态属性:不同分类有不同字段 attributes: { // 生鲜类特有 fresh: { supplier: String, harvestDate: Date, shelfLifeDays: Number }, // 冷冻类特有 frozen: { coldChainTemp: String, // “-18℃” thawingMethod: String // “自然解冻” } } }, { timestamps: true }) // 自动添加createdAt/updatedAt实操心得:attributes字段用嵌套对象而非数组,是为了利用MongoDB的点号查询(db.products.find({"attributes.fresh.supplier": "XX农场"}))。上线后我们新增“进口商品”分类,只需在attributes里加个imported:{country:String,certNo:String},无需改表结构、不停服、不迁移数据。这种灵活性,是关系型数据库花一周写迁移脚本都换不来的。
3. 核心模块实现详解:从登录到库存扣减,每一步都踩过坑
3.1 用户登录与Token续期:为什么不用Session,而选JWT?
超市场景下,Session有两大硬伤:一是多门店部署时,Session存储需共享(Redis集群成本高),二是移动端(如送货员APP)无法像浏览器一样自动携带Cookie。我们选JWT(JSON Web Token),但做了关键改造:
- Token不存localStorage:防止XSS窃取。前端登录成功后,将JWT存入
httpOnly的Cookie(res.cookie('token', jwt, { httpOnly: true, secure: true, sameSite: 'strict' })),前端Axios通过withCredentials: true自动携带。 - 双Token机制防重放:登录返回
accessToken(2小时过期)和refreshToken(7天过期)。当accessToken快过期时,前端调用/api/auth/refresh,后端用refreshToken签发新accessToken,同时在Redis里记录该refreshToken的usedAt时间戳,防止被多次使用。 - 权限实时同步:管理员在后台修改员工权限后,旧Token不会立即失效。我们在
auth.js中间件里加了一层检查:if (req.user.lastPermissionUpdate > jwtPayload.iat) { throw new Error('权限已更新,请重新登录') }。lastPermissionUpdate是用户文档里的时间戳,每次权限变更就更新它。
注意:
secure: true在开发环境会失败(localhost非HTTPS),所以config.js里做了环境判断:secure: process.env.NODE_ENV === 'production'。这个细节不写清楚,新手在本地调试时会卡在“登录成功但后续请求401”。
3.2 商品管理:Element-UI表格的深度定制技巧
client/src/views/product/List.vue里的商品列表,不是简单<el-table :data="list">就完事。超市老板提的需求很具体:“我要按库存从低到高排,但‘缺货’(库存=0)的得排最前面”“点击商品名能直接编辑,不用先点‘编辑’按钮”。我们这样实现:
自定义排序:
<el-table>的default-sort属性只支持基础排序。我们用sort-method:html <el-table-column prop="stock" label="库存" sortable :sort-method="sortStock">javascript sortStock(a, b) { // 缺货商品(stock===0)永远排最前 if (a.stock === 0 && b.stock !== 0) return -1 if (a.stock !== 0 && b.stock === 0) return 1 return a.stock - b.stock // 其余按数字升序 }行内编辑:
<el-table-column>里放<el-input>,但绑定v-model会导致所有行共享一个值。正确做法是用scope.row绑定:html <el-table-column label="操作"> <template #default="scope"> <el-input v-if="scope.row.editing" v-model="scope.row.price" @blur="savePrice(scope.row)" /> <span v-else @click="scope.row.editing = true">{{ scope.row.price }}</span> </template> </el-table-column>
这样每行独立控制编辑状态,且@blur失焦即保存,符合收银员“点一下、改一下、走人”的操作节奏。
3.3 订单创建与库存扣减:分布式事务的朴素解法
订单创建看似简单:POST /api/orders→ 创建订单文档 → 扣减对应商品库存。但在高并发下,两个收银员同时下单同一商品,可能都查到“库存10”,然后都扣减成9,实际应为8。MySQL用SELECT ... FOR UPDATE加行锁,但MongoDB没原生行锁。我们的方案是:
原子操作:
dao/inventoryDao.js里用findOneAndUpdate:javascript const result = await Inventory.findOneAndUpdate( { _id: productId, stock: { $gte: quantity } }, // 条件:库存>=需求数量 { $inc: { stock: -quantity } }, // 原子扣减 { returnDocument: 'after' } // 返回更新后的文档 ) if (!result) { throw new Error(`库存不足,当前库存:${result?.stock || 0}`) }
关键在查询条件{ stock: { $gte: quantity } }——MongoDB保证这个查询和更新是原子的。如果扣减失败(result为null),说明库存不够,前端收到400 Bad Request,提示“库存不足,请刷新后重试”。补偿机制:万一扣减成功但订单创建失败(如网络中断),库存就“丢了”。我们在
services/orderService.js里加了异步补偿:javascript // 创建订单前,先记录“预占库存”日志 await InventoryLog.create({ productId, quantity, orderId: 'PRE_' + Date.now(), status: 'pending' }) // 订单创建成功后,更新日志为'success' // 如果订单创建失败,定时任务每5分钟扫描status='pending'且超过10分钟的日志,自动恢复库存
3.4 库存实时查询:为什么用WebSocket而不是轮询?
超市老板要看“当前各货架剩余多少”,要求秒级更新。传统setInterval(() => axios.get('/api/inventory'), 5000)有三大问题:一是5秒延迟,高峰期漏单;二是无效请求多(库存不变时也频繁拉);三是连接数爆炸(100个员工同时开着页面,每秒20个请求压垮后端)。我们用socket.io实现服务端主动推送:
server启动时初始化io实例,并监听库存变更事件:
```javascript
// server/index.js
const io = require(‘socket.io’)(server)
io.on(‘connection’, socket => {
console.log(‘客户端连接:’, socket.id)
// 加入’inventory’房间,接收库存更新
socket.join(‘inventory’)
})
// 在inventoryDao.js扣减库存后
io.to(‘inventory’).emit(‘inventory:update’, { productId, newStock })
```
- 前端
client/src/utils/socket.js建立连接,并在InventoryList.vue里监听:javascript mounted() { this.socket = io() this.socket.on('inventory:update', data => { // 找到对应商品,更新stock字段 const item = this.list.find(i => i._id === data.productId) if (item) item.stock = data.newStock }) }
实测效果:从收银员扫码扣减,到仓库管理员平板上看到库存变化,延迟稳定在300ms内。而且连接数恒定(每个浏览器一个WebSocket),后端压力反而比轮询小。
4. 一键部署与本地调试:Windows/macOS/Linux三端实测指南
4.1 环境准备:三句话说清依赖安装
- Node.js:必须
v14.21.3或v16.20.2(LTS版本)。不要用v18+,vue-cli-service对新版V8引擎有兼容问题。Windows用户去nodejs.org下载.msi安装包,勾选“Add to PATH”;macOS用brew install node@16 && brew link --force node@16;Linux用curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - && sudo apt-get install -y nodejs。 - MongoDB:推荐
v6.0.15。Windows直接运行mongodb-win32-x86_64-2012plus-6.0.15-signed.msi;macOS用brew tap mongodb/brew && brew install mongodb-community@6.0;Linux用wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - && echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list && sudo apt-get update && sudo apt-get install -y mongodb-org。 - Git:所有平台都需
git clone代码。Windows用Git Bash,macOS/Linux用终端。确认git --version输出≥2.30。
注意:
npm install前,务必删除项目根目录下所有package-lock.json(你看到的多个是Git冲突残留)。只保留client/package.json、server/package.json、vue-api-server/package.json各自目录下的package-lock.json。否则npm ci会因锁文件冲突失败。
4.2 三步启动:从零到首页
步骤1:启动MongoDB
- Windows:打开命令提示符,进入
C:\Program Files\MongoDB\Server\6.0\bin,运行mongod.exe --dbpath "D:\mongodb\data" --logpath "D:\mongodb\log\mongod.log" --bind_ip 127.0.0.1 --port 27017(D:\mongodb\data需提前创建)。 - macOS/Linux:终端执行
sudo mongod --dbpath /usr/local/var/mongodb --logpath /usr/local/var/log/mongodb/mongod.log --bind_ip 127.0.0.1 --port 27017。 - 验证:另开终端,运行
mongo,输入show dbs,能看到admin库即成功。
步骤2:初始化数据库
进入server目录,运行:
npm install npm run init-dbinit-db脚本会执行scripts/initData.js,自动创建admin用户(账号admin,密码123456)、导入10个测试商品、5个员工账号。注意:init-db只运行一次,重复运行会报“用户名已存在”错误。
步骤3:并行启动三端服务
新开三个终端窗口(或标签页):
- 终端1(前端):进入client目录,运行npm install && npm run serve。默认访问http://localhost:8080。
- 终端2(主服务):进入server目录,运行npm install && npm start。监听http://localhost:3000。
- 终端3(API服务):进入vue-api-server目录,运行npm install && npm start。监听http://localhost:8081。
实操心得:Windows用户若遇
npm run serve报错“Error: spawn cmd ENOENT”,是WebStorm终端配置问题。右键项目→Open in Terminal→在弹出的CMD窗口里手动执行npm run serve。macOS用户若mongod启动报“permission denied”,执行sudo chown -R $USER /usr/local/var/mongodb修复权限。
4.3 端口冲突与常见报错速查表
| 报错现象 | 根本原因 | 解决方案 |
|---|---|---|
ERROR in ./src/main.js Module build failed: Error: Cannot find module 'vue-template-compiler' | client目录下node_modules未正确安装,或vue与vue-template-compiler版本不匹配 | 删除client/node_modules,执行npm install;检查package.json中"vue": "^2.6.14"和"vue-template-compiler": "^2.6.14"版本号是否一致 |
MongoNetworkError: failed to connect to server [localhost:27017] on first connect | MongoDB服务未启动,或端口被占用 | 运行netstat -ano \| findstr :27017(Windows)或lsof -i :27017(macOS/Linux)查占用进程,taskkill /PID 进程号 /F(Windows)或kill -9 进程号(macOS/Linux)结束它,再启动mongod |
TypeError: Cannot read property 'permissions' of undefined | 前端登录后未正确设置Vuex状态,或后端/api/auth/login返回的token格式错误 | 检查client/src/store/modules/user.js中loginaction,确认commit('SET_USER', res.data)的res.data结构包含permissions字段;查看server/routes/auth.js中login路由,确认res.json({ code: 200, data: { token, user: { permissions: [...] } } }) |
WebSocket connection to 'ws://localhost:8080/socket.io/' failed | 前端WebSocket连接地址错误,未指向server的socket.io服务 | 修改client/src/utils/socket.js中io('http://localhost:3000')(指向server端口,非前端8080) |
5. 实战经验与避坑指南:那些文档里不会写的细节
5.1 Element-UI的“坑”与填法
- 表单重置失效:
<el-form>的resetFields()方法,在动态表单(如根据商品分类显示不同属性字段)下经常不生效。原因是Vue的响应式系统没追踪到新字段。解决方案:给<el-form>加:key="formKey",重置时先this.formKey++,强制Vue重新渲染整个表单。 - 日期选择器时区错乱:
<el-date-picker>选“2023-10-01”,后端收到却是“2023-09-30T16:00:00.000Z”。这是因为Element-UI默认转UTC时间。在picker-options里加{ valueFormat: 'yyyy-MM-dd' },确保传字符串而非Date对象。 - 表格合计行错位:
<el-table>开启show-summary后,合计行数字右对齐,但表头文字左对齐,视觉割裂。在CSS里加:css .el-table__footer-wrapper .cell { text-align: right !important; }
5.2 MongoDB性能优化实战
- 慢查询杀手:
db.orders.find({ status: 'pending' }).sort({ createdAt: -1 }).limit(20)在10万订单时超时。解决方案:在status和createdAt上建复合索引:javascript db.orders.createIndex({ status: 1, createdAt: -1 })
注意顺序:等值查询字段(status)在前,范围查询字段(createdAt)在后。 - 内存溢出警告:
db.products.find().toArray()加载全部商品到内存,Node.js进程OOM。永远用cursor流式处理:javascript const cursor = Product.find({}).cursor() for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) { // 处理单个文档 }
5.3 权限管理的灰色地带处理
超市有个现实问题:新入职员工当天就要上岗,但管理员可能下班了。我们的妥协方案是:
- 临时通行卡:在
server/config.js里加TEMP_ACCESS_CODE: '2023Q4',前端登录页增加“临时登录”入口,输入此密码可获得24小时employee权限。密码不入库,只在配置里硬编码,每次发布新版本就换。 - 操作留痕:所有敏感操作(删除商品、修改价格、导出订单)都在
server/middleware/audit.js里记录日志:javascript // 记录到MongoDB的auditLogs集合 AuditLog.create({ userId: req.user.userId, username: req.user.username, action: 'product:delete', target: `productId=${req.params.id}`, ip: req.ip, userAgent: req.get('User-Agent') })
老板查账时,直接看auditLogs集合,就知道谁、什么时候、干了什么。
5.4 从“能跑”到“好用”的最后一公里
- 打印适配:超市要打印小票,
window.print()默认带页眉页脚。在client/public/print.css里加:
```css
@media print {
@page { margin: 0; }
body { margin: 1.6cm; }- { -webkit-print-color-adjust: exact; }
}
```
- { -webkit-print-color-adjust: exact; }
- 离线可用:收银机偶尔断网。
client/vue.config.js里加PWA插件:javascript configureWebpack: { plugins: [ new WorkboxPlugin.GenerateSW({ clientsClaim: true, skipWaiting: true, runtimeCaching: [ { urlPattern: /^https:\/\/.*\.js$/, handler: 'StaleWhileRevalidate' }, { urlPattern: /^https:\/\/.*\.css$/, handler: 'StaleWhileRevalidate' } ] }) ] }
首次访问后,即使断网,也能打开商品列表、查看库存(缓存了静态资源和部分API响应)。
最后分享一个小技巧:当你在client/src/views/order/Create.vue里调试下单流程时,别只盯着控制台。打开Chrome开发者工具的Application标签页,点开Clear storage → Clear site data,然后勾选“Cache storage”和“Service workers”,点击“Clear”——这能彻底清除PWA缓存,避免旧JS文件导致“改了代码却没生效”的诡异问题。这个动作,我每周至少做三次。
本文还有配套的精品资源,点击获取
简介:直接可用的超市订单管理后台系统,前端用Vue 2.x搭配Element-UI实现响应式界面,集成Axios统一请求;后端基于Node.js和Express构建,划分清晰的routes、services、dao、models等标准模块,支持用户登录、商品增删改查、订单状态流转、库存实时查询、多角色权限控制(管理员/员工);数据库采用MongoDB,提供初始化数据文件和schema说明;项目结构明确分为client(前台)、server(主服务)、vue-api-server(可选独立API服务)三个核心目录,所有接口已完成前后端联调;配套详细README.md,涵盖本地启动步骤(Windows/macOS/Linux均适配)、环境依赖安装(npm一键)、端口配置、MongoDB连接设置及常见问题排查;无需改造即可运行,适合课程设计、毕设开发或全栈入门练习,WebStorm友好,package.统一管理依赖。
本文还有配套的精品资源,点击获取