☰
SpringBoot+Vue3学生评奖评优管理系统:前后端分离毕设全解析
2026/10/6 10:17:23 网站建设 项目流程

聊到“Java Web 学生评奖评优管理系统”,这应该是很多计算机专业同学毕业设计、课程设计里的“常客”。但你如果翻过网上那些老项目,大多还停留在 JSP + Servlet、SpringMVC + JQuery 那个年代,前后端不分离,代码耦合严重,数据库脚本动不动还是 MySQL 5.x 的老语法。今天分享的这套源码,技术栈直接拉到了当前就业市场的主流配置:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,并且自带完整文档。不论你是准备做毕设,还是想系统练一遍前后端分离项目的完整落地流程,这套东西都很有参考价值。我会从架构思路、核心模块拆解、关键代码实现、环境搭建,一直讲到实际部署中容易踩的坑,全程都用开发者的视角来聊,尽量让你看完就能对着源码理清思路。

1. 项目整体设计与技术选型思路

1.1 为什么是这套技术组合

很多同学选技术栈容易犯一个毛病:跟风追新。看见 SpringBoot3 出来了就直接上,结果发现 JDK17 的环境没装、老教程全对不上、第三方依赖还有兼容坑,折腾两天还没开始写业务代码。这套项目把技术栈定在 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,是非常务实的选择,我把每个环节的选型逻辑拆开说。

SpringBoot2 对应的是 JDK8 生态。JDK8 在高校机房、企业存量系统里依然是绝对主力,资料多、踩坑分享丰富,遇到问题百度一下基本都有答案。SpringBoot3 强制要求 JDK17,虽然性能更好,但对毕设和接手老项目来说反而增加环境负担。SpringBoot2 的自动配置机制、Starter 依赖管理已经非常成熟,开发体验和最终效果足够好。

Vue3 是前端的主流版本,组合式 API 让代码复用和逻辑组织比 Vue2 清爽太多。配合 Vite 构建工具,开发环境秒级热更新,比 Webpack 时代动辄几十秒的编译等待舒服得多。不过要说明一下,这套源码如果用的是 Vue CLI 创建工程也没关系,核心是 Vue3 的语法和组件化思路,构建工具不影响你理解代码。

MyBatis-Plus 对单表 CRUD 的简化是革命性的,内置的 BaseMapper 直接提供了 insert、delete、update、selectById 这一堆通用方法,绝大多数数据访问不用写 SQL。它的 LambdaQueryWrapper 用起来非常顺手,比手拼 XML 里的动态 SQL 直观得多,也比 JPA 更容易控制 SQL 行为,适合这种业务规则比较清晰的管理系统。

MySQL8.0 是当前数据库的事实标准。相比 5.7,默认字符集已经是 utf8mb4,对中文和 emoji 的支持零配置搞定;窗口函数、公用表表达式(CTE)这些新特性虽然在这个项目里用不到,但是学习价值是在的,你以后做数据分析类功能时随时能用上。

1.2 学生评奖评优系统的需求定位与模块规划

评奖评优这个业务场景,核心矛盾在于“提交-审核-公示”这条链路的状态管理和角色权限控制。整个系统按角色拆可以分成三类用户,每类用户看到的界面和能做的操作完全不同。

学生端的主要诉求是查看可申报的奖项、填写申请材料、上传佐证附件、查看自己申请的审核进度。这里的关键点是“申请记录的快照”概念——学生提交申请的那一瞬间,系统要保存他当时的成绩排名、综合测评分数、获奖经历等字段,而不是实时去关联学生表。原因很好懂,学生的成绩单可能会被教务修正,如果评审时读的是实时数据,学生提交的材料和评审看到的材料对不上,是要出纠纷的。好的设计是申请记录里直接冗余存储这些快照字段。

教师端或者辅导员端,核心功能是查看名下学生的申请列表、对申请材料进行初审、填写推荐意见。对于校级奖项,可能还有二级学院到学校的两级审批,那就要设计审批流转的功能。

管理员端的功能最重,包括学生信息管理、奖项配置(奖项名称、等级、名额、申请起止时间)、评审分配(指定某奖项由哪些老师评审)、结果公示管理(公示起止时间、公示状态)、以及最终的获奖名单归档。

