☰
网上超市管理系统全栈实战:SpringBoot+Vue前后端分离开发指南
2026/10/22 18:33:12 网站建设 项目流程

没想到一个“网上超市管理系统”的项目标题,能在我手上翻来覆去改出好几个版本。最早接这个题目的时候,我也以为就是普通的增删改查,结果真上手做下去才发现,里面每一块都挺有讲究:前后端分离怎么拆、订单状态怎么流转、库存扣减怎么避免超卖、购物车在并发下怎么保持一致……如果你正在做类似的JavaWeb项目、毕业设计或者想拿一套完整的源码来练手,这篇文章应该能帮你把思路理顺。我会从技术选型、数据库设计、后端接口、前端页面、联调部署五个层面,把我实际踩过坑、改过bug的地方全部写出来。

1. 网上超市管理系统:整体定位与技术选型拆解

1.1 项目核心需求解析:线上超市到底在“管”什么

先别急着写代码,先想清楚一个网上超市管理系统要解决什么问题。你把线下超市搬上线,最基础的就是两部分:用户端能逛、能买,管理端能上架、能发货。但这两部分细化下去,其实藏着很多琐碎的需求。

我当时梳理完需求,发现这个系统至少包含以下模块:

  • 用户端:注册登录、商品分类浏览、商品搜索、商品详情、加入购物车、购物车管理、确认下单、模拟支付、订单查询、订单取消、个人资料。
  • 管理端:管理员登录、商品管理(增删改查、上下架、库存调整)、分类管理、订单管理(发货、查看详情、处理取消)、用户管理、数据统计(销售额、订单量、热销商品)。
  • 系统公共能力:图片上传、登录鉴权、异常处理、参数校验、日志记录。

合起来看,这就是一个非常典型的全栈业务系统。它不牵涉太复杂的算法,但要求你做到逻辑严谨、流程完整、数据一致。对于学习者来说,它的价值在于让你完整走一遍“需求分析—数据库设计—接口开发—前端联调—测试修复”的全链路。

1.2 为什么选SpringBoot+Vue而不是老牌的JSP单体架构

有些人可能会问:做个超市管理系统,用几年前流行的JSP+Servlet不行吗?真不行的话,培训机构为什么还教SSM?

我个人的看法是,既然要学,就学现在业界主流的工程化方案。SpringBoot+Vue这套组合目前是中小型项目非常常见的搭配。SpringBoot解决了Spring繁琐的XML配置问题,内嵌Tomcat,打包成jar就能跑;Vue则把前端从模板字符串中解放出来,组件化开发、路由控制、数据绑定都清晰很多。两者前后端分离,各司其职,调试效率比JSP高太多。

用生活化的比喻就是:老一代JSP模式是厨房和餐厅在一个房间,炒菜和上菜都得紧着空间来;而前后端分离是后厨(后端API)和前台(前端页面)分开,后厨把菜做好放在出餐口,前台按需取用,互不干扰。尤其到了上线部署阶段,前端静态文件扔Nginx,后端接口独立跑端口,扩容、维护都方便。

1.3 技术选型各关键组件定位:为什么是MySQL和MyBatis

项目标题里点名了Java+MySQL+MyBatis,这三样组合是很多高校和培训班的标配。MySQL不用多说,开源免费、稳定可靠,中小型系统的数据量它完全扛得住。MyBatis则是半自动ORM框架,SQL由你手写,执行逻辑透明,排错方便,特别适合教学和中小型项目。相比JPA那种全自动框架,MyBatis让你对SQL有绝对掌控力,遇到分页、关联查询、动态SQL的时候特别方便。

版本选择上,我当时用的是SpringBoot 2.7.x、MyBatis 1.3.2、MySQL 5.7。为什么不用更高版本?因为要兼容各种教程和现成代码,主流教程里大部分都是这套组合。Vue端我用的是Vue2+Element-UI,这不是说Vue3不好,而是Element-UI对Vue2的支持最成熟,组件丰富,搜问题的答案也多。对于学习项目,稳比新更重要。

