☰
Spring Boot入校申请系统:审批流程与状态机实战解析
2026/9/26 12:16:21 网站建设 项目流程

毕设做"入校申请系统",用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 从提交到入校:我把流程拆成了五个状态

整个流程我按下图思路设计(这里用文字描述,答辩时你可以画时序图):

  1. 申请人填写表单、上传附件后提交,状态变为"待一审"。
  2. 一审人(辅导员或导师)收到待办,可以选择通过或驳回。通过后状态变为"待终审",驳回后状态回到"草稿"或"已驳回"并允许修改重提。
  3. 终审人(保卫处管理员)通过后状态变为"已通过",申请人生成入校凭证。
  4. 入校当天保安核验二维码,扫码确认后状态变为"已入校",离校时再刷一次变为"已完成"。

这个设计的关键在于:**每一步操作都必须更新申请单状态,同时插入一条审批记录。**如果只改状态不写记录,后面用户追问"我这个单子为什么被拒了"的时候,你连个依据都拿不出来。

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 + 拦截器就够了,别上Shiro

Spring Boot整合Shiro做鉴权也是经典方案,但对入校申请系统来说,引入Shiro相当于杀鸡用牛刀,配置繁琐,概念又多,答辩时还容易被追问SeveralFilters的执行顺序,一旦答不上来反而扣分。

我更推荐直接写一个TokenInterceptor。登录成功后用jjwt生成token,TokenInterceptor从请求头取出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 }

成功响应则统一为code=200,data为实际数据。这样前端只需要判断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: 1800000

max-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,这就是和面试官深入沟通的抓手。只要认真跑了这个项目,"自动装配"对你来说就不是一个需要死记硬背的抽象概念,而是你真的看过的源码流程。

最后再分享一个小建议:做这个题目的价值不只在于交一份毕设,更在于你能把一个"有完整业务的系统"拆解成表结构、状态机、权限矩阵和异常处理,再反向把它们组装起来。入校申请系统做完之后,你会发现自己再看其他管理系统类题目,几乎都是同一套方法论在起作用。这种能力,比代码本身值钱得多。

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

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

立即咨询