除了业务模块,还有个容易被忽略但必考的点:统一认证与登录拦截。站点内所有接口除了登录,其余都要校验身份和角色权限,这块一般用 JWT 或者 Spring Security 来做,本项目里用拦截器加自定义注解的方式实现,轻量且够用。

2. 数据库设计与核心业务模型

2.1 核心表结构设计与字段规划

数据库设计是这类管理系统的地基,表建不好后面写代码全是打补丁。评奖评优系统的核心表,我按业务域拆成四组。

第一组是用户与权限域。用户表设计上要注意,学生和教师不应该各建一套账号体系,而是建一张统一的用户表,通过 role 字段区分角色,再通过 profile 表或直接在用户表冗余学号、工号、姓名、所属学院。这样登录逻辑只需要查一张表,不用走多态关联,代码会简单很多。密码字段用 BCrypt 加密存储,不要用 MD5,MD5 在如今算力下等于明文。

第二组是基础数据域,主要是学生信息和奖项信息。学生信息表包含学号、姓名、性别、专业、班级、年级、政治面貌、综合测评成绩、学分绩点、排名等。奖项信息表包含奖项名称、奖项等级(国家级、省级、校级、院级)、评奖年度、名额数量、申请开始时间、申请截止时间、是否允许重复申请等。设计时要想清楚,有些字段是会变化的,比如综合测评成绩,它到底是存在学生表里实时更新,还是在学生提交申请时做成快照,我倾向于后者,实操中这点非常关键。

第三组是业务流转域,包括申请记录表、审核记录表、公示记录表。申请记录表是最复杂的,字段包括关联的学生ID、关联的奖项ID、申请状态(草稿、已提交、审核中、通过、驳回)、各项申报材料的内容或链接、辅导员初审意见、学院审核意见、学校审核意见、驳回原因等。审核记录表记录每一步操作的操作人、操作时间、操作类型、意见内容,方便追溯,这条流水设计很多学生容易漏掉,但对于评奖评优这种敏感业务来说几乎必考。

第四组是公示与归档域。公示表记录每个批次的公示开始时间、结束时间、公示范围内的奖项和名单,公示期内学生可以发起异议。公示结束后数据归档,生成最终的获奖名单表和证书编号。

2.2 业务状态流转与权限控制分析

评奖评优的核心业务逻辑,就是一条申请记录的“状态机”。我用状态值来串起整个流程:

DRAFT(草稿) -> SUBMITTED(已提交) -> COLLEGE_REVIEWING(学院审核中) -> SCHOOL_REVIEWING(学校审核中) -> APPROVED(通过) \-> REJECTED(驳回)

状态流转的触发点对应不同的角色:学生提交申请触发草稿到已提交;辅导员初审触发已提交到学院审核中或驳回;学校评审触发学院审核中到学校审核中或驳回;最终评定通过后进入公示流程。

这个流转过程必须做“防跳转”校验,也就是不能出现学生直接提交一个 APPROVED 状态的申请。实现时可以在 Service 层写一个状态流转校验方法,每个更新操作前先查当前状态,跟期望状态比对,不一致就抛业务异常。

权限控制这块,我没有用 Spring Security 那套重型的过滤器链,而是采用拦截器 + 自定义注解的方式。登录接口颁发 JWT Token,前端每次请求在 Header 里携带 Token,后端拦截器统一解析校验。针对需要特定角色的接口,用 @RequireRole("ADMIN") 这样的注解标记,拦截器里校验通过才放行。好处是轻量、直观,整个权限代码加起来不到两百行,适合中小型管理系统。如果以后要接入更复杂的权限模型,再替换成 Spring Security 也来得及。

3. 后端核心技术实现与代码拆解

3.1 MyBatis-Plus 通用 CRUD 与条件构造器应用

这个项目的后端数据访问层,几乎全部依赖 MyBatis-Plus 的通用能力。你只需要让 Mapper 接口继承 BaseMapper ,就能直接调用 insert、deleteById、updateById、selectById、selectList 等现成方法,完全不用写一行 SQL。

