
毕设做入校申请系统用Spring Boot骨架把申请、审批、统计、导出这些业务在一个项目里完整跑通确实是这几年计算机毕业设计里性价比很高的一档选题。理由很简单它不是一个纯CRUD的玩具项目而是带有明确业务流程、多角色权限、状态流转和实操部署价值的系统在答辩时既有的讲也有得演示比单纯做一个增删改查的管理后台要耐打得多。我带的几个学生做完这个题目之后反馈基本一致开发周期可控技术栈主流代码量足够支撑一篇像样的毕业论文而且还能顺势把JWT认证、异常统一处理、MyBatis分页查询这些高频面试考点全部练一遍。这篇博文我把整个项目从选题到数据库设计再到核心审批流程和部署演示按我当时带项目的思路完整拆开讲。写给你参考也是给正处于脑子里有需求但是不知道从哪一行代码开始阶段的同学一份可以直接对照着落地的路线图。1. 为什么选这个题目入校申请的碎事比想象中多这个题目的第一感觉是简单但我们换个角度想整个校园里每天都在发生多少需要审批的入校动作1.1 一个看似简单的流程背后站着多少个角色访客入校要看受访人是否同意、报备信息是否完整外协施工人员入校要关联项目和安全承诺学生家长要提交申请再由辅导员和保卫处依次审批。每一条申请都要经历填报 - 辅导员/导师审批 - 保卫处终审 - 入校核验 - 离校记录这样的链路中间还穿插着驳回、撤回、超时未审批、手动结束流程等边界情况。把这些角色和动作捋清楚之后这个系统就远不止一套CRUD那么简单了。每个角色的视图不一样可执行的操作不一样看到的数据范围也不一样。这就是权限设计最好的切入点也是毕业论文里系统需求分析章节最扎实的内容来源。1.2 毕设选题的三个判断标准我建议每一个选管理系统类毕设的同学都用三个问题来验证题目是否成立系统有没有明确的多角色边界和审批流转入校申请系统有而且天然适合用状态机来描述。系统有没有实际的应用场景和数据闭环申请单从创建到入校到离校全链路的数据都在一个表里演化不存在为了凑功能而硬造模块的情况。技术栈有没有被验证过的主流实践可以参照Spring Boot MyBatis Plus MySQL Vue这套组合几乎是互联网上入门资料最齐全的方案遇到问题搜到的解决方案一定比问题本身多。这三点都满足之后再动手设计就会很顺。有的同学一上来就写代码结果做到一半发现角色边界没想清楚所有逻辑全堆在一个Controller里返工成本极高。先花三天把流程和表结构定死是这门课真正想教给你的东西。2. 系统架构和数据库设计先想清楚谁来用再画表入校申请系统的核心价值不是界面多好看而是每个角色能否高效地完成自己的那一环。所以数据库设计必须从角色和流程出发而不是从我要做多少张表出发。2.1 角色权限模型别让前端按钮替后端做判断整个系统我按实际业务拆成了四类角色角色核心操作数据可见范围访客/申请人提交申请、撤回、查看进度、补充材料仅自己的申请单受访人确认接待、补充备注、代他人发起申请与自己相关的申请单审批人辅导员/导师一审通过/驳回管辖范围内的申请单保卫处管理员终审、查询统计、导出报表、黑名单管理全部申请单代码层面我建议直接用枚举类型定义角色不要用字符串散落在代码里到处比较。Spring Security可以引入但如果是毕设用拦截器配合自定义RequireRole注解在Controller方法上声明所需角色就已经足够清晰了而且代码量要少很多排查问题也直白。2.2 核心表结构设计状态字段才是流程的骨架流程类系统的数据表设计最忌讳的就是把状态当成一个随便填的普通字段。入校申请核心表我建议至少包含这样几张application_form申请单主表存放申请人基础信息、入校事由、时间、到访部门、状态、当前处理人。application_approval_record审批记录表每一步操作都留痕包括操作人、操作类型提交/通过/驳回/撤回、意见、时间。visitor_info访客信息表姓名、证件号、手机号、随行人数、车辆信息。system_user用户表统一管理所有登录账号和角色。其中状态字段我强烈建议用tinyint并在代码里定义状态常量或枚举不要直接往数据库存中文。比如0代表草稿1代表待一审2代表待终审3代表已通过4代表已驳回5代表已撤回6代表已完成。一来数据库占用小检索快二来后续做统计查询时where status ?写起来非常干净。2.3 框架选型为什么是Spring Boot MyBatis Plus我说句实在话毕设阶段不要为了炫技去选冷门框架。Spring Boot之所以成为事实标准是因为它把配置简化到了极致而且生态里的每一环都有海量踩坑记录。MyBatis Plus的好处更直接单表CRUD几乎不用写SQL自带分页插件还有一套简单的条件构造器。入校申请系统里大量操作都是按条件查列表和状态更新MP能省掉很大一部分重复劳动把精力留给审批流程这种真正需要思考的业务。分页这一块是答辩高频考点直接讲MyBatis Plus的分页插件用法就行引入PaginationInnerInterceptor配置MybatisPlusInterceptor然后Page对象传到Mapper方法里返回的IPage里自带total和records。这里有个细节容易翻车分页插件只对单表查询自动生效如果你写了多表关联的复杂SQL又想分页必须在Mapper.xml里把Page参数传进去否则查出来的records可能是全量数据的累加结果别看total没问题就掉以轻心。3. 申请审批的核心链路状态机是这套系统的灵魂如果面试官只让你讲一个业务场景讲申请审批链路是最稳的。它不是一个线性流程而是存在多个分支和回退路径的动态过程能充分体现你对业务逻辑的设计能力。3.1 从提交到入校我把流程拆成了五个状态整个流程我按下图思路设计这里用文字描述答辩时你可以画时序图申请人填写表单、上传附件后提交状态变为待一审。一审人辅导员或导师收到待办可以选择通过或驳回。通过后状态变为待终审驳回后状态回到草稿或已驳回并允许修改重提。终审人保卫处管理员通过后状态变为已通过申请人生成入校凭证。入校当天保安核验二维码扫码确认后状态变为已入校离校时再刷一次变为已完成。这个设计的关键在于**每一步操作都必须更新申请单状态同时插入一条审批记录。**如果只改状态不写记录后面用户追问我这个单子为什么被拒了的时候你连个依据都拿不出来。3.2 审批操作的代码结构更新状态和写记录要放在同一个事务里以一审通过为例ServiceImpl里核心代码大致是这个样子Transactional(rollbackFor Exception.class) public boolean firstApprove(Long applicationId, Long approverId, String comment) { ApplicationForm form applicationFormMapper.selectById(applicationId); // 校验当前状态是否为待一审防止重复审批 if (form.getStatus() ! ApplicationStatus.PENDING_FIRST) { throw new BusinessException(当前申请单状态不允许该操作); } // 更新申请单状态 ApplicationForm update new ApplicationForm(); update.setId(applicationId); update.setStatus(ApplicationStatus.PENDING_FINAL); update.setCurrentApprover(approverId); applicationFormMapper.updateById(update); // 写入审批记录 ApprovalRecord record new ApprovalRecord(); record.setApplicationId(applicationId); record.setOperatorId(approverId); record.setAction(ApprovalAction.FIRST_APPROVE); record.setComment(comment); approvalRecordMapper.insert(record); return true; }这里Transactional不是可写可不写的装饰。如果更新状态成功但写审批记录失败事务回滚会让两个操作同时撤销不会出现状态变成待终审但审批历史里查不到这条操作的脏数据。我见过很多同学省略事务导致数据对不上查了半天才发现是这个问题。3.3 让人头疼的撤回与驳回后重新提交最难处理的不是通过而是驳回和撤回。驳回后申请人要能修改信息重新提交所以驳回状态不能直接等于终止。我建议给申请单增加一个version字段每次驳回重提时version自增同时把申请单状态置回待一审。这样的话同一份申请单多次提交历史记录都在approval_record表里按version区分第一次为什么被拒、第二次改了什么一目了然。撤回的逻辑则要更严格只有状态为待一审并且当前没有审批人处理过的申请单才允许申请人撤回一旦第一位审批人已经看过这个按钮就必须禁用否则会破坏审批链条的完整性。这个复杂度在论文里可以单独拿出一节写异常状态处理策略很加分。4. Spring Boot落地中的关键细节令牌、全局异常、文件上传流程模型确定后剩下的是Spring Boot项目里一些通用但极其重要的基础设施。这些内容既是项目的骨架也是毕业设计论文里系统实现章节最喜欢的素材。4.1 登录鉴权JWT 拦截器就够了别上ShiroSpring Boot整合Shiro做鉴权也是经典方案但对入校申请系统来说引入Shiro相当于杀鸡用牛刀配置繁琐概念又多答辩时还容易被追问SeveralFilters的执行顺序一旦答不上来反而扣分。我更推荐直接写一个TokenInterceptor。登录成功后用jjwt生成tokenTokenInterceptor从请求头取出token并解析用户信息放入ThreadLocal。后续任何方法里想拿当前登录人ID直接调用SecurityUtils.getCurrentUserId()即可。需要注意拦截器的注册路径登录接口和获取验证码接口要放行其余接口全部拦截注册到Spring MVC的InterceptorRegistry里。public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tokenInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/captcha); }4.2 全局异常处理给前端一个统一的返回结构我们在联调的时候最烦的就是后端在不同场景里返回不同结构的错误信息。靠全局异常处理可以彻底解决这个问题用RestControllerAdvice统一捕获业务异常、参数校验异常和兜底Exception返回结构固定为{ code: 40001, message: 当前申请单状态不允许该操作, data: null }成功响应则统一为code200data为实际数据。这样前端只需要判断code是不是200不需要在promise里讨论这个接口到底返回message还是error字段。这里我给自己定过一个规则业务异常要尽量细。比如申请单不存在和申请单已被审批无法重复操作是两个不同的异常不要全都抛一个通用操作失败。异常信息写得越具体定位问题的成本就越低。4.3 附件上传与类型校验申请入校经常涉及到上传证明材料比如访客的车牌照片、项目进场的安全承诺书。附件这块有两点必须处理一是文件类型校验二是路径安全。我见过一个很典型的错误前端传文件直接就往磁盘存不校验后缀名也不限制大小。攻击者传一个.jsp或者特殊命名的文件进去在部署不当的Web容器里可能直接被当作脚本执行。这虽然是毕设但从一开始就养成安全意识很重要。我建议即便不做专门的病毒扫描也要在后端做三重防护校验Content-Type和文件扩展名白名单jpg、png、pdf、zip。限制单个文件大小Spring Boot里设置spring.servlet.multipart.max-file-size超出直接抛异常。存储时重新生成文件名用UUID拼上合法扩展名绝不使用用户上传的原始文件名。5. 踩坑实录这些坑不踩一遍你根本想不到开题之后动手写代码的过程大体是顺利的但随着模块越来越多一些只在真实运行场景里才会冒出来的问题开始浮出水面。这些问题不大却足够让你卡上一整天我把它们逐一列出来希望对你有帮助。5.1 拦截器里取不到用户信息我一开始写TokenInterceptor的时候解析出来的用户ID放进了一个普通的static变量里。单用户测试一切正常但两个浏览器同时登录时用户A的操作偶尔会带上用户B的身份。原因很简单static变量是被所有线程共享的Servlet是线程并发处理的模型你写的当前用户根本不存在于static变量里。正确做法是用ThreadLocal。用完了在拦截器的afterCompletion里记得remove不然线程池复用的容器环境里会出现信息串味的问题。就这一行remove能帮你躲掉半个下午的排查时间。5.2 并发提交导致状态错乱审批环节里两个审批人同时在各自的浏览器上点通过如果代码里没有状态校验可能出现两个线程都读取到待一审然后都成功更新为待终审这样一个申请单就被重复处理了。解决办法有两个层次应用层加状态判断也就是3.2里先查出来判断status不是预期状态直接抛异常。数据库层加乐观锁update form set status 2 where id ? and status 1通过update影响行数判断是否抢到了操作权。MyBatis Plus的Version注解可以做这件事论文里也可以顺带讲清乐观锁和悲观锁的区别属于比较加分的提问点。5.3 导出Excel中文乱码系统里做了一个导出全部入校记录的报表功能最早就用了简单的response.getOutputStream()输出表格文件前台下载下来一打开所有中文全是乱码。问题出在响应头里漏了Content-Disposition的编码设置。我后来在开发代码里固定一行解决response.setHeader(Content-Disposition, attachment; filename URLEncoder.encode(fileName, UTF-8) .xlsx);同时记得在发送数据之前设置response.setCharacterEncoding(utf-8)。这个坑特别常见几乎每个做过导出功能的人都被它绊过一次。5.4 明明重启了还是404有一次我把新加的Controller写好后重启项目访问接口一直404排查了半天最后发现是启动类的位置放错了。Spring Boot默认只扫描启动类所在包以及子包下的组件我把启动类放到了com.example.application包下而Controller在com.example.modules.application包下它们没有包含关系自然扫描不到。解决办法是把启动类放到所有子包的共同父包下比如com.example.entrysystem。如果非要分模块包可以在启动类上加ComponentScan来指定扫描路径这也是个很经典的必考题。5.5 数据库连接池在黎明时段突然断开系统连续运行多次演示之后隔天一早再打开管理后台第一次查询总是报Connection closed错误。这是典型的MySQL wait_timeout问题空闲连接超过了8小时默认超时时间连接池里的旧连接已经失效。我当时在application.yml里加了两个参数解决spring: datasource: hikari: connection-timeout: 30000 max-lifetime: 1800000max-lifetime一定要小于MySQL的wait_timeout这样HikariCP会在连接被数据库回收之前主动替换掉它问题就消失了。这个坑不常遇到但一旦遇到就是面试官爱听的生产环境稳定性问题写在论文里体现实操深度非常合适。6. 部署、演示和答辩让代码说话也要让人听懂很多同学系统写完就以为万事大吉结果答辩现场一启动项目就出问题或者在演示环节手忙脚乱不知道点哪里。系统做好之后还有两个环节需要专门花费心思。6.1 本地演示环境的准备清单如果你是用Docker部署建议提前打一个镜像把MySQL、Redis、后端服务做成docker-compose.yml。这样在答辩机器上只需要装一个Docker Desktop然后docker compose up -d就能拉起全套环境不用现场装MySQL。再准备一套干净的自测数据至少三个角色的账号申请人、一审人、管理员已经跑到不同状态的申请单各两张。演示的时候直接打开待办列表点审批不要演示到一半再当场发一条申请去等审核观感会打折扣。6.2 演示脚本不要大于五分钟答辩时人是紧张的如果演示流程没有提前排练很容易对着页面发呆。我通常建议准备一条主线登录申请人账号提交入校申请并上传附件刷新查看进度切换审核人账号完成一审和终审切换申请人账号查看入校凭证最后切管理员账号进行按日期筛选和导出。这条链路中间加一句这里是状态机的变化可以看到审批记录同步写入了一条数据把逻辑重点自然带出来比自己干讲架构图有力得多。6.3 常见答辩提问与回答思路答辩老师大概率会围绕几个点问为什么用JWT不用Session、Spring Boot自动装配的原理是什么、MyBatis Plus分页插件底层怎么实现、状态冲突如何保证数据一致性。这些问题其实本项目的代码里都有对应的答案。比如Spring Boot自动装配你可以直接说你通过spring.factories或AutoConfiguration.imports文件引入了对应的XXXAutoConfiguration类条件注解ConditionalOnMissingBean生效之后再装配数据源和MyBatis这就是和面试官深入沟通的抓手。只要认真跑了这个项目自动装配对你来说就不是一个需要死记硬背的抽象概念而是你真的看过的源码流程。最后再分享一个小建议做这个题目的价值不只在于交一份毕设更在于你能把一个有完整业务的系统拆解成表结构、状态机、权限矩阵和异常处理再反向把它们组装起来。入校申请系统做完之后你会发现自己再看其他管理系统类题目几乎都是同一套方法论在起作用。这种能力比代码本身值钱得多。