1. 从“无人”到“智慧”:这个项目到底在模拟什么业务场景
先说个现象。每年到毕设季,Java Web方向的题目里,十个人有七个都在做SpringBoot+Vue的管理系统——图书管理、宿舍管理、班级管理、仓库管理,换汤不换药。但“无人智慧超市”这个题目,它跟那些纯CRUD的管理系统有本质区别:它本质上是在模拟一套完整的人货场闭环,从顾客进店、选品、自助结算到库存联动、运营报表,是一条完整的业务链。你如果只是把“增删改查”换了个壳,答辩时一眼就会被看穿。
这个项目核心要解决的业务问题有三层:
- 第一层是自助化。传统超市靠收银员结算,无人超市要顾客自己扫码、自己付款,所以订单流程里必须有“购物车-结算-支付成功-离店校验”这一串状态流转,而不是简简单单插入一条订单记录。
- 第二层是智能化。库存不能靠人工盘点,每次成交后商品库存要自动扣减,低于阈值要有预警,甚至能模拟自动补货的逻辑。
- 第三层是可视化管理。运营人员需要一个后台,能看到商品销量排行、会员消费习惯、每日营收曲线,这些数据得从订单表里聚合出来。
所以你在做这个项目的时候,脑子里要装的不是“我要写几个接口”,而是“我把一个超市的日常运转搬到了线上,系统里的每一张表、每一个状态字段,都对应着线下的一个真实动作”。
2. 系统技术栈与模块划分:为什么这样拆才能过答辩
2.1 选型理由:SpringBoot+Vue为什么是毕设黄金组合
这个题目用的是SpringBoot+Vue,可以说是目前Java Web方向最稳妥、也最容易被答辩老师认可的组合。SpringBoot负责后端接口,内置Tomcat,不需要额外部署容器,配置起来比传统SSM要省掉大量XML;Vue负责前端页面,单向数据流加组件化开发,界面逻辑清晰,遇到需要展示图表、数据看板的场景特别好用。
但你要知道,选型不能只会说“因为它火”。答辩时老师很可能问“为什么不用JSP+Servlet”“为什么不用SSM”,你得能讲出对比,最关键是能用这套组合把前后端分离的角色边界讲清楚:后端只管业务逻辑和数据安全,前端只管渲染和交互,双方通过JSON格式的RESTful接口通信。这个“前后端分离”四个字,本身就是这个项目的核心亮点。
2.2 模块怎么拆:按业务流程拆,不按数据表拆
我见过很多同学做项目喜欢按“用户模块、商品模块、订单模块”这种数据表维度去拆,东西虽然没错,但答辩讲出来特别干瘪。我建议按照“角色+业务场景”去拆,效果完全不一样。
这个系统拆成五个模块比较合适:
- 会员端(前端Vue):顾客扫码进店、浏览商品、添加购物车、扫码结算、查看自己历史订单。
- 管理端(前端Vue):运营人员登录后台,管理商品上下架、编辑分类、查看订单流、处理退款、看数据报表。
- 后端用户与权限模块:JWT签发、登录鉴权、不同角色的路由访问控制。
- 后端交易模块:购物车、下单、库存锁定、支付回调模拟、订单状态流转。
- 后端数据统计模块:按天聚合营收、商品销量TOP榜、会员消费频次统计。
这样一拆,无论是画架构图还是在文档里写模块说明,逻辑路径都是从“谁在用”出发,而不是从“存什么”出发,答辩听感完全不一样。
2.3 两种角色的权限边界必须提前划清楚
无人超市涉及到两种截然不同的用户:消费者(直接进店的人)和运营者(超市的管理员)。这两种人对系统的要求冲突很大——消费者要的是快,扫码、付款、走人;运营者要的是全,每个商品卖没卖、库存够不够、什么时候补货。
所以从一开始,用户表就必须设计role字段区分身份,后端接口必须做角色级别的访问控制,而不是登录了就能访问一切。比如商品列表所有人能看,但商品入库、下架、修改价格这类操作只能管理员做,会员余额查询只能本人和管理员看。前端路由上要做meta.roles约束,后端接口要做注解权限校验,两边同时卡,才不会有越权漏洞。
3. 数据库设计:SQL脚本里最有价值的不只是建表语句
3.1 核心表结构:不要漏掉一张“业务状态”表
拿到这个项目的SQL脚本,很多同学会直接导入数据库就开始跑,其实脚本里最值得琢磨的是表设计,因为表结构直接反映了你对业务的理解深度。一个无人超市系统,核心表至少包含这些:
| 表名 | 对应业务 | 关键字段 |
|---|---|---|
| member | 会员/顾客 | id, phone, password, nickname, balance, points |
| category | 商品分类 | id, name, sort_order |
| product | 商品 | id, category_id, name, subtitle, price(用decimal), stock, sales, status |
| cart | 购物车 | id, member_id, product_id, quantity |
| orders | 订单主表 | id, order_no, member_id, total_amount, status, pay_time |
| order_item | 订单明细 | id, order_id, product_id, quantity, price |
| stock_record | 库存流水 | id, product_id, change_type, change_quantity, remark |
| device | 设备/闸机 | id, name, status, location |
| entry_log | 进出记录 | id, member_id, type(进/出), timestamp |
这里我要重点强调订单状态字段。普通管理系统订单表通常只有一个status,但无人超市的订单状态会复杂很多,建议至少包含6个状态:
0待支付:顾客加购后生成未付款订单,超时自动取消1已支付:支付成功后进入待出库2已完成:顾客通过离店校验,订单正式完结3已取消:超过支付时限或主动取消4退款中:运营人员介入处理异常5已退款:退款完成,库存回补
为什么单独提这个?因为答辩现场老师最爱追问的一句话就是“你这个订单从创建到结束,中间经历了哪些状态变化?”如果表里只有一个字段、一条记录写到头,说明你根本没理解业务流程。
3.2 金额字段为什么要用decimal不用double
这个是我在所有Java Web项目里都想强调的点,但尤其在这类“涉及交易”的项目里必须拿出来讲:所有涉及金额的字段,一律用decimal(10,2),禁止使用double或float。
原因在于浮点数的二进制精度问题。0.1在double里存的是0.1000000000000000055511151231257827,多个商品累加后误差会累积,最后算出来订单总额可能多一分钱或少一分钱。在超市场景下,哪怕误差只有一分钱,累计到一天几百上千单就是大问题,更不用说财务对账了。
Java后端对应也要用BigDecimal运算,而不是直接加减乘除double。你在写SQL脚本的时候顺手把金额字段精度定好,后续能少改一堆代码。
3.3 初始化数据:脚本里必须带上能出效果的数据
这个项目自带的SQL脚本,我建议拿到之后先看一眼初始化数据量。如果只有几条测试数据,你最好自己补一批更真实的数据进去,原因很简单:老师演示系统的时候,关心的是效果。商品列表只显示三条记录和显示三十条记录,页面观感和系统档次完全不一样;营收报表要有半个月以上的数据,折线图才能画得出来,否则图表就是一根光秃秃的线,毫无说服力。
补充测试数据的几个要点:
- 商品至少15-20个,覆盖3-5个分类,价格要有梯度,从几块钱的零食到几十块钱的日用品
- 订单数据要有任意连续7-14天的时间分布,每天数据量不能一样,要自然波动
- 库存要有几个商品故意低于预警值,方便演示补货提醒
初始化数据建议写成一个独立的INSERT脚本,跟建表脚本分开,方便随时重跑。
4. 后端SpringBoot核心实现:权限模型与接口设计思路
4.1 JWT鉴权怎么做才能在答辩时讲得明白
登录方案我用的是JWT(JSON Web Token),这也是目前SpringBoot+Vue项目的主流姿势。它的核心原理可以用一句话概括:用户登录成功后,服务端不存session,而是签发一个携带用户身份信息的加密Token,前端后续每次请求都把它放在请求头里带上,后端验签通过就放行。
在项目里落地时,要注意几个点:
- 生成Token时把
userId和role放进去,后续接口直接从Token里取,不用每次查库 - 设置过期时间,比如管理端Token有效期8小时,会员端可以调到24小时
- 敏感操作(修改密码、退款)要重新校验身份,不能只看Token过了没过期
你需要在项目里写一个拦截器或过滤器,统一从请求头取Authorization: Bearer <token>,解析校验通过后把用户信息放入ThreadLocal或RequestContext,后续业务代码直接取。这里顺便说一句,ThreadLocal记得请求结束后要remove(),不然线程池复用会导致串号问题——这个细节如果被答辩老师追到,讲对了非常加分。
4.2 统一返回结果和全局异常处理:一个月后你会感谢自己
把所有接口的返回结构统一成Result<T>格式,是我对每个SpringBoot项目的执着。这个项目里我建议的返回结构是:
{ "code": 200, "message": "success", "data": { "list": [], "total": 100 } }状态码的约定要全项目统一,比如:
| 状态码 | 含义 | 场景 |
|---|---|---|
| 200 | 成功 | 正常返回 |
| 400 | 参数错误 | 字段校验失败 |
| 401 | 未登录或Token失效 | 需要鉴权的接口未带Token |
| 403 | 无权限 | 普通用户访问管理端接口 |
| 500 | 服务端异常 | 未捕获的业务异常 |
配套再写一个@RestControllerAdvice全局异常处理类,把业务异常和运行时异常统一拦截,返回固定格式的错误信息。这样做最大的好处是前端能统一处理错误弹窗,不用每个接口单独写一套异常逻辑。
4.3 商品结算与库存扣减:最容易被追问的事务细节
无人超市的自助结算,后端逻辑不能只是“插入一条订单”,而是要保证:
- 订单明细写入成功
- 订单金额计算正确
- 商品库存扣减成功
- 会员余额扣减或支付状态标记成功
这四个动作必须在同一个事务里,要么全部成功,要么全部失败。SpringBoot里用@Transactional注解就能搞定,但这还不够——演讲级的关键在于库存扣减用的是什么SQL。如果先select stock查到库存,再在代码里判断够不够,然后update stock,这叫读改写模式,在高并发下会出现超卖。更稳妥的做法是一条SQL完成条件扣减:
UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}判断受影响行数为1才允许继续下单,否则抛出库存不足异常。这就是现代电商系统里经典的“乐观锁+条件更新”思路,能把这个逻辑讲清楚,比单纯贴代码有力得多。
4.4 接口文档:RESTful设计和Swagger配置
接口文档是这类的标配交付物,但很多同学的接口设计一塌糊涂,路径乱起名字、参数含义不清。这里我分享一套相对规范的RESTful路径设计:
| 资源操作 | 路径示例 | 说明 |
|---|---|---|
| 商品列表 | GET /api/products?page=1&size=10 | 分页查询 |
| 商品详情 | GET /api/products/{id} | 按ID查询 |
| 新增商品 | POST /api/products | 管理员创建商品 |
| 修改商品 | PUT /api/products/{id} | 管理员编辑商品 |
| 删除商品 | DELETE /api/products/{id} | 管理员删除商品 |
| 下单结算 | POST /api/orders | 顾客提交订单 |
| 订单列表 | GET /api/orders/member/{memberId} | 查询某会员订单 |
动词用HTTP方法表达,名词用资源复数形式表达,参数尽量放在路径和查询串里,不要都堆在body里。接口文档我建议直接集成Swagger(SpringDoc),项目启动后访问/swagger-ui.html就能可视化调试接口。有些同学觉得手写Word接口文档更正式,但实测下来Swagger在本地演示和联调时真的好用太多了,答辩现场直接调用接口给老师看,比翻文档更有说服力。
5. 前端Vue的核心处理:页面结构、状态管理与接口对接
5.1 项目结构和依赖选择
Vue前端这边,我看过不少毕设代码,最常见的问题是“什么都能跑,但代码组织像一锅粥”。如果你想把这个项目做得清爽,建议一开始就按下面这个目录结构来:
src/ ├── api/ # 接口请求封装,一个模块一个文件 │ ├── request.js # axios实例封装,统一处理token和错误码 │ ├── product.js │ └── order.js ├── router/ # 路由配置,带角色权限守卫 ├── store/ # 状态管理(Vuex/Pinia),保存用户信息和登录态 ├── views/ # 页面组件 │ ├── member/ # 会员端页面 │ ├── admin/ # 管理端页面 │ └── login/ # 登录页 ├── components/ # 公共组件 └── utils/ # 工具函数依赖方面,Vue2项目对应Element UI,Vue3项目对应Element Plus,不建议混用;状态管理Vue2用Vuex、Vue3用Pinia即可;图标可以引入iconfont或unpkg上的图标库,图表推荐ECharts,商品销售趋势图就是用它画的。
5.2 axios拦截器必须做两件事,否则联调必吵架
后端接口都设计好了,前端对接时最核心的就是封装好request.js。我这个项目里,拦截器固定做两件事:
请求拦截:从localStorage里取Token,放到请求头Authorization字段里。这样所有请求自动带鉴权信息,不需要每个接口单独写。
响应拦截:根据后端返回的code,统一处理业务逻辑。code === 200直接返回data;code === 401清除本地登录态,跳转登录页重新登录;其他code统一弹错误提示。我见过某些项目每个页面里都写一遍if (res.code !== 200) { alert(res.message) },代码量翻倍还容易漏,用拦截器统一处理一下就干净了。
5.3 前端路由守卫:不让普通用户钻进管理后台
无人超市这个项目需要在前端路由层面就拦住非法访问。管理端所有路由的meta里标记roles: ['admin'],会员端路由标记roles: ['member'],然后在路由全局前置守卫里校验:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { const role = localStorage.getItem('role') if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403') } else { next() } } })注意,前端路由守卫只是体验优化,真正的安全防线在后端接口的角色权限校验。前端能挡住的话,一个普通用户连管理入口都看不到,演示效果会专业很多。
5.4 管理端报表页面:用数据可视化撑起“智慧”两个字
管理端的报表页是整个前端最容易出彩的地方,也是体现“智慧”的项目核心价值所在。我建议至少包含三个可视化卡片:
- 今日营收:统计当日已完成订单金额合计,卡片数字实时更新
- 商品销量TOP5:横向条形图,展示销量前五名商品
- 近7天订单趋势:折线图,展示每日订单数和营收额
图表用ECharts实现,数据接口由后端统计模块提供,比如GET /api/admin/stats/daily?days=7返回每日的订单数和销售额。这里有个小经验:报表的数据聚合用SQL的GROUP BY DATE(pay_time)就能搞定,但如果数据量大了,建议加时间索引,否则全表扫描会明显变慢。
6. 本地跑通项目的完整步骤:从零到能演示的checklist
6.1 环境准备清单
先把环境准备好,版本之间不要差太远,否则踩坑概率很大:
| 组件 | 建议版本 | 说明 |
|---|---|---|
| JDK | 1.8或11 | SpringBoot 2.x用JDK8很稳,3.x需要JDK17 |
| Maven | 3.6+ | 依赖管理必备 |
| MySQL | 5.7或8.0 | 8.0注意密码加密方式 |
| Node.js | 14+ | Vue前端构建工具链的基础 |
| IDE | IDEA | 前后端一个IDE全搞定 |
这里特别提醒MySQL8.0的坑:MySQL8默认的密码加密插件是caching_sha2_password,某些旧版本驱动会报Public Key Retrieval is not allowed,解决办法是在JDBC连接串上加上allowPublicKeyRetrieval=true&useSSL=false,否则后端一启动就报错。
6.2 初始化数据库的正确顺序
拿到SQL脚本后,不要一股脑整份全部执行。我的推荐顺序是先单独执行建库语句,再执行建表语句,最后执行初始化数据脚本。中途如果报错,也好定位。建库脚本通常包含类似:
CREATE DATABASE IF NOT EXISTS smart_supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE smart_supermarket;这里再强调一次字符集。商品名称、分类名称、会员昵称全都要支持中文,库和表的字符集必须用utf8mb4,而不是utf8。utf8在MySQL里实际上最多存3字节,某些特殊字符(比如emoji符号)存不进去,会报Incorrect string value错误。用utf8mb4则没有这个限制,而且完全兼容普通中文。
6.3 后端启动顺序与常见启动报错
后端启动前,检查一下application.yml里的数据库连接配置是否和本地环境匹配,重点是用户名、密码、数据库名。启动方式很常规,IDEA里直接运行启动类即可。
以下是几个我在这类项目里高频遇到的启动报错以及处理思路:
Port 8080 was already in use——端口被占用,要么结束占用进程,要么改server.portUnknown database——数据库不存在或名字拼写不一致,回到application.yml检查Table 'xxx' doesn't exist——表没有建成功,重新执行建表脚本Failed to configure a DataSource——数据源配置缺失或连接信息错误,排查配置项
6.4 前端启动步骤与跨域问题
前端启动分两步:先安装依赖,再启动开发服务器。
cd smart-supermarket-frontend npm install npm run servenpm install如果速度慢,可以临时换国内镜像,比如npm config set registry https://registry.npmmirror.com。安装完后如果报版本冲突,优先检查package.json里依赖版本,或者删掉node_modules和package-lock.json重装。
浏览器打开前端默认端口(通常是localhost:8080),如果页面能打开但接口请求全部失败,十有八九是跨域问题。开发环境下最简单可靠的解法是在前端配置代理,以Vue CLI为例,在vue.config.js里加:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', // 后端端口 changeOrigin: true } } } }请求发送到前端/api开头的路径时,开发服务器会转发到后端地址,从而绕开跨域限制。这是我自己一直推荐的方式,比在后端写@CrossOrigin的全局配置更干净,也更接近生产环境的Nginx反向代理姿势。
7. 做完项目之后的答辩求生指南:这些技术点必须提前准备
7.1 老师最常追问的五个问题
这个项目做完以后,功能只是基础,答辩时的自我保护更关键。我把在这个项目上被反复问的问题整理了一下,你提前准备好,到场上就不会愣住了。
第一,“你说这是无人超市,那顾客怎么识别身份?”——答案要靠会员扫码/刷脸登录,项目里实现的是小程序端扫码或者会员卡绑定,后端用JWT维持会话,闸机识别之后把用户与订单关联。
第二,“库存扣减怎么防超卖?”——上面讲的条件更新SQL就是标准答案,把stock >= #{quantity}写进WHERE条件,再用受影响行数判断,这就是乐观锁思路。
第三,“如果用户支付成功但库存扣减失败,怎么保证一致性?”——本地事务加上@Transactional,把扣库存和写订单放进同一个方法、同一个事务,一旦任一步骤异常全部回滚。
第四,“订单完成后库存是怎么流转的?”——完成结算后调用库存回补或扣减逻辑,同时写一条stock_record流水记录,这样任何时刻都能追踪某件商品的进出轨迹。
第五,“项目的亮点和难点分别是什么?”——不要答“没难点”。你可以说:难点在于订单状态机的设计、库存并发扣减、数据统计的SQL聚合优化,亮点是前后端分离、统一鉴权、报表可视化。哪怕只是把这三个词稳住,也比支支吾吾强太多。
7.2 演示时的演示流程设计
最后分享一个建议:上台演示前,把你实际会点的操作从头到尾走一遍,不要即兴发挥。我建议的演示顺序是:
- 注册/登录一个会员账号,演示首页商品浏览
- 把三件不同分类的商品加入购物车
- 去结算,演示支付成功、订单生成
- 切到管理端,登录管理员账号
- 查看刚下单的那笔订单,演示订单状态为“已支付”
- 打开库存管理,确认刚才卖掉的商品库存已经扣减
- 最后切到报表页,展示当日营收和销量数据
这七个步骤就是一个完整闭环,每一步都相互呼应——会员端产生的行为,管理端立刻能看到结果。演示过程中的“秒反馈”恰恰能让老师直观感受前后端交互的实时性,这种关联性比单纯罗列功能模块要打动人得多。
这周毕业设计群里好几个学弟又问我“拿到这种完整项目怎么开始上手”,我的统一回答是:别急着跑起来,先花一晚上把表和接口文档过一遍,把业务流在脑子里走通,第二天再动手启动,你会发现自己省掉了大量调试接口的时间。项目本身是给你省事的,但真正能让你在答辩时站住脚的,始终是你对这套业务流转的理解深度。