☰
Java Web毕业设计避坑指南:Spring Boot工程化实战
2026/10/28 13:44:54 网站建设 项目流程

简介:本资源是一套面向计算机专业本科生的Java Web毕业设计实战项目,聚焦校园二手交易场景,为学习者提供从理论到部署的全流程开发范例。压缩包共43.01MB,包含项目报告、答辩PPT、完整Java Web源代码(基于Servlet+JSP+JDBC实现MVC架构)、MySQL数据库SQL脚本、系统运行截图及Tomcat环境部署视频,覆盖需求分析、数据库设计、前后端交互、功能测试与上线部署等关键环节。已有1721人学习下载,适合Java初学者巩固Web基础,也适合作为课程设计参考或毕设开题原型。通过研读报告可掌握规范文档写作逻辑,对照源码理解用户管理、商品发布、订单流转等核心模块实现,结合部署视频快速复现运行环境,截图与PPT则辅助验证功能完整性与汇报表达技巧。

1. 为什么一个“校园二手平台”毕业设计,能卡住90%的Java Web新手?

不是功能太复杂——它只有发布、浏览、搜索、留言、简单订单这五块;也不是技术栈太新——Spring Boot 2.5 + MyBatis-Plus + Thymeleaf + MySQL 5.7,全是课堂讲过、教材写过的组合。真正让答辩翻车、部署报错、老师皱眉、同学抄了也跑不通的,是三处被教科书刻意忽略的“工程断层”:
第一,数据库表设计表面合理,但实际运行时“用户发闲置→管理员审核→买家下单→卖家发货→确认收货”这条主链里,状态字段没做枚举约束+事务隔离没配READ_COMMITTED,导致并发下单重复扣库存、审核状态被覆盖;
第二,Thymeleaf模板里用th:if="${user.role == 'ADMIN'}"控制后台入口,但登录态校验只靠Session,没做CSRF Token防护+没拦截未登录访问/admin路径,答辩现场被老师用Postman直接绕过登录进后台;
第三,所谓“部署视频”里用java -jar xxx.jar启动,却没说明Windows下中文路径乱码、Linux下JVM堆内存未调优、Nginx反向代理时静态资源404的三个真实环境陷阱。

这个项目不是考你能不能写出Hello World,而是考你能不能把“课堂代码”变成“能扛住30人同时刷首页、不丢数据、不崩权限、不暴露后台”的最小可用系统。适合两类人:一是Java基础尚可但没碰过真实Web工程闭环的同学;二是想用毕业设计当跳板,快速补全Spring Boot工程化能力的转行者。别急着解压ZIP——先看清这三道坎,再动手。


2. 从零搭起可运行骨架:Spring Boot + MyBatis-Plus 最小可行配置

2.1 为什么选MyBatis-Plus而不是纯JDBC或Hibernate?

课堂常教JDBC手写SQL,但毕业设计里你要面对的是:

  • 用户表要查“按学院筛选+按价格排序+支持模糊搜索”,写5个DAO方法太累;
  • 商品表要支持“上架/下架/审核中”三种状态,手动维护update status=2 where id=?易出错;
  • 答辩演示时老师突然问:“如果加个‘最近7天发布’筛选,SQL怎么改?”——你总不能现场重写Mapper XML。

MyBatis-Plus的QueryWrapper和LambdaQueryWrapper能用Java语法生成动态SQL,比如查“计算机学院+价格低于50元”的商品:

LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Goods::getCollege, "计算机学院") .lt(Goods::getPrice, 50.0) .orderByDesc(Goods::getCreateTime); List<Goods> goodsList = goodsService.list(wrapper);

比写XML少80%胶水代码,且IDE能自动提示字段名(防拼写错误),调试时点进去能看到最终执行的SQL——这对答辩现场debug至关重要。

提示:别用最新版3.5.x!毕业设计推荐MyBatis-Plus 3.4.3.4(对应Spring Boot 2.5.x),因为3.5+默认开启useGeneratedKeys=true,而MySQL 5.7在INSERT ... SELECT场景下会报错,这是答辩现场高频翻车点。