2. 数据库设计:网上超市的骨架怎么搭才不乱

2.1 从“购物流程”倒推核心表结构

数据库设计是整个项目的地基。地基没打好的话,后面写接口、做页面全都会别扭。我习惯的做法是:从用户的一次完整购买流程倒推需要哪些表。

用户打开系统 → 看到商品分类和商品列表 → 点击商品看详情 → 加入购物车 → 去结算填写收货信息 → 生成订单 → 模拟支付 —— 这个流程一画出来,核心表就比较清晰了。

需要用户表(user)存账号、密码(加密后)、昵称、手机号、邮箱、头像。需要分类表(category)存商品分类名和父级ID,做两级分类。需要商品表(product)存商品名称、图片、价格、库存、销量、描述、上下架状态、所属分类。需要购物车表(cart)存用户ID、商品ID、数量、加入时间。需要收货地址表(address)存用户地址信息。需要订单表(orders)存订单编号、用户ID、总金额、状态、收货信息、下单时间和支付时间。还有订单明细表(order_item)存每个订单中的商品快照,包括商品名称、单价、数量、小图。

为什么要做订单明细这个“快照”表?这是很多人容易忽略的地方。商品的价格和名称随时可能变,如果订单里只引用商品ID,用户翻开历史订单发现价格变了你都没法解释。所以下单时要把商品的关键信息复制一份到订单明细,让订单记录成为一个独立的历史事实。

2.2 核心表的字段设计与关联关系细节

挑几张关键表,我把字段列一下,顺便说说每个字段存在的理由。

用户表字段建议包含:id、username、password、nickname、phone、email、avatar、role(区分普通用户和管理员)、status(是否禁用)、create_time、update_time。注意role字段不要用字符串存“admin”“user”这种,最好用int类型,0表示普通用户,1表示管理员,程序里再用常量映射。

商品表字段建议包含:id、category_id、name、subtitle(副标题,可不要)、main_image、sub_images(用逗号分隔存多图)、detail(富文本详情)、price(用decimal(10,2))、stock(库存,int)、sales(销量,int)、status(1上架0下架)、create_time、update_time。别忘了给category_id和status建索引,否则商品多了以后按分类查询会很慢。

订单表字段建议包含:id、order_no(唯一订单编号)、user_id、total_price、payment_type(支付方式)、status(订单状态)、receiver_name、receiver_phone、receiver_address、payment_time、send_time、end_time、close_time、create_time。订单状态我设置了几个常量:10待付款、20已付款待发货、30已发货待收货、40已完成、50已取消、60已关闭。状态流转不要乱跳,比如待付款只能到取消或者已付款,已发货只能到已完成。

订单明细表字段建议包含:id、order_id、product_id、product_name、product_image、current_unit_price(下单时单价)、quantity(数量)、total_price。这里没必要再加update_time,因为明细创建后不需要修改,这也是快照表的一个特点。

2.3 初始化数据与字段安全设计

建完表就要考虑初始化数据的问题。系统需要预置一个管理员账号,我用的用户名是admin,密码通过MD5再加盐的方式存储。注意,即使是毕业设计级别的项目,密码也尽量不要明文入库。我在User表里把password字段设计成varchar(255),存的就是MD5加盐后的字符串。虽然MD5不算最强,但比明文强太多了。

另外,所有表都要有create_time和update_time两个时间字段,用datetime类型。不要把这两个字段省掉,后面做默认排序和时间筛选时非常有用。主键统一用BIGINT自增,不要搞什么UUID主键,性能不好还占空间。分页查询用的关键字段,比如商品表的category_id、订单表的user_id和status,都要加上普通索引。

对于数据量的考量,电商系统必备的“热卖商品”“最新上架”这类查询,就是对status、sales、create_time做排序和过滤。我之前测试时手动造了3000条商品数据,在没有索引的情况下分页查询明显卡顿,加了索引后基本毫秒级返回。这个环节建议你在建表时就规划好,别等着数据变慢了再补救。

3. 后端核心功能实现:从Controller到Mapper的完整链路

