又到毕业设计季了。每次看到群里有人问“Java毕设选什么题”,我脑子里第一个冒出来的基本都是这个:基于Spring Boot的家电销售展示平台。它足够经典,经典到技术栈完全不用担心过时;它又足够“小而完整”,登录注册、商品展示、购物车、下单支付流程、后台管理一个不少,正好把大学四年学的东西串成一条线。今天这篇就结合我自己实际带过、帮人改过的好几套类似项目,把这个题目从选题定位、数据库设计、后端编码、前端联调,一直拆到论文和答辩,完整过一遍。不管你是打算从头自己写,还是手头有代码但没吃透逻辑,这篇都能帮你少走很多弯路。
1. 第一步先搞清楚:这个毕设到底要做什么
1.1 需求拆解:销售展示平台的核心功能边界
很多人拿到这个题目第一反应是“做一个像某东一样的商城”,然后就开始焦虑——是不是要做秒杀、要做优惠券、要做推荐算法?不是的,兄弟,这是毕业设计,不是创业项目。毕设的核心目标是把你学过的东西系统地展示出来,让答辩老师看到你具备独立完成一个完整业务系统的能力。所以功能边界一定要收住,做到“麻雀虽小五脏俱全”就够了。
一套标准的家电销售展示平台,用户端核心功能就四块:一是商品浏览,包括分类筛选、关键词搜索、分页展示、详情查看;二是用户中心,包括注册、登录、个人信息维护;三是购物车,加购、改数量、删商品、勾选结算;四是订单,提交订单、支付(模拟)、查看订单列表、确认收货。后台管理端两到三块:商品管理(增删改查和上下架)、分类管理、订单管理(查看、发货)。再往上想,可以加个轮播图管理、公告管理这类锦上添花的功能。但分清楚优先级,把核心链路做到稳定不出错,比堆砌一堆用不上的功能点要重要得多。
我见过不少学生一上来就规划了十几个数据表、二十多个页面,结果数据库关系理不清,代码写了一团浆糊,最后连基本的注册登录都有Bug。所以我的建议是:前端时间不要急着敲代码,先花一个下午,把用户从“进入首页”到“下单成功”这条主路径走一遍,把每一步涉及到的页面、数据、接口列出来。主链路通了,你的毕设就已经完成六成了。
1.2 技术选型:为什么是Spring Boot这套组合
技术选型是整个项目最关键的决策之一。用Spring Boot作为底座,几乎是这类毕设的最优解。理由很简单:Spring Boot内置了Tomcat、自动配置了一大堆常用组件,你不用像以前写SSH框架一样去配一堆XML文件,项目跑不起来大概率是配置写错的情况能减少一大半。对毕设这种“必须当场能跑起来演示”的场景来说,稳定压倒一切。
持久层我建议用MyBatis,不要用Spring Data JPA。原因很实际:MyBatis的SQL是你自己写的,答辩老师问复杂度高的查询怎么实现时,你能拿着XML里的SQL讲得头头是道;而JPA自动生成的SQL,遇到复杂联表查询时容易讲不清楚。当然,现在更流行的是MyBatis-Plus,自带分页插件和条件构造器,写起代码来确实快很多,毕设用也不会有问题。我的建议是:如果项目是参考网上的教学视频做的,那就视频用什么你就用什么,别在技术选型上搞创新,怎么稳怎么来。
前端方案则是另一个需要尽早确定的分叉路口。方向一:Thymeleaf模板引擎加Bootstrap或Layui,服务端渲染,后端写页面,工程量小,一个人两周就能搞定;方向二:前后端分离,Vue加Element UI加Axios,界面好看,交互流畅,但你要同时处理跨域、Token校验、异步渲染等问题,工作量至少增加三成。这个选择题没有标准答案,取决于你距离交稿还有多少时间。时间紧就踏踏实实走方案一,界面干净整洁一样可以拿高分。
2. 把地基打好:数据库表设计与接口规划
2.1 六张核心表的字段设计与关联关系
所有业务系统,数据库设计就是地基。网上那些“数据库有59张表”的毕设模板,听听就行了,实际毕业设计用到的表控制在6到8张最合适。让我给你列一份我反复调整过、觉得最顺手的表结构,你直接对着改就行。
用户表t_user:主键id、用户名username(唯一索引)、密码password(记得加密存储,MD5加盐或BCrypt都可以)、手机号phone、头像avatar、角色role(0代表普通用户,1代表管理员)、注册时间create_time。字段别贪多,够用就行,会员等级、积分这类字段统统不要加。
分类表t_category:主键id、分类名称name、排序字段sort_order。家电的分类一般就是冰箱、洗衣机、空调、电视、厨卫电器这几个,两级分类可以做,但没必要,建议只做一级,后台管理时也省事。
商品表t_product:主键id、分类idcategory_id(外键逻辑关联,不加物理外键)、商品名称name、副标题subtitle、主图main_image、轮播图sub_images(用JSON字符串存,或用逗号分隔,存到一个字段里即可)、详情描述detail、价格price(用Decimal,精确到分)、库存stock、销量sales、上下架状态status、创建时间create_time。这里是重点,字段类型要注意:价格用DECIMAL(10,2),千万别用double或float,存银行家算法算出来的浮点误差够你喝一壶的。
购物车表t_cart_item:主键id、用户iduser_id、商品idproduct_id、数量quantity、选中状态checked。联合唯一索引(user_id, product_id),保证同一个用户对同一个商品只有一条购物车记录,后续加购只需要做累加操作。
订单表t_order:主键id、订单号order_no(用时间戳加随机数生成,唯一)、用户iduser_id、订单总金额total_price、订单状态status(0待付款、1已付款待发货、2已发货、3已完成、4已取消)、收货人receiver_name、收货电话receiver_phone、收货地址receiver_address、下单时间create_time、支付时间pay_time、发货时间deliver_time。订单表一定要有冗余的收货人信息,不能去关联用户表取地址,因为收货地址属于订单快照,用户之后改了地址不能影响历史订单。
订单明细表t_order_item:主键id、订单idorder_id、商品idproduct_id、商品名称product_name、商品主图product_image、购买时单价price、购买数量quantity、小计total_price。这里又是有意冗余,把商品名称和图片也存进来了,理由和订单表冗余收货信息是一样的——商品信息可能变,订单记录不能变。
这六张表之间的关联关系,你在论文画ER图时这样描述:用户与购物车是一对多,用户与订单是一对多,商品与购物车是一对多,商品与订单明细是一对多,订单与订单明细是一对多。逻辑扁平、清晰,答辩时三五句话就能讲透。
2.2 接口清单:前后端约定先谈清楚
表设计好之后,下一步不是急着写代码,而是把接口清单列出来。我做项目习惯先花半小时把接口文档写明白,后面联调会省下大把时间。你甚至不需要用Swagger,Excel表格或者Markdown都行,就是把这些信息列清楚:接口路径、请求方式、请求参数、返回结构。
用户端接口大概有这些:POST /api/user/register注册、POST /api/user/login登录、GET /api/user/info获取当前登录用户信息、GET /api/category/list分类列表、GET /api/product/list商品分页列表(支持分类、关键词、排序参数)、GET /api/product/detail/{id}商品详情、GET /api/cart/list购物车列表、POST /api/cart/add加购、PUT /api/cart/update修改数量与选中状态、DELETE /api/cart/delete/{id}删购物车项、POST /api/order/create提交订单、GET /api/order/list我的订单、PUT /api/order/confirm/{id}确认收货。管理端接口:POST /api/admin/product/save新增或更新商品、DELETE /api/admin/product/{id}删除商品、GET /api/admin/order/list订单列表、PUT /api/admin/order/deliver/{id}发货。
重点来了,所有接口的返回结构必须统一。我的习惯是封装一个Result类,用code、message、data三个字段,200表示成功,500表示业务异常,401表示未登录。这样做的好处太明显了:前端判断逻辑只需要写一次,处理错误也方便。我见过不少项目,有的接口返回成功是{success: true},有的返回{code: 0},前端一堆分支判断,看着就头大。统一封装,狗看了都摇头,但就是很多人不做。
2.3 分页与搜索:数据库层面的两个设计细节
分页查询是商品列表最核心的接口,几乎所有用户操作都会经过它。用MyBatis-Plus就直接用Page对象加分页插件,一行配置搞定。手写MyBatis的话,分页SQL主要靠LIMIT offset, pageSize实现,同时要用COUNT(*)查总记录数。这里有个细节要注意:COUNT(*)不要和列表查询放在同一个复杂SQL里做,最好拆成两个方法,一个数总数,一个查数据,SQL各自优化,性能更好。虽然毕设数据量不大,但这个习惯能体现你懂分页原理。
搜索和排序的SQL写法也需要提前想清楚。商品名称模糊搜索用LIKE CONCAT('%', #{keyword}, '%'),注意不能用'%' + #{keyword} + '%'这种写法,在MySQL里会因隐式类型转换产生隐藏问题。排序场景一般两类:按价格从低到高、按销量从高到低。要让排序字段可配置,SQL里不能用简单的字符串拼接,应该用<choose>标签配合sidx参数做白名单映射,或者直接用MyBatis的${}搭配枚举校验。安全性上,这里不要觉得毕设就不用管,SQL注入的预防措施写出来,论文里还能多一小节内容。
3. 核心代码攻坚:从登录鉴权到订单闭环
3.1 工程初始化:目录结构与基础配置
技术方案定完,直接动手搭建工程。我习惯的做法是直接用IDE(IDEA社区版免费够用)创建Spring Initializr项目,依赖勾选Spring Web、MyBatis(或MyBatis-Plus)、MySQL Driver、Lombok。Java版本别追新,用8或11都行,稳定为主。Spring Boot版本用2.x系列最后一个稳定版就好,3.x对JDK版本和部分依赖有要求,没必要给自己挖坑。
工程目录结构要分层清晰。我常用的分包方式是:controller放接口层、service放业务逻辑(先接口后实现类,impl包放实现)、mapper或dao放数据访问接口、entity或model放实体类、dto放接收参数的类、vo放传给前端的类、config放配置类、common放统一返回结果、异常处理、工具类。网上很多教程把DTO和VO混在一起用,实体类直接返回给前端,省事儿是省事儿,但商品实体里有库存、状态这些不该传给前端的字段,列表接口就会把多余数据都暴露出去。规范一点没什么坏处,论文里还能写“我采用了DTO模式对各层数据模型进行隔离”。
配置文件application.yml里需要提前处理好几件事。数据库连接串设置时区serverTimezone=Asia/Shanghai,不然会报时区错误;配置spring.jackson.date-format和time-zone,否则LocalDateTime在JSON里会变成数组,前端没法解析;上传文件的路径要配好,我是用file.upload-dir自定义配置项指定一个磁盘目录,再把静态资源映射/images/**指向它,这样上传的商品图片才能通过URL访问到。
3.2 登录认证与拦截器:用最稳的方案
登录认证这块,我的建议是毕设用最简单可靠的方式:Session配合拦截器(HandlerInterceptor)。不要上Spring Security,不要上Shiro,更不要自己造JWT轮子。理由很简单,这些框架都有自己的安全模型和配置方式,你光是搞明白Security的过滤器链就要花掉两三天,而且配置不当容易出现各种莫名其妙的拒绝访问问题。用原生Session实现,代码量少、逻辑透明、答辩时老师问起来你能理直气壮地从头讲到尾。
具体实现是这样的:用户登录成功后,把用户信息放进Session,比如session.setAttribute("loginUser", user),loginUser可以是一个只包含id、用户名、角色、头像等基础字段的对象。然后写一个拦截器,实现HandlerInterceptor接口,在preHandle方法里判断当前请求的Session里有没有登录用户。如果没有,而且请求的接口是需要登录权限的,直接返回401或重定向到登录页;如果有,放行。这里的关键点是要把“公开接口”和“需登录接口”分清楚。注册登录这类接口不能拦截;商品列表和详情这类接口建议不拦截(游客也能看商品,这是电商的基本体验);购物车、订单相关接口必须拦截;管理端接口还要额外校验角色是否为管理员。
拦截器还有个作用:把当前登录用户信息填充到业务代码里。你可以写一个UserContext工具类,用ThreadLocal存当前请求的登录用户,业务层随时取用。用ThreadLocal要注意请求结束后必须调用remove()清除,否则线程池复用线程会串数据。这个细节你要是能在答辩时说出来,绝对是个加分项。
3.3 商品搜索分页与详情:高频接口的实现细节
商品列表接口是整个项目被调用最多的接口,实现上值得多花点心思。接收参数大概是:categoryId(分类筛选)、keyword(关键词搜索)、pageNum、pageSize、sort(排序规则)。Service层的逻辑分三步走:第一步组装查询条件,用MyBatis-Plus的LambdaQueryWrapper或者手写动态SQL;第二步执行分页查询;第三步把实体列表转换为VO列表,主要工作是裁剪掉库存、状态等不需要的字段,再补上一些前端需要的派生字段。为什么要单独做一步转换?因为直接用实体类转JSON返回,字段太冗余,前端用起来麻烦,数据传输量也大。一个ProductVO只包含商品id、名称、副标题、主图、价格、销量、分类名称,结构清爽,接口文档也好看。
商品详情接口相对简单,根据id查出商品实体、分类名称、以及从subImages字段拆分出来的图片列表。但这里有性能优化的机会。首页热门推荐、分类页这些场景,用户需要的是列表数据,并不需要detail字段这种几百字的富文本,所以列表查询应该明确指定只查列表需要的列,而不是SELECT *。虽然毕设数据量下看不出差别,但这个习惯体现了你对查询性能有感知。
商品列表还有一个容易忽略的问题:分类树。如果只做一级分类,用不到;但如果做了两级分类,列表接口就得支持“点击父分类看到所有子分类的商品”。这个逻辑写出来不复杂,子分类集合用IN查询就行,但要在设计接口参数时提前预留好categoryId的层级语义。
3.4 购物车与下单:事务和库存一致性怎么保证
购物车这块逻辑不难,但容易出Bug。加购的逻辑是:先查该用户购物车中是否已有这个商品,有的话数量累加,没有的话新建一条记录。修改数量时要判断不能小于1,也不能超过库存上限。删除支持批量删除或单条删除。这些接口实现时都要注意从Session或Token中解析出当前用户id,而不是让前端传入用户id。如果前端能随便传userId操作别人的购物车,那这个系统就太离谱了,答辩时万一被问到这个点就尴尬了。
下单是整个系统的核心链路,也是最能体现设计水平的地方。我按下单流程把关键逻辑梳理一遍:
第一步,从购物车中读取当前用户选中的商品项。第二步,遍历校验:商品是否仍为上架状态,库存是否充足,价格需要从商品表实时取,不能信任购物车里的快照价格。第三步,扣减库存,这里要特别注意并发问题。正确写法是使用带条件的更新SQL:UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}。这条SQL如果影响行数为0,说明库存扣减失败,要么是库存不足,要么是商品不存在,直接抛异常回滚。这就是乐观锁的思路,比先查后改安全得多,能防止超卖。第四步,生成订单主记录和订单明细记录。第五步,清除购物车中已下单的商品项。第六步,模拟支付:毕设里一般是点击提交订单后直接把状态置为已支付,或者单独做一个支付弹窗模拟,调一个不需要接入真实支付网关的接口,随机成功或失败。
整个流程必须放在一个事务里。在Service方法上加@Transactional(rollbackFor = Exception.class),任何一步抛异常,前面所有操作全部回滚,数据库不会处于中间状态。注意rollbackFor一定要指定,默认情况下@Transactional只在RuntimeException下回滚,如果你抛的是自定义的检查异常,事务不会回滚,这坑我很早之前踩过,印象太深了。
4. 前端呈现与联调上线:让系统真正能跑能演示
4.1 前端方案:Thymeleaf还是前后端分离
前端方案前面提了一嘴,这里展开说说我的真实看法。如果你想在短时间内做出一套看着舒服的界面,Thymeleaf加Bootstrap是个稳妥选择。Spring Boot对Thymeleaf的支持很友好,在application.yml里配置好视图解析器,Controller返回"index"字符串就对应templates/index.html模板。模板语法本身也不复杂,th:each遍历商品列表、th:href动态拼接链接、th:if做条件判断,有HTML和Java基础的人一两天就能上手。页面继承用layout:fragment定义公共头部、底部,商品页、详情页复用同一套导航栏,开发效率很高。
前后端分离方案的好处也很明显,页面质感能拉开差距。用Vue加Element UI或者更轻量的Vant(移动端风格也挺好看),页面交互流畅度完全不是服务端渲染能比的。代价是你要自己处理跨域。我推荐的做法是在后端写一个CORS配置类,用WebMvcConfigurer的addCorsMappings方法统一允许跨域,同时allowCredentials设为true,allowedOriginPatterns用具体域名或*,前端Axios配置withCredentials: true,这样Cookie和Session才能在跨域请求中正常传递。这个配置网上抄很容易,但要注意版本差异,Spring Boot 2.4之后通配符配置有变化,你如果照旧教程写可能跨域依然报错。
如果你时间紧张、又想要前后端分离的样子,还有一个折中方案:后端只写一套Thymeleaf首页,其他模块做成接口返回JSON,用原生JavaScript加少量Vue语法写页面。不需要完整脚手架,也不需要Node环境,照样能做出局部动态刷新的效果。
4.2 商品页性能优化与图片处理
前端性能优化是毕业设计里容易被忽视、但产出效果最明显的环节。商品列表页最常见的性能问题就是图片加载:一个页面几十张大图,首屏全部请求,服务端压力大,用户体验也差。最简单的优化方法是图片懒加载。Bootstrap的Layui自带懒加载组件,原生的做法是用>