2.2 application.yml 关键配置:三处必须改的参数

很多同学直接复制网上的配置,结果本地能跑、部署就404。核心是这三行:

server: port: 8080 servlet: context-path: /campus # 必须加!否则Thymeleaf静态资源路径全错 spring: datasource: url: jdbc:mysql://localhost:3306/campus_secondhand?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # 重点:时区必须显式指定! username: root password: 123456 thymeleaf: cache: false # 答辩前务必关掉!否则改HTML不生效 enabled: true prefix: classpath:/templates/ suffix: .html encoding: UTF-8 servlet: content-type: text/html
  • context-path: /campus:不加的话,所有<link href="/css/app.css">都会请求根路径,但你的静态文件在/campus/css/下,必然404;
  • serverTimezone=Asia/Shanghai:MySQL 5.7默认时区是SYSTEM,Spring Boot读取时间戳时会因时区转换出错,表现为“发布时间显示为1970年”;
  • thymeleaf.cache: false:开发阶段关闭缓存,否则你改完index.html刷新页面还是旧的——答辩老师点开网页说“这页面没更新啊?”,你就凉了。

2.3 自动生成实体类与Mapper:用MyBatis-Plus逆向工程避坑

别手敲User、Goods、Order这些类!用官方代码生成器,但必须避开两个坑:

  1. 表名下划线转驼峰失效:生成器默认strategy.setNaming(NamingStrategy.underline),但若数据库表名是user_info,生成的实体类字段是userInfo,而MyBatis-Plus默认按userInfo找数据库列user_info——这没问题;但如果你的表名是tb_user,生成器会生成TbUser类,字段tbUser,而数据库列是tb_user,此时必须手动加@TableField("tb_user")。
  2. 主键策略冲突:MySQL用AUTO_INCREMENT,生成器要设strategy.setIdType(IdType.AUTO),否则插入时ID为null。

生成器核心代码(放在test目录下运行):