3.1 后端项目分层与包结构规划

后端代码的组织,直接决定了你后续写功能、查bug的效率。我用的是经典的分层架构,包结构如下:

com.supermarket ├── config —— 配置类(跨域、拦截器、MyBatis分页插件等) ├── controller —— 接口入口,只做参数接收和结果返回 ├── service —— 业务逻辑层,接口+实现类 ├── mapper —— MyBatis数据访问层(接口类) ├── entity —— 实体类,和数据库表字段一一对应 ├── dto —— 数据传输对象,用于接口参数接收 ├── vo —— 视图对象,用于接口返回值封装 ├── common —— 公共类(统一返回结果、异常处理、常量、工具类) └── interceptor —— 拦截器(登录校验、权限校验)

Controller的职责是“翻译官”:接收前端的请求参数,转给Service处理,再把Service返回的数据封装成统一格式返回前端。Service是“业务大脑”,所有业务规则都写在里面,比如下单时校验库存、计算价格、生成订单号。Mapper是“搬运工”,只负责和数据库交互,写SQL查询。

项目里我定义了一个通用的Result类,统一返回格式:code(状态码)、message(提示信息)、data(数据)。前端Axios响应拦截器里直接判断code,不是200就走错误提示,避免每个页面重复处理异常逻辑。

3.2 登录鉴权方案:JWT Token代替Session

网上超市系统,用户需要登录才能购物、下单,管理员也要登录才能进后台。这里我用的是JWT(JSON Web Token)方案,而不是传统的Session。原因很简单:前后端分离后,后端接口是无状态的,Session存在服务器内存里,不仅不好横向扩展,前端跨域时处理Cookie也麻烦。JWT把用户信息加密成一个Token字符串返回给前端,前端每次请求都带上,后端解密验证,不用在服务端保存任何状态。

具体实现分这么几步:

  1. 用户登录成功后,用工具类生成Token,把userId和role存进去。
  2. 写一个拦截器,拦截需要登录的接口(比如购物车接口、订单接口、后台管理接口),从请求头取出Token并验证。
  3. 验证通过就把用户信息存入ThreadLocal,方便后续接口直接获取当前用户ID。
  4. Token过期、无效,直接返回401状态码,前端收到后跳转登录页。

密码存储这里再强调一次:不要用明文。我在用户表里存的是MD5加密后再加盐的字符串。具体写法是MD5(password + salt),salt可以用用户注册时生成的一串随机字符。网上教程很多用固定盐,虽然也能用,但每个用户独立盐更安全一点。

3.3 购物车与订单核心流程:事务与状态流转

购物车这块,后端提供的接口包括:加入购物车、更新购物车商品数量、删除购物车商品、获取用户购物车列表。核心逻辑就是围绕user_id和product_id做upsert:如果该用户购物车里已经有这个商品,数量加一,否则新建记录。注意这里在User表+Product表上建了唯一索引,防止重复数据。

订单流程是整个系统最核心、最容易出bug的地方。我的实现步骤是这样的:

  1. 用户提交下单请求,参数包含地址ID、购物车选中的商品ID列表。
  2. 后端根据商品ID查出所有商品信息,循环计算总价。
  3. 校验商品状态为上架状态,库存充足。
  4. 生成订单号(规则:时间戳+用户ID+随机数),插入订单主表和订单明细表。
  5. 扣减库存、增加销量。
  6. 清空购物车里对应的商品。
  7. 返回订单号和订单金额给前端模拟支付。

这里有个极其重要的点:整个下单过程必须加事务。SpringBoot里很简单,在Service方法上加上@Transactional注解就行。但要注意一个坑:事务方法A调用同类中的事务方法B时,B的@Transactional是不生效的,因为Spring通过代理对象调用,同类内部直接调用绕过了代理。所以写代码时,事务方法要放到独立的Service类里,或者用AopContext.currentProxy()调代理。我当时就因为这个坑,下单时明细插入了但库存没扣减,排查了老半天。

