1. 选题复盘一个“看起来普通”的购物网站为什么能成毕业设计的好牌每年毕业设计选题那阵子都会有学弟学妹跑来问我“师兄做什么题不容易翻车”我的答案通常很简单做个像样的中小型Web项目比选题看起来高大上但最后跑不通的更稳妥。飘香水果购物网站这个题目就是典型代表。先说结论这个项目属于“一听就知道要做什么”的经典题目。但经典不等于水。它完整覆盖了一个真实业务系统的所有环节——游客浏览、用户注册登录、商品搜索与分页、购物车管理、下单支付、后台管理、数据统计。换句话说它把前后端数据交互的逻辑全走了一遍而且业务量适中本科生在两个月内完全可以独立做完。1.1 为什么水果购物网站比“XX管理系统”更适合当毕设很多同学第一反应是做图书管理系统、宿舍管理系统这类题目确实好写但存在一个硬伤业务场景太扁平。管理系统的核心无非是增删改查老师一眼就能看穿工作量。购物网站不一样它含有一条完整的用户行为链路用户注册登录后才能下单这涉及身份认证。商品有分类、有上下架状态涉及复杂查询条件。购物车和订单有关联涉及数据一致性。下单时要从购物车生成订单、扣减库存、清空购物车这是一个典型的事务场景。后台还要有商品、订单、用户、公告等管理模块能体现完整的RBAC权限思想。选题时我给自己定了三条标准业务能讲清楚、技术能体现难度、答辩能演示出来。水果购物网站三条全中。更妙的是水果这个主题自带演示效果——数据库里塞满苹果、香蕉、车厘子的图片页面做出来色彩鲜艳答辩时比一堆灰蒙蒙的管理界面好看得多。1.2 从题目拆出来的完整需求清单拿到题目先别急着敲代码第一步一定是做需求拆解。我当时的拆法是分成“前台用户端”和“后台管理端”两条线每条线再按“游客、用户、管理员”三类角色去梳理功能点前台用户端游客浏览首页轮播图、查看商品列表、按分类查看商品、查看商品详情、搜索商品。注册用户登录注册、修改个人信息、管理收货地址、加入购物车、修改购物车数量、删除购物车商品、订单结算、模拟支付、查看订单状态、取消订单、对已购商品发表评论。所有用户查看公告、使用快捷搜索。后台管理端管理员登录。商品管理新增商品、编辑商品、上下架、设置库存、上传商品图片。分类管理商品分类的增删改。订单管理查看订单列表、订单详情、订单发货、退款处理。用户管理查看用户列表、禁用用户账号。公告管理发布公告、编辑公告。数据统计用图表展示商品销量和用户增长趋势。这个清单在写论文时也有大用。第三章需求分析的用例图、第四章的功能模块图都可以直接从这张清单生成。所以选题之后的第一个动作永远是“把需求写满一页纸”而不是去新建Spring Boot工程。2. 技术选型的几个岔路口Spring Boot版本、前端模板、中间件怎么定技术选型这块我的原则是“不炫技但要讲得出为什么”。毕设答辩老师不会要求你用最新的技术栈但一定会问“你为什么选它”。所以选型的每一环都要有一个能自圆其说的理由。2.1 后端框架为什么是Spring Boot而不是传统SSM这个题目直接点名要求Spring Boot原因其实很直白它已经是当前Java服务端开发的绝对主流。我在对比时特意去看了Spring Boot和SSM的差异SSMSpring Spring MVC MyBatis需要手动配置web.xml、spring-mvc.xml、mybatis-config.xml等一堆XML文件光是配置就能耗掉几天而且配置错误很难排查。Spring Boot的核心是自动配置和约定优于配置内嵌Tomcat一个main方法就能启动极大降低了环境搭建成本。毕设的核心时间应该花在业务逻辑上而不是花在“让Tomcat不报404”上。所以用Spring Boot不是跟风而是把省下来的配置时间投入到业务代码和论文撰写里。这里提一句如果你在网上下载的参考工程用的是Spring Boot注意看它的版本。Spring Boot 3.x要求JDK 17以上而很多学校实验室的电脑还停留在JDK 8这种情况下老老实实选Spring Boot 2.7.x避免到演示前一天才发现环境起不来。2.2 前端方案的取舍Thymeleaf服务端渲染还是Vue前后端分离前端是很多Java方向学生的痛点。我当时的决策过程是这样的方案一前后端分离Vue3 Element Plus写界面通过Axios调REST接口。好处是界面现代、代码结构清晰答辩时“前后端分离”本身就是加分项。坏处是工程复杂度翻倍需要安装Node.js、使用npm构建如果本地环境不熟悉联调阶段非常痛苦。方案二Thymeleaf模板引擎 Bootstrap 少量原生JavaScript。后端接口直接返回页面数据通过模型对象渲染到HTML。好处是只有一个Spring Boot工程打包成一个jar包就能运行部署演示极其稳定坏处是页面交互逻辑没那么炫。我最后选了方案二并做了一点升级前台用户页面用Thymeleaf Bootstrap走服务端渲染后台管理页面用Layui的表格和表单组件提升开发效率。选择这个方案最现实的理由是——答辩环境不可控。机房可能没有Node环境网络可能不稳定前后端分离项目一旦前端构建不出来整个演示就凉了。而一个jar包加一个MySQL脚本在任何一台电脑上都能五分钟跑起来。2.3 辅助组件的配套选择技术栈不只是Spring Boot本身我在这个项目里还搭配了以下组件每个都对应一个明确痛点组件解决的问题替代方案MyBatis Plus简化单表CRUD内置分页插件减少手写XML原生MyBatis、Spring Data JPAMySQL 8.0数据存储开箱即用其他关系型数据库均可Redis缓存首页公告和验证码提升访问速度可省略但加上能体现技术深度JWTjjwt无状态登录认证用户每次请求携带tokenSession、Spring SecurityLombok减少实体类的getter/setter样板代码手动生成Hutool验证码生成、文件上传等工具方法Google GuavaECharts后台商品销量统计图表Chart.js有人会问毕设有必要上Redis吗我的看法是如果时间充裕就加。因为论文的技术介绍章节总要写点东西Redis缓存是个很容易讲清楚的亮点首页公告高频读取放入Redis可以减少数据库压力。但要注意用了Redis就必须保证功能演示前也把Redis服务启动否则代码里一调用缓存就报错这是很常见的翻车点。3. 数据库设计的关键点订单快照、状态机、库存扣减一个都不能少数据库设计是这篇毕设真正的重头戏。很多同学数据库就建三四张表硬把所有信息塞在一起结果写订单功能时才发现字段不够用。我的建议是第一版就按12张表来设计后面实现每个功能都会顺利很多。3.1 核心表结构清单我按业务模块把表拆成用户域、商品域、交易域、内容域四组。下面这张表是当时的完整清单字段名属于“可直接抄作业”级别表名主要字段说明t_userid, username, password, nickname, phone, avatar, status用户表status控制账号是否禁用t_adminid, username, password, last_login_time管理员表t_categoryid, name, parent_id, sort, status商品分类表支持两级分类t_productid, category_id, name, subtitle, main_image, detail, price, stock, sales, status商品表status1为上架0为下架t_bannerid, image, link, sort, status首页轮播图t_addressid, user_id, consignee, phone, province, city, district, detail, is_default收货地址t_cartid, user_id, product_id, quantity, checked购物车建议加唯一索引(user_id, product_id)t_orderid, order_no, user_id, total_amount, pay_type, status, create_time, pay_time, deliver_time, finish_time订单主表t_order_itemid, order_id, product_id, product_name, product_image, price, quantity, subtotal订单明细表商品信息为下单时快照t_commentid, user_id, product_id, order_id, content, rating, images, status商品评价t_noticeid, title, content, create_time, status公告表t_favoriteid, user_id, product_id, create_time商品收藏可选模块这里重点提醒三个设计细节。第一用户表的密码字段不要存明文。我用的是MD5加盐的方式虽然业界现在更推荐BCrypt但毕设阶段写清楚“加盐哈希”已经比一片乌云的明文存储强太多了。答辩老师问密码安全时就讲这个。第二商品表和订单明细表之间的关联不要做成强依赖。订单明细里必须冗余product_name、product_image、price这几个字段因为商品价格随时可能调整而订单一旦生成就是历史事实。现在改分了只能影响以后的新订单不能把用户半年前买水果的记录也改了。这就是“订单快照”的用意。第三轮播图、公告这类内容表一定要带一个sort排序字段和status启用状态。否则后台上传了一张新图却没法手动控制它在首页的顺序和展示状态管理端就形同虚设。3.2 订单状态机的流转设计订单是这个项目的数据核心我把它单独画了一张状态流转表论文和答辩都直接复用状态值状态含义触发动作记录字段0待支付用户提交订单create_time1已支付模拟支付成功pay_time2已发货后台点击发货deliver_time3已完成用户确认收货或超时自动完成finish_time-1已取消待支付时用户取消update_time4已退款后台处理退款申请update_time订单状态机的意义在于系统里任何一张订单在任何时刻都落在某一个明确状态管理后台的订单列表也靠它过滤。答辩时老师如果问“取消订单后库存怎么办”你直接回答“取消且未支付时把商品库存回补”——这背后就是状态机加上一次库存更新操作。3.3 库存扣减的防超卖处理库存扣减虽然只是update语句但在设计上必须防住“超卖”。所谓超卖就是两个人同时各买10个苹果但库存只剩15个结果两个订单都成功了库存变成-5。我当时的实现是在SQL层面做条件更新Integer count productMapper.deductStock(productId, quantity); // 对应SQL: // UPDATE t_product SET stock stock - #{quantity} // WHERE id #{productId} AND stock #{quantity}这行SQL的意思是只有库存大于等于购买数量时才执行扣减受影响行数count为0就说明库存不够抛出业务异常。这个写法不复杂但它是真正的“工程级”处理思路比先查再改的方式可靠而且能在论文里明确写出“利用数据库行锁保证了库存一致性”。4. 主干功能怎么落地从登录到下单核心代码这样写这个项目最需要动手的核心模块有四个登录认证、商品分页搜索、购物车、下单事务。我按照实际开发中碰到问题的顺序来讲这些代码片段都经过了完整测试可以直接参考改造。4.1 JWT登录认证的实现要点登录流程不复杂用户提交用户名密码校验通过后签发一个JWT令牌返回给前端前端将它存到localStorage。之后每次请求在Header里带上Authorization: token后端通过拦截器解析。核心代码分两段。签发token的逻辑String token JWT.create() .withClaim(userId, user.getId()) .withClaim(username, user.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .sign(Algorithm.HMAC256(your-secret-key));拦截器里解析tokenString token request.getHeader(Authorization); try { DecodedJWT jwt JWT.require(Algorithm.HMAC256(your-secret-key)) .build() .verify(token); request.setAttribute(userId, jwt.getClaim(userId).asInt()); } catch (Exception e) { response.setStatus(401); response.getWriter().write(未登录或登录已过期); return false; }这里有个容易踩的坑拦截器会拦截所有请求但登录接口、注册接口、商品列表、商品详情这些游客也能访问的接口必须放行。我直接在拦截器配置里维护了一个白名单列表凡是需要登录的功能才进入鉴权这样既安全又不会把游客挡在门外。4.2 商品分页和关键字搜索商品列表是前台访问量最大的页面必须支持分类筛选、关键字模糊搜索和分页。我的实现思路是用MyBatis Plus的LambdaQueryWrapper避免手写复杂XMLLambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword) .or().like(Product::getSubtitle, keyword); } if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.eq(Product::getStatus, 1) .orderByDesc(Product::getSales); PageProduct page productMapper.selectPage( new Page(pageNum, pageSize), wrapper);这里的keyword来自前端搜索框categoryId来自侧边栏分类点击。整套下来整个前端只需要传pageNum、pageSize、keyword、categoryId四个参数后端返回分页对象包含records、total、current等字段前端用Thymeleaf的th:each遍历列表再用th:href拼接下一页的分页链接逻辑非常直白。4.3 购物车的增删改逻辑购物车的设计重点在“加入购物车”这一步。一个用户点击很多次“加入购物车”不应该产生多条同商品的记录而是把数量累加。所以核心逻辑是先查再增改Cart cart cartMapper.findByUserIdAndProductId(userId, productId); if (cart null) { Cart newCart new Cart(); newCart.setUserId(userId); newCart.setProductId(productId); newCart.setQuantity(quantity); cartMapper.insert(newCart); } else { cart.setQuantity(cart.getQuantity() quantity); cartMapper.updateById(cart); }修改数量时前端传新数量和购物车项id后端直接update。删除购物车项就一行delete。这里有一个体验细节购物车列表接口要join商品表查出商品名称、当前价格、主图这样前端才能展示完整的商品信息而且价格展示用商品表的实时价格不要用购物车表里冗余的旧价格。4.4 下单事务与库存扣减的完整链路下单是最能体现功力的部分因为多个数据操作必须保证“要么全成功要么全失败”。比如用户下单时库存扣了、订单生成了但清空购物车失败这就会造成数据不一致所以整体必须加事务。Transactional(rollbackFor Exception.class) public Result createOrder(OrderCreateRequest request) { // 1. 创建主订单状态设为待支付 Order order buildOrder(request); orderMapper.insert(order); // 2. 遍历购物车选中的商品写入订单明细 for (CartItem item : request.getItems()) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setProductName(item.getProductName()); orderItem.setPrice(item.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); // 3. 扣减库存数量不足直接抛异常触发回滚 int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BizException(商品库存不足); } } // 4. 清空购物车中已下单的商品 cartMapper.deleteBatchIds(request.getItemIds()); return Result.success(order); }这段代码里我最想强调的就是Transactional注解。很多参考工程里写了这个注解但没注意rollbackFor Exception.class结果业务异常抛出后事务没回滚。原因在于Spring默认只对RuntimeException回滚自定义的BizException如果不继承RuntimeException就不会触发回滚。这个细节在答辩时讲出来老师会认为你真懂事务机制。下单完成后的模拟支付更简单前端跳到一个收银台页面展示订单号和金额点击“确认支付”后端执行UPDATE t_order SET status 1, pay_time NOW() WHERE order_no ?。我没有接真实支付论文里明确写“为保障安全支付功能为模拟实现可扩展接入支付宝沙箱环境”这就够了。4.5 统一结果返回和全局异常处理为了让前后端交互整洁我加了一个Result类所有接口都返回统一格式public class ResultT { private Integer code; // 200成功其他失败 private String msg; private T data; }再用RestControllerAdvice做全局异常处理业务异常统一返回Result.fail(e.getMessage())像库存不足这种提示就能直接展示到前端页面。这个设计在论文的“系统实现特色”部分是个不错的亮点篇幅不大但显得项目规范。5. 论文和答辩怎么讲清楚这个项目从目录到高频问答的完整套路很多人代码写完了论文却不知道怎么下笔。我的经验是论文不要最后突击每完成一个模块立刻写对应章节。代码还在脑子里热乎时写出来的设计思路比事后对着代码回忆要流畅得多查重风险也低。5.1 论文目录框架我的论文目录大致如下这个结构基本是所有“设计与实现”类毕设的标准模板第一章 绪论选题背景与意义、国内外研究现状、论文组织结构。第二章 相关技术介绍Spring Boot、MyBatis Plus、MySQL、Redis、JWT、Thymeleaf。第三章 需求分析可行性分析技术、经济、操作、功能性需求用例图用例描述、非功能性需求性能、安全、易用性。第四章 系统设计总体架构设计、功能模块设计、数据库设计ER图、表结构、下单流程时序图。第五章 系统实现按前台用户模块、后台管理模块逐一截图并搭配核心代码说明。第六章 系统测试测试环境、功能测试用例表、测试结果分析。第七章 总结与展望项目完成情况、存在的不足、后续改进方向。这里要特别提醒两个地方。一个是“国内外研究现状”写得别太像百度百科。我的处理是分两条线国外线侧重生鲜电商平台的技术模式国内线写京东到家、盒马等平台的发展脉络最后收一句“本项目正是在此背景下通过Spring Boot实现一个功能完整的水果购物网站”。这段落在查重环节风险极高一定要用自己的话改写到不像任何百科原文。另一个是第三章不能只放一张用例图。老师更看重你能否把“用户下单”这种用例拆成前置条件、基本流程、异常流程。我写用例描述时基本是按这个格式前置条件用户已登录、购物车有商品、基本流提交订单→生成待支付订单→支付→状态改为已支付、异常流库存不足→提示并中断。这样写出来的需求分析含金量立马上来了。5.2 答辩PPT的页面安排PPT我控制在12页左右再多老师就没耐心看了。页面顺序如下封面题目、姓名、学号、指导老师。目录。选题背景与意义带一两句时代背景快速过。需求分析放功能模块总览图强调三类角色。技术选型列出技术栈表格每个技术给一句话理由。系统总体设计放架构图浏览器→Controller→Service→Mapper→MySQL/Redis。数据库设计放精简版ER图只展示用户、商品、订单、订单明细的关系。核心功能实现讲两个亮点一是JWT登录认证二是下单事务与库存扣减。系统演示截图放几个重点页面现场再演示一遍。系统测试展示测试用例表说明覆盖率。总结与展望一句话概括成果提两三个未来方向。致谢页请各位老师批评指正。5.3 老师最爱问的高频问题及回答思路我把答辩准备的重点押在下面这些问题上每个都准备了“一句话核心两句展开”的答法为什么选择Spring Boot答简化配置内嵌容器自动装配是目前Java主流的开发框架生态完善。密码怎么存储的答使用MD5加盐哈希避免明文存储若加强安全可换成BCrypt。购物车和订单的区别答购物车是预下单状态订单是用户明确购买意愿后的业务单据生成订单时会对商品信息做快照。库存并发超卖怎么办答用UPDATE ... WHERE stock quantity条件扣减受影响行数为0则中断防止超卖。支付是真实的吗答模拟支付出于安全考虑不接入真实渠道但预留了支付接口可扩展支付宝沙箱。用户禁用了还能登录吗答登录校验时会检查status字段为0则拒绝登录并提示账号异常。订单超时未支付怎么处理答可以在后续扩展中使用定时任务扫描超过30分钟未支付订单自动将状态改为已取消并回补库存。商品删除时分类下有商品怎么办答分类删除前会统计该分类下商品数非空则禁止删除保证数据完整性。这个项目的难点在哪答难点集中在订单事务的一致性控制和库存扣减的并发安全。测试是怎么做的答功能测试按模块设计测试用例接口层面用Postman验证并记录测试结果。答辩的核心是自信但自信的前提是真懂自己写的代码。哪怕源码是从别处参考来的只要把上面这些问题吃透老师问任何细节你都能接住。6. 手头有参考源码时怎么改造成“自己的作品”最后一块内容纯粹是经验之谈。每年这个时候很多人手里都会有一份“参考源码”我的态度是参考完全没问题但绝对不能原封不动提交。拿源码当脚手架把它改造成真正属于你自己的项目既省时间又安全。下面这几步是我觉得最关键的。6.1 跑通之前不要改代码拿到一份源码第一件事不是打开IDEA看代码而是先把环境和运行步骤搞清楚。务必确认三件事JDK版本是否匹配。编译报错先看Project Structure里的SDK版本Spring Boot 3.x配JDK 8必炸。MySQL版本和连接串。很多工程用的是MySQL 8驱动和连接参数是useSSLfalseserverTimezoneAsia/Shanghai版本不对会连不上。Redis是否需要启动。代码里凡是用了RedisTemplate的地方Redis没启动接口直接报错。如果演示环境实在不方便跑Redis干脆把缓存相关代码注释掉把原本走缓存的地方改成直接查数据库先把系统跑稳。顺序一定是先跑通再动刀。跑都没跑通的源码改起来你根本不知道是自己改坏了还是原来就坏了。6.2 改包名、改信息、换皮肤跑通之后用IDEA的全局替换把所有涉及原作者信息的痕迹清掉包名重命名右键包名→Refactor→Rename把com.xxx.fruit改成你自己的项目包名比如com.student.freshfruit。数据库名替换把application.yml里的数据库名改掉重新执行SQL脚本。前端页面文案首页标题、页脚版权、网页图标全部替换成“飘香水果购物网站”的对应内容。管理端Logo和登录页背景换成自己的图片。这些操作技术含量不高但能极大提升项目的“个人感”。答辩时老师打开页面看到的是你的信息而不是某个陌生人的ID第一印象就完全不同。6.3 至少新增一个原创功能模块要真正体现工作量最好在源码基础上新增一个独立功能。我推荐几个容易实现又出效果的方向优惠券模块后台发放优惠券前台下单时选择使用涉及订单金额重新计算。商品收藏模块用户点击收藏个人中心查看收藏列表这是逻辑最简单的新功能。我常推荐签到积分模块每日签到获得积分积分可在下单时抵扣。这个功能既有业务闭环还能在论文里写成一个完整的子模块。加一个自己独立设计的功能论文的第五章就能名正言顺地写“本人在参考现有系统基础上重点实现了XX模块”既诚实又加分。6.4 六个可以立刻落地的扩展方向如果学分和精力都允许这些扩展方向随便挑一个做成“系统展望”或者干脆作为期末加分项增加微信小程序端复用后端接口前端用uniapp或者原生小程序实现。接入支付宝沙箱支付把模拟支付流程替换成真实沙箱调用。用Redis缓存热点商品详情降低数据库查询压力。引入Elasticsearch做全文搜索提升商品搜索体验。基于用户购买记录做简单推荐同分类商品、购买组合推荐。增加数据可视化大屏前端用ECharts把后台统计页面做成大屏效果。这些方向在论文第七章“展望”部分写出来能让老师觉得你具备前瞻意识而不只是完成任务。说实话这个项目做完之后我最大的收获并不是学会了Spring Boot的某个注解而是把一个模糊的“做个水果网站”拆解成了需求、表结构、接口、页面、论文、答辩一系列可执行的任务。尤其是订单和库存那套一致性保障的思路等以后去公司做真实业务系统时依然用得上。最后提醒一句演示前一天把数据库时间和系统时间对好把浏览器缓存清干净把Redis和MySQL按正确顺序启动现场别慌你就能稳稳收场。