我举一个实际的例子,比如学生提交申请时,要查询某个奖项是否在申请时间窗口内、是否还有剩余名额。用 LambdaQueryWrapper 写起来是这样的:

// 查询指定ID的奖项,并校验申请时间窗口和名额 Award award = awardMapper.selectOne(new LambdaQueryWrapper<Award>() .eq(Award::getId, awardId) .ge(Award::getEndTime, LocalDateTime.now()) .le(Award::getStartTime, LocalDateTime.now())); if (award == null) { throw new BusinessException("该奖项不在申请时间范围内"); } // 统计该奖项已提交的申请数量 Long appliedCount = applicationMapper.selectCount(new LambdaQueryWrapper<Application>() .eq(Application::getAwardId, awardId) .in(Application::getStatus, Arrays.asList("SUBMITTED", "COLLEGE_REVIEWING", "SCHOOL_REVIEWING", "APPROVED"))); if (appliedCount >= award.getQuota()) { throw new BusinessException("该奖项申请名额已满"); }

LambdaQueryWrapper 最大的好处是类型安全,字段名写错了编译期直接报错,不像 XML 里写错列名要等到运行期才炸。另外注意 eq、ge、le 这些方法的调用顺序没有强制要求,但为了代码可读性,建议保持条件字段从左到右、从主到次的顺序写。

分页查询也是管理系统的硬需求。MyBatis-Plus 提供了分页插件,配置好之后只需调用 selectPage 方法:

Page<ApplicationVO> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Application> wrapper = new LambdaQueryWrapper<Application>() .eq(Application::getStudentId, currentUser.getStudentId()) .orderByDesc(Application::getCreateTime); IPage<ApplicationVO> result = applicationMapper.selectPageWithAwardName(page, wrapper);

注意这里如果你要联表查询返回 VO 对象,需要在 Mapper 里写自定义方法。MyBatis-Plus 的 BaseMapper 只处理单表,涉及多表关联的业务查询还是要自己写 SQL,这就体现出 MyBatis 系比 JPA 灵活的地方了。selectPageWithAwardName 背后的 XML 里就是一个 left join 奖项表,把业务字段和奖项名称查出来封装成 VO 返回。

3.2 Service 层业务逻辑设计与事务处理

Service 层是系统的“大脑”,所有业务规则都在这里落地。我用 Interface + Impl 的方式定义 Service,这是 MyBatis-Plus 官方推荐的做法:服务接口继承 IService ,实现类继承 ServiceImpl<M, T>,这样既能获得 MyBatis-Plus 内置的通用服务方法,又能扩展自己的业务方法。

以“学生提交申请”这个场景为例,核心 Service 方法长这样:

