做毕设选的这个"基于Spring Boot的免税商品优选购物商城",听名字挺唬人,但真正上手之后你会发现,它本质上就是一个加了差异化业务的电商系统。这一篇我把整个项目的设计思路、技术架构、核心实现、踩坑记录全部捋一遍,源码已经整理成完整压缩包免费分享,里面包含数据库脚本、启动文档、演示截图,需要的同学直接拿去参考。
为什么推荐这个题目?免税商城保留了电商系统最经典的业务骨架——用户、商品、购物车、订单、支付,这套东西网上案例多,不用怕做不出来;同时又比普通的"XX商城"多了两个差异化卖点:免税价格对比和优选商品机制,答辩时老师想问细节,你有东西可讲,而不是只会说"我做了个CRUD"。
1. 项目整体设计与思路拆解
1.1 选题定位与差异化亮点
电商类毕设是Java方向最卷的选题之一,一抓一大把做"图书商城""二手交易平台"的,这类题目不是不行,但很难出彩。免税商品优选购物商城这个题目有三个关键词值得拆开看:
- 免税:带来了普通商城没有的价格对比逻辑。一件商品同时展示市场价、含税价、免税价、平台到手价,用户下单时还能叠加会员折扣和积分抵扣,业务层次一下子丰富起来。
- 优选:体现了商品筛选机制。管理端可以手动标记优选商品,后端还可以按销量、优选指数等维度做加权推荐,首页有专门的优选专区。
- 商城:完整保留了电商核心闭环。注册登录、浏览加购、下单支付、后台管理,一样不落。
项目定位上我定了两个原则:第一,必须能跑通、能演示、能答辩;第二,在不堆复杂度的前提下拉开业务差距。整个系统采用前后端分离架构,后端Spring Boot负责接口,前端Vue负责展示,分清楚边界,开发效率高,答辩时讲架构也更清晰。
1.2 技术选型分析
毕设技术选型不是越新越好,也不是越重越好,而是匹配自己掌控力的最好。我的选型是这套:
| 技术层次 | 具体选型 | 选择理由 |
|---|---|---|
| 开发语言 | Java 8 | 生态成熟,JDK 1.8是绝大多数毕设环境标配 |
| 后端框架 | Spring Boot 2.7.x | 自动配置简化开发,资料多、兼容性好 |
| 持久层 | MyBatis Plus | 内置通用CRUD、分页插件,写SQL的时间省一大半 |
| 前端 | Vue 2 + Element UI | 组件库齐全,拿来就能拼出完整后台界面 |
| 数据库 | MySQL 8.0 + Redis | MySQL存业务数据,Redis做缓存、购物车、Token管理 |
| 认证方案 | JWT + 拦截器 | 轻量、透明、好讲解 |
| 文件存储 | MinIO | 开源对象存储,图片上传后走URL访问,不污染数据库 |
| 构建工具 | Maven | 主流方案,依赖管理方便 |
Spring Boot版本我特意锁在2.7.12,没用3.x。原因很简单:Spring Boot 3.x要求JDK 17起步,MyBatis Plus和网上大量教程的兼容性都有坑,很多同学把一个毕设项目卡在版本兼容上,完全没必要。自研接口认证也没上Spring Security,Security的过滤器链对新手很不友好,用拦截器加JWT二十行代码就能实现同样的登录校验和角色控制,逻辑完全透明,答辩讲起来也顺畅。
1.3 功能模块全景拆解
整个商城分前台和后台两大块。前台面向普通用户,后台面向管理员。
前台功能:
- 用户模块:手机号/邮箱注册、登录、个人信息维护、收货地址管理。
- 商品模块:商品列表分页展示、按分类和关键词搜索、价格排序、商品详情、商品收藏。
- 购物车模块:增加、删除、修改数量、选中结算、登录前后购物车数据合并。
- 订单模块:提交订单、订单列表、订单详情、取消订单、确认收货。
- 支付模块:模拟支付流程,订单状态机流转。
- 优惠模块:会员等级折扣、积分抵扣、优惠券满减。
后台功能:
- 商品管理:商品CRUD、上下架、优选标记、库存调整。
- 分类管理:商品分类维护。
- 订单管理:订单列表、发货处理、订单状态跟踪。
- 用户管理:用户列表、禁用/启用账号。
- 数据统计:近7日销售额折线图、商品销量排行柱状图。
模块划分清爽之后,我开始列接口清单,把每个接口的路径、方法、参数、返回结构提前约定好。这个习惯强烈推荐——前后端分离项目最怕自己跟自己脱节,后端按清单写,前端按清单调,联调阶段能少吵十次架。
2. 核心细节解析与实操要点
2.1 数据库设计核心表
数据库是整棵项目的根,表结构设计得差,后面代码怎么写都别扭。我总共设计了11张核心表,说几个重点的。
user用户表:用户ID、手机号、密码(BCrypt加密存储)、昵称、头像、会员等级、积分。密码加密这个点必须做,答辩时被问到安全问题,直接答"使用BCrypt哈希存储,加盐处理",专业性一下就上来了。
product商品表:商品ID、商品名称、分类ID、市场价、含税价、免税价、库存、优选指数、销量、状态、主图URL。这里最核心的就是价格字段,把市场价、含税价、免税价分开存,而不是只存一个售价,这样才能支撑免税价格对比的业务展示。
product_sku商品规格表:商品ID、规格名称、价格、库存。为什么加SKU表?很多毕设商品只有单一价格单一库存,答辩时老师问一句"你的商品怎么处理不同规格?"就卡住了。加了SKU表,同一件商品可以有不同规格和独立库存,这是电商系统的专业分界线。
order订单表和order_item订单明细表:订单表存订单号、用户ID、订单总额、实付金额、状态、收货信息;明细表存商品ID、商品名称、商品图片、单价、数量、小计。主表和明细表分离是为了支持一单多品的业务,同时方便统计每个商品的销量。
表设计有三个硬性提醒:
- 金额字段一律用decimal,绝不能用double。double的精度问题会导致金额对不上,这不是理论问题,是真踩过的坑。
- 每张表都要带created_time和updated_time,MyBatis Plus的MetaObjectHandler可以自动填充,后续排查问题时无比好用。
- 不要物理删除数据。商品表加deleted字段做逻辑删除,演示过程中误删数据也不会直接消失,避免演出事故。
索引方面,user_id、product_id、order_no都要建索引。order_no建议建唯一索引,保证订单号唯一性。数据量小的时候感受不出区别,但老师问"数据量大了怎么优化",你能答出索引加分页,就赢了。
2.2 免税价格对比与优选推荐逻辑
免税商城和普通商城的最大区别,就在价格计算上。我在商品详情页展示四层价格:市场价、专柜含税价、免税价、平台到手价。到手价等于免税价乘以会员折扣再减去积分抵扣。前端用一个价格明细折叠面板展示各环节的金额是怎么算出来的,信息透明,演示效果也好看。
后端我把价格计算独立成了一个PriceCalculator组件,输入商品SKU、用户ID、积分抵扣金额,输出价格明细VO。这么做有两点考虑:一是价格计算涉及会员等级、积分、优惠券多个数据源,集中在一个组件里便于复用;二是方便下单时二次校验金额,避免用户篡改价格。
优选推荐逻辑由两个部分组成。管理端手工打标"优选商品",打标的商品会出现在首页优选专区;另外后端提供一个优选推荐接口,按销量、优选指数、好评率三个维度做加权排序,权重参数支持配置。答辩现场演示时调整权重,推荐列表实时变化,这个演示效果比静态列表好太多。
2.3 后端接口设计规范
接口设计遵循RESTful风格,但实际落地做了折中,保留了点动词语义,更贴合毕设项目的接受度。比如:
- GET /api/product/list 分页查询商品
- GET /api/product/{id} 商品详情
- POST /api/cart/add 加购物车
- POST /api/order/submit 提交订单
- POST /api/user/login 登录
- POST /api/admin/product/save 后台保存商品
统一返回结构是必须做的。我定义了一个Result对象,包含code、msg、data三个字段,code为200表示成功,500表示失败。前端所有请求都走axios拦截器,根据code做统一处理,不用每个接口单独写错误逻辑,代码干净一大截。
Controller层我坚持瘦身原则。很多人写毕设喜欢把业务逻辑堆在Controller里,一个类几百行,看起来功能丰富其实完全没法维护。我的分层是Controller只做参数接收和结果返回,业务逻辑全部沉到Service层,事务也打在Service层方法上。答辩时讲三层架构(Controller、Service、Mapper)清晰流畅,老师也认可这种工程习惯。
3. 实操过程与核心环节实现
3.1 搭建Spring Boot项目骨架
创建项目我用的Spring Initializr,选择的依赖是Web、MySQL、Redis,JDK设置为Java 8。Maven的pom.xml核心依赖大致是这些:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.20</version> </dependency> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.7</version> </dependency> </dependencies>包结构采用经典分层:
com.example.mall ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo ├── config ├── utils ├── exception └── common有一个细节值得强调:entity和VO不要混用。直接把数据库实体返回给前端,轻则多返回了密码字段,重则接口字段与前端需求不匹配,还得被迫改表结构。我为接口单独定义VO对象,虽然多写几个类,但接口设计清晰,数据安全也有保障。
3.2 用户登录与JWT权限控制
登录认证这块,我用JWT实现了无状态认证。整体流程是:用户提交手机号和密码,后端用BCrypt校验;校验通过后生成JWT Token,里面包含用户ID、昵称、会员等级;前端把Token存到localStorage,每次请求通过拦截器放到Authorization请求头;后端用HandlerInterceptor统一拦截校验,解析出用户ID存入ThreadLocal,Service层直接用。
登录拦截器核心代码如下:
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 预检请求直接放行 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BizException(401, "未登录或登录已过期"); } Claims claims = JwtUtils.parseToken(token.replace("Bearer ", "")); Long userId = Long.valueOf(claims.get("userId").toString()); UserContext.set(userId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后清除ThreadLocal,防止线程池复用导致数据串线 UserContext.clear(); } }这套实现里有两个容易踩的点。一是JWT Token中不要放密码等敏感字段,Token本身是明文可解码的,放了密码等于裸奔。二是afterCompletion里必须清理ThreadLocal,Java的线程池会复用线程,不清的话下一个请求可能拿到上一个用户的ID,这是并发安全较隐蔽的坑。
3.3 商品模块与缓存处理
商品列表是整个商城访问量最大的接口,直接用Redis做缓存。查询逻辑是:先查Redis,命中直接返回;未命中则查数据库,把结果写入Redis并设置过期时间,过期时间设置为10分钟加一个随机值,避免大量缓存同一时刻过期。这里至少要理解三个缓存问题:
- 缓存穿透:恶意请求一个不存在的商品ID,会绕过缓存直打数据库。简单防护是在查询前校验商品ID的有效性,或者用布隆过滤器拦截。
- 缓存击穿:某个热点商品的缓存刚好过期,大量请求同时打在数据库上。我用SETNX做了一把互斥锁,只让一个请求去重建缓存,其余请求短暂等待。
- 缓存雪崩:大量缓存集中在同一时间失效,数据库压力骤增。解决办法就是我上面说的过期时间加随机值,错开集中失效。
商品详情的返回VO要包含价格明细、库存状态、优惠信息、评价摘要。这里有个实战经验:查询商品列表时,SQL条件里就要过滤掉下架商品,不能让用户列表里出现"已下架"这种反常识数据。后台管理的商品列表才需要展示全量数据。
3.4 购物车与订单流程
购物车我采用了Redis临时存储加数据库持久化的方案。用户未登录时购物车数据先存Redis,登录成功后把临时购物车数据合并进数据库,再清掉Redis数据。如果时间紧张,全程用数据库存购物车也不是不行,只是高并发场景下效果差一些,但毕设阶段够用。
订单提交流程是整个项目最难写的部分,核心步骤如下:
- 前端传来购物车选中项和收货地址ID。
- 后端重新计算金额,绝不信任前端传的金额。
- 校验商品库存,库存不足直接报错返回。
- 扣减库存,创建订单主表和订单明细表。
- 清空购物车对应商品。
- 返回订单ID和待支付金额。
整个流程必须在一个事务里完成。我用@Transactional(rollbackFor = Exception.class)保证任何一步异常时全部回滚,避免出现"订单生成了但库存没扣"这种数据不一致的严重事故。
模拟支付模块我实现了一个订单状态机:待支付、已支付、已发货、已完成、已取消。每一次状态变更都更新订单表的状态字段和时间戳,前端订单详情页可以展示时间线,比如"2025-04-10 14:30 订单提交""2025-04-10 14:32 支付成功",演示体验真实,答辩也更有说服力。
3.5 后台管理与数据统计
后台管理端用的是Vue加Element UI的经典布局:左侧菜单栏,右侧内容区。商品管理页支持分页列表、按名称和分类搜索、上下架操作、优选标记、库存调整。这些功能的开发效率非常高,因为后端接口已经按规范定义好,前端就是调API渲染表格。
管理端权限和用户端分开实现。我给用户表加了一个角色字段区分普通用户和超级管理员。后台接口统一用/admin前缀,在拦截器里额外校验管理员身份。相比Spring Security的复杂授权模型,这个方案代码量少、理解成本低,毕设完全够用。
数据统计我接入了ECharts:近7日销售额折线图和商品销量TOP10柱状图。SQL写法不复杂,订单明细表按天分组求和即可。这个功能是项目加分项,很多毕设只做到基础CRUD,能拿出一个图表页面的,答辩印象分完全不一样。
4. 常见问题与排查技巧实录
4.1 Spring Boot版本太高导致的兼容问题
这个坑几乎每个做Spring Boot毕设的同学都会踩。Spring Boot 3.x要求JDK 17才跑得起来,网上搜到的教程和MyBatis Plus的兼容方案也大多是2.x时代的,一个版本错位,全局崩盘。
我的建议是直接锁定Spring Boot 2.7.x加JDK 1.8,不折腾。具体操作就是pom.xml里把spring-boot-starter-parent版本改成2.7.12,同时确保maven-compiler-plugin的source和target是1.8。有同学问我Spring Boot 3.2能不能用,能用,但没必要,毕设第一目标是稳定跑通,不是追新。
另外高版本Redis也会出问题。如果Redis版本是7.x,连接时可能要检查密码认证配置。确保spring.redis.password和redis.conf里的requirepass一致,不一致就会一直报认证失败,这个排查顺序要放在最前面。
4.2 Maven依赖下载超时
国内直接访问Maven中央仓库大概率慢到怀疑人生。我强烈建议在Maven的settings.xml里配好阿里云镜像:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>配好之后在IDEA里执行mvn clean重新导入依赖,速度立竿见影。如果项目里还有中央仓库拉不到的私服依赖,也别硬等,直接把依赖降到能解析的版本是更务实的做法。
4.3 前后端分离跨域问题
前端跑8080端口,后端跑9527端口,浏览器发请求必定触发跨域拦截。解决办法是在后端配一个全局CORS配置类:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }这里有个知识点:如果设置了allowCredentials(true),就不能用addAllowedOrigin(""),得用addAllowedOriginPattern(""),否则浏览器仍然会拦截,这个细节卡了很多天。
4.4 商品图片上传与MinIO存储
图片存MinIO对象存储而不是数据库字段,这是生产级方案。核心逻辑是前端把文件传到后端,后端调用MinIO客户端写入Bucket,返回访问URL给前端展示。
@Service public class FileStorageService { @Autowired private MinioClient minioClient; public String upload(MultipartFile file) throws Exception { String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename(); minioClient.putObject( PutObjectArgs.builder() .bucket("mall") .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return "http://localhost:9000/mall/" + fileName; } }MinIO的坑集中在权限配置。启动MinIO服务后,默认端口9000,默认控制台能访问,但上传完的图片URL在浏览器里可能会报AccessDenied,原因是Bucket访问策略是私有读写。需要在MinIO控制台把对应Bucket的策略设置为公共读(readonly),否则生成的文件链接根本打不开。这个权限问题我调试了快一天,现在写出来给大家避坑。
4.5 数据库时区与连接报错
MySQL 8.0连接时的三个必配参数是serverTimezone、useSSL、allowPublicKeyRetrieval。时不我待,直接给标准连接串:
jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=trueallowPublicKeyRetrieval=true这个参数最容易漏,MySQL 8的默认认证插件对某些客户端不允许自动获取公钥,不加就会连接失败,报Public Key Retrieval is not allowed。还有中文乱码问题,建库时指定utf8mb4字符集,连接串里带上characterEncoding=utf8,双保险。
4.6 答辩前的自查清单
项目做完不等于答辩稳了,最后一天我建议按这个清单自查一遍:
- 数据库脚本从零执行能否一次成功,SQL文件里有没有注释和空格导致中途报错。
- 核心流程能否一口气演示:注册、登录、浏览商品、加购、下单、支付、后台发货、确认收货。
- 接口返回的code和msg是否统一,前端有没有把异常信息展示成用户能看懂的文案。
- README文档是否写清楚启动步骤、数据库配置、默认账号密码、MinIO启动方式。
- 源码里不要残留System.out.println和调试代码,老师翻到会拉低印象分。
还有一个实战技巧:答辩前把演示数据做得"有演戏感"。订单状态要有已支付、已发货、已完成的多种状态;商品销量要有明显的高低层次;统计图表要有近7日的完整数据曲线。老师看到的是一套被正常运营的系统,而不是刚初始化完的空库,这种细节的加分效果非常明显。
4.7 常见问题速查表
我把完整项目里最容易出现的问题整理成了一张速查表,也贴在了项目README里,有需要可以对照排查:
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 启动报Unable to start web server | 端口被占用 | 修改application.yml的server.port或关闭占用进程 |
| Maven依赖下载卡住 | 中央仓库访问慢 | 配置阿里云镜像(见4.2) |
| Redis连接超时 | 密码不一致 | 检查redis.conf的requirepass与spring.redis.password |
| 图片上传后URL打不开 | Bucket权限为私有 | MinIO控制台设置Bucket策略为readonly公共读 |
| 前端请求跨域报错 | 后端CORS未配置 | 在配置文件加CorsConfig(见4.3) |
| MySQL连接失败 | JDBC URL缺少参数 | 补上allowPublicKeyRetrieval与serverTimezone |
| 接口返回的金额出现精度错误 | 使用double存储金额 | 数据库字段和实体类都改为decimal |
| 用jwt登录后获取不到用户信息 | ThreadLocal未清理 | afterCompletion中执行UserContext.clear() |
最后再分享一个我做毕设时的真实体会。源码免费送其实反而容易让人不珍惜,很多人拿到一个压缩包不知道从哪里开始跑。所以我整理这份项目资料时,特意把启动文档、数据库初始化脚本、常见问题FAQ三样东西放在最显眼的位置。拿到源码后建议先别急着读代码,第一步是按启动文档把项目跑起来,看到首页出来的那一刻,整个项目的掌控感就建立起来了。之后带着"我想改个XX功能"的目标去读代码,效率比从第一行读到第一百行高十倍,这个经验放到任何一个毕设项目里都适用。