每年到这个时候我总能收到一堆私信问毕业设计选什么题目。今年被问得最多的是这个基于SpringBoot的动漫商城管理系统。说实话这个题目非常适合当毕设它没有浮夸的微服务、高并发但把后端开发的核心技能全串了一遍——SpringBoot、鉴权、分页、购物车、订单事务、文件上传、部署上线再加上“动漫”这个自带情怀的业务场景演示效果好老师看了也有兴趣。这篇攻略我按自己当初踩坑总结的完整流程来写从需求拆解到数据库设计再到核心编码和部署实录每一步都尽量说清楚“为什么这么做”。无论你是刚拿到题目无从下手还是已经写了一半卡在某个环节这篇都能给你一个可以直接复制的路线图。这个题目适合计算机相关专业做毕设也适合想通过一个完整项目把Java后端串起来的学习者。我会把自己做过的取舍和踩过的坑都写出来少走弯路比什么都强。1. 选题打磨与需求拆解动漫商城到底要做成什么样1.1 为什么这个题目适合当毕设一个合格的毕设题目要满足三个条件技术覆盖度够、业务故事好讲、工作量适中偏上。动漫商城管理系统刚好全占。技术上商城项目天然包含用户体系、商品管理、购物车、订单、支付、后台管理这些模块几乎覆盖了SpringBoot开发的主流场景。业务上动漫商城面向的是年轻用户群体有明确的用户画像做演示的时候可以联系热点IP、周边手办、漫画轻小说这些场景比做一个“通用商城管理系统”有记忆点答辩时老师提问不容易跑偏到过于抽象的领域知识。工作量也合适。如果你一个人从零开始写核心模块控制在一到两个月能完成再加一个前端页面就绰绰有余。比起那些动辄“分布式秒杀系统”的题目这个题目不会一语惊人也不至于把自己逼到墙角。1.2 功能模块拆分用户端和管理端各管什么很多同学拿到题目的第一反应是“我该先写哪个功能”然后打开IDE开始敲。我强烈建议先拿一张纸把功能画出来把用户端和管理端分开列清楚。用户端是登录注册后看到的所有东西用户注册、登录、退出个人资料修改与头像上传首页轮播图、商品分类导航、商品列表与搜索商品详情页包括商品图片、价格、库存、规格简介、评价展示购物车支持加购、修改数量、删除、全选结算订单确认页选择收货地址、生成订单、模拟支付个人中心查看订单列表、取消订单、确认收货、删除订单商品评价可选但做出来很加分管理端则是运营人员使用的后台管理员登录与权限控制首页数据统计比如商品数量、订单数量、用户数量商品分类管理增删改查商品管理包括上架、下架、库存调整、图片上传订单管理查看订单详情、发货、处理退款用户管理禁用、启用账号轮播图管理首页推荐位的图片维护把这些模块列出来之后你会发现后端接口大概需要60到80个前端页面大概15到20个。工作量对比很多“学生管理系统”大不少但对一个毕设来说完全合理不会在中期答辩时被质疑“做的太简单”。1.3 技术栈选型为什么是SpringBoot而不是别的先说结论我用的是SpringBoot 2.7.x MyBatis-Plus MySQL 8.0 Redis JWT前端用Vue Element UI。如果你前端不熟也可以用Thymeleaf做服务端渲染把整个项目塞进一个SpringBoot应用里部署更简单。选SpringBoot的核心原因不是因为它比SSM“高级”而是因为它的自动装配机制解决了传统SSM项目最大的痛点配置堆积。在SSM时代你需要手写web.xml、spring-mvc.xml、mybatis-config.xml任何一个xml写错都会在启动时报一个让你怀疑人生的异常。SpringBoot通过starter依赖加自动配置把这些配置变成了规范化的默认值你只需要在application.yml里覆盖自己需要改的部分。这里我多讲两句SpringBoot自动装配的原理不要觉得毕设不需要懂原理这是答辩时的高频问题。SpringBoot的启动类上有一个SpringBootApplication注解它由SpringBootConfiguration、EnableAutoConfiguration和ComponentScan组合而成。EnableAutoConfiguration往容器里导入了一个AutoConfigurationImportSelector这个类会去读取META-INF/spring.factories文件里配置的所有AutoConfiguration类然后根据当前classpath下的依赖包以及你配置的属性按条件注解ConditionalOnClass、ConditionalOnProperty决定要不要实例化这些配置。这就是为什么你加了spring-boot-starter-data-redis依赖SpringBoot就会自动帮你创建RedisTemplate而你什么都不用做。所以你在毕设答辩时可以这样说SpringBoot通过条件化的自动配置把开发从繁琐的xml配置中解放出来让开发人员专注于业务逻辑本身同时通过starter体系对第三方框架做了标准化整合。这段话朴实但准确比背一堆概念强得多。2. 架构设计与数据库建模先想清楚表结构再动手2.1 项目分层与目录规划一个好的项目结构能让后续开发顺畅很多。我用的是经典的四层结构先看目录com.example.anime ├── controller // 接口层接收前端请求 ├── service // 业务层处理核心逻辑 │ └── impl ├── mapper // 数据访问层继承MyBatis-Plus的BaseMapper ├── entity // 数据库实体类 ├── dto // 接收前端参数的传输对象 ├── vo // 返回给前端的视图对象 ├── config // 配置类如跨域、拦截器、分页插件 ├── common // 公共类如统一返回结果、异常处理、常量 ├── util // 工具类如JWT工具、文件上传工具 └── AnimeApplication.java这个结构的核心原则是分层清晰职责单一。controller只做参数接收和结果返回不在controller里写业务逻辑。service层处理具体业务事务加在service层的方法上。mapper层只做数据库操作通过MyBatis-Plus的BaseMapper可以少写大量SQL。我见过很多学生把业务逻辑全写在controller里一个接口几百行虽然运行起来没问题但答辩老师一旦问“你这个模块怎么复用”场面会很尴尬。分层的作用不是应付检查而是你自己改代码的时候也会舒服很多。建议再加一个dto与vo的区分。dto用于接收前端参数比如注册时的RegisterDTO包含用户名、密码、邮箱vo用于返回前端数据比如ProductVO里可能包含商品分类名称而实体Product里只有分类id。不要让前端直接面对实体类因为实体类的部分字段比如密码、逻辑删除标记不应该暴露出去。2.2 数据库表设计每一张表都是业务的一个切面数据库设计是整个项目的地基。如果表结构不对后面写接口全是别扭的。我先说总原则宁可多拆几张关联表也不要一张表塞满所有字段。以我这个项目为例核心表大概10张左右用户表 userCREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, email varchar(100) DEFAULT NULL, phone varchar(20) DEFAULT NULL, status tinyint DEFAULT 1 COMMENT 1正常 0禁用, deleted tinyint DEFAULT 0 COMMENT 逻辑删除, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商品表 productCREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, category_id bigint DEFAULT NULL, name varchar(100) NOT NULL, subtitle varchar(255) DEFAULT NULL COMMENT 副标题, main_image varchar(255) DEFAULT NULL, detail text COMMENT 商品详情, price decimal(10,2) NOT NULL COMMENT 价格, stock int NOT NULL DEFAULT 0 COMMENT 库存, sales int DEFAULT 0 COMMENT 销量, status tinyint DEFAULT 1 COMMENT 1上架 0下架, deleted tinyint DEFAULT 0, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表 orders 和订单明细表 order_item 是另一个关键设计点。订单表和明细表必须分开因为一个订单可能包含多个商品。订单表保存订单整体信息包括订单编号、用户id、总金额、状态、收货地址快照明细表保存每一个商品的id、名称、单价、数量、小计。收货地址一定要在生成订单时做快照存成字符串字段否则以后用户改了地址历史订单的收货信息全乱套了。这里特别强调几个容易踩坑的点。第一价格字段必须用decimal(10,2)绝不能用double或float二进制浮点数存在精度问题金额计算会出现0.10.20.30000000000000004这种情况放到订单上就是事故。第二所有需要逻辑删除的表都加deleted字段脱手一个数据比删除一个数据更优雅也便于老师问“你们的数据是怎么做软删除的”。第三用户表和商品表之间不要直接建关系购物车、订单、评价都是独立的表通过用户id和商品id关联这样业务边界清晰。分类表建议用简单的父子结构而不是添加无限极分类的复杂逻辑。动漫商城的一级分类比如“手办模型”“漫画轻小说”“动漫服饰”“周边配件”如果后续真的需要二级分类加一个parent_id字段关联自己即可不用一上来就给自己上嵌套查询的强度。2.3 统一返回结果与状态码约定前后端分离的项目最忌讳每个接口的返回格式都不一样。这个接口返回{code:200, data:...}那个接口返回{success:true, result:...}前端对接的人会被逼疯。我建议在common包下建一个ResultT类public class ResultT implements Serializable { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }同时定义一个状态码常量类ResultCode把常见的状态码约定符合语义200成功、400参数错误、401未登录或token过期、403无权限、500服务器异常。这样做的好处是整个项目从接口到前端的错误处理逻辑都统一了前端只要判断code是不是200不是就弹message不用每个页面单独判断。3. 核心功能编码实战从登录到下单的完整链路3.1 用户注册登录与JWT鉴权用户模块几乎是所有系统的入口也是拦截器、密码加密、token管理这些知识点的主战场。密码不能明文存数据库这是底线。我用的加密方案是Spring Security里的BCryptPasswordEncoder你也可以用MD5加盐但BCrypt的强度和安全性更好加盐过程是自动内置处理的只需要调用encode和matches两个方法。有一点要注意BCrypt生成的密码每次都不一样因为每次盐不同这是正常现象不要拿两次加密结果不一样觉得代码有问题。登录成功之后签发JWT。用JWT的好处是无状态服务端不用保存session分布式部署时也友好。我在util包下新建一个JwtUtilComponent public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String createToken(Long userId, String username) { Date now new Date(); Date expireDate new Date(now.getTime() expire); return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }JWT的secret千万不要写死在代码里放在application.yml里上传代码时把真正的密钥脱敏。另外token过期时间建议设成24小时以内别设7天不合理的时间限制在答辩时容易被问住。登录鉴权的拦截器写法要注意放行逻辑。我通过HandlerInterceptor实现TokenInterceptor在preHandle里取出请求头Authorization解析token解析成功就把用户id放入request域中解析失败直接返回401统一响应。然后在WebMvcConfigurer中注册拦截器并排除登录、注册、商品列表、商品详情、首页轮播这些不需要登录的接口。我在这个环节踩过一个很典型的坑拦截器把所有请求都拦截了结果前端点击登录时发现登录接口也在拦截范围内导致无法登录。后来才意识到登录接口必须显式放行否则还没拿到token就被拦住了。放行路径要写成/api/user/login这种精确路径不要图省事直接/**放行所有路径那样拦截器形同虚设。3.2 商品分页查询与多条件搜索商品模块看起来简单实际上手写分页的人会掉进很多坑。这里强烈建议用MyBatis-Plus的分页插件先配置一下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }有了分页插件之后查询接口只需要构造PageProduct和LambdaQueryWrapperProductpublic PageProductVO getProductPage(ProductQueryDTO dto, int pageNum, int pageSize) { PageProduct page new Page(pageNum, pageSize); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(dto.getCategoryId() ! null, Product::getCategoryId, dto.getCategoryId()) .like(StringUtils.isNotBlank(dto.getKeyword()), Product::getName, dto.getKeyword()) .eq(Product::getStatus, 1) .orderByDesc(Product::getCreateTime); PageProduct productPage productMapper.selectPage(page, wrapper); // 转成VO补充分类名称、图片路径等 }这里注意两点。第一eq条件里的第一个参数是布尔值只有当条件成立时才拼接这个查询条件这是防空参数的好方式不要自己手写if去拼接SQL字符串既啰嗦又有SQL注入风险。第二like查询是直接在SQL里用%keyword%天然带通配符用户输入百分号时可能会影响结果必要时可以对keyword做一下转义不过毕设阶段问题不大。商品详情页还需要一个“浏览量1”的逻辑简单起见直接UPDATE product SET views views 1 WHERE id ?不要先查出来再设值再更新那种操作在并发时会丢失计数。虽然毕设阶段没有并发压力但养成写原子操作的习惯没有坏处。如果项目里引入了Redis我还会把首页的轮播图和热门商品缓存到Redis中设置10分钟过期减少数据库压力。这部分代码不复杂但在答辩时是一个加分点证明你不仅会CRUD还考虑过性能问题。3.3 购物车的累积逻辑与库存判断购物车表设计时以“用户id 商品id”作为唯一逻辑判断依据。加购时先查购物车表里有没有这个用户的这个商品如果有就数量加一没有则新增一条记录。这个逻辑看起来朴素但有一个并发隐患如果用户连续快速点击两次“加入购物车”两个请求同时查出没有记录就可能插出两条同样的商品记录。防得住的办法是在表上建唯一索引(user_id, product_id)然后在代码里捕获DuplicateKeyException并做更新处理。这个细节如果你能写在论文里绝对是亮点。用户修改购物车商品数量时一定要判断库存。不要在生成订单时才报“库存不足”而是在用户改数量时就给提示。前端通常会在输入框失焦时调用更新接口后端拿到数量后和商品库存比较超过库存直接返回错误信息。订单生成的流程是整个项目逻辑最重的部分我用事务来保证一致性。核心代码逻辑梳理如下接收订单请求参数包含收货地址id、购物车商品id列表或直接的商品id与数量列表。根据用户id查出购物车物品逐条校验商品是否存在、是否上架、库存是否充足。计算总价。总价一定要后端算不能信前端传回来的金额前端传的总价是可以篡改的。用商品表的当前价格乘以数量累加得到总金额。生成订单往orders表插入主单状态设为“待支付”。批量往order_item表插入订单明细。扣减库存并发安全做法是使用乐观锁UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}如果影响行数为0说明库存不足需要抛出异常。清除购物车中已下单的商品。由于订单生成过程中任何一步出错都需要回滚所以这个方法加Transactional(rollbackFor Exception.class)。这里有一个很多新手搞混的点事务注解只对运行时异常回滚如果业务中手动catch了异常并返回一个错误结果事务是不会回滚的。所以要么让异常往上抛在统一异常处理器里去捕获并返回要么在catch里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。我个人推荐前一种代码更干净。订单状态建议用一个int字段维护0待支付、1已支付待发货、2已发货、3已完成、4已取消。每次订单状态的变更都通过接口操作不要允许前端直接传任意状态值比如取消订单只能从“待支付”或“已支付待发货”状态发起这样的状态机逻辑清晰后边写发货、取消、退款功能都方便。3.4 文件上传与图片访问本地存储和虚拟路径映射商城的商品图片、用户头像、轮播图都涉及文件上传。毕设项目一般没有钱买OSS本地存储就行但要注意几个配置。在application.yml里加上spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB upload: path: /home/anime/upload/ # Linux下建议绝对路径 static-pattern: /images/**然后写一个配置类把本地磁盘路径和URL路径映射起来Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceHandler(file: uploadPath); } }这样前端访问http://localhost:8080/images/product/123.jpg时实际读取的是服务器磁盘上upload/product/123.jpg文件。上传文件的代码逻辑要处理文件名的唯一性不要直接用用户上传的原始文件名否则容易重名覆盖且中文路径可能出问题。我习惯用UUID加文件后缀拼一个新文件名String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName UUID.randomUUID().toString().replace(-, ) ext;文件类型校验也要做不能只判断后缀还可以读文件的MIME类型。商品图片一般允许jpg、png、webp不允许上传exe、jsp这种危险文件。如果上传文件被当作静态资源直接访问而文件内容是可执行的html或脚本那就存在存储型XSS的隐患。所以后缀白名单是必须的。如果你后续用Nginx做反向代理记得Nginx默认有client_max_body_size的限制不配置的话大图上传会报413错误。要在nginx.conf的location里加client_max_body_size 10m;这一点经常有人忽略后端明明配了上传大小前端还是失败其实就是Nginx层挡住了。3.5 参数校验与全局异常处理写后端接口参数校验是面子工程也是区分“会写代码”和“写得好”的分界点。不要在controller里手写一堆if (username null || username.isEmpty())用JSR 303规范加Spring Boot的校验框架。在DTO字段上打注解public class RegisterDTO { NotBlank(message 用户名不能为空) Size(min 3, max 20, message 用户名长度必须在3到20位之间) private String username; NotBlank(message 密码不能为空) Size(min 6, max 20, message 密码长度必须在6到20位之间) private String password; Email(message 邮箱格式不正确) private String email; }controller参数上用Valid或Validated标记Spring Boot会自动校验不合规时抛出MethodArgumentNotValidException。然后再写一个全局异常处理器RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(MethodArgumentNotValidException.class) public ResultVoid handleValidException(MethodArgumentNotValidException e) { String message e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining()); return Result.error(400, message); } ExceptionHandler(BusinessException.class) public ResultVoid handleBusinessException(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { return Result.error(500, 系统异常请稍后重试); } }这里要注意写全局异常处理器不是让你所有方法都不用try-catch了。业务里确实可能出现一些不可控依赖比如第三方接口这时你自己捕获并转换成更友好的业务提示是合理的。但像“库存不足”“商品已下架”这种业务校验直接抛一个BusinessException让全局处理器返回给前端更优雅不用在service层写繁琐的返回判断。4. 版本兼容、配置细节与Linux部署实录4.1 SpringBoot版本与JDK版本的匹配这个板块是最容易被新手打懵的地方因为很多教程给你展示的都是SpringBoot 2.x的配置但你用IDEA新建项目时默认拉下来却是3.x然后各种javax改jakarta、配置失效的问题就来了。SpringBoot 2.x与3.x的最核心差异是2.x基于Java 8使用javax.*命名空间3.x基于Java 17使用jakarta.*命名空间。很多老代码里的import javax.servlet.http.HttpServletRequest在3.x里必须改成import jakarta.servlet.http.HttpServletRequest。如果你习惯用2.x的写法直接新建3.x项目会有一堆编译错误。我自己的选定是SpringBoot 2.7.18 JDK 8这是我的建议。不是说新版不好而是毕设追求的是稳定可控网上大部分资料、你参考的代码、老师推荐的教程都是基于2.x遇到问题容易搜到答案。如果你的电脑只装了JDK 17且不想切换版本那就用SpringBoot 3.x但要接受部分依赖如MyBatis-Plus的低版本可能出现不兼容。另外IDEA创建SpringBoot项目时有一个常见问题选择SpringBoot 3.0以上时SDK选项里居然不能选JDK 1.8这是因为新版SpringBoot根本不在JDK 8上运行所以IDEA直接禁用了。如果你的环境只装了JDK 8就在start.spring.io或阿里云镜像初始化项目时手动选SpringBoot 2.7.25再导入IDEA不要在创建向导里死磕。每个小版本之间也可能有细节差异比如SpringBoot 2.5之后spring.redis.*配置变成了spring.data.redis.*如果你照搬2.3的教程写旧配置Redis连接参数不会生效。我的排查经验是启动时多留意控制台日志SpringBoot每次版本升级有配置变更时都可能在日志里打warning提示你哪些配置被废弃了哪些属性迁移了位置。多看一眼日志能省一小时踩坑时间。4.2 跨域、上传、数据库连接等高频配置复盘前后端分离最容易出问题的就是跨域。浏览器拦的是“从哪个源发起的请求”前端运行在localhost:5173后端跑在localhost:8080这个就叫跨域。解决方式有两种一是后端在WebMvcConfigurer里加CORS映射二是在controller类上加CrossOrigin注解。我推荐全局配置方式Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); }这里有个细节allowCredentials(true)表示允许携带cookie但它和allowedOrigins(*)冲突在较新的Spring Boot版本使用allowedOriginPatterns(*)可以绕过这个限制。我以前用allowedOrigins加allowCredentials(true)时请求直接报错换成allowedOriginPatterns就正常了。数据库连接池建议在application.yml里标明druid或hikari参数。SpringBoot 2.x默认用HikariCP它性能很好但默认配置比较保守如果是本机开发无所谓如果部署到服务器上建议把最大连接数调低一点因为一个学生项目几十个并发就撑死了给连接池配maximum-pool-size: 20就够了设太大反而浪费内存。还有一个常见坑是时区问题。MySQL连接串里一定要写serverTimezoneAsia/Shanghai不然默认UTC时区会让数据库里的时间比北京时间早8个小时。你插入一条订单在晚上十点用户看到却是下午两点。第一次遇到这个问题我还以为是Jackson序列化的锅排查半天才发现是JDBC连接时区。连接串你这样写jdbc:mysql://localhost:3306/anime_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse4.3 打包、上传与后台运行本地开发完成后部署到Linux服务器是毕设展示的关键一步这一步做好了给老师的印象分直接上升一个档次。先用SpringBoot打包IDEA右侧Maven面板里选择package或者在项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个anime-mall-0.0.1-SNAPSHOT.jar。启动前需要先保证MySQL和如果用了RedisRedis已经装好并且数据库数据和配置文件对应。服务器上执行nohup java -jar /home/anime/anime-mall.jar --spring.profiles.activeprod /home/anime/logs/app.log 21 nohup的意思是让进程不受终端退出影响继续运行是放到后台日志写到app.log。启动完成后通过tail -f /home/anime/logs/app.log看启动日志看到Started AnimeApplication就说明启动成功。然后要确认端口能被外网访问。如果你用的云服务器Linux防火墙和云控制台的安全组都要放行对应的端口有一个地方没设置外网就是打不开。我曾经在阿里云把服务器安全组的8080端口放开了但是忘了防火墙firewall-cmd也要放行结果自己本地访问正常手机流量一测试就超时。最稳妥的部署方式建议用Nginx做反向代理用户访问80端口Nginx转发到内网的8080端口。这样前端页面也放在Nginx下API路径通过/api前缀转发还能统一处理静态资源。虽然毕设不要求上生产级架构但“用了Nginx做反向代理和静态资源托管”这句话写在论文里绝对是加分项。4.4 常见报错速查这些坑我帮你提前踩了第一个高频报错是java.sql.SQLException: Unknown column deleted in where clause。这是因为MyBatis-Plus开启了逻辑删除功能但你的表里没有加deleted字段。解决办法是在配置里指定逻辑删除字段或者给实体类字段加TableLogic记得表结构要同步加字段。第二个高频报错是There is no getter for property named xxx in class ...。通常是实体类的字段名和数据库列名对不上比如数据库列是user_idJava字段是userId使用LambdaQueryWrapper时没问题但如果使用了自定义SQL或者TableField没配置就可能映射失败。建议统一使用驼峰命名并开启MyBatis-Plus的map-underscore-to-camel-case: true配置。第三个是文件上传时前端报413 Request Entity Too Large。这个错误出现时优先检查两层SpringBoot的max-file-size和Nginx的client_max_body_size。如果你没有用Nginx就只查SpringBoot配置如果用了Nginx大概率是Nginx默认1M限制导致的。第四个是Whitelabel Error Page没有任何JSON信息。这个一般是controller没有包在SpringBoot扫描路径下或者返回类型不是对象而是void。检查启动类所在的包路径是否覆盖了controller包的父包。第五个是Invalid bound statement (not found): com.xxx.mapper.ProductMapper.selectPage。这个八成是MyBatis-Plus分页插件没配置或者MapperScan没有生效。记住分页插件的关键点分页插件配置要注入到MybatisPlusInterceptor里顺序也讲究分页插件建议放在最前面。最后提醒一个很现实的问题项目做完之后把数据库的初始化脚本放到项目根目录的sql/文件夹下把应用的可执行jar包和配置文件单独放一个deploy/目录。这不仅是好习惯也是答辩时体现工程素养的地方。老师要演示你的项目时你给他一个一键导入脚本和清晰的部署文档体验比让他到处找文件强一万倍。我做过几次这个题目类型的指导也在真实项目中踩过上面这些坑。经验之谈毕设代码不追求极致的架构和性能但流程一定要完整从需求、设计、编码、测试到部署每一个环节都有迹可循。一个能跑起来的完整商城项目比十篇空谈“分布式高并发”的论文有说服力。动手写吧把每一行代码都理解透这个项目做完你对SpringBoot的认识绝对会提升一个层次。