每年竞赛季,高校里最忙的往往是两类人:一类是负责竞赛组织的教务老师,另一类是帮老师整理材料的学生助理。几十个竞赛项目、几百份报名表、一堆又一堆的Excel和PDF,光是核对学生信息和报名时间就够折腾好几个通宵。我把这套基于Spring Boot的高校竞赛信息管理系统做完之后,最大的感触不是哪个框架有多难,而是"信息管理"这四个字背后,其实藏着一整套被大多数毕设和Demo项目忽略掉的业务逻辑。这篇文章就把我从需求梳理、表结构设计、核心接口实现到最终部署踩过的坑完整写一遍,给正在做同类系统的人一个能直接参考的蓝本。
1. 竞赛信息管理到底在管什么:需求梳理与功能拆解
很多人拿到这个题目,第一反应是"做一个竞赛报名网站",鼠标点几下就能报名,管理员在后台看一眼名单。真做起来才发现,竞赛管理远不止报名这一件事。
1.1 一个反直觉的结论:报名录入只是冰山一角
我最初和某高校的老师聊需求时,他对我说了一句特别实在的话:"我们缺的不是让学生填表的页面,是能让院里几十个竞赛项目不再出错地跑完'发布—报名—审核—比赛—评奖—归档'这条链的东西。"
这句话改变了我的设计方向。竞赛信息管理系统表面上是面向学生的报名入口,实际上的核心用户是三类人:学生、指导老师/院系管理员、校级竞赛管理员。学生想看有哪些竞赛、什么时候报名、最后拿了什么奖;老师要审核谁有资格参加、谁需要被调剂;校级管理员要统一下发通知、掌握全校竞赛项目清单和获奖数据。这三类人的诉求叠加起来,系统的功能边界就不是"一个报名页面"能覆盖的。
1.2 三类用户与四条业务主线
我最终把需求收敛成四条业务主线,后面的所有设计都围绕这四条线展开:
- 竞赛全生命周期管理:从竞赛信息录入、状态流转(未发布、报名中、已截止、评审中、已结束)到归档,每一步都有对应的状态字段。
- 学生报名与审批:学生提交报名材料,指导老师审核,管理员确认名单,整个过程要有留痕。
- 成绩与获奖档案:比赛结束后录入成绩、评定奖项,并生成可导出的获奖名单、证书编号。
- 通知与数据统计:竞赛发布、截止、结果公布都要触发通知;统计报表要能按学院、按年份、按竞赛级别汇总。
确定这四条线之后,系统的表结构设计就变得清晰了。因为每条主线都可以拆出一张或几张核心表,而且彼此之间的关联关系是明确的。
1.3 功能清单落定:系统边界到底划在哪
做毕设或课设最容易犯的毛病是"什么都想加",最后代码写了一万多行,功能却没法串起来。我在这个项目里给自己定了一条边界:不做在线考试、不做完整的OA审批流、不做聊天模块,只做和竞赛信息强相关的功能。理由很简单,竞赛管理系统最重要的指标是可维护性,教务老师能自己上手发布竞赛、导出名单,比系统里多十个炫酷功能更实际。
最终的功能清单分为管理端和学生端两部分。管理端有竞赛信息维护、报名审核、成绩管理、获奖登记、公告发布、学生信息管理;学生端有竞赛浏览、报名申请、我的报名记录、成绩查询。同时预留了"指导老师"的角色,用来做院系维度的报名初审。
2. 数据库设计:六张核心表的建模思路与字段取舍
数据库设计是整个系统的地基。地基歪了,后面写什么都别扭。我的做法是先画出实体关系,再一张张表去抠字段,每做一个字段都想清楚它将来会被哪个查询用到。
2.1 用户表与角色设计:三种权限一次说清
用户表是系统的入口表,我用的字段包括:id、username、password、real_name、role、student_no(学号)、college(学院)、major(专业)、grade(年级)、phone、email。
这里有一个容易被忽略的点:角色字段我建议直接用字符串ADMIN/TEACHER/STUDENT,不要用0/1/2这种数字,因为直接可读的枚举值在SQL排查和页面展示时都能少掉一层转换。密码一定要用加密算法存储,Spring Security自带的BCryptPasswordEncoder就够用,不要用MD5,数据库泄露时MD5几乎是裸奔。
学生信息为什么要单独存学号和学院?因为后面统计"某学院参赛人数""各年级获奖占比"时,这些字段就是分组依据。如果报名表里没有冗余这些字段,每次统计都要join用户表,SQL会变得啰嗦,而且在数据量大时性能会明显下降。
2.2 竞赛信息表:时间与状态字段的规范
竞赛信息表(competition)的核心字段是:id、name、level(竞赛级别:校级/省级/国家级)、category(竞赛类别)、organizer(主办方)、description、registration_start、registration_end、competition_time、location、quota(名额限制)、status。
这里最想提醒大家的是时间字段的统一。报名开始时间、报名截止时间、比赛时间这三个字段全部用datetime类型,不要用varchar存字符串,否则后面做"当前时间是否在报名窗口内"的判断时要反复做字符串转日期,性能和正确性都吃亏。
状态字段我建议用status存当前状态,同时允许通过时间字段自动推导。比如系统启动时做一个定时任务,把超过registration_end的竞赛状态从"报名中"改成"已截止";比赛结束且成绩已录入,再由管理员手动置为"已结束"。为什么要自动加手动双保险?因为有些竞赛会临时延期,只靠自动推导会误改状态,留手动入口才是真实的教务场景。
2.3 报名表和成绩表:数据的闭环设计
报名表(signup)的字段:id、competition_id、student_id、teacher_id、status、apply_reason、audit_remark、create_time、audit_time。
status状态我设计了五个:PENDING(待审核)、APPROVED(已通过)、REJECTED(已驳回)、CANCELLED(已取消)、WAITLIST(候补)。WAITLIST很关键,因为竞赛都有名额限制,前面的同学审核通过后因故退出,候补名单能直接顶上,避免重新组织报名。
成绩表(score/award)的字段:id、competition_id、student_id、signup_id、score、award_level、certificate_no。award_level用字典值存:FIRST/SECOND/THIRD/EXCELLENCE(一等奖/二等奖/三等奖/优秀奖),并允许为NULL,因为不是所有人都有奖。certificate_no是证书编号,生成规则可以用"年份+竞赛ID+序号",保证唯一性。
之所以要冗余signup_id关联,是便于从报名记录反查整个参赛历程:学生端"我的获奖"页面只需要查成绩表,再关联竞赛表就能展示全部历史。
2.4 字典表与参数表:把可配置还给我
另外两张辅助表在开发时容易被忽略,但上线后非常有用。一张是字典表(dict),存竞赛级别、竞赛类别、奖项等级这些可枚举值的映射;另一张是参数表(config),存系统级配置,比如"单场竞赛最多报名人数默认值""证书编号前缀"等。
这样设计的直接好处是:当学校新增一个竞赛类别时,不需要改代码、不需要重新部署,管理员在后台字典管理里加一条记录就行。对于没有专职开发人员维护的高校系统来说,这种可配置性比什么都重要。
3. 后端核心实现:安全验证、报名状态机与成绩统计
后端我用的是Spring Boot 2.x + MyBatis-Plus + MySQL,这套组合在同类项目里最成熟,遇到问题搜索时答案也多。下面挑三个容易做错的地方重点说。
3.1 登录鉴权的落地方式
登录鉴权我采用的是Sa-Token框架,它比纯Shiro配置简单,也比手写JWT拦截器省事。学生、老师、管理员三种角色共用一张用户表,登录成功后把用户ID和角色写入会话,前端发起请求时携带token,后端通过注解做角色校验。
实际开发里我补充了一个拦截器:白名单放行登录接口和竞赛列表查询接口,其他接口都必须验证token。前端路由也要配合,比如管理端页面在未登录时直接重定向到登录页。这里有个小坑是前后端联调时容易"前端觉得登录了、后端觉得没登录",排查时先看前端请求头里是否带上了Authorization字段,十有八九是请求拦截器配置漏了。
3.2 报名接口如何防止"重复报名"和"超时报错"
报名接口是整个系统里最需要谨慎处理的地方。我遇到过的真实问题有两个:学生疯狂点报名按钮导致生成多条报名记录;竞赛名额马上满时,两个同学同时提交,后端没做限制导致超员。
第一个问题靠数据库唯一索引解决:在signup表上加unique key(competition_id, student_id),从数据库层面保证一个竞赛一个学生只能有一条报名记录。第二个问题需要加事务和行锁,核心代码如下:
@Transactional(rollbackFor = Exception.class) public synchronized Result signUp(SignupRequest req) { Competition competition = competitionMapper.selectById(req.getCompetitionId()); // 1. 校验竞赛状态 if (!"REGISTERING".equals(competition.getStatus())) { return Result.error("当前不在报名时间内"); } // 2. 校验名额 Long count = signupMapper.selectCount( new LambdaQueryWrapper<Signup>() .eq(Signup::getCompetitionId, req.getCompetitionId()) .in(Signup::getStatus, "APPROVED", "PENDING")); if (count >= competition.getQuota()) { return Result.error("报名人数已满"); } // 3. 插入报名记录 Signup signup = new Signup(); signup.setCompetitionId(req.getCompetitionId()); signup.setStudentId(req.getStudentId()); signup.setStatus("PENDING"); signupMapper.insert(signup); return Result.success(); }这里有个经验之谈:如果同时用synchronized加事务注解,要注意事务的提交时机。synchronized锁住的是当前方法,但事务提交发生在方法返回之后,极端情况下第二个线程进入方法时第一个事务还没提交,仍会读到旧的报名数量。更稳妥的做法是把校验和插入放到一个独立的事务方法里,或者直接依赖数据库的唯一索引兜底。在单人并发量不高的管理系统中,synchronized + 唯一索引已经够用,不必过度设计成分布式锁。
3.3 成绩录入与获奖排名计算的两种模式
成绩录入我做了两种模式:一种是管理员在后台逐条录入,另一种是Excel批量导入。批量导入用EasyExcel,导入时先做模板校验,包括学号是否存在、分数是否在0到100之间、竞赛是否存在,全部通过才允许入库。
获奖排名计算的逻辑说起来简单:按分数从高到低排序,然后按比例切一等奖、二等奖、三等奖。但实际中会遇到并列分数的问题,比如第10名和第11名同分,而一等奖名额恰好只有10个。
我的处理方式是使用MySQL的窗口函数RANK(),先按竞赛分组排序,再根据排名区间判定奖项等级。这样并列名次会占用同一排名,后面的人顺延,比较符合真实评奖规则。
SELECT student_id, score, RANK() OVER (PARTITION BY competition_id ORDER BY score DESC) AS rank_no FROM score WHERE competition_id = #{competitionId}拿到排名数据后,再根据"一等奖取前5%、二等奖取前15%、三等奖取前30%"的配置算出每个名次对应的奖项等级,回填到成绩表。整个过程我用定时任务收尾:比赛结束后自动计算并生成公示名单,管理员确认后才对外发布。
4. 前端页面与交互设计:管理端和学生端的关键界面
系统的界面部分,学生端我用的是Vue + Element UI,管理端同样用Vue,通过路由懒加载拆成两个不同的布局。之前很多人喜欢一套代码同时兼容学生和管理员,实际开发下来发现复杂度更高,不如干脆分成两个入口。
4.1 学生端:从找竞赛到看结果的完整体验
学生端首页是竞赛列表,按报名状态分Tab展示:报名中、即将开始、已结束。每张竞赛卡片展示竞赛名称、级别、报名截止时间这几个最关键的信息,点击卡片进入详情页,详情页里有完整的竞赛介绍和报名按钮。
报名按钮的交互要注意状态细化。系统至少要有四种按钮状态:可报名、已报名待审核、报名人数已满、报名已截止。这四种状态下按钮颜色和文案必须不同,而且要禁用不可点击,否则学生反复点提交就会触发前面说的重复报名的坑。
"我的竞赛"页面展示的是学生所有报名记录,每一条要能看当前审核进度,是通过了、被驳回了,还是进了候补名单。被驳回时旁边要显示老师的审核意见,这是我在试运行后被老师专门要求加的,因为学生被驳回后往往不知道为什么,只能到处问人,有了意见之后沟通成本直线下降。
4.2 管理端:审批流程与数据可视化的必要性
管理端的第一屏是数据概览,我用ECharts做了四个核心指标:本月竞赛数量、报名总人次、审核通过率、各学院参赛人数排行。为什么第一屏放统计图表?因为管理者打开系统最想看到的是"现在情况怎么样",而不是先点进某个菜单。这也是我在回访中得到的真实反馈,没有可视化的管理端会被当作"只是个录入后台"。
报名审核页面是管理端使用频率最高的功能。列表要支持按竞赛、按状态、按学院三个维度筛选,每条记录右侧有通过、驳回两个操作按钮,点击驳回弹窗时强制要求填写原因。这里有一个体验优化:审核时可以勾选"连续审核",系统自动加载下一条待审核记录,这个功能被老师反复夸过,因为它省掉了大量的重复点击。
竞赛发布表单里有一个细节:时间选择器要禁用已过去的日期,报名截止时间必须晚于报名开始时间。这些校验在前端做一层,后端接口也要做一层,双保险,防止有人绕过前端直接调接口。
4.3 接口对不齐是前端联调最常见的坑
前后端分离开发时最容易出现的坑就是接口路径和字段名对不上。所以我在项目初期就先写了接口文档,把每个接口的请求参数、返回结构约定清楚。前后端都按这个文档开发,联调时出问题的概率大幅降低。
我自己遇到的典型问题有两个:一个是日期格式,后端返回的是2024-05-01T10:00:00,前端组件要求的是2024-05-01 10:00:00,不一致导致页面上显示一片空白;另一个是分页参数,后端接口从0开始,前端组件从1开始,第一次查询总是少一页数据。这些问题提前在文档里写清楚,联调能快很多。
5. 开发环境、调试部署与论文写作配套经验
系统开发完成不等于项目结束,部署上线和配套论文是毕设或课设里绕不开的两件事。这一部分是我自己折腾了很多次之后总结出的经验,可以直接照抄。
5.1 开发环境版本匹配:不要跟风用最新版
这个系统的开发环境我建议固定为:JDK 1.8、Maven 3.6+、MySQL 5.7或8.0、Spring Boot 2.7.x。别用Spring Boot 3.x和JDK 17的组合,虽然新,但很多第三方依赖和网上的教程都还停留在2.x时代,遇到问题时能参考的资料少得多。
application.yml里的关键配置我贴出来,照着改就能跑:
server: port: 8080 servlet: context-path: /competition spring: datasource: url: jdbc:mysql://localhost:3306/competition_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意serverTimezone=Asia/Shanghai这一项,MySQL 8.0默认时区是UTC,不加这个参数,插入数据库的时间会比本地时间早8个小时,查出来数据全不对。这个问题我排查了整整一个下午才找到,谁都想不到是时区在捣鬼。
5.2 部署中遇到的三类问题与排查路径
我把部署阶段踩过的坑总结成三类,几乎每个做同类系统的人都会遇到。
第一类是数据库连接失败。现象是项目启动报Access denied for user或Communications link failure。前者是账号密码或权限问题,检查MySQL里的用户授权;后者多半是IP和端口配置错误,注意MySQL默认端口是3306,如果本机装了多个MySQL实例,端口可能被占用。
第二类是端口冲突。Spring Boot默认8080,如果本机有别的服务占用,启动时会报Port already in use。Windows下用netstat -ano | findstr 8080找到占用进程的PID,然后任务管理器结束它,或者直接改配置文件里的端口。
第三类是中文乱码。乱码出现的位置可能是页面展示、Excel导出、数据库存储。排查思路按这三层走:数据库连接URL有没有加characterEncoding=utf8(配置文件里必须有);数据库表本身的字符集是不是utf8mb4(建表语句最后要加DEFAULT CHARSET=utf8mb4);前端页面有没有设置<meta charset="UTF-8">。三层都对了,乱码基本消失。
5.3 带论文文档的系统的常见问题
很多人在这个项目上会配套写论文,通常要凑够一万字以上。我的体会是,论文最核心的不是字数,而是"技术实现章节能不能讲清楚数据流"。建议按这个结构组织:题目确定后先写需求分析,把用例图和每条业务主线的流程描述放进去;然后是系统设计,包括架构图、功能模块图、数据库ER图和核心表结构;再是系统实现,选取登录、报名、成绩导出等3到4个关键功能,贴核心代码并解释为什么这么写;最后是系统测试,列出测试用例表和测试结果,包括功能测试、兼容性测试和简单的性能测试。这样写下来,字数自然就够一万字了,而且评审老师很容易看到工作量。
系统界面截图我建议在论文附录里放一整套,从学生端首页、报名详情、管理端审批页到最终的成绩公示,每张图配一句话说明。这个前后对照可以减轻答辩时"只看代码不知道页面长什么样"的压力。
最后再分享一个我在实际调优里觉得很有意思的细节:报名截止时间的前一小时,系统会自动给还没报名的学生发提醒通知。这个功能代码量不大,但它是整个系统里被提起次数最多的功能之一。从业务价值来看,它的作用比排行榜和统计图表都更突出——竞赛信息管理的本质是让对的人在对的时间知道对的事情。你如果时间充裕,建议也把这个"人性化"的小功能加进自己的设计里,它在答辩时很能说明你对业务场景的理解。