毕设季又双叒叕来了每年这时候总有一批人拿着“基于SpringBoot的高校学生奖项管理系统”这种题目来问该怎么写。实话讲这类选题看起来平平无奇但你真正动手做起来才会发现难点根本不在SpringBoot那几个注解上而在你理不理解“奖项管理”这四个字背后藏着的申报、审核、退回、公示、归档这一整条业务流程。这篇咱不聊空泛的概念就顺着一个典型的高校奖项管理系统把从需求梳理、表设计、状态机、权限、防重复到导出部署的完整链路都讲一遍主要给正在准备毕设的同学提供一套可以直接落地复现的思路。1. 奖项管理系统这个选题表面是增删改查内核是流程控制1.1 先想清楚系统到底要管什么业务不同学校的奖项业务差距挺大。有的学校只要管校级“三好学生”评选有的学校要同时处理国家励志奖学金、企业奖学金、学科竞赛奖励、优秀毕业生评选等好几类还牵扯学院初筛、学生工作处复核、学校终审。如果一上来就照着网上的代码模板建表大概率做出来的就是一个空壳。我的建议是拿到题目后先别写代码花一个晚上把业务角色和流程画出来。这里可以用一套比较通用但不失真实的业务模型学生提交奖项申报上传证明材料查看审核进度退回后修改再提交。辅导员负责初审检查材料是否齐全、条件是否满足可以退回或推荐。学院管理员做院系级审核决定是否推荐到学校。校级管理员做终审确定获奖名单设置公示期导出名单。系统管理员维护奖项目录、用户账号、角色权限、年度设置。把这些角色落到一张表里就是用户表、角色表、奖项类型表、申报单表、审核记录表再配合文件上传和公告模块业务闭环基本就有了。很多选手喜欢把系统做得特别大动不动就十几张表反而让答辩时自己都讲不清楚。少而全比多而乱强得多。说得直白一点奖项管理系统的价值点在于“一件申报材料从交上来到最后公示中间经历了什么、每一步是谁操作的、为什么被退回”而不是单纯把数据存进数据库再读出来。谁的流程设计得清晰谁的项目就能拉开差距。1.2 流程闭环申报、审核、公示、归档一条线我倾向于把整个系统的主流程收敛成一条状态链学生提交申报 → 辅导员初审 → 学院复核 → 校级终审 → 结果公示 → 名单归档每个环节都必须可回退。比如辅导员发现材料不齐应该能把申报单退回给学生学生重新提交后状态再回到辅导员待审如果校级管理员发现学院推荐名单有问题也可以退回给学院。这种回退能力是评审老师特别爱问的“如果中间一步错了怎么办”所以设计状态的时候不能只设计单向流程。归档也不能省。奖项评审结束后名单要保留痕迹下一年还能查历史记录。很多毕设做到公示就结束了归档是用一个简单的“评审年度”字段加导出功能来落地。除此之外还要考虑通知需求。学生提交后辅导员怎么第一时间知道常见做法是在审核操作时给相关用户生成一条站内消息别把这事想复杂系统里加一张消息表审核后insert一条记录就够了。如果学校要求严格一点可以对接邮件或者企业微信通知但是毕设阶段加站内消息足够展示思路。2. 技术选型SpringBoot到底配什么组件才最稳2.1 版本和JDK先敲定别为了“新”踩坑截至2026年Spring Boot已经全面铺开3.x但一定要先确认自己电脑上的JDK版本。Spring Boot 3.x要求JDK 17以上如果你还停留在JDK 8环境更适合用Spring Boot 2.7.x这条稳定线搭配MyBatis-Plus也完全够用。做毕设不是做企业难点攻关稳定能跑、自己能讲清楚才是第一位的。我曾经看到一个同学在答辩现场被问“为什么你的启动类可以运行但打包后却报错”原因就是他在本机用IDE内置的JDK版本和打包环境不一致。这种坑提前固定版本就不会出现。2.2 依赖组合MyBatis-Plus Redis JWT是通用方案后端框架选SpringBoot没悬念ORM层建议用MyBatis-Plus。原因是单表操作太方便了内置分页插件、条件构造器可以省掉大量重复的XML SQL让你把精力放在业务流程上。但还是得清楚一点MyBatis-Plus只是提升开发效率不能替代你对SQL的理解答辩老师问起多表关联时你要能说出自己写的SQL逻辑。权限模块我一般不推荐直接引入完整版Spring Security除非你非常熟。毕设阶段用“JWT生成Token 自定义拦截器 注解标记角色”这一套更简单可控。核心逻辑就是登录成功后签发一个带用户ID和角色的Token前端后续请求在请求头带上这个Token后端拦截器解析Token并判断当前接口允许哪些角色访问。Redis在这里的作用是缓存Token和验证码。登录验证码用Redis存一份设置有效期校验完再删掉逻辑很简单。Redis也用不到特别高级的功能不需要硬上分布式锁、延迟队列。2.3 数据库表设计的核心申报表、审核记录表、奖项类型表奖项管理里最主要的核心表是申报单表我一般建议把“奖项类型”单独拆一张表而不是直接塞一个字符串在申报单里。这样以后新增奖项系统管理员直接后台配置不用改代码。申报单表的关键字段可以拆成下面这个样子CREATE TABLE t_award_application ( id BIGINT PRIMARY KEY COMMENT 主键, student_no VARCHAR(20) NOT NULL COMMENT 学号, student_name VARCHAR(50) NOT NULL COMMENT 姓名, award_type_id BIGINT NOT NULL COMMENT 奖项类型ID, award_year VARCHAR(10) NOT NULL COMMENT 评选年度如2025-2026, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态枚举值, material_path VARCHAR(255) COMMENT 申报材料附件路径, reason VARCHAR(500) COMMENT 退回原因/申报说明, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 奖项申报单表;很多同学做评审系统只记得记录最终状态忘了审核人、审核时间、审核意见。这样一旦老师追问“退回是因为什么”系统里根本查不到。所以一定要加一张独立的审核记录表CREATE TABLE t_audit_record ( id BIGINT PRIMARY KEY COMMENT 主键, application_id BIGINT NOT NULL COMMENT 申报单ID, from_status TINYINT NOT NULL COMMENT 原状态, to_status TINYINT NOT NULL COMMENT 新状态, operator_id BIGINT NOT NULL COMMENT 操作人ID, operator_name VARCHAR(50) NOT NULL COMMENT 操作人姓名, comment VARCHAR(500) COMMENT 审核意见, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) COMMENT 审核记录表;有了这张表前端就能把一个申报单从创建到最终结果的每一步操作按时间线展示出来。这一个细节答辩时非常加分。3. 评审链路的状态机设计把“退回”“撤回”“通过”全部显式化3.1 状态枚举先定好流程才不会乱状态不要用字符串比如“已通过”“审核中”中文存库容易出错而且状态扩展时不好维护。正确姿势是定义成数字枚举再用常量类或者枚举类统一管理。我这里设计一套适用于多数高校场景的状态状态值含义可操作人草稿0学生填写中未提交学生待院系审核1已提交等待初审辅导员院系退回2初审不通过退回修改学生院系通过3学院推荐到学校学院管理员校级退回4校级驳回学生待校级终审5等待校级管理员审核校级管理员获奖6最终审核通过系统生成未获奖7终审不通过系统生成已撤回8学生主动撤回学生为什么要把“院系退回”和“校级退回”分开因为退回后学生需要知道是哪一级审核出了问题前端可以直接展示处理层级。如果不分开只用一个统一的“退回”状态最后统计退回原因时就会很吃力。状态机不是写死在if-else里的而是维护一张状态流转表或者用一个枚举Map来表示“当前状态 操作角色 操作类型 下一状态”。我见过比较优雅的做法是写一个状态流转校验类审核操作进来后先判断当前状态是否允许当前角色做这个操作不允许直接抛业务异常。这样写还有个好处答辩时老师问“学生自己能不能撤回一个已经获奖的单子”你可以直接回答状态机里已经禁止了这个转移。3.2 审核记录与乐观锁避免两个人同时处理同一份申报多级审核系统最怕出现并发问题。比如辅导员初审通过了同时学院管理员也在系统里点了推荐结果两边都以为自己操作成功。解决这个问题不是靠加个synchronized就完事最简单有效的方式就是在申报单表加一个version版本号更新状态时带上旧版本号。Integer rows awardApplicationMapper.updateByVersion( application.getId(), application.getVersion(), StatusEnum.PENDING_COLLEGE.getValue() ); if (rows 0) { throw new BizException(该申报单已被其他人处理请刷新后重试); }对应的SQL大致是UPDATE t_award_application SET status #{newStatus}, version version 1 WHERE id #{id} AND version #{oldVersion}这样同时发起的两个审核操作只有一个能更新成功。另一个要么报错重试要么刷新名单重新判断。代码层面并不复杂讲清楚乐观锁与悲观锁的取舍就足够体面了。3.3 审核记录的时间线怎么展示既然有一张审核记录表前端就能为每个申报单生成一条时间轴。比如2026-05-10 09:00 学生提交申报2026-05-10 14:30 辅导员退回佐证材料缺少盖章2026-05-11 08:20 学生重新提交2026-05-11 10:00 辅导员初审通过2026-05-12 16:00 学院审核通过2026-05-14 10:30 校级终审通过进入公示这个时间线做起来不复杂后端按审核记录表查询列表返回即可前端用一个纵向的时间线组件做展示。但有两点值得注意第一审核操作必须是“新增记录 修改申报单状态”放在同一个事务里否则会出现状态变了却没记录的情况第二表单里如果能看到操作人姓名和操作时间会比只看状态更可信。4. 权限、参数校验和防重复申报最容易扣分的三个隐形坑4.1 RBAC权限的最小实现注解加拦截器很多毕设系统喜欢把“角色”做成用户表里的一个字段然后所有接口都去判断roleId。这样做虽然简单但接口多了以后逻辑到处都是而且没法做方法级控制。我用的是很小的RBAC方案用户、角色、权限三张表但在接口层不引入复杂的权限框架只用一个自定义注解。Retention(RetentionPolicy.RUNTIME) Target({ElementType.METHOD, ElementType.TYPE}) public interface RequireRole { String[] value(); }然后在Controller方法上标RequireRole({college_admin, school_admin}) PostMapping(/audit) public Result audit(RequestBody AuditRequest request) { ... }拦截器做三件事解析请求头里的Token、取出当前用户角色、判断是否在注解允许的集合里。这套方案代码量不大却能让答辩老师清楚看到你理解“角色与接口权限分离”的设计思想。如果非要用Spring Security也不是不行但你要提前准备好一个问题“为什么你的权限配置放在SecurityConfig里某些接口仍然可以直接访问”这种问题往往是因为忽略了AntPath匹配顺序导致的。4.2 防止同一学生重复申报同一奖项奖项系统里特别容易出现这种情况学生在5月1日提交了“优秀学生干部”申报5月10日修改材料后又提交了一次。数据库里出现两条状态相同的记录统计获奖人数时就会出错。第一道防线是业务校验提交时查询是否存在同一个学号、同一个奖项类型、同一个评选年度且状态不是“未获奖”的记录。第二道防线是数据库唯一索引两者缺一不可。只靠业务校验在高并发下也可能出现两个请求同时通过校验只靠唯一索引异常信息又不够友好。ALTER TABLE t_award_application ADD UNIQUE INDEX uk_student_award_year (student_no, award_type_id, award_year);当唯一索引冲突时MyBatis-Plus会抛出DuplicateKeyException在全局异常处理器里单独捕获并返回“您已申报过该奖项请勿重复提交”的信息用户体验会好一大截。另外真正提交时建议把整个状态校验、插入申报单、写入审核记录包在一个事务里避免中间步骤失败后留下脏数据。4.3 上传文件不能只限制前端申报系统免不了上传成绩单、奖状扫描件。后端校验必须检查文件后缀名、文件大小、以及Content-Type文件名不要直接使用用户传来的原始文件名而是生成一个UUID字符串再拼接原后缀。这么做不只是防止文件名冲突更是为了防路径穿越一类的安全问题。存储目录也尽量不要放在静态资源目录而是放在独立的上传目录由后端接口根据业务权限控制访问。这里有一个常见的坑本地开发时上传文件正常部署到服务器上用Nginx反向代理后前端访问不到附件。原因通常是上传目录没有被映射成静态路径。我的建议是写一个专门的附件下载接口从数据库查路径再通过文件流输出这样Nginx不用额外配置静态映射权限也能统一控制。5. 名单导出与统计图表把数据变成答辩里的加分项5.1 导出用EasyExcel不要自己手动操作POI评审老师问“导出”这个功能时最怕你回答“就是把数据库查出来用POI逐行写”。实际做的时候用阿里的EasyExcel能省很多内存。它底层是流式读取和写入本地导出一万条记录内存占用也很小。核心做法是给实体类字段加注解。public class AwardExportRow { ExcelProperty(学号) private String studentNo; ExcelProperty(姓名) private String studentName; ExcelProperty(奖项名称) private String awardTypeName; ExcelProperty(评选年度) private String awardYear; }导出时只需要传入查询条件和输出流EasyExcel自己处理表头与单元格格式。答辩时被问到“导出Excel中文表头为什么乱码”你就能答出是因为没配置Content-Disposition里的filename编码加上URLEncoder.encode就可以了。5.2 统计页面怎么做才不出错奖项系统里常见的统计需求是各学院获奖人数、各类奖项申报人数、年度申报趋势。后端就查一张表用MyBatis-Plus的QueryWrapper做groupBy返回一个Map或者List不用特别复杂。有一点我要强调统计时要注意“去重”和“过滤无效数据”。比如未获奖的申报单虽然在系统里但统计“获奖名单分布”时不应纳入同一个学生如果申报了两个不同奖项也不应该被算成一个人。这个逻辑如果不对页面上的图表数据就会跟导出名单不一致答辩时被老师对着数据一核对直接露怯。前端展示层面用ECharts画柱状图和饼图就足够出效果不用引入太重的大屏模板。大屏模板很炫但如果你解释不了里面的数据口径反而掉分。6. 部署与答辩把代码跑起来只是及格线6.1 一套可复现的部署流程项目做完了不要只在IDE里跑至少得能在本地打一个jar包并成功运行。一份合格的部署记录应该包含这些步骤在application.yml里把数据库连接、Redis连接拆成环境变量通过--spring.profiles.activeprod切换生产配置。使用Maven执行mvn clean package -DskipTests确认构建产物是target目录下的jar包。服务器上安装对应版本的JDK和MySQL、Redis把jar包上传后执行nohup java -jar xxx.jar --spring.profiles.activeprod app.log 21 启动。使用Nginx将前端构建产物放到静态目录并配置/api/反向代理到后端端口。部署过程中最容易出问题的三个点Java版本不对导致启动失败连接MySQL时没有把serverTimezone设置成Asia/Shanghai导致时间偏移前端接口地址写死成localhost导致打包后无法访问。建议前端统一用相对路径请求后端或者通过Nginx代理转发。6.2 答辩高频问题怎么答我建议每位同学在答辩前当场模拟老师提问下面这几个问题基本躲不开“你的状态为什么要用数字不用字符串” 答数字存储占用小、比较快而且可以通过枚举集中管理扩展新状态时只需要增加枚举项和状态流转配置不需要改动表结构。“如果用户在页面点击审核按钮的同时另一个管理员也点击了这个按钮怎么办” 答通过乐观锁版本号控制更新只有版本号一致才能更新成功一方失败后重新读取最新数据再操作。“学生直接绕过前端手动请求后端接口重复提交怎么办” 答后端所有权限接口都做角色校验同时数据库对学号、奖项、年度字段建立唯一索引兜底防止重复数据。“你自己演示一下把一份申报从提交到导出名单完整走一遍。” 答这个必须在答辩前至少完整跑过三次我见过太多同学现场卡在文件上传或审核状态跳转上。最后分享一个我自己长期保持的习惯做这类管理系统的第一件事永远是先画角色和状态流转图再动手写实体类。很多同学一拿到题目就开建表结果建到一半发现漏了退回状态又要回头改。你先把所有业务路径列出来一条条对一遍状态代码量反而会少很多评审老师问起来你也能底气十足。如果你正在做这个题目建议你也试试先花一晚上梳理业务再动手写代码整个过程会顺得多。
阅读完成 · 觉得有帮助?