1. 为什么"奖学金评定管理系统"常年是毕设热门:先想清楚要解决什么
1.1 传统评定流程的痛点:Excel、邮件与公告栏
每年评奖学金的时候,辅导员的工作量都相当大。学生交纸质材料或Excel,辅导员逐个核对学习成绩、综测加分、获奖证书,再汇总成一张大表,经过教研室、院系层层上报,最后把结果打印出来贴在公告栏。这个过程有几个非常现实的问题:
- 规则不透明:奖学金类型、名额、加分标准靠口头通知,学生经常因为"没有提前看到通知"而错过申请。
- 材料难追溯:纸质材料容易丢,扫描件分散在邮箱和聊天记录里,事后有人提出异议时,很难快速找到当时的原始材料。
- 审核效率低:同样是"核对一项加分",在不同的老师手里重复做很多遍,而且每次都是人工比较,容易看错数据。
- 结果公示不严谨:公示期、异议渠道、最终名单,没有一个统一的记录,出了问题说不清楚。
我帮某高校的同学拆解这个课题时,第一件事就是把上述痛点列成使用场景。你会发现,只要把"申请—材料—审核—公示—异议处理"这条链路理清,系统的大部分功能需求就已经浮出水面了。
1.2 毕设课题的合理边界:既要完整,又不能失控
选题最忌讳的是两种极端:一种是只做一个简单的增删改查,页面几个表格就完了,工作量撑不起一篇毕业论文;另一种是梦想着对接教务系统、打通财务系统、集成消息推送,最后在毕设周期内连核心流程都跑不通。
合理的边界是:在"奖学金评定"这个垂直场景里,把业务闭环做完。我通常建议的功能清单是这七块:
- 用户认证与角色权限:学生、辅导员(班主任)、院系管理员、校级管理员、系统管理员。
- 奖学金类型管理:维护各类奖学金的名称、学年、名额、申报时间窗。
- 评定指标管理:每个奖学金类型挂多个指标,配置权重、分值上限。
- 学生在线申请:填写自评信息、上传证明材料、提交后锁编辑。
- 多级审核流程:按"辅导员→院系→校级"逐级审核,支持通过、退回、驳回。
- 评分与排序:按指标权重自动计算总分,按名额截取入围名单。
- 公示与统计:管理员发起公示、到期自动关闭,按学院、类型、学年做统计导出。
这个范围做下来,既能把SpringBoot、MyBatis-Plus、Vue、权限控制、文件上传、状态机这些主流知识点全部覆盖,又不至于超出毕设周期。我在后面每一章里,都会说明具体怎么落地。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计:SpringBoot不是唯一答案,但确实是稳妥答案
2.1 后端框架:为什么SpringBoot比SSH/SSM更适合毕设
很多同学一开始会纠结框架选型。如果你是纯Java初学者,又选了Java方向,十年前的学习路径可能是SSH(Spring + Struts + Hibernate),现在基本已经被SpringBoot + SpringMVC + MyBatis-Plus的组合取代。原因很简单:
- SpringBoot的自动配置把SpringMVC、内置Web服务器、JSON序列化这些零碎配置都帮你完成了,起步成本低得多,项目结构也更统一。
- 内嵌Web服务器,开发时一个主类就能跑起来,部署时打一个jar包,对服务器环境要求极低。
- Starter生态完善,接入MySQL、文件上传、Excel导出等能力基本都是加依赖的事。
- 语法和思想是通用的:依赖注入、面向切面、事务管理,这些在面试和研究生复试里都常问,做毕设过程中就能顺手掌握。
如果你已经学过SSM,也没有必要推翻重来。SpringBoot本质上就是SSM的"开箱即用"封装,切换到SpringBoot反而会省掉大量XML配置。
2.2 整体技术栈与分工
我给出的个人推荐组合如下(参考某高校模拟项目X的实际落地版本):
| 层 | 技术选型 | 说明 |
|---|---|---|
| 后端框架 | SpringBoot 2.x | 使用范围最广的稳定版本,网上资料最多 |
| 持久层 | MyBatis-Plus | 单表CRUD不用写SQL,复杂查询用条件构造器 |
| 数据库 | MySQL 8.0 | 5.7也可以,注意字符集统一utf8mb4 |
| 权限认证 | JWT + 拦截器 | 轻量,前后端分离友好 |
| 前端 | Vue 2.x + Element UI | 后台管理界面有现成组件,开发效率高 |
| 文件存储 | 本地目录 + Nginx映射 | 毕设级别不用上对象存储,但别把文件堆在项目运行目录里 |
| 表格导出 | EasyExcel或Hutool | 大数据量导出不爆内存,比原生POI API省心 |
前后端分离是当前的主流做法,也方便在答辩时用浏览器演示。有的导师要求单体JSP或Thymeleaf模板,那就把前端换成Thymeleaf,核心业务逻辑不受影响。
2.3 数据库设计:六张核心表怎么建模
整个系统的核心业务都围绕"申请"展开。我建议的库表关系如下,实际建表时可以再加扩展字段。
- user表:id、username、password(BCrypt加密)、real_name、student_no、role_type、college_id、phone等。
- scholarship_type表:id、type_name、school_year、total_quota、apply_start_time、apply_end_time、status。字段里必须有申报时间窗,否则流程控制无从谈起。
- evaluation_criterion表:id、scholarship_type_id、criterion_name、weight、max_score、sort_order。权重用0到1之间的小数存,也可以存百分数整数。
- application表:id、student_id、scholarship_type_id、semester、self_evaluation、total_score、rank_no、status、create_time、update_time。total_score是评定后计算出来的结果。
- application_material表:id、application_id、material_type、file_name、file_url、remark。一个申请对应多条材料记录。
- review_record表:id、application_id、reviewer_id、level、action(PASS/RETURN/REJECT)、comment、create_time。这条表是审核"留痕"的关键,也是答辩时体现业务严谨性的加分点。
额外还可以有announcement表(公示内容)和operation_log表(操作日志)。这里需要额外提醒一句:不要把材料文件直接存成byte[]放数据库,一是占空间,二是导出和展示都很别扭,用文件路径字段就够了。
3. 核心业务流程:从申报、评定到公示的完整闭环
3.1 奖学金类型与评定指标:权限配置是业务的第一步
系统上线后,第一步不是让学生提交申请,而是让管理员把"奖学金种类"和"评定指标"配好。有一年某高校在录入某个奖学金时把指标权重填成了120%,系统在生成评分时直接报错,根源就是缺少校验。
所以我在指标管理中加了一个服务端校验:同一奖学金类型下所有指标权重之和必须等于100%,否则不允许保存。这个逻辑放在Service层,界面端用表单校验做第一道拦截,服务端再兜底。因为权重拿捏不准、指标变动频繁,这个模块值得多花时间打磨。
3.2 学生申请:提交即锁定的设计
学生的申请入口在"申报中"的奖学金类型上。前端根据apply_start_time和apply_end_time控制能否填写;后端在submit接口里也做一次时间校验,防止有人绕过前端直接调接口。这里有两个细节:
- 草稿机制:学生可以先保存草稿,但草稿状态别人看不到,也不占用名额。提交后从DRAFT变成SUBMITTED,同时申请内容锁定,学生不能再编辑。
- 重复申请拦截:同一个学生在同一个学年针对同一个奖学金类型只能有一条有效申请,后端用唯一索引兜底,前端也要在进入页面时就提示"已申报"。
材料上传部分,我建议按材料类型区分(成绩单、获奖证书、贫困证明等),每个申请最多限定5个文件,单个文件大小控制在10MB以内。文件类型白名单比黑名单可靠,只允许jpg、png、pdf,这样既避免格式混乱,也从源头减少前端预览的问题。
3.3 多级审核:用状态字段驱动流程,而不是用一堆布尔值
审核流程是这个系统里最容易写乱的部分。常见的错误写法是给application表加pass_by_class、pass_by_department、pass_by_school三个字段,流程一扩展就崩溃。正确做法是:一个status字段 + 一张审核记录表。
我这个项目里的状态流转是这样的:
- DRAFT:草稿。
- SUBMITTED:已提交,等待辅导员审核。
- CLASS_PASS / CLASS_RETURN / CLASS_REJECT:辅导员通过、退回、驳回。
- DEPT_PASS / DEPT_RETURN / DEPT_REJECT:院系级审核结果。
- SCHOOL_PASS / SCHOOL_REJECT:校级最终通过或驳回。
这里我刻意没有把退回和驳回混在一起:退回(RETURN)表示材料有问题,学生可以修改后重新提交;驳回(REJECT)表示资格不符或材料弄虚作假,流程直接终止。这两个动作在业务上语义不同,落到代码里就是状态分支不同。重新提交时,需要把审核层级回退到"已提交"状态,学生改完材料后再走下一轮。
每个审核动作都在review_record里写一条记录:谁、在哪个层级、做了什么动作、备注了什么。这个设计在后面"材料异议处理"时特别好用,查一条时间线就能还原整个审核过程。
3.4 评分计算与入围名单:权重计算和名额截取
评分规则按指标拆:学生的学习成绩、综合素质、科研成果等每项先算出原始分,再乘以权重,最后汇总。以"学习成绩"为例,可以用学生成绩单上的加权平均分换算成百分制;其他加分项由审核老师在审核时根据材料给分。
我把评定计算放进了Service层的独立方法里:
java复制public BigDecimal calcTotalScore(Long applicationId) {
Application app = applicationMapper.selectById(applicationId);
List<EvaluationCriterion> criteria = criterionMapper.selectByTypeId(app.getScholarshipTypeId());
BigDecimal total = BigDecimal.ZERO;
for (EvaluationCriterion criterion : criteria) {
BigDecimal rawScore = getRawScore(applicationId, criterion);
total = total.add(rawScore.multiply(criterion.getWeight()));
}
// 保留两位小数,避免浮点误差
return total.setScale(2, RoundingMode.HALF_UP);
}
每个指标的原始分,后台可以配置成"人工录入"或"按规则自动计算"两种模式。毕设阶段建议至少做一个自动计算的指标,比如"绩点加权分"从成绩表里取,这样答辩时可以直接展示一条申请从提交到得分的完整链路。
入围名单则是在校级审核通过后,对同一个奖学金类型下所有通过审核的申请按total_score降序排列,再结合奖学金名额total_quota截取前N名,生成公示名单。
4. 关键代码与实现细节:权限、状态机、导出分别怎么写
4.1 基于角色的接口权限控制
我用JWT做登录态,token里放入userId和roleType,配合自定义注解做接口级权限控制。核心逻辑是一个拦截器:
java复制@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
if (requireRole == null) {
return true;
}
String token = request.getHeader("Authorization");
LoginUser loginUser = JwtUtil.parseToken(token);
String[] roles = requireRole.value();
if (!Arrays.asList(roles).contains(loginUser.getRoleType())) {
throw new BusinessException(403, "没有权限执行该操作");
}
return true;
}
}
使用上,Controller方法上加一行注解即可,比如只有辅导员能调用"班级审核"接口:
java复制@RequireRole({"CLASS_TEACHER", "DEPT_ADMIN", "SCHOOL_ADMIN"})
@PostMapping("/review/applications/{id}")
public Result<Void> review(@PathVariable Long id, @RequestBody ReviewDTO dto) {
applicationReviewService.review(id, dto);
return Result.success();
}
注意,接口权限判断一定是基于后端token里的角色,不能完全信任前端传过来的角色参数。这是很多毕设被答辩老师质疑安全性的地方。
4.2 审核状态机的落地:状态枚举 + 条件更新
状态流转建议用一个枚举类统一管理,避免魔法字符串。核心的审核动作写到Service里,关键是"加状态条件更新":
java复制public void review(Long applicationId, ReviewDTO dto) {
Application app = applicationMapper.selectById(applicationId);
StatusEnum current = app.getStatus();
if (!StatusMachine.canTransit(current, dto.getAction())) {
throw new BusinessException("当前状态不允许执行该操作");
}
int updated = applicationMapper.updateStatusIfMatch(
applicationId,
current.getCode(),
StatusMachine.targetStatus(current, dto.getAction()).getCode()
);
if (updated != 1) {
throw new BusinessException("操作冲突,请刷新后重试");
}
reviewRecordMapper.insert(ReviewRecord.builder()
.applicationId(applicationId)
.reviewerId(dto.getReviewerId())
.level(dto.getLevel())
.action(dto.getAction())
.comment(dto.getComment())
.build());
}
这段代码有两个关键点。一是updateStatusIfMatch,用SQL的WHERE status = ?做条件更新,天然防止两个人同时对同一条申请操作导致状态错乱;二是先更新后写审核记录,如果状态更新成功而记录写入失败,需要靠@Transactional把它们包在同一个事务里。
4.3 评分计算的Service层实现与事务边界
评分计算务必在事务里完成:先锁申请记录,再算分,再更新总分,最后写入评分明细。否则在并发场景下可能出现"两个审核人各自算分,后写的人覆盖先写的人"的问题。计算加分项时有一点容易被忽略:加分材料必须绑定具体的审核记录,也就是"哪个老师给哪条材料批了多少分",这样才能在公示异议时回答"这个分数怎么来的"。
4.4 Excel导出:用EasyExcel而不是原生POI
公示名册、统计报表几乎是必做功能。我用EasyExcel实现,几行代码就能写一个sheet:
java复制@GetMapping("/export/result")
public void exportResult(@RequestParam Long typeId, HttpServletResponse response) throws IOException {
List<ScholarshipResultDTO> list = applicationService.listApprovedByType(typeId);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Disposition", "attachment;filename=result.xlsx");
EasyExcel.write(response.getOutputStream(), ScholarshipResultDTO.class)
.sheet("获奖名单")
.doWrite(list);
}
POI原生API写一个复杂报表要几十行单元格操作代码,EasyExcel把模型注解和列表导出都封装好了,对毕设来说足够了。导出时记得统一依赖版本,避免跟SpringBoot内嵌容器冲突,这个坑虽然不难解决,但排查起来很费时间。
5. 开发过程中踩过的坑:这些问题不遇到,答辩时也会被问
5.1 金额与百分制小数:double是万恶之源
奖学金评分经常出现类似94.6的分数,如果用double去算,加法和乘法会悄悄引入0.000000001的误差。我第一次写完评分模块后,发现某个申请的最终分是87.8999999999,虽然四舍五入后显示正常,但排名用的排序字段如果不做scale处理就会出问题。
解决办法:数据库用DECIMAL(10,2),Java计算一律用BigDecimal,排序时在SQL里也按DECIMAL排。比较大小不要用equals,用compareTo。
5.2 并发审核导致状态错乱
两个审核人同时打开同一个申请,一个人点"通过"后,另一个人还认为状态是"待审核",也点"通过"。如果只按id更新,第二次操作会把第一次的审核结果覆盖掉。用上面提到的"状态条件更新"是最简洁的方案,比加锁直观得多,而且代码量很少,答辩时也能讲清楚思路。
5.3 文件上传后的残局:目录、大小与安全
一开始我把上传文件写到项目目录的upload/下面,本地开发没问题。后来在服务器上重启应用,上传的图片全没了,因为打包后的运行路径是临时的。改成配置化的绝对路径后问题解决,配合Nginx的静态映射,前端可以直接用URL访问。另外SpringBoot默认上传文件最大是1MB,记得在配置里调大:
properties复制spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=50MB
5.4 逻辑删除和唯一索引的冲突
我在application表上建了"同一学生对同一奖学金只能申请一次"的唯一索引,同时开了MyBatis-Plus的逻辑删除。结果学生删掉一条申请再重新申请时,数据库里其实是两条记录,唯一索引直接报错。这类问题的通用解法是:要么不用逻辑删除,改成把旧申请标记为"已作废";要么唯一索引改成包含delete_flag的复合索引。毕设里更推荐前者,业务上更干净。
5.5 拦截器里的白名单漏配
JWT拦截器加上后,最容易出现的bug是登录接口和静态资源被拦截,导致前端连登录都做不到。需要维护一个permitAllUrls列表,放行登录、注册、验证码、静态资源、Excel导出等接口。排查这类问题最快的办法是打开SpringBoot的日志输出,看请求是否到了DispatcherServlet。还有个细节:接口文档路径也要放行,否则本地调试时文档一片空白,会浪费很多时间。
6. 从实现到答辩:如何让项目在评审环节拿高分
6.1 用"评审老师视角"重新审视代码
我每次写完一个项目,都会用一个外部开发者的视角过一遍:不看自己熟悉的那几个页面,而是先看登录接口有没有做参数校验、权限注解有没有漏、异常有没有统一处理、数据库有没有索引。评审老师不一定真的去翻你代码,但只要他动手翻,这几个点是必定先看到的。
统一异常处理用@RestControllerAdvice加一个全局异常处理器,把业务异常转成统一返回体,代码干净很多。数据库层面,application表的scholarship_type_id、student_id、status这几个查询高频字段加上索引,批量统计时速度差异明显。
6.2 值得加分的低成本功能
在不扩大需求范围的前提下,有几个功能实现成本很低,但对评价影响很大:
- 数据可视化:用图表库做一个"各学院获奖人数分布"和"各奖学金类型申报趋势"的统计页,一眼就能看出系统价值。
- 公示期控制:管理员设置公示起止时间,到期后状态自动变为已结束,前端导出最终名单。
- 消息提醒:学生提交后,给审核人产生一条待办消息;审核通过后,给学生提示进度。用一张待办消息表就能搞定。
- 操作日志:审核、修改、删除都记录在案,答辩时讲"可审计性"非常加分。
6.3 我个人在实操中的一点体会
这类管理系统的核心难点从来不在技术,而在对业务角色的理解。只要把"谁、在什么时候、对什么数据、做什么操作"这条线理清楚,表结构和接口设计会自然清晰很多。我比较推荐先花一天时间画一张申请状态的流转图,再开始写代码,比边写边想省下大把返工时间。
另外,毕设项目不是商业项目,不需要过度设计。中间件能少用就少用,除非有明确的缓存或异步场景,否则加Redis、消息队列反而让答辩提问变多。我的建议是:把SpringBoot、MyBatis-Plus、权限框架、事务、文件上传这几个基本功练扎实,把业务闭环做完,就已经是一份很完整的工程实践了。最后再分享一个小经验:答辩演示前一定要准备一套干净的数据,删除所有测试产生的脏数据,把申请流程从头到尾走一遍确认所有按钮可用,以免现场网络或环境出问题。这个看似不起眼的准备,往往决定演示是否流畅。
