
又到毕业设计季了后台经常有人问我“有没有既简单又能拿得出手的Java毕设项目最好是Spring Boot系的能讲清楚、能跑通、还能扩展的。”今天就把我之前折腾过的“基于Spring Boot的饰品商城系统”完整复盘一下从需求拆解到技术选型从核心代码到常见坑位全部摊开讲明白。这个项目特别适合做Java毕设或者Spring Boot入门练手因为饰品商城是一个极其典型的“前后端交互数据库落盘业务状态流转”的教学案例复杂度介于图书管理(太简单)和电商秒杀(太难)之间刚好能覆盖Controller、Service、Mapper、事务、权限、文件上传、订单状态这些Spring Boot高频考点同时又能让面试官看到你有“业务建模”的意识。在整个设计和实现过程中我刻意避开了纯“骨架代码”模式把每一层都做成你能直接在答辩时讲出“为什么这么做”的形态。而且我特意把演示、调试和定制相关的经验也写进去了因为毕设不光要求代码能跑更要求你能在讲台前把项目“讲活”。下面我会按照“整体设计→技术选型→核心模块实现→实操运行→常见问题→个性化扩展”这条线一步步往下说所有配置和关键代码都给你标注清楚照着操作大概率能一次跑通。1. 项目整体设计与需求拆解1.1 为什么选“饰品商城”作为Java毕设题目很多同学选毕设题目容易走两个极端要么选“学生管理系统”这种图书馆里躺了十年的老题目要么选“高并发微服务电商平台”这种答辩时自己都讲不清楚的新题目。饰品商城系统的聪明之处在于它的业务域非常收敛但技术覆盖面一点都不窄。用户登录注册、商品浏览搜索、多条件筛选、购物车增删改、订单创建与状态跟踪、后台管理员对商品和订单的管理这些能力几乎就是一个小型电商的核心骨架既能体现你对Spring Boot框架的整体把握又能展示你对数据表设计和业务异常处理的理解。更重要的是饰品商品有它独特的模型特征商品既有分类(耳环、戒指、项链)、材质(银饰、珍珠、合金)、风格(复古、简约、轻奢)又有库存、价格、主图、详情图、上架状态、推荐权重等属性。这种“多字段多状态”的实体设计比单纯的“图书-作者”多对多关系更有真实感你做数据库设计时能顺理成章地讲出范式规范、索引设计、逻辑外键取舍这些细节。我实际做下来把全部模块理清楚大概花了三周每天两到三个小时时间非常可控。还有一个容易拿到好处的地方饰品商城天然适合做图片展示前端页面会很“漂亮”答辩演示时第一眼的视觉冲击力直接拉满。而且你可以很自然地把“图片上传”这个Spring Boot高频考点塞进项目里没有那种硬凑功能的感觉。1.2 核心功能模块与角色权限划分先看我最终落地的功能清单这是我在需求阶段用思维导图梳理出来的两层角色模型前台用户端(未登录/已登录)商品列表分页、按名称模糊搜索、按分类/风格筛选、按价格区间排序商品详情页(主图轮播、参数表、库存与销量展示)购物车加入购物车、修改数量、删除条目、全选/批量结算订单创建订单、订单详情展示、模拟支付、取消订单、确认收货用户中心个人信息修改、我的订单列表、修改密码登录注册基于JWT或Session的用户认证密码BCrypt加密后台管理端(管理员角色)管理员登录校验(独立入口区别于用户端)商品管理新增商品、编辑上下架、修改库存、上传图片分类管理添加/删除商品分类统计该分类下商品数量订单管理查看所有订单、按订单状态筛选、发货操作、处理退款标记用户管理禁用/启用账号查看注册用户列表注意我刻意把“模拟支付”而不是“真实支付”做进去这是毕设项目一个重要的取舍逻辑。对接支付宝或者微信支付意味着你要处理回调、签名、商户号申请这些对课程设计来说太重而且答辩时反而会陷入“支付安全”的深水区。做成模拟支付接口路径和业务逻辑完全独立后续你如果想升级真实支付只需要替换支付实现类完全不伤核心架构这个设计思路是面试官很喜欢的“可扩展性”。2. 技术选型与架构搭建解析2.1 Spring Boot版本与依赖规划项目我选择的是Spring Boot 2.7.x而不是最新的3.x。为什么这是我在踩过几次坑之后得出的结论Spring Boot 3要求JDK 17基线很多学校机器和之前的教程模板都还是JDK 8毕设阶段没必要跟这个风。2.7.x是基于JDK 8的最后一个大版本稳定性极佳MyBatis、Swagger、Spring Security等生态的兼容性资料也最多。万一你遇到问题搜索引擎里几乎能找到现成答案。如果你非要用3.x其实也能跑但必须把javax命名空间改成jakarta这个坑在导入旧教程代码时特别痛苦没必要冒这个险。依赖规划我采用这样一个组合组件选型理由ORM框架MyBatis-Plus单表CRUD不用写SQL多表查询用自定义XML实现效率高模板引擎Thymeleaf前后端不分离一个jar包直接部署适合课程设计展示认证方案Spring Security JWT既能体现安全知识又能让前后端接口无状态鉴权数据库MySQL 8.0用InnoDB引擎支持事务和外键演示环境用8.0特性(CTE、窗口函数)文件存储本地磁盘存储图片上传到项目的upload目录静态资源映射对外访问接口文档springfox Swagger 3.0生成可在线调试的API文档答辩时加分工具库Hutool封装了日期、加密、ID生成减少重复代码这个组合的好处是每个技术点都有非常明确的“答辩话术”。比如你讲MyBatis-Plus可以说“基于MapStruct和通用Mapper减少了70%的单表SQL”讲Security可以说“基于过滤器链实现JWT自动刷新和权限校验”。技术选型不是越新越好而是在资源最丰富的版本上落地得最稳。2.2 项目分层架构与目录结构我采用的是经典的“controller-service-mapper-po”四层结构额外加了一层config包放配置类。整个代码结构是这样的sbshop/ ├── pom.xml ├── src/main/java/com/example/shop/ │ ├── ShopApplication.java // 启动类 │ ├── config/ │ │ ├── SecurityConfig.java // 安全配置 │ │ ├── WebMvcConfig.java // 资源映射、拦截器 │ │ └── SwaggerConfig.java │ ├── controller/ │ │ ├── UserController.java │ │ ├── ProductController.java │ │ ├── CartController.java │ │ ├── OrderController.java │ │ └── AdminController.java │ ├── service/ │ │ ├── UserService.java impl/ │ │ ├── ProductService.java impl/ │ │ ├── CartService.java impl/ │ │ └── OrderService.java impl/ │ ├── mapper/ │ │ ├── UserMapper.java │ │ ├── ProductMapper.java │ │ ├── CartMapper.java │ │ └── OrderMapper.java │ └── po/ │ ├── User.java │ ├── Product.java │ ├── Category.java │ ├── CartItem.java │ └── Order.java └── src/main/resources/ ├── application.yml ├── mapper/ // MyBatis XML文件 ├── static/ // 前端静态资源 └── templates/ // Thymeleaf页面一个很实用的心得分层时一定要把每个类的职责边界说清楚。比如Controller只负责接收参数和返回视图Service只负责业务逻辑和事务控制Mapper只负责数据库交互。很多同学喜欢在Controller里直接写一大堆业务代码确实跑得通但答辩老师一看就知道你没有“软件工程意识”。我在项目里用事务注解Transactional专门包裹了“创建订单时扣减库存”“支付时修改订单状态”这两个业务方法这一点强烈建议保留——这是面试官最看重的数据一致性处理能力。2.3 核心配置文件与参数逻辑application.yml里最关键的配置项我做成一个清单你直接照着改数据库账号密码就能跑server: port: 8080 servlet: context-path: /shop spring: datasource: url: jdbc:mysql://localhost:3306/shop_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false servlet: multipart: max-file-size: 10MB max-request-size: 10MB mvc: static-path-pattern: /static/** mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两个细节我必须强调。第一数据库连接URL里必须加上serverTimezoneAsia/Shanghai否则你在国内服务器或本地部署时会遇到“时区差8小时”的诡异问题插入数据库的时间比当前时间少8小时。第二Thymeleaf缓存必须设置为false开发时改模板才能及时生效不然你改了个HTML页面刷新浏览器还是老样子会困扰很久。我在首次调试时就是被这两个小坑卡了两个多小时现在直接写出来希望你别再重复踩。3. 核心业务模块实现与关键代码剖析3.1 用户认证与权限控制的不绕路写法用户认证我采用Spring Security JWT方案。之所以不用纯粹的Session是因为毕设答辩时会有老师问“你怎么做用户鉴权会不会有Cookie安全问题”JWT的无状态设计能让你答得更有底气。核心思路是用户登录成功之后后端签发一个包含用户ID和角色的token返回给前端前端把token存在localStorage里每次请求在header里带Authorization: Bearer xxx后端过滤器解析token放行有效用户。关键实现可以分成两步。第一步自定义Filter来处理tokenComponent public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); // 解析JWT校验签名并提取用户信息 Claims claims JwtUtil.parseToken(token); if (claims ! null) { // 使用Spring Security上下文设置当前用户 String username claims.getSubject(); // 可以把用户信息放入request的作用域方便Controller获取 request.setAttribute(loginUser, username); } } chain.doFilter(request, response); } }第二步在Security配置里放行登录、注册、商品列表等公开接口其余接口走认证Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeHttpRequests(auth - auth .antMatchers(/user/login, /user/register, /product/**).permitAll() .antMatchers(/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }这里有一个特别容易犯错的地方Spring Security默认会为所有请求开启CSRF保护但你后端是JWT无状态接口如果忘了csrf().disable()你POST登录会被直接拒绝返回403页面找半天都不知道为什么。因为我当初在这个地方绕了好久现在基本看一眼报错信息就能反应过来但新同学还是容易蒙圈。密码加密我使用的是BCryptPasswordEncoder。它最大的特点是每次加密结果都带有随机盐就算两个用户密码相同数据库里存的hash也不同。你完全不需要自己写MD5加盐算法直接用Spring Security自带的加密器即可答辩时可以说“使用BCrypt算法防止彩虹表攻击”。3.2 商品与分类管理的数据库设计饰品商城数据库我总共设计了5张核心表外加一张用户_地址附表。核心表设计如下表名主关键字段说明userid, username, password, role, avatar, status用户表role区分用户/管理员categoryid, name, description, sort商品分类表productid, category_id, name, subtitle, main_image, detail_images, price, original_price, stock, sales, status, recommend商品表detail_images用JSON数组字符串存储cart_itemid, user_id, product_id, quantity, checked购物车项checked表示是否选中结算orderid, order_no, user_id, total_amount, status, receiver_name, receiver_phone, receiver_address, create_time, pay_time, ship_time订单表状态用int类型预留扩展有同学会问为什么商品表里要冗余一个sales销量字段而不是每次都用SUM(order_item.sales_count)计算这是个好问题直接回答就是“用空间换时间”。在饰品商城的首页和商品列表页销量排行是一个高频查询如果每次都要多表关联聚合计算数据库压力大到页面加载会变卡。而当订单完成时只要同时执行一条UPDATE语句给销量1即可。这就是典型的“读多写少”场景下的反范式设计我把它作为答辩论点之一。商品详情里的多图我用的是detail_images字段存JSON数组字符串(比如[/static/img/p1_1.jpg,/static/img/p1_2.jpg])。这样做的优点是实体表结构简单不用单独建一张图片子表修改图片时直接覆盖整个JSON字段。缺点是没有外键约束但这不影响使用者因为你是通过Service层进行写入和读取的统一做了JSON序列化和反序列化。很多互联网公司在业务不复杂的场景也是这样做的完全可以讲得通。商品查询我写了一个支持多条件动态拼接的Mapper核心片段如下select idselectByCondition resultTypecom.example.shop.po.Product SELECT * FROM product where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR subtitle LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC /select这段代码中where标签会自动处理首条AND的删除非常核心。如果你自己手写SQL很容易在动态条件中多出一个“WHERE AND”语法错误而MyBatis的where帮我们做了这个拼接优化这也是我推荐使用XML动态SQL而不是纯注解写SQL的原因。顺带讲一个搜索排序细节当用户点击“按销量排序”时我们不需要写复杂窗口函数直接加一个ORDER BY sales DESC即可因为sales已经是实时更新的。3.3 购物车与订单状态的流转设计购物车模块本质上是“用户-商品”关联表但有两个业务细节容易踩坑。第一个是重复加入购物车。用户把同一件商品加两次购物车不应该生成两条记录而是取得原记录并将quantity字段1。我用一个简单查询实现先根据userId和productId查存在就update数量不存在才insert。这个幂等操作如果不处理购物车会显得非常杂乱答辩演示时一下子就被看穿。第二个是库存的预检查。在添加购物车时不必锁库存但结算时必须在事务内检查库存是否充足不足则抛出异常回滚事务。我在OrderServiceImpl里是这样写的Transactional(rollbackFor Exception.class) Override public Order createOrder(Long userId, ListCartItem cartItems, OrderAddress address) { // 1. 计算总金额并扣减库存 BigDecimal total BigDecimal.ZERO; for (CartItem item : cartItems) { Product product productMapper.selectById(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BizException(商品【 product.getName() 】库存不足); } product.setStock(product.getStock() - item.getQuantity()); product.setSales(product.getSales() item.getQuantity()); productMapper.updateById(product); total total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 2. 生成订单号并插入订单表 String orderNo SHOP System.currentTimeMillis() RandomUtil.randomNumbers(6); Order order new Order(); // ... set 各种字段, 状态为待支付 orderMapper.insert(order); // 3. 清空购物车中已购买的商品 cartMapper.deleteByIds(cartItems.stream().map(CartItem::getId).collect(Collectors.toList())); return order; }这里使用Transactional(rollbackFor Exception.class)非常重要。因为Spring事务默认只在运行时异常(RuntimeException)时才回滚如果你捕获了异常不抛出事务就不会回滚极容易导致“扣减了库存但订单没生成“这种数据不一致。我强烈建议在Service层不要轻易catch异常而是定义BizException并抛出去由全局异常处理器统一转成友好提示。订单状态流转我用一个int字段表示0待支付、1待发货、2已发货、3已完成、4已取消。为什么要用数字而不是字符串因为后续扩展状态机的成本低只要在枚举类里维护一套映射即可而且数字存储效率更高。前端展示时再通过Map转成文字。如果你的电脑上配置了Redis甚至可以把订单状态放进缓存但毕设阶段用数据库字段足够。4. 完整实操从零到跑通整个项目4.1 环境准备与项目创建在开始之前你需要准备好这几个基础工具JDK 1.8或以上(推荐8或11)、Maven 3.6、MySQL 5.7或8.0、IDEA(或者Eclipse但IDEA会更顺手)。然后通过Spring Initializr创建一个Spring Boot项目也可以在start.spring.io网站勾选依赖后直接下载。我通常直接在IDEA里新建Spring Initializr项目Group填com.exampleArtifact填shopJava版本选8然后勾选Spring Web、Thymeleaf、Spring Security、MySQL Driver、MyBatis Framework(注意MyBatis-Plus需要单独添加)。手动在pom.xml里添加MyBatis-Plus和JWT依赖这里给一个我调好的版本组合parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency /dependencies一个容易被坑的点MyBatis-Plus自带的分页插件和Spring Boot自动配置有时会冲突导致分页不生效。你需要在配置类里手动注入PaginationInterceptor(旧版)或MybatisPlusInterceptor(新版)Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }完成之后记得在启动类上加上MapperScan(com.example.shop.mapper)注解让所有Mapper接口被扫描到。没有这个注解的话启动时会报“Invalid bound statement (not found)”的错误甚至整个Mapper注入失败。4.2 代码导览与调试运行的关键点项目创建好后最直接的方式是从Controller层开始看请求入口。比如商品列表接口Controller RequestMapping(/product) public class ProductController { Resource private ProductService productService; GetMapping(/list) public String list(RequestParam(value page, defaultValue 1) int page, RequestParam(value size, defaultValue 8) int size, ProductQuery query, Model model) { PageResultProduct result productService.pageQuery(page, size, query); model.addAttribute(products, result.getList()); model.addAttribute(pageInfo, result); return product/list; } }这里的Model对象是Spring MVC提供的用来向模板传递数据。Thymeleaf模板里我用th:each循环展示列表片段如下div th:eachproduct : ${products} classcard img th:src${product.mainImage} alt h3 th:text${product.name}/h3 span classprice th:text¥ ${#numbers.formatDecimal(product.price, 1, 2)}/span /divThymeleaf语法里最关键的是${}变量表达式、th:each循环、以及th:if/th:unless条件判断。刚上手时最容易犯的错是写成原生EL表达式${}并用${product.name}方式但前端页面渲染不出来。你需要确保Controller里往Model里addAttribute的key和模板里取的名字完全一致大小写也不能错。调试运行的过程用一张图来表达:启动Spring Boot主类看到“Started ShopApplication in XX seconds”日志说明Tomcat端口8080已经起来了然后在浏览器访问http://localhost:8080/shop/product/list。这里特别注意context-path我设置为/shop所以所有URL的前缀都要带上。4.3 数据库初始化与测试数据准备我提供了一份初始化SQL脚本在项目resources目录下创建schema.sql直接执行即可。但有一点很关键就是要把建库语句放在外面因为MySQL的source命令不太方便直接在一个文件里切库。我推荐使用Navicat或DataGrip新建数据库shop_db然后执行这些建表和插入语句。示例建表CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, category_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL, subtitle VARCHAR(200), main_image VARCHAR(255), detail_images TEXT, price DECIMAL(10,2) NOT NULL, original_price DECIMAL(10,2), stock INT NOT NULL DEFAULT 0, sales INT NOT NULL DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, recommend TINYINT DEFAULT 0, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除标记, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;测试数据至少准备20条商品、5条分类、3个用户、若干购物车项。一个好的技巧是把饰品图片放在本地静态目录/static/img/product/下然后在商品数据的main_image字段里直接写入/static/img/product/ring1.jpg这样的路径。上线时后端做静态资源映射商品图就能正常显示。5. 常见问题与排查技巧实录5.1 依赖冲突与Spring Boot版本太高的问题这是我在搜索热词时发现非常多人在问的“springboot版本太高”“怎么将springboot jar反编译成项目”其实都和毕设有关。我的建议是固定Spring Boot版本以2.7.x为准不要集成为最新release特别是毕设答辩学期往往只有四个星期没必要去尝试新特性。如果你已经遇到版本过高导致JWT的javax依赖失效最直接的解决方式是重新创建项目时把parent改为2.7.18。版本不对的典型报错有java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException→ 因为JDK 9移除了Java EE模块需要额外引用javax.xml.bind依赖或者直接退回JDK8。Spring Security 6.x中antMatchers方法没了改成requestMatchers如果你的旧教程代码还在用antMatchers那一定是版本不匹配。MyBatis-Plus启动后控制台提示ClassNotFound多半是mybatis-plus和mybatis-spring版本不协调直接用starter并指定版本即可。如果你拿到一个别人的jar包想反编译成项目来追赶进度我个人不建议直接这么干。反编译代码可读性差注释丢失而且容易带着别人的环境配置错误反而是死路。更好的方式是学习它的接口设计思路自己重写核心模块代码。5.2 数据库连接与时区、编码问题的经典解法这一类问题几乎每个人都有。异常信息通常是java.sql.SQLException: The server time zone value Öйú±ê׼ʱ¼ä is unrecognized...解决方案就是在JDBC URL里显式加上serverTimezoneAsia/ShanghaiMySQL驱动8.0版本以后必须指定时区不指定就会报错。另一个常见变体是中文乱码插入数据库后文字显示为“??”这大概率不是Java代码的问题而是数据库连接没有指定characterEncodingutf8或者表本身的字符集不对。解决步骤是先确认表建表语句是DEFAULT CHARSETutf8mb4再确认JDBC URL带有characterEncodingutf8。这里我建议统一用utf8mb4而不是utf8因为utf8mb4支持表情符号和生僻汉字演示时插入一条带emoji的商品备注也不会翻车。如果你是在Windows上运行项目MySQL和IDEA的编码都建议改成UTF-8。IDEA窗口右下角有一个文件编码切换按钮要确保你的.java和.properties文件都以UTF-8保存否则编译后中文会乱成一团。5.3 前端模板页面加载不出来或接口返回500这个坑我几乎每个项目都要遇到一次。现象是浏览器能访问首页但商品列表页报500。排查步骤非常简单看控制台完整异常栈定位是否因为某个字段为null导致前端解析错误例如detail_images字段为nullJSON序列化时抛异常。如果不希望为空的字段参与序列化可以在实体类上加JsonInclude(JsonInclude.Include.NON_NULL)注解。确认MyBatis映射XML的namespace对应正确的Mapper接口比如com.example.shop.mapper.ProductMapper。如果namespace写错启动时不会立即报错但调用时会报Invalid bound statement。检查是否因为页面中引用了不存在的静态资源。比如你的CSS路径写成/css/style.css但静态资源目录是/static/css/style.css要按实际路径修改。还有一个我容易忽略的地方前端用$.ajax请求接口返回的数据格式可能被浏览器跨域拦截。如果你是在独立端口跑前端Vue则必须在后端配置CORS过滤器。若你后端代码基于Thymeleaf同源部署可以不在配置里加CORS但接口调试时用Postman或者浏览器控制台测要注意如果没加Content-Type后端RequestBody会解析失败。6. 项目定制扩展与毕设答辩加分技巧6.1 低成本的功能扩展方向如果你的目标是让项目比同组同学“多一点东西”我建议优先扩展这几个轻量级模块商品推荐位(首页轮播)在product表加一个recommend字段后台勾选后即可置顶展示。实现得非常简单只需要在查询接口里ORDER BY recommend DESC, id DESC。但效果非常直观答辩演示时首页能呈现明显视觉层级。热销排行接口基于订单表和订单项表统计商品销量写一个SELECT product_id, SUM(quantity) AS total FROM order_item GROUP BY product_id ORDER BY total DESC LIMIT 10再用定时任务或简单查询刷新到Redis缓存。访问日志与操作日志用Spring AOP切面记录每个用户的关键操作存到一张log表。这个扩展完全可以展示你对“面向切面编程”的理解是面试点名率很高的加分项。消息通知比如用户下单成功后收到站内信或邮件。你可以直接用JavaMailSender发送邮件但在课程设计里需要提前配置邮箱授权码我建议放在最后做以免卡在环境问题。6.2 文档撰写和演示时的“主次分明”技巧一份合格的毕设文档目录结构应包含绪论、需求分析、系统设计、数据库设计、系统实现、测试与总结。其中数据库设计和系统实现是最容易拿分的两个章节。数据库设计里要画出ER图并写明每张表的主外键、索引、字段注释。系统实现里不要全篇贴代码而是用“业务流程图核心代码片段运行截图”的方式展示。演示时尽量按下面顺序启动项目→登录注册→浏览商品→加入购物车→下单支付→后台发货管理。这个过程能覆盖90%的功能亮点。我在多次演示中学到一点不要一上来就秀复杂代码先让老师眼前一亮然后再用“这里我用了Spring Security的JWT过滤器”引出技术点这种“先结果后原理”的讲解节奏会让答辩更快进入你擅长的节奏。6.3 给不同基础同学的定制建议如果你是零基础初学者建议不要整个人工一行行敲太多代码研究源码比照抄代码更重要。先把项目的Controller→Service→Mapper调用链走通再自己动手增加一个小功能(比如“商品收藏”)这比对所有代码都一知半解强得多。如果你有一定Spring Boot经验可以把项目中的模拟支付改为对接支付宝沙箱或微信支付沙箱虽然复杂一些但要格外注意回调URL的配置和验签逻辑。如果你时间紧张但对面试有要求可以保留项目核心代码再配一个简单的Redis缓存装饰层在用户点击商品详情时缓存商品数据这样在简历中写“基于Redis优化热点查询”就完全真实可信。最后再分享一个实操中的小技巧我在调试订单事务时发现一个特别隐蔽的问题订单创建成功后购物车清空失败。原因是购物车的deleteByIds用了MyBatis-Plus的批量删除方法但它的底层是根据主键批量删除如果你把ID列表不小心传成了productId集合就会删除其他用户或商品的购物车记录。最稳妥的做法是在Service层循环删除或在SQL里明确加AND user_id ?条件。后来我把购物车的删除操作单独拆成一个方法在事务内依次处理避免了误删问题。另外一个容易被忽略的点是ID生成。我用的是System.currentTimeMillis() 随机数生成订单号平时没有任何问题但如果你在演示时连续快速点击两次“生成订单”是有可能撞订单号的。如果你想把项目做得更严谨可以使用工号雪花ID生成器(Hutool工具里就有)这样订单号不仅全球唯一还自带时间信息而且能给答辩加分。这个项目我最后打包的时候特别注意把application.yml里的数据库密码和密钥配置作了脱敏处理改成从环境变量读取这样分享给同学时不会把敏感凭据泄露。如果你是拿到别人的源码第一步也建议先把这些关键配置换成自己的避免直接用别人的数据库连接串导致连不上或误操作他人数据库。做完这些整个“基于Spring Boot的饰品商城系统”从设计、编码、调试、扩展再到文档基本就形成了一个完整闭环。按照这个流程走哪怕是第一次做Spring Boot项目的同学也能在两周时间内交付一个能跑能讲、逻辑清晰、有真实业务感的毕业设计作品。