每年毕设开题季,网上购物系统这类Spring Boot项目几乎是计算机专业学生最常碰到的题目之一。我当年写下"基于Spring Boot的网上购物系统"这个题名时,也觉得这不就是照着一个普通商城抄一遍嘛,但真正从需求分析做到答辩,才发现这个题目的水比想象中深得多。这篇文章就把我当时从选题、数据库设计、核心模块实现到部署答辩的完整思路和踩坑过程展开聊聊。如果你正在准备的是Spring Boot毕设,尤其是商城/购物类系统,这篇可以直接拿来当行动清单。
1. 网上购物系统的选题定调:功能边界决定毕设评分
1.1 先想清楚"网上购物系统"到底要做什么
很多同学拿到这个题目的第一反应是打开某宝照着抄功能,这其实是选题阶段最大的误区。毕设系统不是复刻完整电商平台,而是用有限的时间证明你掌握了核心技术栈,并把业务闭环打通。我建议把功能边界画成两条线:一条是核心主线,一条是加分支线。
核心主线是所有购物系统绕不开的部分:
- 用户端:注册登录、商品分类浏览、商品搜索、商品详情、购物车、下单结算、订单列表、确认收货
- 管理端:管理员登录、商品管理(增删改查上下架)、分类管理、订单管理(发货/取消)、用户管理
加分支线则根据你的精力和答辩风格来定:轮播图管理、评论模块、优惠券、模拟支付、数据统计图表、物流跟踪。说实话,认真做完核心主线,配合1到2个加分模块,在答辩中已经能站稳脚跟。我见过不少同学堆了一堆半成品功能,结果每个模块都有小bug,答辩被问倒,反而不如把核心功能做扎实。
另一个容易忽略的是角色权限的区分。前台用户和管理员到底怎么共用一个登录入口?我的做法是在用户表里加role字段,普通用户是USER,管理员是ADMIN,前端根据登录接口返回的角色控制页面路由,后端在需要管理的接口上加权限校验。这样既不用拆两套用户体系,又能讲清楚"权限控制"这个技术点。
1.2 技术栈选型:为什么Spring Boot成了毕设默认答案
网上购物系统的技术栈方案其实很多,可以SSM、可以Spring Boot + JSP、也可以前后端分离。我的推荐组合是:
Spring Boot 2.7.x + MyBatis Plus + MySQL 8 + Redis + Vue 3 + Element Plus
先说Spring Boot本身。它解决了SSM时代最大的痛点——繁琐的XML配置。Spring Boot通过自动装配把数据源、Web容器、事务管理器这些基础设施默认配置好,让开发者把精力集中到业务代码上。毕设答辩被问到"为什么用Spring Boot",你要有个清晰回答:它不是功能更强大的框架,而是让Spring生态的集成成本大幅降低,内嵌Tomcat让项目可以一键启动。
MyBatis Plus在这个项目里帮了大忙。商品、购物车、订单这些都是典型单表CRUD,如果用原生MyBatis写,每张表都要写Mapper接口、XML、resultMap,光重复代码就几千行。MyBatis Plus的BaseMapper自带selectById、insert、updateById这类方法,单表操作基本不用写SQL。复杂查询比如订单分页、商品搜索,再用LambdaQueryWrapper搞定。
数据库我选MySQL 8,这是目前高校机房和云服务器最常见的选择。如果你选了MySQL 5.7也不是不行,只是记着驱动连接串里时区参数要配好,后面我会讲。
Redis在这个项目里的定位很明确:做验证码存储、JWT令牌的续期和热门商品缓存。千万别为了显得技术含量高就硬塞Redis,答辩时问你数据一致性,反而容易给自己挖坑。
前端技术选型上,我建议有一定基础的同学直接走前后端分离:Vue 3 + Element Plus + Axios + Vite。原因很简单——分离架构让"接口设计"和"跨域处理"成为答辩时的技术亮点。如果你对前端不太熟,用Spring Boot + Thymeleaf这种模板引擎方案也能过关,只是系统架构上少一个可以聊的点。
2. 数据库设计是这个系统的地基,别急着写代码
2.1 核心表结构怎么设计最合理
我先分享一个血泪教训:数据库表设计不严谨,后面每个模块都在给前期设计还债。购物系统核心表大概6张,外加1张可选的地址表,我列一下字段设计:
| 表名 | 核心字段 | 关键说明 |
|---|---|---|
user | id, username, password, phone, avatar, role, status | 密码不存明文,用BCrypt加密;role区分USER和ADMIN |
category | id, name, sort, status | 商品分类,sort控制前台显示顺序 |
product | id, name, photo, price, stock, sales, detail, status, category_id | status控制上下架;sales为虚拟销量字段 |
cart | id, user_id, product_id, quantity | 联合唯一索引(user_id, product_id),防止重复加购 |
orders | id, order_no, user_id, total_price, status, receiver_name, receiver_phone, receiver_address, create_time | 订单号唯一;status为订单状态 |
order_item | id, order_id, product_id, product_name, product_photo, price, quantity | 冗余商品名称和图片,防止商品删除后订单明细查不到 |
数据库设计上有几个细节我在实际开发中反复踩到:
第一,订单表不能只存用户ID,一定要冗余一份收货人信息。你想,用户改地址后历史订单如果关联地址表,显示就会变,而且地址删除后订单直接成了脏数据。直接把收货人姓名、电话、地址存进订单表,才是电商系统的常规做法。
第二,order_item要冗余商品快照。商城里商品价格会变,但订单里的价格应该维持下单那一刻的状态,所以order_item里存了当时的价格、商品名和图片。这和业务逻辑有关,答辩时主动讲出来是很加分的。
第三,订单号不要用数据库自增主键直接暴露给前端。我在项目里用的是"时间戳 + 随机数"生成的order_no,这样任何人没法通过订单号推断平台的单量。
2.2 订单状态机的定义:让流程可控而不是一团乱麻
订单状态是购物系统最核心的业务状态,在我实际设计里是整型枚举,五个状态走一个闭环:
待付款(0) → 待发货(1) → 待收货(2) → 已完成(3),另有已取消(4)作为分支状态
这么设计的好处是清晰。待付款的订单如果用户取消,状态变为4;待发货可以由管理员发货后变成待收货;用户点击确认收货后变为已完成。每次状态流转后端要做状态校验,比如已取消订单就不能再发货。如果不在后端校验,前端直接调接口就能把订单状态改乱。
另外orders表要建create_time索引,因为后台管理需要按时间倒序查订单。很多人在建表时忽略这个查询场景,数据量一大,后台订单列表就慢得让人抓心。
地址表我建议如果做就认真做,不做也可以。用下单时直接填写收货信息的方式,一张订单表就全搞定了。我当时把地址表省了,换来的是核心流程更聚焦,答辩时也没有因为地址模块被追问。
3. 核心功能模块的实现链路:从登录到下单全流程
3.1 登录认证用JWT:比Session更合适前后端分离
在这个项目里,登录功能首先要回答的是认证机制选型问题。传统Session方案在前后端分离架构下比较尴尬:跨域要配Cookie,前端要维护Session ID,后端重启后Session丢失。我直接用JWT(JSON Web Token)。JWT把用户ID和角色信息加密放进Token里,后端无需存Session,做到无状态认证。
具体流程是:用户传用户名密码 → 后端用BCrypt校验密码 → 生成JWT返回前端 → 前端每次请求在请求头带Authorization: Bearer token→ 后端用拦截器解析token获取当前用户。
我实现JWT用的是io.jsonwebtoken包(jjwt),核心配置在application.yml:
jwt: secret: your-secret-key-change-in-production expire: 604800 # 7天有效期,单位秒拦截器里我放行了登录、注册、商品浏览这些无需认证的接口,订单相关接口统一校验权限。这里有个关键点要提醒:拦截器注册要排除静态资源和前端页面,否则登录页的图片样式全部加载不出来,排查起来非常晕。
密码存数据库之前一定要加密。我试过用MD5,后来发现常规的MD5加不加盐都容易被彩虹表破解,最后改用了BCryptPasswordEncoder。这个类在Spring Security里,也可以单独引入spring-security-crypto依赖,只加密不启用整个Security,非常轻量。
3.2 商品浏览与购物车:细节全部藏在"最简单的模块"里
商品列表接口本身不难,难点在分页和条件查询的组合。用MyBatis Plus的LambdaQueryWrapper,把分类ID、关键字、价格排序组合起来:
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(categoryId), Product::getCategoryId, categoryId) .like(StringUtils.isNotBlank(keyword), Product::getName, keyword) .eq(Product::getStatus, 1) .orderByDesc(Product::getSales); Page<Product> page = new Page<>(pageNum, pageSize); productMapper.selectPage(page, wrapper);这段代码很好的点在于:eq和like前面的Boolean条件保证参数为空时不拼接该条件,这样前端不用传一堆空参数,后端也只有一条查询链路。
购物车模块我采用了"数据表存储"而不是Redis存储。为什么?因为购物车需要持久化,用户换了设备购物车还在,这是消费者视角的基本预期。用表存储的话就一行判断:
// 查询购物车是否已存在该商品 Cart existing = cartMapper.selectOne( new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getProductId, productId)); if (existing != null) { // 存在则数量+1 existing.setQuantity(existing.getQuantity() + quantity); cartMapper.updateById(existing); } else { // 不存在则新增 cartMapper.insert(cart); }这个逻辑简单却是购物车模块的核心,很多商城项目在这里没做"合并同商品"处理,导致购物车里出现两行同款商品,体验很差,答辩演示时也很尴尬。
3.3 下单事务与库存扣减:把"超卖"问题讲明白
下单购物车商品时,我做的是一个事务性操作:创建订单主记录、批量创建订单明细、扣减商品库存、清空购物车。这几个操作必须绑在同一个事务里,否则可能出现订单创建了但库存没扣、或者库存扣了但订单失败了。
Spring里最直接的写法是在Service方法上加上@Transactional注解。但要注意,事务不是加上了就一定生效,后面避坑部分我会详细展开。
库存扣减的超卖问题,是购物系统答辩的高频考点。我当时用了MySQL的原子更新语句:
int updated = productMapper.update( null, new LambdaUpdateWrapper<Product>() .eq(Product::getId, productId) .ge(Product::getStock, quantity) // 库存必须够 .setSql("stock = stock - " + quantity)); // 原子扣减 if (updated == 0) { throw new RuntimeException("商品库存不足"); }stock = stock - quantity是数据库层面的原子操作,再加上stock >= quantity条件,两个线程同时下单同一商品时,数据库锁会保证只有一个更新成功,从源头上防止了超卖。这段代码我建议在答辩时主动解释,属于"你知道并发问题并采取过措施"的加分项。
支付模块我没有实际对接支付宝,因为个人毕设申请商家接口比较麻烦。我实现的是模拟支付:前端点"去支付"后,后端做一次状态校验,把订单从0(待付款)变到1(待发货),并记录支付时间。这个方案能满足毕设业务闭环,但也建议你在论文里写明"生产环境可对接支付宝沙箱或微信支付Native支付",并简单提一下对接流程,证明你了解实践方案。
3.4 后台管理模块:最容易被忽略却最影响答辩分数
后台管理和前台业务比,工作量不大,但对答辩的影响非常直接——评委打开系统第一眼看的往往是你功能管理界面是否完整。后台核心我做了四块:
- 商品管理:列表分页、新增编辑、图片上传、上下架。图片上传落地到本地磁盘,数据库存访问路径
- 分类管理:分类的增删改,删除分类前要检查该分类下是否还有商品
- 订单管理:订单列表按状态筛选、订单发货、查看订单详情
- 数据统计:用聚合SQL按日期统计销售金额和订单数,配合ECharts展示折线图
这里我想提醒一个简单但回报极高的点:后台界面不要只做CRUD列表,加上几个统计数字。我在首页放了"今日订单数""今日销售额""总用户数""总商品数"四个卡片,再画一个近七天的销售趋势折线图。这个功能只需要一句SQL:
SELECT DATE(create_time) AS day, SUM(total_price) AS amount FROM orders WHERE status IN (1, 2, 3) AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time)就这么一个功能,让系统在演示时立刻有了"完整度"和"可视化"的感觉。
4. 开发阶段最容易翻车的四个环节:我的排查笔记
4.1 Spring Boot版本选择:2.x还是3.x不是小事
如果你是用Spring Initializr在IDEA里直接建项目,默认很可能是Spring Boot 3.x。这个版本有几个对毕设来说很致命的改变:JDK最低要求17,并且原有的javax.servlet包全部替换成了jakarta.servlet。如果你查资料找到的大部分参考代码都是2023年以前的,那么它们用的都是javax,直接粘到Spring Boot 3项目里就是一片红色的import报错。
我当时果断锁定了Spring Boot 2.7.18,配合JDK 8或JDK 11,这是目前网上购物系统毕设参考代码最丰富的组合。网上找的任何示例基本都能直接跑通。选版本不是越新越好,生态兼容性和搜索资源的可得性,在毕设场景里优先级更高。
Spring Boot 2和3还有一个差异要注意,就是Spring Security 5和6的配置写法差异很大。如果MyBatis Plus你用的是老版本,在Spring Boot 3里也容易碰到starter兼容问题。一句话总结:别在版本问题上追求尝鲜,稳定复现比什么都重要。
4.2 @Transactional不生效:这题在答辩现场出现概率极高
我下单模块测试时遇到过一个问题:模拟库存不足抛出异常后,订单居然还是创建成功了。排查下来是@Transactional失效,这个问题我自己踩过几个常见的坑,列出来供你自查:
第一个,方法被同类内部调用。Spring的事务是通过AOP代理实现的,同类里另一个方法直接this.orderCreate()调用,走的是对象自己,而不是Spring代理,事务拦截器根本接不到。解决办法是拆到不同的Service里互相调用,或者自己注入自己。
第二个,异常被try-catch吃掉了。@Transactional默认只在RuntimeException和Error上回滚。如果你在事务方法里catch了异常没有重新抛出,那事务判断"一切正常",只会正常提交。正确做法是catch后要throw new RuntimeException("xxx"),或者用@Transactional(rollbackFor = Exception.class)来覆盖所有异常。
第三个,方法不是public。Spring的CGLIB代理无法拦截非public方法,这个比较隐蔽,因为代码不会报错,只是静默失效。
第四个,数据库引擎不是InnoDB。MySQL的MyISAM引擎不支持事务,如果你建表时用了默认之外的引擎,事务怎么配都不会回滚。建议确认所有表的引擎是InnoDB。
4.3 跨域配置与POST请求乱码
前后端分离开发时,前端Vite跑在5173端口,后端跑在8080端口,浏览器直接请求就会出现CORS跨域报错。我当时加了一个全局CorsFilter:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns("*")和allowCredentials(true)要同时使用,因为带了凭证的跨域不能直接用allowedOrigins("*")。最开始我用了allowedOrigins("*"),浏览器报了"CORS request not http",改掉就好。
POST请求乱码这个坑,更多出现在传递中文参数时。要确保前端Axios请求头是Content-Type: application/json;charset=UTF-8,后端Spring Boot在application.yml里配一下编码:
server: servlet: encoding: charset: UTF-8 force: true4.4 图片上传后重启就丢了:本地存储路径的一课
商品图片上传我最初存在了项目里的src/main/resources/static/upload下。开发时一切正常,但每次重启Spring Boot,图片全不见了。原因很简单:IDE在rebuild时会把target目录清掉重来,而上传文件如果落在target/classes/static里,自然没法存活。
正确的做法是把上传文件保存在项目之外的固定磁盘目录,同时用WebMvcConfigurer把URL路径映射过去:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:D:/upload/"); }这样前端访问/files/20250101.jpg实际上是读取D:/upload/20250101.jpg。数据库里也存/files/20250101.jpg这个路径。部署到服务器时把file:D:/upload/改成file:/usr/local/upload/就行。这个改动虽小,但能避免一个看起来特别低级的演示事故——上传的新图片刷新就404。
5. 打包部署与答辩准备:做完项目只是完成了50%
5.1 Maven打包和后端部署的完整流程
Spring Boot项目打包部署,说起来就是一条命令,但里面的坑也不少。最标准的流程:项目根目录执行
mvn clean package -DskipTests在target目录下会生成一个可执行的jar包(Spring Boot内置Tomcat,所以是完整可运行的),然后执行:
java -jar shopping-system.jar默认端口8080。如果你在本机测试,注意8080端口是否被占用,Windows下用netstat -ano | findstr 8080查看。如果部署到云服务器,还需要在安全组放行8080端口,不然外部一直连不上。
对前后端分离项目,前端构建更简单:
npm run build生成dist目录。你可以把整个dist复制到Spring Boot的src/main/resources/static下重新打包,也可以单独用Nginx部署,把后端接口反代到8080。毕设场景我推荐前者,一个jar包全搞定,演示时只需一键启动,省去Node环境依赖。
有个部署细节值得专门提一下:application.yml里的数据库连接字符串要改成部署环境的配置。本地开发用的是localhost,服务器上要改成云数据库的内网地址或公网地址,账号密码也可能不同。很多同学项目本地跑得好好的,一部署就报数据库连接失败,大多是这里没改。
5.2 演示数据准备和演示流程设计
一个很实在的建议:在答辩前准备好一套完整的演示数据。我准备了两个账户(普通用户test、管理员admin)、6个商品分布在3个分类下、每个分类都有商品在售、还有几条不同状态订单(待付款、待发货、待收货、已完成各一条)。演示时从注册登录开始,走一遍检索商品、加购、下单、模拟支付、管理员发货、确认收货的完整链路。这套流程走完大概8分钟,正好覆盖评委最关心的业务闭环。
演示时务必先把MySQL、Redis、后端、前端全部启动好一遍确认无报错,再开始录屏。我的习惯是本地录一遍完整流程存在手机里,万一现场演示时网络卡顿或浏览器异常,放录屏兜底。
5.3 答辩追问环节,这五个问题提前背熟
答辩被问技术问题是有规律可循的。我把自己被问到的和同学被问过的整理成高频五问:
第一个,Spring Boot自动装配原理。核心回答:@SpringBootApplication包含@EnableAutoConfiguration,它通过META-INF/spring.factories加载自动配置类,再由@ConditionalOnClass等条件注解按需生效。你说到"条件注解"这个层级,基本就是满分答案。
第二个,MyBatis Plus和MyBatis的区别。站在项目角度回答:MyBatis Plus内置通用Mapper和条件构造器,单表CRUD零SQL;但复杂多表查询仍然用原生的@Select注解或XML编写,避免滥用。这个回答体现出你了解工具边界。
第三个,购物车数据为什么放数据库而不是Redis。回答:Redis读写快,适合做缓存;购物车放数据库保证持久性和跨端一致性。Redis在这里做验证码缓存、高频读取的热门商品缓存。
第四个,库存超卖如何解决。回答了上面update ... set stock = stock - #{num} where id = #{id} and stock >= #{num}的原子更新方案后,可以再补一句:如果在低并发场景已经够用,高并发则可以升级为Redis Lua脚本预扣减或者引入分布式锁。这句话表示你理解更深一层的延伸。
第五个,订单状态为什么不用字符串而用int。回答:状态是一个固定集合,存储用int节约空间,查询走索引更快,语义用常量类统一管理,避免字符串散落各处。项目里我用了OrderStatusEnum枚举来统一维护这层语义。
这段内容写到这,想说的是:网上购物系统这个题目看起来很"烂大街",但它覆盖了Spring Boot生态中最主流的Web开发全流程,从分层架构到事务、缓存、认证、文件上传、部署运维,每个点都是毕设的核心得分点。希望这份从功能边界到数据库设计,从核心实现到避坑记录的完整链路,能让你在这个经典题目里做出自己的深度。踩过的坑我写在前面了,你绕过去,把时间花在把业务细节打磨完整上——答辩时你会感谢自己当时没在版本问题和不生效的事务上死磕太久。