@Test public void generateCode() { AutoGenerator generator = new AutoGenerator(); // 数据源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig() .setUrl("jdbc:mysql://localhost:3306/campus_secondhand?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai") .setUsername("root").setPassword("123456").setDriverName("com.mysql.cj.jdbc.Driver"); generator.setDataSource(dataSourceConfig); // 全局配置 GlobalConfig globalConfig = new GlobalConfig() .setOutputDir("src/main/java") // 输出到java目录 .setAuthor("student") // 作者名,答辩PPT里要写 .setOpen(false) // 生成后不自动打开文件夹 .setServiceName("%sService"); // 接口名去掉I前缀,如UserService generator.setGlobalConfig(globalConfig); // 包配置 PackageConfig packageConfig = new PackageConfig() .setParent("com.campus") // 根包名 .setEntity("entity").setMapper("mapper").setService("service").setController("controller"); generator.setPackageInfo(packageConfig); // 策略配置 StrategyConfig strategyConfig = new StrategyConfig() .setNaming(NamingStrategy.underline) // 数据库表名转驼峰 .setColumnNaming(NamingStrategy.underline) // 字段名转驼峰 .setEntityLombokModel(true) // 用Lombok .setRestControllerStyle(true) // 生成@RestController .setInclude("user", "goods", "order", "message"); // 只生成这几个表 generator.setStrategy(strategyConfig); generator.execute(); }

运行后会在com.campus.entity下生成带@Data、@TableName("user")、@TableId(type = IdType.AUTO)的实体类——比手敲快10倍,且字段类型精准(如price是BigDecimal,不是Double)。


3. 让状态流转不翻车:基于枚举+事务的订单与审核流程实现

3.1 为什么用枚举管理状态,而不是int或String?

看这段常见代码:

// ❌ 危险!数字魔法值,后期维护地狱 if (order.getStatus() == 1) { // 1代表待支付?还是待发货? // ... } // ❌ 更危险!字符串硬编码,拼错就静默失败 if ("shipped".equals(order.getStatus())) { // 少个d?不报错,逻辑永远不走 // ... }

答辩时老师问:“如果新增‘已取消’状态,要改几处代码?”——你答“大概七八处”,他就知道你没工程思维。

正确做法:定义OrderStatus枚举,且必须带数据库映射值:

@Getter public enum OrderStatus { WAIT_PAY(1, "待支付"), PAID(2, "已支付"), SHIPPED(3, "已发货"), RECEIVED(4, "已签收"), CANCELLED(5, "已取消"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public static OrderStatus fromCode(int code) { for (OrderStatus status : values()) { if (status.code == code) return status; } throw new IllegalArgumentException("Unknown order status: " + code); } }

实体类中用@TableField绑定:

@TableField("status") private OrderStatus status; // MyBatis-Plus自动转换code存库、desc用于展示

这样:

  • 数据库存的是1,2,3...整数,节省空间且查询快;
  • Java代码里用order.getStatus() == OrderStatus.PAID,语义清晰、IDE自动补全、编译期检查;
  • 新增状态只需加枚举项,无需改SQL或DAO。

3.2 审核流程的事务边界:为什么@Service方法必须加@Transactional?

校园二手平台里,管理员审核商品时要:

  1. 更新商品状态为“已审核”;
  2. 给发布者发站内信;
  3. 记录操作日志。

如果第2步发信失败(比如消息队列宕机),第1步和第3步还执行了,数据就脏了。必须保证三者要么全成功,要么全回滚。

关键代码:

@Service public class GoodsAuditService { @Transactional(rollbackFor = Exception.class) // 必须!否则异常不回滚 public boolean auditGoods(Long goodsId, Boolean pass) { Goods goods = goodsMapper.selectById(goodsId); if (goods == null) return false; // 1. 更新商品状态 goods.setStatus(pass ? GoodsStatus.AUDITED : GoodsStatus.REJECTED); goodsMapper.updateById(goods); // 2. 发送站内信(模拟) Message message = new Message(); message.setSenderId(0L); // 系统ID message.setReceiverId(goods.getUserId()); message.setContent(pass ? "您的商品已通过审核" : "商品未通过审核,请修改后重新提交"); messageService.save(message); // 3. 记录日志 AuditLog log = new AuditLog(); log.setGoodsId(goodsId); log.setOperatorId(1001L); // 管理员ID log.setPass(pass); auditLogService.save(log); return true; } }

注意:

  • @Transactional必须加在public方法上,private方法加了无效;
  • rollbackFor = Exception.class:默认只对RuntimeException回滚,但发消息可能抛Checked Exception(如IOException),必须显式声明;
  • 方法内不能用this.auditGoods()调用自身,否则事务失效(Spring AOP代理机制限制)。

3.3 并发下单的库存扣减:乐观锁实战

学生抢购低价教材时,两个请求同时读到库存=1,都判断“够买”,然后都扣减成0——超卖!

解决方案:在商品表加version字段(int类型,默认值0),MyBatis-Plus自动支持乐观锁:

@TableField("version") @Version private Integer version;

扣库存时:

// 查询时带上version Goods goods = goodsMapper.selectOne(new LambdaQueryWrapper<Goods>() .eq(Goods::getId, goodsId) .select(Goods::getId, Goods::getStock, Goods::getVersion)); if (goods.getStock() < buyCount) { throw new BusinessException("库存不足"); } // 更新时where条件包含version,且version自增 LambdaUpdateWrapper<Goods> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(Goods::getId, goodsId) .eq(Goods::getVersion, goods.getVersion()) // 关键!只更新version匹配的行 .setSql("stock = stock - " + buyCount) .set(Goods::getVersion, goods.getVersion() + 1); // version+1 int updated = goodsMapper.update(null, updateWrapper); if (updated == 0) { throw new BusinessException("库存已被抢光,请刷新重试"); // update返回0说明version不匹配,被别人抢先扣了 }

这就是典型的“先查后更”并发问题解法——不用synchronized锁整个方法(性能差),也不用Redis分布式锁(毕业设计没必要),用数据库层面的乐观锁最轻量、最可靠。


4. 权限与安全:从登录校验到CSRF防护的毕业设计级实践

4.1 Session vs JWT?毕业设计选Session更稳

网上教程狂推JWT,但毕业设计用JWT有三大坑:

  • 要自己实现Token刷新、黑名单注销(学生很难写对);
  • 前端需存Token到localStorage,XSS攻击可窃取(答辩现场被老师指出安全漏洞);
  • JWT过期时间难设定——设1小时,用户填表填到一半Token过期;设7天,账号被盗无法立即冻结。

而HttpSession天然支持:

  • 服务端存储,Token(JSESSIONID)只在Cookie里传输;
  • session.invalidate()一键登出;
  • Tomcat默认配置就防Session Fixation。

登录控制器示例:

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto, HttpServletRequest request) { User user = userService.getByUsername(dto.getUsername()); if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.fail("用户名或密码错误"); } // ✅ 正确:设置Session属性 HttpSession session = request.getSession(true); session.setAttribute("user", user); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作过期 return Result.success("登录成功"); }

关键点:request.getSession(true)确保Session存在,setMaxInactiveInterval设合理过期时间(别用默认30秒!)。

4.2 拦截未登录访问:用HandlerInterceptor而非Filter

很多同学用Filter拦截/admin/**,但Filter里拿不到Spring容器Bean(如UserService),无法查用户角色。

正确做法:写HandlerInterceptor,它在DispatcherServlet中执行,能注入Service:

@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { // 未登录,重定向到登录页 response.sendRedirect(request.getContextPath() + "/login.html"); return false; } User user = (User) session.getAttribute("user"); String uri = request.getRequestURI(); // 后台路径仅管理员可访问 if (uri.startsWith("/admin/") && !"ADMIN".equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问"); return false; } return true; } }

注册拦截器:

@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .excludePathPatterns("/login.html", "/login", "/css/**", "/js/**", "/images/**"); // 静态资源放行 } }

这样既拦截了/admin/user/list,又能在拦截时查数据库验证角色——比Filter灵活得多。

4.3 CSRF防护:Thymeleaf模板里的隐藏域必须加

Spring Security默认开启CSRF,但很多毕业设计没配,导致:

  • 表单提交时403 Forbidden;
  • 答辩时老师用Postman发POST请求,直接进后台——安全漏洞实锤。

启用CSRF后,所有表单必须带token:

<!-- Thymeleaf模板中 --> <form th:action="@{/goods/publish}" method="post"> <!-- ✅ 必须加这一行 --> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <input type="text" name="title" placeholder="商品标题"/> <input type="number" name="price" placeholder="价格"/> <button type="submit">发布</button> </form>

后端Controller无需额外代码,Spring自动校验。如果漏了这行,表单提交必403——这是答辩现场最高频的“咦?怎么点不动?”问题。

注意:CSRF token只对POST/PUT/DELETE等非GET请求生效,GET请求不需要。所以搜索框用GET,不用加token。


5. 部署与演示:Windows/Linux双环境零报错启动指南

5.1 打包前必做的三件事

很多同学mvn clean package后直接扔到服务器上,结果:

  • Windows下启动报java.io.FileNotFoundException: class path resource [application.yml] cannot be resolved;
  • Linux下nohup java -jar xxx.jar &后进程立刻退出;
  • Nginx反向代理时CSS/JS全部404。

根源在于打包配置没调。pom.xml中必须确认:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!-- ✅ 关键:把依赖打进fat jar --> <fork>true</fork> <finalName>campus-secondhand</finalName> <classifier>exec</classifier> </configuration> </plugin> </plugins> </build>

且src/main/resources/application.yml里:

spring: profiles: active: prod # 生产环境用prod配置 --- spring: config: activate: on-profile: prod server: port: 8080 servlet: context-path: /campus # 数据库连接用生产环境地址 spring: datasource: url: jdbc:mysql://192.168.1.100:3306/campus_secondhand?...

这样打包后campus-secondhand.jar里既有代码又有依赖,java -jar就能跑。

5.2 Windows部署:解决中文路径与控制台乱码

在Windows Server上,如果项目路径含中文(如D:\毕业设计\校园二手平台\),启动时会报:

Caused by: java.nio.charset.MalformedInputException: Input length = 1

解决方案:

  1. 路径全英文:把jar包放到D:\campus\这种纯英文路径;
  2. 启动脚本加JVM参数:
@echo off java -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -jar campus-secondhand.jar pause

-Dfile.encoding=UTF-8强制JVM用UTF-8读配置文件,避免application.yml里中文注释乱码;
-Dsun.jnu.encoding=UTF-8解决Windows控制台输出中文乱码(答辩现场演示时,日志里“商品发布成功”不能显示成“??发布??”)。

5.3 Linux部署:Nginx反向代理与静态资源404修复

Linux上常见问题:

  • java -jar campus-secondhand.jar启动后,浏览器访问http://ip:8080/campus能打开,但http://ip/campus打不开;
  • Nginx配置了location /campus { proxy_pass http://127.0.0.1:8080/campus; },但CSS/JS路径是/css/app.css,Nginx找不到。

正确Nginx配置:

server { listen 80; server_name campus.example.com; location /campus/ { proxy_pass http://127.0.0.1:8080/campus/; # 注意结尾斜杠! proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # ✅ 关键:静态资源由Nginx直接服务,不走Java location /campus/css/ { alias /var/www/campus/css/; expires 1h; } location /campus/js/ { alias /var/www/campus/js/; expires 1h; } location /campus/images/ { alias /var/www/campus/images/; expires 1h; } }

然后把src/main/resources/static/下的css/、js/、images/文件夹复制到/var/www/campus/下。这样:

  • Java只处理动态请求(/campus/goods/list),静态资源由Nginx高效返回;
  • location /campus/末尾的/保证路径重写正确,避免/campus/css/app.css被代理成http://127.0.0.1:8080/campuss/css/app.css(多一个s)。

6. 答辩现场救急技巧:三招让老师眼前一亮的细节打磨

6.1 数据库设计文档:用PlantUML画ER图,比截图更专业

答辩PPT里放数据库截图,老师一眼看出字段命名不规范(如user_name和username混用)。不如用PlantUML写代码生成ER图,自动同步:

@startuml ' 学生表 [User] as U { + Long id + String username + String password + String realName + String college + String phone } ' 商品表 [Goods] as G { + Long id + String title + BigDecimal price + String description + String college + Integer status + Long userId + LocalDateTime createTime } ' 订单表 [Order] as O { + Long id + Long goodsId + Long buyerId + Long sellerId + Integer status + LocalDateTime createTime } U --> G : "发布" G --> O : "下单" @enduml

用 PlantText 在线渲染成高清图,插入PPT。老师看到“status字段在Goods和Order中语义一致”,就知道你懂设计一致性。

6.2 截图里的玄学:用Chrome DevTools模拟手机视图

答辩时老师问:“移动端适配做了吗?”你打开手机拍屏幕——像素糊、边框歪、按钮错位。其实Chrome按F12,右上角点📱图标,选iPhone 12,再按Ctrl+Shift+M进响应式模式,刷新页面。此时截图:

  • 宽度自动适配375px;
  • 字体大小、按钮间距、图片缩放全部真实;
  • 还能点“Toggle device toolbar”看不同尺寸表现。

把这张图放进PPT,老师点头:“嗯,确实做了响应式”。

6.3 部署视频的后悔药:录屏时加终端命令高亮

部署视频里,如果只录黑底白字的命令行,老师看不出你执行了什么。用Windows Terminal或iTerm2,装oh-my-posh主题,让命令高亮:

# 录屏时显示效果: $ cd /home/student/campus $ java -Dfile.encoding=UTF-8 -jar campus-secondhand.jar & [1] 12345 $ curl http://localhost:8080/campus/login.html {"code":200,"msg":"success"} # ✅ 返回JSON说明服务起来了

关键点:

  • &后台运行后,必须用curl验证端口监听成功,否则视频里“启动完成”只是假象;
  • curl返回{"code":200}比“页面打开”更可信——证明Spring Boot内嵌Tomcat真在跑。

我当年答辩前夜发现Nginx配置少了个/,重录部署视频到凌晨两点,就为让老师看到那行绿色的{"code":200}。现在想想,那行JSON比一百句“已部署成功”都有力。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询