
计算机毕设选“SpringBoot网上宠物店”这套题的人我这两年至少见了上百个。说句实在话这套题的难点根本不在SpringBoot本身而在于你能不能把一个电商业务闭环讲清楚、做完整。商品、购物车、订单、支付、用户体系五个模块串起来才是这套题真正的价值所在。而多数人栽就栽在只做了个“能跑起来的CRUD”答辩时一问业务逻辑就卡壳。这篇文章我就以过来人的身份把“基于SpringBoot的宠物用品电商平台”从选型、设计、编码到排错整条链路拆开揉碎了讲一遍。项目适合Java基础尚可、想通过毕设系统梳理SpringBoot全家桶的同学参考也适合那些拿到题目但不知道从哪儿下手的同学直接“抄作业”。我会尽量把每一步的“为什么这么做”讲清楚而不是只丢一堆代码让你自己猜。1. 项目整体设计与技术选型思路1.1 为什么是SpringBoot而不是SSH或SSM这个问题几乎每个答辩老师都会问所以先把它想明白。早期Java Web开发的主流组合是SSHStruts2 Spring Hibernate和SSMSpring SpringMVC MyBatis。这两个组合没毛病但都有一个共同痛点配置极其繁琐。数据源要配、事务要配、SpringMVC要配、web.xml要写一堆监听器和过滤器光把项目跑起来就得折腾半天。SpringBoot的出现本质上是“约定优于配置”思想的落地。它把Spring、SpringMVC内嵌整合用自动装配机制把原来需要手写的配置变成默认行为。你只需要在pom.xml里引入spring-boot-starter-web写一个带SpringBootApplication注解的启动类一个Web项目就跑起来了。内嵌Tomcat更是省掉了部署war包到外部容器的步骤打包直接扔服务器上java -jar就能跑。对毕设而言选SpringBoot还有一个非常现实的好处答辩通过率高。现在绝大多数岗位JD里写着“熟悉Spring Boot优先”面试官和答辩老师对这套技术栈天然有好感项目技术栈贴合主流本身就加分。1.2 业务模块拆解前台、后台、数据库三线并行网上宠物店说白了就是一个垂直领域的电商系统。别被“宠物”两个字限制住它的核心模型和淘宝京东的商品交易流程没有本质区别只是商品类目变成了宠物用品、宠物零食、宠物玩具可能顺带一个宠物信息展示。我建议你把系统拆成两个端前台商城端面向C端用户核心功能包括用户注册、登录、个人信息维护商品分类浏览、关键词搜索、商品详情查看购物车管理加购、修改数量、删除、批量结算订单管理提交订单、在线支付模拟、查看订单状态收货地址管理后台管理端面向运营人员核心功能包括管理员登录商品管理新增、上下架、编辑、删除商品分类管理订单管理发货、查看订单详情、订单状态流转用户管理用户列表、禁用/启用轮播图管理、公告管理数据库设计方面核心表至少有这几张-- 用户表 CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 加密密码, nickname varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1正常 0禁用, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表 CREATE TABLE t_product ( id bigint(20) NOT NULL AUTO_INCREMENT, category_id bigint(20) NOT NULL COMMENT 分类id, name varchar(100) NOT NULL, subtitle varchar(255) DEFAULT NULL COMMENT 副标题, main_image varchar(255) DEFAULT NULL COMMENT 主图, price decimal(10,2) NOT NULL, stock int(11) NOT NULL DEFAULT 0, status tinyint(1) DEFAULT 1 COMMENT 1上架 0下架, detail text COMMENT 富文本详情, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 购物车表 CREATE TABLE t_cart ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, product_id bigint(20) NOT NULL, quantity int(11) NOT NULL DEFAULT 1, checked tinyint(1) DEFAULT 1 COMMENT 是否勾选, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id,product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL, total_price decimal(10,2) NOT NULL, status smallint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name varchar(50) NOT NULL, receiver_phone varchar(20) NOT NULL, receiver_address varchar(255) NOT NULL, pay_time datetime DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE t_order_item ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL, product_id bigint(20) NOT NULL, product_name varchar(100) NOT NULL, product_image varchar(255) DEFAULT NULL, current_price decimal(10,2) NOT NULL COMMENT 下单时快照价格, quantity int(11) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个关键点要特别说明为什么订单明细要把商品名称、价格单独存一份而不是直接关联商品表因为商品的价格和名称是会变的而订单一旦生成这笔交易的商品信息就不能再改了。做快照是订单系统的铁律。把这个细节讲出来答辩老师会觉得你有真实项目思维。1.3 一个容易被忽略的设计购物车的唯一键购物车表我加了uk_user_product唯一索引这是实战中很容易踩坑的地方。用户反复点击“加入购物车”时如果每次都插一条新记录购物车里就会出现好几行一模一样的商品。有了唯一索引后续就能用ON DUPLICATE KEY UPDATE quantity quantity 1或先查再改的方式做幂等处理。这也是我为什么推荐数据库表结构用t_前缀而不是直接叫user的原因。虽然MySQL里user不是严格意义上的关键字但order是货真价实的SQL关键字如果你把订单表命名为t_order并在SQL里写select * from t_order能省去一大堆不必要的转义麻烦。2. 核心技术原理与实现要点2.1 SpringBoot自动装配原理答辩必问但很多人说不清自动装配这个词很多同学能默写定义但被问到“SpringBoot到底是怎么知道你引入了哪些依赖的”就懵了。我换个方式给你讲明白。SpringBoot的自动装配核心入口是SpringBootApplication它是个组合注解等于SpringBootConfigurationEnableAutoConfigurationComponentScan。真正干活的是EnableAutoConfiguration。这个注解通过Import(AutoConfigurationImportSelector.class)导入了一个选择器。选择器会去读classpath下所有jar包里的META-INF/spring.factoriesSpringBoot 2.x或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsSpringBoot 2.7及3.x文件把里面列出的配置类全部加载进来。但注意它只是“候选”最终能不能生效取决于条件注解。比如你引入了spring-boot-starter-webclasspath里有Servlet和DispatcherServlet相关的类那么DispatcherServletAutoConfiguration上的ConditionalOnClass条件满足Web自动配置就生效了。如果没引入自动配置类会被跳过。给答辩准备一句话版本自动装配 扫描候选配置类 条件注解过滤 自定义属性绑定。三者缺一不可。2.2 持久层选型MyBatis-Plus比原生MyBatis更适合毕设毕设选题热词里频繁出现“springboot mybatis”说明这确实是主流搭配。但我的建议是直接用MyBatis-Plus它本质上是对MyBatis的增强不会改变MyBatis本身。选它的理由很实在BaseMapper自带selectById、selectPage、insert、updateById等常用方法单表CRUD几乎不用写SQL省时间。条件构造器LambdaQueryWrapper能写出类型安全的查询条件比拼字符串SQL舒服太多。分页插件PaginationInnerInterceptor对MySQL物理分页支持得很好不用自己拼limit。代码生成器可以直接从数据库表生成实体类、Mapper接口、Service类毕设中最枯燥的重复劳动可以大幅减少。一个典型的Mapper接口就是这样的public interface ProductMapper extends BaseMapperProduct { }分页查询商品LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword); } wrapper.eq(Product::getStatus, 1); wrapper.orderByDesc(Product::getCreateTime); PageProduct page productMapper.selectPage(new Page(pageNum, pageSize), wrapper);这段代码的逻辑很清晰按关键字模糊查询、只查上架商品、按创建时间倒序排列。如果你用原生MyBatis得自己写select标签、配resultMap、处理动态SQL里的if条件同样的功能代码量至少翻一倍。2.3 登录鉴权JWT 拦截器别一上来就上Spring Security毕设的登录鉴权方案我强烈推荐JWT 自定义拦截器不要用Spring Security。原因很简单Spring Security的学习曲线陡峭过滤器链、UserDetailsService、BCryptPasswordEncoder、授权表达式一套组合拳下来很容易把人劝退。而毕设的权限模型通常只有“普通用户”和“管理员”两种角色用拦截器完全够用而且所有代码都是自己写的答辩时思路更清晰。JWT的本质是一个经过签名的JSON字符串服务端签发后返回给前端前端后续请求把Token放在Authorization头里带回来。服务端验签通过即可信任用户身份。它的好处是服务端无状态不需要在Session里存用户信息天然适合前后端分离。核心就这么几步骤// 登录成功签发JWT String token Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(username, user.getUsername()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(secretKey) .compact();拦截器里的逻辑就是放行登录取、注册等白名单路径其余请求校验Token解析成功就把用户信息放进ThreadLocal或request attribute解析失败直接返回401。这里有两个细节你必须注意JWT的密钥不要写在代码常量里至少放到application.yml配置文件中答辩时可以补一句“后续可以接入配置中心统一管理”。Token过期时间不要设太长我见过有人直接设一年这就等于把密码泄露风险窗口拉满了。7天比较合适配合前端登录页的“记住我”选项调整。2.4 文件上传本地存储还是MinIO毕设中商品图片上传是躲不掉的功能。热词里最近“minio加入到springboot”出现频率不低这里就一并说清楚。MinIO是一个开源的对象存储服务兼容Amazon S3协议。生产环境下用它存图片确实专业因为有独立的存储服务、支持扩容、支持桶策略、还能配CDN加速。但毕设环境如果只有一台本地电脑或者答辩演示用的是别人提供的云服务器再装一个MinIO服务端有点杀鸡用牛刀而且你还需要保证MinIO服务先启动项目才能正常工作。我的建议是分阶段处理开发阶段直接用本地磁盘存储。上传的文件保存到项目根目录下的upload/文件夹前端通过一个映射后的URL访问。如果你的毕设想体现“生产级”能力或者你需要分布式文件存储这个亮点那就在项目中把MinIO的工具类写好通过一个storage.type配置项切换本地存储和MinIO存储这就叫“面向接口编程”讲起来也很加分。一个靠谱的本地存储实现需要注意把虚拟路径映射配好。否则项目重启、文件路径变化图片就全挂了。spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB custom: upload: dir: ${user.dir}/upload/ url-prefix: /upload/**对应的WebMvc配置Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir); }这里有个隐藏的大坑路径末尾不加斜杠资源映射大概率失效。比如你配置file:D:/project/uploadSpring在拼路径时会出问题改成file:D:/project/upload/才稳妥。3. 实战开发流程与核心代码实现3.1 项目初始化从零把一个SpringBoot工程跑起来如果你用的是IDEANew Project时直接选Spring InitializrGroup填com.petmallArtifact填pet-shopJava版本选8或11。如果你不想Initalizr下载慢也可以在start.spring.io网站生成压缩包再导入。关键依赖我建议这样加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.32/version /dependency这里要注意版本兼容如果你用的是SpringBoot 2.7.x这版本最稳JDK8也能跑MyBatis-Plus用3.5.x没问题。如果你一上来选了SpringBoot 3.2.x那要求JDK17起步而且MyBatis-Plus要用对应的新版本很多老教程都不适配了。毕设图稳就上SpringBoot 2.7.18 JDK8 MyBatis-Plus 3.5.3这个组合我实测两年没踩过版本坑。项目结构按职责分包这是答辩老师一定会看的“代码整洁度”com.petmall ├── PetShopApplication.java # 启动类 ├── common │ ├── Result.java # 统一返回结果 │ ├── ResultCode.java # 状态码枚举 │ └── GlobalExceptionHandler.java # 全局异常处理 ├── config │ ├── MybatisPlusConfig.java # 分页插件 │ ├── WebMvcConfig.java # 资源映射拦截器注册 │ └── JwtInterceptor.java # JWT拦截器 ├── controller # 控制层 │ ├── UserController.java │ ├── ProductController.java │ ├── CartController.java │ ├── OrderController.java │ └── AdminController.java ├── service # 业务层 │ └── impl ├── mapper # 数据访问层 ├── entity # 数据库实体 ├── dto # 接收参数的对象 ├── vo # 返回给前端的数据对象 └── util ├── JwtUtil.java └── UploadUtil.java分层规范必须遵守Controller只做参数接收和结果封装不写业务代码Service负责业务逻辑Mapper只做数据库操作。有些同学把SQL写在Controller里忙活半天项目功能都能跑但答辩老师翻开代码眉头一皱分数就往下走了。3.2 商品模块一个典型的前后端接口实现全流程商品模块是这套系统的重头戏它包含了一个电商系统最基本的“列表 → 详情 → 操作”闭环。我直接以“商品分页查询条件筛选”为例给你走一遍完整代码。3.2.1 实体类Data TableName(t_product) public class Product { TableId(type IdType.AUTO) private Long id; private Long categoryId; private String name; private String subtitle; private String mainImage; private BigDecimal price; private Integer stock; private Integer status; private String detail; private Date createTime; }3.2.2 Controller层RestController RequestMapping(/api/product) public class ProductController { Resource private ProductService productService; // 前台分页查询只能看上架商品 GetMapping(/list) public ResultPageProductVO list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { return Result.success(productService.queryPage(pageNum, pageSize, categoryId, keyword)); } // 商品详情 GetMapping(/{id}) public ResultProductVO detail(PathVariable Long id) { return Result.success(productService.queryDetail(id)); } }// 统一返回结果 Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } }统一返回结果这个类务必要有。否则你前一版接口返回Map后一版返回JSONObject代码越写越乱前端对接的时候也说不清楚到底什么结构。3.2.3 Service层public PageProductVO queryPage(Integer pageNum, Integer pageSize, Long categoryId, String keyword) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword).or().like(Product::getSubtitle, keyword); } wrapper.eq(Product::getStatus, 1); wrapper.orderByDesc(Product::getCreateTime); PageProduct page productMapper.selectPage(new Page(pageNum, pageSize), wrapper); // 实体转VO把不需要暴露的字段隐藏 PageProductVO voPage new Page(page.getCurrent(), page.getSize(), page.getTotal()); voPage.setRecords(productConverter.toVOList(page.getRecords())); return voPage; }这里有一个细节我觉得值得多说一句为什么要把detail字段单独在列表接口里隐藏掉商品详情通常是富文本或者长文本列表页根本用不到如果每次都查出来传输既浪费带宽又拖慢响应。列表接口只返回主图、价格、标题这些关键字段就够了详情打开时再调用详情接口。这个优化点在答辩中可以算一个小亮点。3.3 购物车与订单状态机思维是核心购物车模块的接口不多无非是查询购物车列表、添加商品、修改数量、勾选商品、删除、清空。每个接口都要先拿到当前登录用户的ID而用户ID从哪里来从前面说的JWT拦截器解析后的信息里来。所以你必须建一个UserContext工具类public class UserContext { private static final ThreadLocalLong userIdHolder new ThreadLocal(); public static void setUserId(Long userId) { userIdHolder.set(userId); } public static Long getUserId() { return userIdHolder.get(); } public static void clear() { userIdHolder.remove(); } }拦截器在放行前调用UserContext.setUserId(claims.getUserId())Controller里直接UserContext.getUserId()就能拿到当前用户。注意请求处理完毕后要在afterCompletion里调用UserContext.clear()否则线程池复用线程时下个请求会把用户数据串了。订单模块的设计更考验综合能力。我把订单状态定义成一组常量public class OrderStatus { public static final int WAIT_PAY 0; // 待支付 public static final int PAID 1; // 已支付 public static final int SHIPPED 2; // 已发货 public static final int COMPLETED 3; // 已完成 public static final int CANCELED 4; // 已取消 }下单的核心逻辑是从购物车中取勾选的商品 → 校验库存是否充足 → 冻结库存 → 生成订单号和订单主记录 → 生成订单明细 → 清空购物车对应商品 → 返回订单ID。如果库存不足整个流程要回滚不能出现“订单生成了但库存没扣”或者反之的情况。所以下单方法必须加Transactional。库存校验有一个经典陷阱不能先查库存再扣减这样并发下单时会超卖。正确做法是在扣减SQL里加上条件UPDATE t_product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}如果这条SQL影响行数为0说明库存不足。这个机制叫“乐观锁思想”以条件更新代替先查后改。把这个细节写进毕设论文的“并发控制”章节含金量会明显提升。3.4 模拟支付与订单超时关闭毕设阶段真的去接支付宝或微信支付接口流程上很麻烦需要企业资质、需要申请商户号、需要配回调验签大概率还拿不到测试资质。所以我的建议是做一个“模拟支付”接口用户在前端点“确认支付”前端调一个/api/order/pay接口后端校验订单状态是待支付直接把它置为已支付并写入当前时间作为支付时间。真正的支付系统逻辑比这个复杂得多比如支付回调要做幂等、要对账、要处理退款。但毕设的主题是电商平台系统设计不是支付网关开发做一个能讲清楚“支付成功后状态怎么流转”的模拟流程已经覆盖了核心知识范围。订单超时关闭这块热词里的“springboot定时任务”就派上用场了。可以用Spring自带的Scheduled注解写一个定时任务Component public class OrderTimeoutTask { Resource private OrderMapper orderMapper; // 每30秒执行一次关闭超过30分钟未支付的订单 Scheduled(cron 0/30 * * * * ?) public void closeExpiredOrders() { LocalDateTime deadline LocalDateTime.now().minusMinutes(30); ListOrder expiredOrders orderMapper.selectList( new LambdaQueryWrapperOrder() .eq(Order::getStatus, OrderStatus.WAIT_PAY) .lt(Order::getCreateTime, deadline)); for (Order order : expiredOrders) { // 恢复库存 ListOrderItem items orderItemMapper.selectList( new LambdaQueryWrapperOrderItem().eq(OrderItem::getOrderId, order.getId())); for (OrderItem item : items) { // UPDATE t_product SET stock stock #{quantity} WHERE id #{productId} productMapper.restoreStock(item.getProductId(), item.getQuantity()); } order.setStatus(OrderStatus.CANCELED); orderMapper.updateById(order); } } }注意启动类要加EnableScheduling否则注解不生效。另外说句实在话这种基于Scheduled的定时轮询方案在真实订单量大的场景里效率不高专业一点的做法是用RabbitMQ延迟队列或Redisson延迟队列。毕设阶段用Scheduled完全够答辩时如果能补充一句“生产环境建议改成延迟队列方案”就是加分项。4. 常见问题与排查技巧实录4.1 版本问题SpringBoot版本太高JDK8直接撑不住热词列表里出现“springboot版本太高”绝对不是偶然。SpringBoot 3.x要求JDK17如果你实验室的电脑还装的是JDK8一启动就会报UnsupportedClassVersionError或者一堆依赖不兼容的错误。我的建议非常清晰毕设统一用SpringBoot 2.7.18 JDK8 MyBatis-Plus 3.5.3.1。这套组合在我指导过的项目里零版本兼容事故。别觉得“版本越新越好”毕设的目标是稳定跑通和顺利答辩不是追新版本。如果你非要用SpringBoot 3.x记得检查三件事JDK必须17以上、javax.servlet要改成jakarta.servlet即javax命名空间变为jakarta、MyBatis-Plus要升级适配版本。这一套替换工作量完全没必要在毕设阶段背上。4.2 跨域问题和前端打包放不进SpringBoot如果你做了前后端分离比如Vue SpringBoot本地开发时前端在localhost:8080后端在localhost:9090前端发请求必然遇到跨域。处理方案有两个方案一是后端开启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); } }方案二是在前端配置代理Vue项目里改vue.config.jsmodule.exports { devServer: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } } }我推荐前后端分离的同学用方案二因为生产环境你迟早要把前端npm run build出来的dist目录扔进SpringBoot的src/main/resources/static下统一部署这种“开发分离、生产同源”的方式最省心。但这里有一个高频坑如果你的Vue路由用了history模式打包放进SpringBoot后刷新非首页路径会白屏报404。原因是刷新时浏览器直接请求了后端路径而SpringBoot的静态资源映射找不到对应的路由。解决方式是在后端加一个转发把非API路径全部转发到index.htmlController public class PageForwardController { RequestMapping(value {/, /product/**, /cart/**, /order/**, /user/**}) public String forward() { return forward:/index.html; } }还有一个办法更省事前端路由改用hash模式URL会带#丑一点但刷新不会404。毕设演示阶段讲究稳妥hash模式足够。4.3 SpringBoot配置与Banner、端口这些小事热词里出现“springboot banner生成器”和“springboot banner在线”估计很多人想在启动时显示一个自定义字符画。这事很简单找一个在线的banner生成器把生成的ASCII艺术字复制到src/main/resources/banner.txt即可。想换成自定义文案的话在application.yml里配置spring.banner.location指向自定义文件位置。端口配置也是高频问题。默认8080端口经常被本地其他服务占用改端口server: port: 9090但要注意后端端口改了前端请求地址和CORS配置、代理目标端口都要同步改否则“前端连不上后端”的报错分分钟找上门。另外一个很实用的小技巧项目里配置多个环境的配置文件比如application-dev.yml、application-prod.yml主配置文件里用spring.profiles.activedev切换。数据库密码、文件上传路径等差异就能分开管理。这个习惯对后续找工作面试也是加分项。4.4 常见报错排查速查表我把自己带项目过程中遇到频率最高的报错整理成了一张速查表直接存下来可以对号入座报错现象根本原因解决方式Whitelabel Error PageController路径没映射到或全局异常未处理检查RequestMapping路径是否与前端请求一致配全局异常处理器Error creating bean with name xxxMapperMapper接口没加Mapper注解或启动类没加MapperScan统一在启动类加MapperScan(com.petmall.mapper)Invalid bound statement (not found)Mapper接口方法名与XML中的id不匹配确认XML命名空间、方法名一致用MyBatis-Plus的话检查依赖是否冲突Port 8080 was already in use端口被占用换端口或netstat -ano找到PID后强杀进程前端访问图片404静态资源映射没配置或路径末尾少斜杠检查WebMvcConfig资源映射URL前缀和本地目录是否匹配日期字段返回JSON变成时间戳或格式错误Jackson默认序列化格式问题在实体日期字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)接口返回401但登录接口正常JWT拦截器拦截了公共接口拦截器放行白名单中加入登录、注册等路径Maven依赖冲突导致启动报错不同starter传递依赖版本不一致mvn dependency:tree查看冲突排除多余依赖版本最后一条经验很值钱遇到任何诡异报错第一件事先看mvn clean package -DskipTests能不能编译通过。很多运行时报错是“编译期问题被IDE掩盖”后的连锁反应。清掉target目录重新构建能解决一半以上的“玄学问题”。4.5 答辩准备你的项目还要能讲出“深度”功能跑通只是及格线答辩想拿高分得在技术深度上多讲两句。我这里给你准备三个可以直接讲出来的“亮点”第一个是Redis缓存。商品详情接口是最典型的高频读接口可以在查询时先查Redis没命中再查数据库并回填缓存同时设置过期时间防止雪崩。热词里“springboot整合activemq”“springboot集成kettle”都有人搜说明大家知道“中间件体系”是加分项但选Redis最贴合电商场景。第二个是超卖问题就是前面讲到的条件扣库存SQL。那一个UPDATE ... WHERE stock #{quantity}足以证明你考虑过并发场景。第三个是订单状态机。你可以把订单状态流转图画出来说明状态之间的合法跳转以及哪些操作会触发哪些状态变化。状态机思维不仅适用于订单也适用于售后、退款等模块。能讲出这个说明你不只是“照着别人的代码抄”而是真正理解了业务状态流转的逻辑。写在最后的一点私货我见过太多人做毕设陷入一个误区觉得框架越花哨越好代码越长越有面子。但实际操作下来这两年的经验告诉我毕设项目最重要的其实是“完整度”和“自洽性”。所谓完整度是指业务闭环要能走通——用户从前台注册登录、浏览商品、加购、下单、模拟支付到后台管理员能看到订单、发货、管理部门这一整条链路都是通的。所谓自洽性是指你的技术选型和业务场景是匹配的而不是为了堆技术硬上。这套宠物电商项目做下来最大的收获不是代码量而是理解了“数据表设计如何影响接口开发”“事务和并发控制到底在保护什么”“一个完整业务从需求到落地要过哪些环节”。这些能力在以后工作里比单单会写一个Controller值钱得多。如果你正在为毕设发愁我建议你就按这个思路来先把数据库表建好再把后端接口跑通最后用Vue把页面串起来。遇到问题别硬扛去找日志、查源码、搜报错一步步排。做毕设本来就是一个把课堂知识变成工程能力的过程把过程做好结果自然不会差。