@Transactional(rollbackFor = Exception.class) public Long submitApplication(ApplicationSubmitDTO dto) { // 1. 校验学生身份和奖项信息 Student student = studentService.getById(dto.getStudentId()); Award award = awardService.getById(dto.getAwardId()); validateSubmitPermission(student, award); // 2. 校验是否重复申请 Long count = applicationService.count(new LambdaQueryWrapper<Application>() .eq(Application::getStudentId, dto.getStudentId()) .eq(Application::getAwardId, dto.getAwardId()) .ne(Application::getStatus, "REJECTED")); if (count > 0) { throw new BusinessException("不能重复申请同一奖项"); } // 3. 创建申请记录并保存快照 Application application = new Application(); BeanUtils.copyProperties(dto, application); application.setStatus("SUBMITTED"); application.setCreateTime(LocalDateTime.now()); application.setUpdateTime(LocalDateTime.now()); applicationService.save(application); // 4. 记录操作日志(埋点) operateLogService.record("SUBMIT_APPLICATION", application.getId(), currentUserId(), "学生提交评奖申请"); return application.getId(); }

这里我看到过很多初级同学犯的错误:写了一大堆业务逻辑,但忘记加 @Transactional 注解。比如保存申请记录之后还要扣减名额、记录日志,只要中途任何一个操作抛异常,前面对数据库的写入就变成脏数据了。加 rollbackFor = Exception.class 的意思是所有异常都触发回滚,包括 RuntimeException,这应该成为你写任何涉及多表变更的 Service 方法的默认习惯。

3.3 统一返回体、异常处理与登录鉴权实现

前后端分离项目里,接口返回格式必须是统一的 JSON 结构,否则前端处理起来很痛苦。我的统一返回体定义如下:

{ "code": 200, "message": "success", "data": { } }

code 为 200 表示成功,其他为业务错误码;message 给前端弹提示用;data 放业务数据。对应 Java 类是一个泛型类 Result ,含静态方法 success(data)、error(code, message)。

全局异常处理器是整个项目非常关键的一个类。我用 @RestControllerAdvice + @ExceptionHandler 捕获所有异常,分类处理:

  • 业务异常 BusinessException:返回前端友好的提示信息,不打印堆栈,避免刷日志
  • 参数校验异常 BindException/MethodArgumentNotValidException:把校验失败信息拼装返回
  • 兜底 Exception:打印完整堆栈,返回“系统繁忙”的通用提示

这样接口层代码就非常干净,不用每个方法都 try-catch,业务代码只管抛异常。

登录鉴权我的实现方案是 JWT + 拦截器。用户登录成功后生成 Token 返回前端,Token 里携带用户ID和角色信息。拦截器里解析 Token 并放入 ThreadLocal 上下文,后续 Service 层通过上下文获取当前用户,避免在 Controller 层方法签名里反复传 userId。自定义注解 @RequireRole 配合拦截器做角色控制:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole == null) { return true; } // 从Header取Token并校验角色 String token = request.getHeader("Authorization"); LoginUser loginUser = JwtUtil.parseToken(token); if (loginUser == null) { throw new BusinessException("登录状态已过期,请重新登录"); } String role = loginUser.getRole(); String requiredRole = requireRole.value(); if (!role.equals(requiredRole)) { throw new BusinessException("无权限访问"); } UserContext.set(loginUser); } return true; } }

注意拦截器里做权限判断后,要在 finally 或拦截器 afterCompletion 里清理 ThreadLocal,否则线程池复用会导致用户信息串号,这是一个很经典且隐蔽的 bug,排查起来极其痛苦。

4. 前端 Vue3 工程化实践与页面实现

4.1 Vue3 组合式 API 与选项式 API 的选择

这套系统前端用的 Vue3,这里我想好好聊一下 Composition API 和 Options API 怎么选,因为这是 Vue3 学习路上的第一个岔路口。我先给结论:新项目直接用组合式 API,不要留恋选项式 API。

选项式 API 的代码组织方式是按“选项”划分的,你把一个组件的数据放在 data 里、方法放在 methods 里、生命周期钩子放在 mounted 里。组件小的时候看着整齐,等业务逻辑一多,比如一个页面要同时处理表格查询、弹窗表单、文件上传、状态切换,“数据、方法、侦听器”这一套撑下来几百行,找代码的时候得在 data 和 methods 区域来回翻,维护成本很高。

组合式 API 的思路是按“业务逻辑”来组织代码。同一个业务功能的状态和方法放在一起,比如“查询申请列表”相关的 loading、list、queryParams、fetchList 方法全写在一块,修改时只看这一段就够。代码复用上也更优雅,自定义一个 useApplicationList 的 composable 函数,多个页面都能引用。

我拿这套系统里的“学生申请列表”页面来对比说明。选项式 API 的写法大致是:

export default { data() { return { loading: false, list: [], queryParams: { page: 1, size: 10 } } }, methods: { async fetchList() { this.loading = true const res = await getApplicationList(this.queryParams) this.list = res.data.records this.loading = false } }, mounted() { this.fetchList() } }

换成组合式 API:

<script setup> import { ref, onMounted } from 'vue' import { getApplicationList } from '@/api/application' const loading = ref(false) const list = ref([]) const queryParams = reactive({ page: 1, size: 10 }) const fetchList = async () => { loading.value = true try { const res = await getApplicationList(queryParams) list.value = res.data.records } finally { loading.value = false } } onMounted(fetchList) </script>

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

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

立即咨询