库存扣减还有一个细节,SQL语句要用UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity},这样才能保证并发下不会超卖。如果受影响行数为0,说明库存不足,直接抛异常回滚。这个写法比“先SELECT出来再JAVA里判断库存”要安全得多。

3.4 MyBatis使用心得:动态SQL和结果映射避坑

MyBatis最强大的地方是动态SQL。比如商品列表查询,用户可能按分类筛选、按价格区间、按关键词搜索,一个方法要适配多种情况。我用<where>标签配合<if>判断来解决,代码清晰又安全。举个例子:

<select id="selectProductList" resultType="map"> SELECT * FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (name LIKE CONCAT('%', #{keyword}, '%') OR subtitle LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>

MyBatis的resultType和resultMap也要说清楚。简单查询用resultType="map"就够了,Map的key对应字段名。但涉及多表关联(比如订单详情需要关联商品表、用户表),最好是手写resultMap明确映射关系,避免字段对不上、嵌套对象为空的问题。另外,数据库下划线字段名和Java驼峰属性名的映射,记得在application.yml里开启map-underscore-to-camel-case: true。

分页就交给PageHelper插件,一行配置搞定:PageHelper.startPage(pageNum, pageSize),然后正常写查询,它自动在SQL后面拼LIMIT。这个插件用起来很爽,但要注意它只在“紧接着”的方法调用时生效,中间不要插别的数据库操作,否则容易计算出错误的总数。

4. 前端设计与交互:Vue工程化实践与页面拆解

4.1 前端工程结构与开发环境

前端部分,我用Vue CLI搭建的工程(Vue2 + Element-UI + Vuex + Vue Router + Axios)。目录结构如下:

src ├── api/ —— 接口请求模块,按功能拆文件 ├── assets/ —— 静态资源、样式 ├── components/ —— 公共组件(轮播图、商品卡片、分页等) ├── router/ —— 路由配置 ├── store/ —— Vuex状态管理 ├── utils/ —— 工具函数(request封装、token存取) ├── views/ —— 页面组件 │ ├── home/ —— 首页 │ ├── product/ —— 商品列表/详情 │ ├── cart/ —— 购物车 │ ├── order/ —— 订单相关 │ ├── user/ —— 个人中心 │ └── admin/ —— 管理后台

一个非常重要的习惯:接口请求不要散落在各个页面里,而是统一放在api目录。比如api/product.js里导出getProductList、getProductDetail等方法,页面里import调用。这样改接口地址或者统一处理参数时,只动一处,不用满项目找请求。实测下来,这个习惯在大项目里能省无数时间。

配置开发环境时,前后端联调要解决跨域问题。我在Vue CLI的vue.config.js里配置了devServer代理:

devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

前端所有请求都以/api开头,后端Controller的路由也加了/api前缀,这样请求直接打到代理,完美绕开跨域。后端那边也要配合开启跨域配置,我之前用的是CorsFilter加允许源配置。

4.2 Axios封装与统一请求处理逻辑

封装Axios是前端工程化必不可少的一步。我在utils/request.js里做了这么几件事:

  1. 创建Axios实例,设置baseURL和timeout。
  2. 请求拦截器里,从localStorage取出Token,放到请求头Authorization里。
  3. 响应拦截器里,统一处理返回的Result结构。code为200就返回data字段给业务代码;code为401就清掉Token跳登录页;其他code用Element-UI的Message组件弹出错误提示。
  4. 对网络异常(超时、断网)做统一处理,弹“网络异常,请稍后重试”。

这样做的好处特别明显。前端几百个接口调用的地方,不需要每个都写loading状态、错误弹窗,只关心成功后的业务逻辑就行。代码体积少说减少30%,出错概率也大大降低。

4.3 用户端核心页面拆解:从首页到订单提交

用户端的首页,我做了导航栏(搜索框+分类菜单+购物车入口+用户信息)、商品分类侧边栏、轮播图、热卖商品区、最新上架区。商品卡片用的Element-UI的Card组件,图片、名称、价格、销量都展示出来,点击跳详情。搜索功能就是调商品搜索接口,切换tab时重新请求数据。

商品详情页要特别注意“选择数量”这个交互。我用的el-input-number组件,设置min为1,max为商品库存数,避免用户输入超过库存的数量。加入购物车按钮,点击后调后端接口,成功后Message提示并更新导航栏购物车角标(Vuex管理购物车数量)。

购物车页面是一个表格:商品图、商品名、单价、数量(可增减)、小计、操作。每行可以勾选,底部显示合计金额,点击“去结算”跳订单确认页。这里要注意表格里金额的精度计算,前端别直接用浮点数累加,我用的是把所有金额转成整数分再计算,最后除以100展示,时间久了就知道为什么这么做。

订单确认页就是展示商品清单、收货地址(支持新增编辑)、总金额,然后提交订单。提交成功后跳到模拟支付页,做一个倒计时或者提示框,点击“立即支付”调用支付接口,把订单状态从待付款改成已付款。

4.4 管理后台:权限控制与数据可视化

管理后台是独立的路由模块,进入前会做权限校验。我的方案是:路由meta里标记需要admin权限,全局前置守卫检查Token和用户角色,不满足就跳登录页。这样拦截器+前端守卫双重校验,基本能保证普通用户进不了后台。

后台页面主要包括:

  • 商品管理:表格展示商品,支持搜索、分页、新增、编辑、删除、上下架。新增编辑商品弹窗里,我用el-upload组件做图片上传,选择文件后先调后端上传接口拿到图片URL,再把URL和商品信息一起提交。
  • 分类管理:树形或列表结构,支持增加子分类、改名、删除。删除时要校验该分类下是否有商品,有的话提示先转移商品。
  • 订单管理:按状态筛选订单,待发货的可以点“发货”按钮更新状态。
  • 数据统计:一个简单的Dashboard,展示今日销售额、总订单数、总用户数、热销商品TOP5。图表用ECharts,从后端统计接口拿数据。

后台整个开发思路和前台差不多,核心就是这个权限控制和数据统计接口。数据统计的SQL相对复杂一些,用到了JOIN、GROUP BY、SUM函数。我当时做“按日期统计支付订单金额”这个接口时,SQL调了几次才写对,核心原因是订单状态和支付时间要同时过滤,建议你直接写:

SELECT DATE(payment_time) AS pay_date, SUM(total_price) AS total_amount FROM orders WHERE status IN (20, 30, 40) AND payment_time IS NOT NULL GROUP BY DATE(payment_time) ORDER BY pay_date DESC

5. 前后端联调部署与高频问题排查实录

5.1 开发期联调配置与常见错误排查

前后端分离的联调阶段,我遇到过不少让人头大的问题,这里挑几个高频的写出来。

第一个是跨域报错。浏览器报“CORS Policy”错误,十有八九是后端没配跨域,或者前端代理配错。排查方法很简单:打开浏览器Network面板,看请求是否真的经过代理,Response里有没有CORS头。直接请求后端地址测一下接口通不通,不通就检查后端的CorsFilter。

第二个是Axios请求带不上Token。检查请求拦截器里在不在(有没有从localStorage正确读取),字段名和后端拦截器里取的一致吗。我项目里统一用Authorization,你可以换别的,但前后一定得对齐。

第三个是404问题。前端访问/api/xxx提示404,可能是后端Controller的路由写错了,也可能是请求路径少了一层上下文。我一般先看后端控制台有没有打印请求日志,有但返回404,说明路由匹配不上;没有输出,说明请求根本没到后端,要检查代理配置。

第四个是时间格式化问题。后端返回的日期是一串时间戳或者英文格式,前端显示很丑。解决办法是给后端全局配置一下JSON序列化,或用@JsonFormat注解在日期字段上指定时区和格式:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime;

5.2 高频bug速查表:一个典型的排查清单结构

我把最常见的问题整理成下面这个速查表,你调试这个项目时直接对着查:

现象可能原因解决方案
登录接口报500密码加密逻辑不一致检查注册和登录的加密算法、盐是否一致
购物车数量不更新接口幂等性没做好使用带唯一索引的联合查询,先查后更新
下单后库存没变事务未生效确认@Transactional作用在代理对象调用的方法上
前端分页不生效PageHelper线程不安全确认PageHelper.startPage紧挨查询,且不在循环中调
跨域报错前后端配置不一致检查代理路径和后端CORS配置
图片上传失败文件保存路径不存在在配置类里创建上传目录,或设置绝对路径
商品搜索中文乱码数据库连接URL没有指定字符集连接串加上characterEncoding=utf-8
订单列表查询慢缺少索引给user_id和status加联合索引

5.3 打包部署上线:从开发到生产的关键调整

项目做好之后,打包部署是另一个容易被忽视的环节。

后端用的是SpringBoot的Maven插件。在pom.xml里配置了打包插件后,直接执行mvn clean package,会在target目录生成一个可直接运行的jar文件。启动命令是java -jar 你的jar包.jar。但要注意生产环境数据库地址、账号密码不要写在配置文件里提交到代码库,我一般用外部化配置的Spring profile方式管理,分成application-dev.yml和application-prod.yml,启动时用--spring.profiles.active=prod指定环境。

前端打包执行npm run build,生成dist静态目录。最省事的部署方式是把dist扔到Nginx做静态文件服务器,然后配置/api反向代理到后端的SpringBoot端口。Nginx配置大概长这样:

server { listen 80; server_name your_ip_or_domain; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } }

这里有个小坑:Vue Router用history模式时,刷新页面会404,需要在Nginx里配置try_files回退到index.html,让前端路由接管,上面配置已经包含了。

数据库导入也需要提醒。生产环境不能用开发库里乱七八糟的测试数据,我一般在启动前用项目根目录的supermarket.sql脚本重建数据库结构,只保留必要的管理员账号和少量演示分类、演示商品。

主板部署部分,如果服务器内存不高,SpringBoot默认的JVM参数可能吃得比较紧。可以在启动命令加-Xmx256m限制堆内存,避免OOM。数据量不大时这套配置跑得很轻松。我实测在单核2G的服务器上,后端+前端+MySQL三个进程同时跑,资源占用依然在可接受范围。

6. 这套系统的可扩展方向与经验沉淀

项目做到能跑通、能演示、能部署,只是第一步。真正有价值的,是你搞清楚哪些地方可以继续往下延展。我当时做完后主要想了下面几个方向,你也可以照着思考。

第一个是引入Redis做缓存。商品列表、商品详情这些高频读接口,目前每次请求都打MySQL,数据量大之后扛不住。加一层Redis缓存,设置合理的过期时间,能显著提升接口响应速度。购物车也可以从MySQL搬到Redis里,用Hash结构以user_id为key存购物车数据,操作起来更灵活。

第二个是接入真正的支付网关。现在是模拟支付,真实场景要对接第三方支付平台的接口,处理回调、签名校验、对账等一堆事情。这个方向能学到的东西更多,比如分布式事务、消息队列,但复杂度也会上一个台阶。

第三个是后台增加更丰富的数据分析功能。目前只有销售额、订单量的简单聚合。可以做用户行为分析、漏斗分析(浏览到加购、加购到下单的转化率)、商品关联推荐(买了A的人也买了B),这些都是目前电商行业非常实用的功能。

第四个是完善管理后台的操作审计日志。谁在什么时间改了商品价格、调整了库存,都应该记录下来。这虽然不是业务功能的核心,但在真实系统中是合规和安全的重要组成部分。

我自己的体会是,这类管理系统虽然看起来“很课程设计”,但它涵盖的知识点其实非常密集:从数据库设计到事务管理,从JWT鉴权到跨域处理,从接口封装到前端权限控制,每一条都是实际开发中天天用到的东西。认真把这里面的原理和坑搞懂,比你刷几十道面试题都管用。最后再分享一个小习惯:做这种全栈项目,一定要从第一天就坚持写接口文档,哪怕只是简单的表格,不然联调阶段你会被自己之前设计的参数坑哭。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询