资讯动态

高校学科竞赛训练管理系统:报名组队成绩分析全流程设计与实现

发布时间:2026/9/25 9:41:29 来源:尧图企业网站定制
简介这是一套面向高校师生日常学科竞赛训练的管理系统源码围绕比赛报名、成绩录入、自由组队与水平分析等核心场景设计适合计算机相关专业学生作为课程设计、毕业设计或全栈练手项目也可供教师参考搭建竞赛管理平台。压缩包共165个文件约453KB以60个js与57个jsx构成前后端业务逻辑与页面组件19个less负责样式16个png提供界面素材另有json配置、md说明及eslint、prettier、git等工程化配置文件整体结构清晰、便于二次开发。内容预览可见登录、组队、监控、工具函数等模块划分覆盖用户权限、通知推送、数据安全与移动端适配等设计要点能帮助读者理解竞赛训练系统的完整业务链路与工程组织方式。目前已有77人学习下载适合希望快速上手一套可运行管理系统的开发者参考借鉴。1. 学科竞赛训练管理系统从报名到水平分析一套系统怎么串起来高校竞赛训练这件事最怕的不是没学生而是信息散在十几个 Excel 和微信群里。我带过两年 ACM 集训队报名靠接龙、成绩靠手动誊抄、组队靠私下商量到了赛季末想分析一下谁进步快、谁该补哪块短板发现数据根本凑不齐。学科竞赛训练管理系统要解决的就是这个把比赛报名、成绩录入、自由组队、水平分析四件事收进一个平台让老师看到全局让学生看到自己。它适合高校竞赛负责人、集训队教练也适合想拿它当课程设计或毕业设计的同学。下面按我实际搭过的一版方案从数据模型讲到部署排错能抄的地方直接给代码和参数。2. 数据模型与角色权限先把地基打对2.1 四张核心表怎么设计系统再花哨底层就是几张表的事。我一般会先画 ER 图再动手避免后期改字段改到崩溃。核心实体有四个用户、竞赛、队伍、成绩。用户表要区分角色老师和学生的权限完全不同。竞赛表记录比赛的基本信息。队伍表是自由组队的载体。成绩表既要存个人成绩也要能关联到队伍和竞赛。-- 用户表role 字段区分 teacher / student / admin CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, -- bcrypt 哈希别存明文 real_name VARCHAR(50) NOT NULL, role ENUM(student,teacher,admin) DEFAULT student, student_no VARCHAR(20) DEFAULT NULL, -- 学号学生必填 college VARCHAR(100) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 竞赛表status 控制报名开关 CREATE TABLE contests ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, category VARCHAR(50) NOT NULL, -- 如 ACM、数学建模、电子设计 sign_start DATETIME NOT NULL, sign_end DATETIME NOT NULL, contest_date DATE NOT NULL, max_team_size INT DEFAULT 3, status ENUM(draft,signing,ongoing,finished) DEFAULT draft ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;队伍表和成绩表通过外键关联队伍成员用中间表存多对多关系。这里有个关键决策成绩到底挂在人身上还是队伍身上我的做法是两张成绩表——个人成绩表和团队成绩表因为有些竞赛是个人赛比如蓝桥杯个人赛有些是团队赛比如 ACM。混在一张表里后期查询会非常难受。-- 队伍表 CREATE TABLE teams ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contest_id BIGINT NOT NULL, team_name VARCHAR(100) NOT NULL, leader_id BIGINT NOT NULL, -- 队长 user_id status ENUM(forming,locked,dissolved) DEFAULT forming, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (contest_id) REFERENCES contests(id), FOREIGN KEY (leader_id) REFERENCES users(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 队伍成员中间表 CREATE TABLE team_members ( team_id BIGINT NOT NULL, user_id BIGINT NOT NULL, joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (team_id, user_id), FOREIGN KEY (team_id) REFERENCES teams(id), FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 角色权限的边界在哪权限设计最容易翻车的地方是「老师能不能替学生报名」。我的原则是老师有管理权但没有代操作权。老师可以创建竞赛、审核队伍、录入成绩但不能替学生提交报名——报名必须学生本人确认否则出了问题责任说不清。实现上用 RBAC每个接口加角色注解。# FastAPI 依赖注入做角色校验 from fastapi import Depends, HTTPException, status def require_role(*allowed_roles): def checker(current_user Depends(get_current_user)): if current_user.role not in allowed_roles: raise HTTPException( status_codestatus.HTTP_403_FORBIDDEN, detailf需要 {/.join(allowed_roles)} 权限 ) return current_user return checker # 创建竞赛仅老师和管理员 app.post(/contests) def create_contest(data: ContestCreate, user Depends(require_role(teacher, admin))): ... # 报名仅学生本人 app.post(/contests/{cid}/signup) def signup(cid: int, user Depends(require_role(student))): ...参数说明require_role接收可变参数传入允许的角色列表。get_current_user从 JWT token 解析用户信息token 里带 role 字段。注意 token 过期时间别设太长我一般设 2 小时配合 refresh token 使用。这里有个坑如果前端把 role 存在 localStorage 里做菜单显隐后端一定要再校验一次前端隐藏不等于后端安全。3. 比赛报名与自由组队并发和状态机是重点3.1 报名接口怎么防超卖和重复提交报名看着简单但报名截止那一刻的并发量能把你数据库打穿。我踩过的坑两个学生同时点报名结果都插入了同一条记录。解决办法是数据库唯一索引加应用层幂等。-- 报名表加唯一索引从数据库层面兜底 CREATE TABLE signups ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contest_id BIGINT NOT NULL, user_id BIGINT NOT NULL, sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_contest_user (contest_id, user_id), FOREIGN KEY (contest_id) REFERENCES contests(id), FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;# 报名逻辑先查状态再插入捕获唯一键冲突 from sqlalchemy.exc import IntegrityError app.post(/contests/{cid}/signup) def signup(cid: int, user Depends(require_role(student))): contest db.query(Contest).get(cid) if not contest: raise HTTPException(404, 竞赛不存在) now datetime.now() if now contest.sign_start: raise HTTPException(400, 报名尚未开始) if now contest.sign_end: raise HTTPException(400, 报名已截止) if contest.status ! signing: raise HTTPException(400, 当前状态不可报名) signup Signup(contest_idcid, user_iduser.id) db.add(signup) try: db.commit() except IntegrityError: db.rollback() raise HTTPException(409, 你已经报名过了) return {msg: 报名成功}逻辑说明先做业务校验时间窗口、状态再插入。唯一索引uk_contest_user保证同一竞赛同一用户只能有一条记录。捕获IntegrityError后返回 409前端提示「已报名」。参数上sign_start和sign_end用 DATETIME 而不是 DATE因为很多竞赛报名截止到具体几点几分。注意时区问题服务器统一用 UTC 存储前端展示时转本地时区否则跨时区学生会看到错误的时间。3.2 自由组队的邀请与锁定机制自由组队不是拉个群就完事系统要管三件事邀请、接受/拒绝、锁定。我设计的流程是队长创建队伍 → 生成邀请码或直接邀请指定用户 → 被邀请人确认 → 队伍满员后队长锁定 → 锁定后不可更改。状态机是关键forming状态下可以增删人locked之后只能由老师解锁。# 邀请加入队伍 app.post(/teams/{tid}/invite) def invite_member(tid: int, invitee_id: int, user Depends(require_role(student))): team db.query(Team).get(tid) if team.leader_id ! user.id: raise HTTPException(403, 只有队长可以邀请) if team.status ! forming: raise HTTPException(400, 队伍已锁定无法邀请) contest db.query(Contest).get(team.contest_id) current_count db.query(TeamMember).filter_by(team_idtid).count() if current_count contest.max_team_size: raise HTTPException(400, 队伍人数已满) # 检查被邀请人是否已报名该竞赛 signed db.query(Signup).filter_by( contest_idteam.contest_id, user_idinvitee_id).first() if not signed: raise HTTPException(400, 对方尚未报名该竞赛) invite TeamInvite(team_idtid, invitee_idinvitee_id, statuspending) db.add(invite) db.commit() return {msg: 邀请已发送}参数说明max_team_size从竞赛表读取不同竞赛可以设不同人数上限ACM 一般 3 人数学建模 3 人电子设计可能 2-3 人。TeamInvite表需要单独建记录邀请状态pending / accepted / rejected。被邀请人接受时再往team_members插记录同时检查人数上限——这里要加行锁SELECT ... FOR UPDATE否则两个人同时接受邀请可能超员。注意组队锁定后如果有个别成员退出不要直接删记录而是把队伍状态改回forming并通知队长。直接删会导致成绩关联断裂。4. 成绩录入与水平分析从手工誊抄到自动画像4.1 成绩录入的批量导入与校验成绩录入最烦的是老师拿着一堆 Excel 让你导。我的做法是提供模板下载 批量导入导入时做三层校验格式校验、人员校验、重复校验。格式校验看列名和数据类型人员校验看学号是否在系统里重复校验看同一竞赛同一人是否已有成绩。import pandas as pd def import_scores(file_path: str, contest_id: int): df pd.read_excel(file_path) required_cols {学号, 姓名, 成绩, 奖项} if not required_cols.issubset(set(df.columns)): raise ValueError(f缺少列{required_cols - set(df.columns)}) errors [] success 0 for idx, row in df.iterrows(): student_no str(row[学号]).strip() user db.query(User).filter_by(student_nostudent_no).first() if not user: errors.append(f第{idx2}行学号 {student_no} 不存在) continue exists db.query(Score).filter_by( contest_idcontest_id, user_iduser.id).first() if exists: errors.append(f第{idx2}行{row[姓名]} 成绩已存在) continue score Score( contest_idcontest_id, user_iduser.id, scorefloat(row[成绩]), awardstr(row[奖项]).strip() ) db.add(score) success 1 db.commit() return {success: success, errors: errors}逻辑说明用 pandas 读 Excel逐行校验。required_cols是硬性要求缺列直接报错。学号统一转字符串并 strip防止 Excel 把学号读成数字丢前导零。错误信息带上行号老师好定位。参数上score用 float 存但展示时可以按竞赛类型决定是否取整。奖项字段建议用枚举值一等奖/二等奖/三等奖/优胜奖/无方便后续统计。4.2 水平分析的三个维度怎么算水平分析是这个系统最有价值的部分也是最能做出差异化的地方。我一般从三个维度做成绩趋势、获奖分布、能力雷达。成绩趋势看学生历次竞赛的排名变化获奖分布看获奖等级的比例能力雷达需要把竞赛分类映射到能力维度上。# 成绩趋势按时间排序计算排名变化 def score_trend(user_id: int): scores db.query(Score).join(Contest).filter( Score.user_id user_id ).order_by(Contest.contest_date).all() trend [] for s in scores: # 计算该次竞赛中的排名 rank db.query(Score).filter( Score.contest_id s.contest_id, Score.score s.score ).count() 1 total db.query(Score).filter( Score.contest_id s.contest_id).count() trend.append({ contest: s.contest.title, date: s.contest.contest_date, score: s.score, rank: rank, total: total, percentile: round((1 - rank / total) * 100, 1) }) return trend参数说明percentile是百分位比绝对排名更能反映水平。比如 100 人里排第 10百分位是 90%说明超过了 90% 的人。能力雷达需要一张映射表把竞赛类别映射到能力维度。竞赛类别编程能力算法能力团队协作工程实践论文写作ACM高高高中低数学建模中中高中高电子设计中低中高低蓝桥杯高高低中低这张表是分析的核心每个竞赛类别给各维度一个权重学生参加多次竞赛后加权求和就能画出能力雷达图。权重可以调我一般让老师根据实际比赛内容微调。注意雷达图的数据要归一化到 0-100否则不同维度量纲不一致没法比。5. 避坑与排查那些让我熬夜的坑5.1 报名时间窗口的时区问题现象学生反馈「明明还没到截止时间系统就说报名已截止」。原因服务器用 UTC 存储时间但比较时用了本地时间datetime.now()两者差 8 小时。解决统一用datetime.utcnow()比较或者用带时区的datetime.now(timezone.utc)。前端展示时再转本地时区。这个坑我踩过两次血泪经验是所有时间字段在数据库里存 UTC在代码里比较也用 UTC只在展示层转。5.2 组队人数超限的并发问题现象队伍设了 3 人上限结果出现了 4 人队伍。原因两个邀请同时被接受各自查人数时都是 2都通过了校验。解决在检查人数时加行锁。# 接受邀请时加行锁 team db.query(Team).filter_by(idtid).with_for_update().first() current_count db.query(TeamMember).filter_by(team_idtid).count() if current_count contest.max_team_size: raise HTTPException(400, 队伍人数已满)with_for_update()会对该行加排他锁第二个请求会等待第一个完成后再查此时人数已经更新。注意别锁太多行只锁队伍这一行就够了。5.3 成绩导入的编码问题现象Excel 导入时报UnicodeDecodeError。原因老师用 WPS 保存的 xlsx 有时编码不是标准 UTF-8。解决pandas 读 Excel 一般不会有编码问题但如果老师存成了 CSV就要指定编码。我一般让老师统一用 xlsx 模板避免 CSV 的编码坑。如果非要支持 CSV加encodingutf-8-sig参数能兼容带 BOM 的文件。5.4 水平分析的数据量问题现象学生多了之后水平分析页面加载超过 10 秒。原因每次请求都实时计算排名和百分位数据量大了之后 SQL 全表扫描。解决加缓存或预计算。我一般用 Redis 缓存分析结果key 是analysis:user:{user_id}过期时间设 1 小时。成绩更新时主动删缓存。如果不想引入 Redis可以在成绩表加一个rank字段导入时算好存进去查询时直接读。5.5 权限校验的遗漏现象学生通过直接调接口修改了别人的成绩。原因接口只校验了登录状态没校验角色和数据归属。解决每个写接口都要校验「当前用户是否有权操作这条数据」。比如修改成绩要检查当前用户是老师或者成绩属于当前学生且竞赛未结束。我一般写一个check_ownership的装饰器统一处理。6. 部署与进阶让系统真正跑起来部署这块我推荐 Docker Compose 一把梭别在服务器上手动装 MySQL 和 Python 环境后期迁移会哭。下面是我常用的 compose 配置。version: 3.8 services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PWD} MYSQL_DATABASE: contest_db volumes: - db_data:/var/lib/mysql command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci redis: image: redis:7-alpine volumes: - redis_data:/data app: build: . ports: - 8000:8000 environment: DATABASE_URL: mysqlpymysql://root:${DB_ROOT_PWD}db:3306/contest_db REDIS_URL: redis://redis:6379/0 depends_on: - db - redis volumes: db_data: redis_data:参数说明--character-set-serverutf8mb4必须加否则中文和 emoji 会乱码。depends_on只保证启动顺序不保证 db 就绪应用启动时要加重试逻辑。DB_ROOT_PWD从环境变量读别写死在文件里。进阶用法上我建议加一个「训练建议」模块。基于水平分析的结果给学生推荐该补哪些题。比如算法能力低但编程能力高就推荐刷动态规划的题团队协作低就建议多参加组队赛。实现上可以用简单的规则引擎不需要上机器学习。def recommend_training(user_id: int): radar get_ability_radar(user_id) suggestions [] if radar[算法能力] 60: suggestions.append(建议刷 LeetCode 动态规划专题每天 2 题) if radar[团队协作] 60: suggestions.append(建议参加一次组队赛练习分工与沟通) if radar[工程实践] 60: suggestions.append(建议完成一个完整的小项目并部署上线) return suggestions验证方法很简单找 5 个学生跑一遍看推荐结果是否符合直觉。如果推荐明显不合理调阈值或权重。我一般每学期根据实际数据调一次权重让分析结果更贴近真实水平。最后说个习惯每次上线新功能前先用测试账号把报名、组队、录成绩、看分析全流程走一遍别只测自己写的那个接口。我吃过亏报名接口没问题但组队时发现报名数据没同步导致邀请失败。全流程走一遍能省很多事后救火的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