
每年到毕业季最热闹的永远是那群被毕设折磨的准毕业生。我见过太多拿到题目后一脸茫然的同学也见过不少答辩前一周才匆匆忙忙开始写代码的“勇士”。今天想围绕一个非常典型的题目——“SpringBootVue 在线租房和招聘平台”聊聊这类前后端分离的Java Web毕设项目到底应该怎么拆、怎么做、怎么避开那些没人会提前告诉你的坑。这篇文章不是单纯贴一段代码就交差而是希望你把它当成一份可以参考的完整项目复盘来读尤其是那些标题里看不出来的细节我会尽量讲透。这个题目之所以值得单独写一篇长文是因为它同时踩中了两个高频业务场景租房和招聘。如果你能把这两块业务背后的权限设计、数据关联、搜索逻辑、状态流转都想清楚那么不仅仅是为了应付毕设答辩你以后做任何Java Web系统其实都是在复用同一套方法论。接下来我会从选题思路、技术架构、数据库设计、后端实现、前端联调一直到部署上线的完整流程把各个环节的关键问题逐个说清楚。1. 选题拆解一个项目如何同时覆盖两块高频业务第一次看到这个题目的人大概率会有一个疑惑租房和招聘一个是围绕房源和租客一个是围绕职位和求职者这俩业务怎么揉到一个平台里如果只是硬把两个功能模块堆在一个系统里会出现权限模型混乱、页面结构臃肿、数据库表关系扯不清的问题。但换个角度想这两块业务的底层用户模型其实高度一致都需要用户注册登录都分为发布方和需求方都需要信息展示和状态管理都需要站内咨询或申请操作。1.1 用“发布方—需求方”模型统一两块业务我在实际梳理这类项目的时候会先画一张很朴素的业务关系图租房侧房东或中介发布房源租客浏览并发出看房/申请意愿。招聘侧企业发布职位求职者投递简历。看到没有两边的核心动作都是“一个人发布另一个人获取信息后发起互动”。这意味着我们的后端认证模块可以完全复用前端页面也能抽出大量公共组件比如搜索栏、列表卡片、详情弹窗、状态标签。不要一上来就想着做两套截然不同的系统那样只会增加工作量答辩的时候也很难讲清楚设计思路。1.2 从标题看这套毕设必须具备的能力边界这套项目的标题里明确了几个关键交付物完整项目源码、SQL脚本、接口文档。这意味着你不光要把代码跑起来还得把代码背后的数据库结构和对外接口抽出来单独交付。很多人最后被导师挑毛病不是代码不行而是SQL脚本缺数据、接口文档写得太敷衍。所以在规划阶段就要把这三件事当成三个独立成果来做而不能只盯着功能页面。另外标题里的“Java Web毕设”这几个字决定了技术选型的基调SpringBoot是后端核心Vue是前端框架数据库通常用MySQLORM用MyBatis或MyBatis-Plus。这几乎是当前高校Java Web毕设的默认组合了选它最大的好处是你遇到任何报错都能在网上找到大量经验帖不至于卡住。1.3 功能清单的合理切分根据我对同类毕设项目经验的理解完整功能应该这样切分模块功能点角色用户认证注册、登录、退出、Token校验全部用户个人中心资料编辑、密码修改、头像上传全部用户租房管理发布房源、编辑/上下架、房源列表与搜索、收藏房东/租客租房互动预约看房、在线留言租客→房东招聘管理发布职位、编辑/关闭、职位列表与搜索、简历投递企业/求职者简历管理简历编辑、投递记录、收到的简历求职者/企业平台管理端用户管理、内容审核、数据统计管理员这七个模块覆盖了信息发布、内容检索、用户互动、后台管理四个维度放在一个毕设里工作量适中而且每个模块都能在答辩时讲出子系统的设计逻辑。我见过不少同学把简历详情、职位申请记录这些功能砍掉结果答辩时被老师一问“求职者投了简历企业去哪里查看”就愣住。关键闭环一定不能缺。2. 整体架构设计与数据库建模功能清单理清楚之后下一步就是设计代码结构。很多同学一上来就建工程写代码写到后面Controller、Service、Mapper混在一起改一个需求牵一发动全身。正确的姿势是先想清楚后端要暴露哪些接口、前端要渲染哪些数据再把工程拆成分层结构。2.1 前后端分离的目录结构与请求链路后端我一般建议用标准的分层结构不搞花活src/main/java ├── config # 配置类CORS、拦截器、Swagger ├── controller # 控制器只做参数接收与响应封装 ├── service # 业务层核心逻辑与事务控制 ├── mapper # MyBatis的Mapper接口 ├── entity # 实体类对应数据库表 ├── dto # 请求/响应对象避免实体直接暴露 ├── common # 统一返回体、异常处理、工具类 └── web # 全局异常捕获、登录拦截器前端我用Vue工程来组织重点包含这几个目录src ├── api # 每个模块的接口封装 ├── router # 路由配置 ├── store # 用户状态管理Vuex或Pinia ├── views # 页面组件 ├── components # 公共组件 └── utils # axios实例、token缓存等请求链路看起来很简单页面调用axios - 进入Vue Router - 请求后端接口 - SpringBoot的DispatcherServlet - Controller - Service - Mapper - 数据库。但你必须在项目一开始就定好统一返回体否则后面每个接口的返回结构都不一样前端拿到数据要到处判断。public class RT { private Integer code; private String msg; private T data; public static T RT ok(T data) { RT result new R(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } public static T RT error(String msg) { RT result new R(); result.setCode(500); result.setMsg(msg); return result; } }2.2 数据库表设计的关键取舍数据库设计是整个项目的地基也是最容易在答辩时被追问的地方。我建议核心表控制在10到12张左右既不算冗余又能把业务逻辑展示完整。表名核心字段说明userid, username, password, role, avatar, phone角色用1/2/3区分普通用户、房东/企业、管理员houseid, user_id, title, address, price, status, imagestatus标记待审核/出租中/已下架house_applyid, house_id, user_id, message, status租客看房申请状态为待处理/已联系/已拒绝favoriteid, user_id, house_id房源收藏jobid, user_id(企业), title, salary_min, salary_max, degree, status职位字段尽量用数值区间方便搜索resumeid, user_id, real_name, education, experience, advantage简历表建议只保留一个基础版本job_applyid, job_id, user_id, resume_id, status简历投递记录企业侧查看这里noticeid, title, content, create_time管理员发布的公告admin_logid, admin_id, action, target_id后台操作日志加分项有两点我想特别提醒。第一不要在两张表之间堆太多中间表比如收藏功能用一张favorite就能解决不必额外搞收藏夹、收藏分组之类除非你想把自己累死。第二house表和job表里凡是牵涉到“状态”的字段一定要用tinyint类型0、1、2而不是varchar否则后面写SQL统计的时候到处都要写字符串匹配查起来很痛苦。2.3 SQL脚本里的“隐形分数”数据预置很多时候答辩老师并不会认真看你的代码但会直接打开Navicat看你的数据库表和数据。一份完整的SQL脚本要包含三部分内容建库建表语句、基础字典数据、演示用样例数据。我强烈建议你预留8到10套房源、8到10个职位、3到5个用户含不同类型角色样例数据里的图片可以先用占位图但文字描述一定要真实比如“地铁口精装两室一厅”这种会让演示效果完全不一样。-- 样例创建三个角色的演示账号 INSERT INTO user (username, password, role, phone, avatar) VALUES (landlord01, e10adc3949ba59abbe56e057f20f883e, 2, 13800138001, /upload/avatar1.png), (jobseeker01, e10adc3949ba59abbe56e057f20f883e, 1, 13800138002, /upload/avatar2.png), (company01, e10adc3949ba59abbe56e057f20f883e, 2, 13800138003, /upload/avatar3.png);注意这里的password字段存储的是MD5加密后的字符串而实际项目中更推荐用BCrypt。在毕设阶段用MD5加盐问题不大但如果导师较真你在答辩时主动强调“如果把代码用于生产环境我会换成BCrypt并且加盐”反而能体现工程意识。3. SpringBoot后端业务分层的落地与踩坑记录后端是整个项目最核心的交付物也是你答辩时最能体现水平的部分。这一章我就直接按实际开发的顺序来讲从环境配置到核心接口再到我真实踩过的一些坑尽量还原一个完整的后端开发过程。3.1 SpringBoot版本与依赖管理一座隐藏的冰山很多同学在做这个项目时首选Spring Boot 2.7.x因为资料多、教程多。但如果你是从零搭建我建议你直接看Spring Initializr默认给的版本再结合自己本地JDK决定。这里有一个非常典型的坑Spring Boot 3.x要求JDK 17及以上如果你电脑装的是JDK 8强行用3.x版本会导致项目启动直接失败。我遇到过一位同学的报错环境是JDK 8Spring Boot版本是3.1.0IDEA编译报“无法访问org.springframework.boot.SpringApplication”之类的问题最后降级到2.7.18才跑通。所以如果你用的是JDK 8就直接在pom.xml里锁定版本parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent依赖声明的部分这几个starter是必须的dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependencyMyBatis-Plus也是一个不错的选择它可以省掉大量单表CRUD的XML配置。但如果你想在答辩时讲清楚SQL是怎么写的我建议核心查询还是自己手写XML这样当你面对“这个查询为什么能查出来”的时候不会心虚。3.2 房屋和职位的状态流转最容易忽略的业务逻辑租房和招聘的核心业务表面上是增删改查实际上暗含一组严格的“状态流转”。我见过不少人的毕设里房屋状态只有0和1结果下架之后的房源还能被搜索到这就会在演示时闹笑话。租房侧的正常状态流转应该是这样的待审核(0) - 出租中(1) - 已下架(2) 出租中(1) - 已出租(3) - 已下架(2)房东发布房源后进入待审核管理员通过后变成出租中。租客在房源详情页发起“看房申请”不会直接改变房源状态而是写入house_apply表。房东在个人中心看到申请后手动标记“已联系”房源本身的状态仍由房东控制。招聘侧的逻辑类似企业发布职位后可以直接上线但为了体现管理员审核角色的价值我建议保留一个待审核状态否则后台管理模块能做的事就不够多了。职位关闭之后该职位不能再投递简历之前投递的简历仍然保留这些细节都对应着你要写在前端页面的条件判断。这种“状态机”设计在答辩时是很好的加分点。你可以直接跟老师说我参考了订单系统的状态设计将房源和职位的业务行为拆分成可跟踪的离散状态每一步操作都有明确的前置和后置约束。话不用多老师能听出你懂设计而不只是在堆代码。3.3 基于JWT的认证与权限控制对于前后端分离的项目Session登录的体验是比较差的因为Vue页面跨域请求时还要处理Cookie、CORS凭证等一堆问题。我这里使用的方案是JWT 拦截器这也是目前Java Web毕设的主流做法。JWT的原理可以简单理解为用户登录成功后后端签发一段加密的JSON字符串前端存到localStorage里之后每次请求都在Authorization请求头带上这段字符串后端拦截器解析校验通过后放行。后端签发代码核心片段public String createToken(User loginUser) { try { Algorithm algorithm Algorithm.HMAC256(your-secret-key); Date expireAt new Date(System.currentTimeMillis() 10 * 60 * 1000); return JWT.create() .withClaim(userId, loginUser.getId()) .withClaim(role, loginUser.getRole()) .withExpiresAt(expireAt) .sign(algorithm); } catch (Exception e) { throw new BusinessException(Token签发失败); } }拦截器里要排除掉登录注册、房源展示、职位展示这些公开接口其他接口都要校验Token。这里常见的问题是拦截器写完后登录功能直接挂掉——请求被拦截器拦下后前端仍然以为自己登录成功了但后端实际返回401。解决方法是配置一个费白名单registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/house/list, /api/job/list );3.4 业务查询中的筛选与分页从接口设计到SQL书写租房和招聘两个核心模块最核心的接口是列表页。这个列表接口要同时支持分页、关键词搜索、价格/薪资区间筛选、状态筛选设计得好不好直接影响前端页面的复杂程度。后端接收参数的DTO可以这样设计public class HouseQueryDTO { private Integer pageNum; private Integer pageSize; private String keyword; private BigDecimal minPrice; private BigDecimal maxPrice; private Integer status; private String district; // 区域筛选 }对应的分页查询SQL用MyBatis XML写select idselectHousePage resultTypecom.example.entity.House SELECT h.*, u.username AS publisherName FROM house h LEFT JOIN user u ON h.user_id u.id where if testkeyword ! null and keyword ! AND (h.title LIKE CONCAT(%, #{keyword}, %) OR h.address LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND h.price gt; #{minPrice} /if if testmaxPrice ! null AND h.price lt; #{maxPrice} /if if teststatus ! null AND h.status #{status} /if /where ORDER BY h.create_time DESC /select这里有几个写SQL的细节模糊搜索记得用CONCAT(%, #{keyword}, %)不要直接在XML里拼串否则会有SQL注入风险。虽然毕设系统一般不会真被攻击但老师在代码里看到参数化查询印象分会高不少。价格筛选用gt;和lt;而不是因为XML里这些符号必须转义。分页有两个选择自己用LIMIT #{offset}, #{pageSize}或者集成PageHelper。我建议直接集成PageHelper一页代码就能搞定答辩时讲起来也顺。分页响应给前端的结构也要统一通常返回总条数、每页大小、当前页数据列表public class PageResultT { private Long total; // 总条数 private Integer pageNum; // 当前页 private Integer pageSize; private ListT list; }4. Vue前端从脚手架到完整功能闭环Vue这边的工作量通常比后端还要大因为页面多且杂。很多同学在毕设里给Vue的定位只是“套一个别人的后台管理模板”这其实是一个误区。毕设答辩时老师真的会逐个页面点过去看交互模板上的水印、Demo数据、无关菜单如果不清理干净会留下很不好的印象。4.1 工程初始化和环境准备Vue前端我建议直接用Vite构建如果用的是Vue 3那Vite几乎是唯一合理的选择。创建命令很简单npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 axios element-plusVue 2和Vue 3的选择上如果教程储备是Vue 2为主我也不会拦你但我个人更推荐Vue 3因为Element Plus组件库比Element UI维护得更好新项目没有理由去用旧生态。网上关于Vue 3环境配置的报错非常常见最典型的坑有两个Node版本太低导致Vite跑不起来建议直接装Node 18以上的LTS版本。最终打包后浏览器打开页面白屏多半是publicPath配置的问题需要在vite.config.js里加base: ./否则你用相对路径打开打包产物时所有资源都变成绝对路径直接被浏览器拒绝。4.2 Axios封装与路由守卫把前端工程化做到位凡是体验好的前端工程一定不会在每个页面里直接调axios。我建议在utils/request.js里统一封装axios实例集中处理Token注入、响应状态码判断、错误提示三件事import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器带上JWT request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) // 响应拦截器统一处理后端返回结构 request.interceptors.response.use( response { const res response.data if (res.code 200) { return res } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { if (error.response error.response.status 401) { ElMessage.error(登录状态已过期请重新登录) localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } ) export default request路由守卫则负责控制访问权限。比如未登录用户不能进入个人中心普通用户不能进入企业发布页面管理员不能访问普通用户的租房功能。这些判断全部在router.beforeEach里做router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role Number(localStorage.getItem(role)) if (to.path /login || to.path /register) { next() return } if (!token) { next(/login) return } if (to.meta.role to.meta.role.indexOf(role) -1) { ElMessage.warning(无权访问该页面) next(/) return } next() })4.3 租房和招聘的信息展示与搜索交互核心列表页建议做成同一套布局顶部是筛选区关键词搜索、价格/薪资区间、区域选择中间是卡片式列表点击卡片进入详情页。前端搜索的逻辑很直接每当筛选条件变化时重新调用一次接口并传入当前页和页面大小。有一点要特别注意Element Plus的分页组件绑定的current-page和page-size必须和后端接口的pageNum、pageSize保持一致。我见过太多因为参数名不一致导致分页死活跳不过去的情况排查起来非常浪费时间。前端页面里这样写就稳了el-pagination v-model:current-pagequery.pageNum v-model:page-sizequery.pageSize :totaltotal current-changeloadList layouttotal, prev, pager, next /详情页里除了展示信息之外还要根据登录用户的身份渲染不同的操作按钮租客看房源显示“我要看房”按钮点击弹出对话框填写留言。房东看自己的房源显示“编辑”“下架”按钮。求职者看职位显示“投递简历”按钮如果已投递则显示“已投递”不可重复点击。企业看自己的职位显示“查看收到的简历”按钮。这些身份判断在真实项目里就是一堆v-if配合role和userId的比对代码本身不复杂难的是你需要在每个详情页里想清楚“谁看见了什么、能干什么”这恰好也是答辩时老师喜欢追问的地方。4.4 毕设前端最容易忽视的演示体验问题这里我要说几个演示时非常容易翻车的点路由刷新404如果前端使用history模式后端不做处理的话直接刷新某个子页面会得到404。解决方法是后端写一个视图控制器把所有非API路径都转发到index.html。图片加载失败数据库里的图片路径写的是本地/upload/xxx.jpg但后端服务是在另一台机器或者端口下跑的前端自然加载不出来。建议前端统一在请求地址前拼接一个baseURL。状态标签颜色房源状态和职位状态一定要用不同颜色的标签区分。Element Plus的el-tag可以直接绑定动态type比如“出租中”用绿色“已下架”用灰色演示时一眼看到状态差异你的界面会显得非常专业。5. SQL脚本与接口文档为什么是加分项很多时候项目源码本身反而不是决定成绩的唯一因素。老师拿到你的压缩包之后首先打开的往往就是SQL脚本和接口文档因为这两个文件最能反映你有没有工程规范意识。5.1 SQL脚本不只是建表语句我在文章前面提到SQL脚本要包含建库建表、字典数据、样例数据三部分。但从交付角度来说还有三个容易被忽略的细节在每张表创建语句前加上DROP TABLE IF EXISTS方便反复导入不报错。字段注释必须写清楚。不是让你在SQL里随便写两行而是每张表、每个字段都要有COMMENT。Navicat打开表结构后如果所有字段都写了注释老师会觉得你非常严谨。注意字符集。CREATE DATABASE时要指定DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci否则插入中文数据时可能乱码。如果你的MySQL版本是5.7以下还要额外注意索引长度问题。5.2 接口文档怎么写才能让答辩不卡壳接口文档建议使用Swagger自动生成加上离线Markdown版本。Swagger不但能生成在线调试页面还能在答辩的时候直接演示“点击Try it out - 调用登录接口 - 拿着Token访问受保护接口”的完整流程这套动线非常加分。在SpringBoot里集成Swagger非常简单2.7.x版本用springfox-boot-starter3.x版本用springdoc-openapi-starter-webmvc-ui。我的建议是直接在pom里加这些依赖再写一个简单的配置类访问/swagger-ui/index.html就能打开接口页面。如果不想集成Swagger也可以用YApi或Apifox导出一个离线接口文档。核心原则是每个接口有三个信息必须写全即请求地址、请求参数含类型和是否必填、返回示例。很多人的文档里只写接口名和路径参数全部不写这种文档对使用者来说基本等于没用。5.3 在线调试与离线文档的配合使用我自己的习惯是同时保留两个版本一个Swagger在线调试文档用于自己测试接口时快速调用一个导出的Markdown文档放到项目的docs目录底下和README放在一起。这样无论老师习惯哪种查看方式都能快速了解系统提供了哪些接口。做这部分工作的额外收益是写接口文档的过程会逼你把所有接口重新梳理一遍很多前后端参数名对不上的问题都是在这个阶段暴露出来的。你提前发现总比答辩现场前端点不动、后端报参数缺失要好得多。6. 部署、联调与常见坑位汇总最后这个部分我把自己在实际调试这类项目时踩过的一些高频问题整理出来每条都是真实场景建议收藏起来当避坑清单用。6.1 前后端联调时的跨域和代理问题开发模式下的跨域问题最简单。Vite自带代理配置一下就行server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }之后前端所有请求都发到同样域名的/api路径上由开发服务器帮忙转发到后端浏览器层面就不存在跨域了。但如果你选择在生产环境部署时把前后端分开跑后端就必须开启CORS。用SpringBoot设置全局CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }注意这里有个经典坑设置allowCredentials(true)之后前端就不能用*作为allowedOrigins必须用allowedOriginPatterns来通配否则浏览器会直接拒绝响应。6.2 前端打包后的资源托管与刷新404如果选择把前端打包后放在后端的src/main/resources/static目录下启动后端直接访问8080端口就能看到完整页面这是最省事的部署方式。打包前必须修改两处配置vite.config.js里设置base: ./。路由使用createWebHashHistory而不是createWebHistory。虽然hash模式URL里带个#不太好看但省掉了刷新404的麻烦毕设阶段我不建议在这里折腾history模式的转发规则。如果你坚持用history模式则要在后端加一个重定向控制器把非API请求全部转发到index.html否则刷新页面就会白屏Controller public class ViewController { RequestMapping(value {/, /house/**, /job/**, /user/**, /admin/**}) public String forward() { return forward:/index.html; } }6.3 一台电脑上同时启动前后的完整检查清单我负责地告诉你很多同学的毕设源码发给别人之后跑不起来不是因为代码有问题而是因为缺少启动说明。所以我强烈建议你在压缩包里放一个README里面写清楚这几件事事项具体内容环境版本JDK、MySQL、Node的具体版本号初始化步骤导入SQL脚本 - 修改application.yml - 启动后端后端启动地址http://localhost:8080前端初始化npm install - npm run dev - 浏览器访问演示账号分别给出管理员、房东/企业、求职者的账号密码常见问题端口占用、Token过期、图片不显示分别怎么解决这种说明文档不需要写得多华丽但一定要能让人照做就能跑起来。7. 如何将这套项目扩展成答辩的“亮点工程”功能做完了不代表万事大吉答辩时你要能讲清楚“为什么这么做”以及“有哪些可以继续优化的地方”。如果时间还充裕我强烈建议加一两个成本低、但呈现效果好的扩展功能7.1 管理员端的数据可视化管理员首页完全可以放两张图表一张展示房源发布趋势一张展示职位投递热度。这类图表不需要自己写Canvas直接用ECharts或者AntV G2Plot后端只需要提一个统计接口返回近七天的发布数量前端把数据喂给图表组件就行。后端统计接口的核心SQL也就这么一点SELECT DATE(create_time) AS day, COUNT(*) AS count FROM house WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time)答辩时老师看到图表比看到一堆表格数据的直观感受要强得多。7.2 站内信与评论留言如果你的预留时间比较多可以在租房详情里加一个评论留言功能让租客和房东在房源页面互相留言或者在招聘详情页加“公司评价”的功能。这会在功能上多出一张消息表但前后端实现的套路跟house_apply几乎完全一样不需要额外学习新知识。7.3 定时任务与过期处理你可以用一个SpringBoot的Scheduled定时任务每天凌晨扫描已经过期未处理的看房申请或投递记录自动标记为超时关闭。这个功能原理特别简单但能在答辩时体现你对业务生命周期的思考比硬堆几个页面要有说服力。写在最后的一些体会说实话做这种前后端分离的Java Web毕设真正的难点从来不是某个技术点有多难而是很多零碎的问题会反复出现且相互纠缠。我在这个项目里经历过版本不兼容、跨域被拦、分页数据不对、前端打包后白屏、上传图片无法访问等一堆状况每次排查到最后都发现是环境或者配置层面的细节问题而不是代码本身复杂。所以我特别想把这条经验分享给正在做这套毕设的同学拿到项目后先不要急着写代码先把环境、版本、数据库脚本跑通再用最小链路走通“登录-查列表-发起操作”这三个功能后面所有的页面和接口都是在这个骨架上填充肉。项目骨架稳定了你只需要按模块推进多数问题都能控制在小范围内。这篇内容是基于这类毕设项目的常见实践整理出来的完整思路。如果你正卡在某个具体报错上对照这篇文章里的排查清单一步一步走多半能自己解决。最后希望你的毕设都能顺利通过答辩写出一份毕业后自己回头看也不觉得羞愧的代码。