尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Spring Boot+Vue.js的生鲜超市管理系统设计与实现

基于Spring Boot+Vue.js的生鲜超市管理系统设计与实现 简介面向毕业设计、课程设计及前后端分离项目学习者这是一套基于 Spring Boot 与 Vue.js 的生鲜超市管理系统完整源码覆盖登录、商品管理、订单处理、用户管理等常见业务场景项目以后端接口加前端页面的方式实现了前后端分离可直接运行调试适合快速搭建完整 Web 项目的学生和开发者参考。资源包共包含 854 个文件压缩后约 14.66MB前端部分以 53 个 Vue 组件、164 个 JavaScript 文件、53 个 CSS 样式及 50 个 HTML 页面为主后端部分有 147 个 Java 源码文件并附带 1 个 SQL 数据库脚本完成表结构和初始数据导入。压缩包内还提供 Maven 工程配置、项目说明文档以及安装依赖、启动服务、构建打包三个一键脚本可快速完成环境准备与运行目录规划清晰便于按模块阅读和学习前后端整合方式。目前已有 1879 人学习下载适用于毕业设计、课程设计或个人项目练手除了可直接运行的项目源码和数据库文件外工程中的分层结构、接口调用与前端路由组织方式也展示了 Spring Boot 与 Vue.js 的常见落地实践对后续二次开发和扩展业务模块具有参考价值。1. 生鲜超市管理系统不止是增删改查做过生鲜行业系统的人应该都有体会同样一套订单流程放到生鲜场景里坑会多出好几倍。称重商品和计件商品的价格计算逻辑不同商品有保质期需要批次追踪早市的并发下单集中在早上六点到八点数据库和接口压力都在峰值区间。这个基于 Spring Boot Vue.js 的生鲜超市管理系统正是把这套业务模型完整落地的可运行源码包含数据库文件和部署脚本解压即跑拿来做课程设计或入职练手都很合适。如果你只是在做普通的商品管理系统可能体会不到它的价值但有生鲜业务背景的人看一遍代码里的表结构和订单处理逻辑就知道这个项目的分量在哪里。2. Spring Boot 后端从自动配置到业务边界2.1 为什么选 Spring Boot 而不是 SSM早期的 SSM 项目配置文件动辄五六份Spring 的 XML、MyBatis 的 mapper 扫描、数据库连接池参数每一项都要手动拼装。Spring Boot 把这些基础设施做成了 starter 依赖引入一个spring-boot-starter-web内嵌 Tomcat 直接启动不需要再打 war 包部署到外部容器。这个项目里能看到典型的微服务分层controller 接收请求并做参数校验service 层处理业务规则mapper 层对应 MyBatis 的数据访问entity 和 DTO 分离避免将数据库字段直接暴露给前端。对比维度SSM 传统项目Spring Boot配置方式XML 多文件application.yml 注解Web 容器外部 Tomcat内嵌默认 8080依赖管理手动导入 jarMaven starter 自动管理部署产物war 包可执行 jar用 Spring Boot 带来的直接收益是controller 层只需要写RestController和RequestMapping事务用Transactional一行注解搞定JWT 拦截器通过HandlerInterceptor注册进 WebMvcConfigurer 即可。做课程设计时评审老师问到的自动配置原理、starter 机制、约定优于配置在这个项目里都有落点可以直接讲。2.2 Controller 层设计与统一返回结构前后端分离的项目最怕各写各的数据格式。前端拿到一个接口有时候是data直接返回对象有时候又包了一层result调起来全靠猜。这个项目在 controller 层统一使用了Result包装类所有接口都返回固定结构前端 axios 拦截器里只需要处理这一个格式即可。RestController RequestMapping(/api/goods) public class GoodsController { Autowired private GoodsService goodsService; GetMapping(/list) public Result getGoodsList(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { PageInfoGoods pageInfo goodsService.findPage(pageNum, pageSize); return Result.success(pageInfo); } PostMapping(/save) public Result saveGoods(RequestBody Goods goods) { if (goods.getGoodsName() null || goods.getGoodsName().isEmpty()) { return Result.error(商品名称不能为空); } goodsService.save(goods); return Result.success(); } }这段代码里有两个值得注意的参数。第一个是pageNum和pageSize使用RequestParam接收分页参数默认值分别设为 1 和 10调用时传pageNum2pageSize20就能切换页码。第二个是RequestBody它告诉 Spring 将前端传来的 JSON 字符串反序列化为 Goods 对象如果前端请求头里没有Content-Type: application/json这个注解会直接抛异常。分页返回用的PageInfo是 PageHelper 插件的标准返回对象包含total、list、pageNum等字段前端分页组件拿到这几个值就能渲染页码。2.3 JWT 认证登录态如何做到无状态传统 Session 方案在前后端分离场景下有个痛点前端部署在一台服务器后端在另一台Session 存储在服务端内存里跨域请求要额外配置 CORS 允许携带 Cookie分布式部署时还要引入 Redis 做 Session 共享。JWT 把用户信息签名后直接发给前端存储后续请求带上 token后端验签通过即认为是有效登录态这恰恰是生鲜门店早市场景下需要的——收银台的登录状态不需要服务端额外存储。public class JwtUtil { private static final String SECRET_KEY fresh-market-secret; public static String generateToken(Integer userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 8)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }JWT 生成时设置了三个关键参数setSubject保存用户名作为主体标识claim添加自定义字段userId方便查询当前登录人setExpiration设置 8 小时过期时间。HS256 是对称签名算法同一个密钥既负责签发也负责验签。1 小时后要调成 24 小时只改1000 * 60 * 60 * 8里的数字即可。过期后前端会收到 401 状态码这时候就需要重新登录了。需要注意一点JWT 一旦签发在过期前无法主动作废所以修改密码后要强制重新登录项目里的update-password.vue在提交新密码后带着旧 token 访问接口密码保存用 BCrypt 加密。2.4 跨域配置与拦截器放行规则跨域问题在前后端分离项目中几乎必然遇到生鲜超市项目的管理端前端跑在http://localhost:8081后端接口是http://localhost:8080端口不同即构成跨域。项目里通过实现WebMvcConfigurer来统一配置跨域规则和登录拦截器。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8081) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/login, /api/logout); } }addMapping(/**)表示对所有接口生效但注意allowedOrigins要精确指定前端地址不能用*加allowCredentials(true)同时出现。excludePathPatterns放行了登录接口JWT token 在这些接口上不校验。拦截器里从请求头取Authorization字段没有或解析失败就返回 401前端 axios 响应拦截器收到 401 后执行router.push(/mylogin)跳转这是前后端分离工程里常见的登录失效处理链路。3. Vue 管理后台组件拆分与路由组织3.1 从项目中的 .vue.bak 文件看懂页面骨架解压项目后前端目录的src/views下能看到IndexMain.vue.bak、IndexHeader.vue.bak、IndexAsideStatic.vue.bak这些备份文件。之所以叫.bak说明项目在改造过程中把这些主要组件重新调整后保留了备份方便随时回滚。管理后台页面遵循 Element UI 的经典布局顶部是IndexHeader左侧是IndexAsideStatic菜单栏中间通过IndexMain承载路由视图router-view。template el-container classlayout-container el-header index-header :collapsedisCollapsed toggle-sidebartoggleSidebar / /el-header el-container el-aside :widthisCollapsed ? 64px : 200px index-aside-static :collapsedisCollapsed / /el-aside el-main bread-crumbs / router-view / /el-main /el-container /el-container /template这套布局的价值在管理后台类项目中具有高度的复用性。el-aside的宽度绑定在isCollapsed变量上控制折叠宽度 64px 和展开宽度 200px菜单收起后只显示图标适合门店收银场景下的小屏设备。BreadCrumbs组件读取当前路由的matched数组自动生成面包屑导航。路由变化时IndexMain里的router-view切换对应组件整个过程不刷新页面这就是 Vue SPA 的核心体验。3.2 路由参数传递与登录守卫管理后台中常见的路由参数传递场景是从商品列表页点击编辑按钮携带商品 ID 跳转到商品详情页。Vue Router 提供了query和params两种传参方式这个项目两种都有使用分开看它们各自适合什么场景。// 方式一:query传参,参数显示在URL中,刷新不丢失 this.$router.push({ path: /goods/edit, query: { id: row.id } }); // 方式二:params传参,参数不显示在URL中,刷新丢失 this.$router.push({ name: GoodsEdit, params: { id: row.id } }); // 接收参数 const idFromQuery this.$route.query.id; const idFromParams this.$route.params.id;选择了query方式传参URL 会变成/goods/edit?id12分享链接或刷新页面后参数依然存在。params方式适合传敏感内容比如订单详情页内部跳转缺点是刷新页面后参数丢失需要通过 sessionStorage 或 pinia 做持久化补偿。项目中编辑商品页使用的是query方式因为商品 ID 不涉及敏感信息。列表页跳转详情页时用params传订单号因为订单号出现在 URL 里容易被视图源获取虽然项目是后台系统不面向普通用户但这是一种更严谨的参数传递习惯。登录守卫通过router.beforeEach实现每次路由切换前检查 Vuex 和 localStorage 中的 token 是否存在不存在则强制跳转到/mylogin登录页存在才放行。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /mylogin) { next({ path: /mylogin }); } else { next(); } });判断逻辑里有两个分支这不是死板的模板。实际项目中token 有效才放行无效则跳登录页这种「非黑即白」的代码是多数人写的第一版。但更符合真实业务的做法是虽然 vuex 里有 token也要通过调用后端/api/user/status接口确认 token 是否仍在有效期内因为 JWT 登录对于已删除的用户token 在 8 小时有效期内依然可以访问后端接口。3.3 axios 封装与前后端联调管理后台的每个接口请求如果都直接写 axios 调用会分散「token 注入」和「错误弹窗」的逻辑一旦后端把状态码从 200 改成 201 或 202要改的地方就太多了。项目在src/utils/request.js中做了一层统一封装所有组件都改为封装后的request方法。import axios from axios; const request axios.create({ baseURL: process.env.NODE_ENV development ? /api : /production-api, timeout: 15000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); request.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message || 请求出错); return Promise.reject(res); } return res; }, error { if (error.response error.response.status 401) { localStorage.removeItem(token); router.push(/mylogin); } return Promise.reject(error); } );封装里有三个方向值得注意。第一是baseURL开发环境下请求资源放到/api前缀生产环境则指向/production-api切换环境时只需要改一处变量即可比在组件里到处写死请求地址的代码要容易维护得多。第二是请求拦截器统一注入 token登录之后每个接口都会自动携带Authorization头后端拦截器从请求头取到 token 后再验签。第三是响应拦截器统一处理业务码后端返回的code字段不等于 200 时统一弹出 Message 提示前端就无需在每个页面重复处理同样的错误逻辑了。3.4 修改密码与表单校验项目中的update-password.vue是给管理员提供修改密码功能的页面。这里有两个容易被忽略的地方一是同时校验旧密码和管理员新密码二是前端校验规则要与后端完全一致不能出现前端限制最小 6 位、后端只校验非空的情况否则绕过前端直接请求接口时就无法保证密码强度了。script export default { data() { return { form: { oldPassword: , newPassword: , confirmPassword: }, rules: { oldPassword: [ { required: true, message: 请输入旧密码, trigger: blur } ], newPassword: [ { required: true, message: 请输入新密码, trigger: blur }, { min: 6, max: 20, message: 长度在 6 到 20 个字符之间, trigger: blur } ], confirmPassword: [ { required: true, message: 请再次输入新密码, trigger: blur } ] } }; }, methods: { submitForm() { this.$refs.form.validate(valid { if (!valid) return; if (this.form.newPassword ! this.form.confirmPassword) { Message.error(两次输入的密码不一致); return; } request.post(/api/user/updatePassword, this.form).then(() { Message.success(修改成功请重新登录); localStorage.removeItem(token); this.$router.push(/mylogin); }); }); } } }; /script这里有一个细节。Element UI 的表单校验是「异步回调」validate拿到valid之后才执行提交逻辑如果写成同步代码直接取表单值来进行判断就有校验还没完成导致错误提示未及时渲染的问题。另外提交成功后强制清除 token 并跳转登录页是因为后端的旧密码是保存在 token 里的修改密码后旧 token 应该失效强迫用户重新登录防止修改密码前的旧 token 继续使用 8 小时有效期。4. 数据库设计生鲜业务藏在表结构里的心思4.1 商品表命名是小事字段是大事打开数据库文件第一张表就值得花时间看它不叫product而叫goods原因可能是项目早期沿用了进销存系统的叫法也可能原本就按「商品」而不是「产品」来建模。这不重要重要的是字段设计。商品表goods的关键字段包括主键自增 ID、商品名称、商品类别 ID 关联类别表、进价、零售价、会员价、保质期天数、单位、是否称重商品标识、库存上限和预警下限。生鲜行业的特殊性逼迫它必须考虑这些字段肉类进价和售价日波动很大要留出进价字段规格多到无法枚举要有分类关联蔬菜容易腐坏要有保质期天数和库存预警来支撑报损。CREATE TABLE goods ( id int(11) NOT NULL AUTO_INCREMENT, goods_name varchar(100) NOT NULL COMMENT 商品名称, category_id int(11) NOT NULL COMMENT 关联分类表, purchase_price decimal(10,2) DEFAULT NULL COMMENT 进价, sale_price decimal(10,2) NOT NULL COMMENT 零售价, member_price decimal(10,2) DEFAULT NULL COMMENT 会员价, shelf_life int(11) DEFAULT NULL COMMENT 保质期天数, unit_type tinyint(1) DEFAULT 0 COMMENT 0按件销售 1按重量称重, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, warning_stock int(11) DEFAULT 10 COMMENT 库存预警下限, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段里的decimal(10,2)是生鲜系统最值得强调的写法价格不能使用float或double二进制浮点数无法精确表达 0.1在累计金额时会出现 0.30000000000000004 这类结果。在生鲜早市高峰一笔订单称重商品金额是29.40另一笔是31.80用浮点数求和误差会在多次运算后被逐渐放大结算时账不平是非常严重的生产事故。unit_type字段把商品切成两种按件销售的商品如盒装酸奶用整数库存按重量称重的商品如散装蔬菜需要配合称重模块返回的数量。shelf_life是生鲜系统区别于其他进销存系统的关键差异生鲜商品先入库先出库过期商品要触发报损流程。4.2 订单与库存扣减什么时候扣什么时候锁订单表的设计上一般电商系统的订单货物的单价数量直接写死在订单明细里不会在后续商品改价后影响历史订单的展示。商品价格波动时今日猪肉 15 元一斤明天涨到 17 元订单表里存储的是下单当天的购买价格这两个价格是不一致的因此订单明细表里要单独存goods_name的快照和商品单价。生鲜系统的订单表还多了会员折扣标记会员价和称重商品的实时重量是在订单创建时完成的。库存扣减发生在订单生成时。如果先用一个接口创建订单然后另写一个接口单独扣减库存就会留下两接口之间时间窗口数据不一致的问题。正确做法是在orders明细表插入的同时用一条 update 语句扣减goods表的库存两个动作包在同一事务里。UPDATE goods SET stock stock - #{quantity} WHERE id #{goodsId} AND stock #{quantity};这一段 SQL 的关键在最后的stock #{quantity}条件它起到一种原子性的校验作用。当两个收银台同时卖出同一种商品时第一个事务把库存从 5 扣到 3第二个事务执行时发现库存只有 3 不满足购买 3 件的条件更新失败、订单回滚就不会出现超卖的情况了。如果不加这个条件两个事务同时读到库存为 5各自减去 3删除后变成一条记录剩余 2、另一条剩余 2加起来还剩 4 件账面库存永远对不上实物。4.3 批次表生鲜溯源与报损的原点生鲜商品和标准工业品的最大区别在于「生命周期」。一箱牛奶的保质期是 6 个月同一批货可以压着卖一把菠菜的保质期是 3 天第 3 天还没卖出去就要下架报废。所以生鲜系统的库存管理不能只有「总数」这一个维度必须能回答「这批货是什么时候进的」和「还剩几天到期」这两个问题。项目数据库里设计了批次相关表结构每个入库批次记录进货时间、供应商、进价、数量、已售数量、过期时间。出库时按「先进先出」规则优先扣减最早批次的库存同时在界面上展示每个批次的剩余天数和预警状态。CREATE TABLE goods_batch ( id int(11) NOT NULL AUTO_INCREMENT, goods_id int(11) NOT NULL COMMENT 关联商品表, supplier varchar(100) DEFAULT NULL COMMENT 供应商, purchase_price decimal(10,2) DEFAULT NULL COMMENT 本批次进价, quantity int(11) NOT NULL COMMENT 本批次入库数量, sold_quantity int(11) NOT NULL DEFAULT 0 COMMENT 已售数量, expire_date datetime DEFAULT NULL COMMENT 过期时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;查询剩余库存时不能直接查商品表里的stock字段要从商品批次表按“goods_id ? AND expire_date NOW()”过滤后汇总。早市高峰期系统要快速算清「今日可售库存」把 batch 表和 goods 表的查询合并成一条 SQL每日结算时通过任务统计各过期批次的数量自动生成报损单。这是生鲜系统区别于普通进销存的核心设计也是答辩或考核时最值得展开讲的一块。5. 一键部署脚本与高频踩坑5.1 三个 bat 脚本的交付逻辑项目根目录有1-install.bat、2-run.bat、3-build.bat三个脚本对应一套完整的交付流程安装依赖、启动开发环境、构建生产包。设计逻辑是让拿到源码的人不需要看部署文档直接双击执行即可。# 1-install.bat 安装前端依赖和后端依赖 cd backend call mvn install -DskipTests cd ../frontend call npm install pause # 2-run.bat 同时启动前后端 cd backend start java -jar target/xxxx.jar --server.port8080 cd ../frontend start npm run serve pause # 3-build.bat 构建生产包 cd frontend call npm run build cd ../backend call mvn package -DskipTests pause安装脚本先走mvn install拉取后端依赖并跳过测试等BUILD SUCCESS再进入前端npm install安装 node_modules。运行脚本默认后端占 8080 端口前端用npm run serve启动在 8081 端口用 8080 端口启动后端后前端页面中baseURL指向的/api会通过 devServer 代理转发到 8080 端口形成一个稳定的联调链路。构建脚本生成的前端 dist 目录放进后端 resources 目录实现打成一个 jar 包分发的效果。5.2 部署时最容易踩的坑生鲜门店场景下最常见的部署问题是数据库连接失败。项目默认连的是 MySQL 8.xSpring Boot 2.7 以上版本的数据库驱动类名是com.mysql.cj.jdbc.Driver不是旧的com.mysql.jdbc.Driver而且 8.x 版本的 URL 里必须加serverTimezoneAsia/Shanghai否则连接时区报错。数据库密码如果含特殊字符如#或在application.yml里要加单引号包裹不然到#就被当成注释截断了。npm install 在高版本 Node 上安装依赖时报 node-sass 编译错误这是个经典问题。node-sass的每个版本只对应特定版本的 NodeNode 18 上装node-sass4.x几乎必然报错换用sass或dart-sass来替代即可。如果项目跑起来后页面能打开但所有接口请求 404浏览器 F12 看看请求地址是不是打到了 8080 端口对应vue.config.js里 devServer 的proxy配置目标是否正确设置了changeOrigin: true后代理才能正常运行。还有一类错误在部署到服务器后才出现npm run build出的静态文件用 Nginx 托管刷新非首页路径直接 404这是vue-router的history模式在匹配不到服务端静态文件时直接返回了 404。对应解法是在 Nginx 配置加一行try_files $uri $uri/ /index.html;把找不到文件的路由重定向到入口文件。如果项目本身用的是hash模式部署就不会出现这个问题所以先确认路由模式再决定要不要做重定向。本文还有配套的精品资源点击获取
返回列表