运动会智能编排系统 - 完整详细需求规格说明书
文档版本:V5.0
更新日期:2026-01-20
状态:待开发
目录
- 项目概述
- 技术架构
- 用户角色与多端设计
- 功能需求详细
- 4.1 系统配置模块
- 4.1.1 基础规则配置
- 4.1.2 号码簿规则配置
- 4.1.3 编排规则配置
- 4.1.4 Excel 列别名配置
- 4.1.5 积分规则配置
- 4.2 班级管理模块
- 4.3 运动员管理模块
- 4.4 项目管理模块(可自定义)
- 4.5 报名管理模块
- 4.6 编排算法模块
- 4.7 成绩管理模块
- 4.8 排名与积分模块
- 4.9 统计报表模块
- 4.10 Excel 导入导出模块
- 4.1 系统配置模块
- 多端功能详细
- 非功能需求
- 数据模型详细
- [API 接口详细](#API 接口详细)
- 前端界面详细
- 私有部署方案
- 附录
1. 项目概述
1.1 项目背景
学校运动会组织过程中,涉及多个角色(体育老师、班主任、学生),存在以下痛点:
- 体育老师:需要统一管理所有项目、编排、成绩统计
- 班主任:需要为本班学生报名、查看本班成绩
- 学生:希望查看自己的比赛安排和成绩
- 数据分散,缺乏统一平台
- 编排工作依赖人工,容易出现同班运动员在同一跑道、跨年级混排等问题
1.2 项目目标
开发一套完整的运动会智能编排系统,实现:
- 多角色端:体育老师端(管理)、班主任端(报名)、学生端(查看)
- 私有部署:学校内网部署,数据安全可控
- 自定义项目:项目管理端可灵活配置比赛项目
- 自动化编排:满足禁止跨年级、同班尽量不同道等约束
- 全流程管理:报名→编排→成绩→排名→导出
1.3 项目范围
包含范围
- 基础数据管理(班级、运动员、项目)
- 报名管理(班主任报名、体育老师审核)
- 智能编排(道次分配、分组)
- 成绩录入与管理
- 排名与积分统计
- Excel 导入导出(支持别名映射)
- 多端界面(体育老师端、班主任端、学生端)
- 报表生成与导出
不包含范围
- 电子计时硬件对接(预留接口)
- 移动端 APP(提供响应式 Web)
- 在线直播/实时成绩推送(后续版本)
2. 技术架构
2.1 整体架构图(多端 + 私有部署)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 前端层 │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 体育老师端 │ │ 班主任端 │ │ 学生端(可选) │ │
│ │ Vue 3 + Element │ │ Vue 3 + Element │ │ Vue 3 + Vant │ │
│ │ 管理后台风格 │ │ 管理后台风格 │ │ 移动端风格 │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ HTTPS/HTTP(内网)
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Nginx(反向代理 + 静态资源) │
└─────────────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────────────────┐
│ FastAPI 网关 │ │ Django + DRF │
│ ┌─────────────────────────┐ │ │ ┌─────────────────────────────────────┐ │
│ │ 编排算法 API │ │ │ │ 用户认证与权限(多角色) │ │
│ │ - 道次编排 │ │ │ │ - JWT Token │ │
│ │ - 成绩处理 │ │ │ │ - RBAC(体育老师/班主任/学生) │ │
│ │ - 排名计算 │ │ │ ├─────────────────────────────────────┤ │
│ │ - 实时预览 │ │ │ │ 数据管理 API │ │
│ └─────────────────────────┘ │ │ │ - 运动员 CRUD │ │
│ │ │ │ - 项目 CRUD(可自定义) │ │
│ │ │ │ - 报名管理 │ │
│ │ │ │ - 成绩管理 │ │
│ │ │ ├─────────────────────────────────────┤ │
│ │ │ │ Django Admin(内网管理) │ │
│ │ │ └─────────────────────────────────────┘ │
└───────────────────────────────┘ └───────────────────────────────────────────┘
│ │
└───────────┬───────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 数据层(私有部署) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL / MySQL(推荐生产环境) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 班级表 │ │运动员表 │ │ 项目表 │ │ 报名表 │ │编排结果表│ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 成绩表 │ │ 用户表 │ │角色权限表│ │ 配置表 │ │ 日志表 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 文件存储 │ │
│ │ /data/uploads/ - 导入文件 │ │
│ │ /data/exports/ - 导出文件 │ │
│ │ /data/logs/ - 系统日志 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
2.2 技术栈详细
后端
| 组件 | 技术 | 版本 | 说明 |
|---|---|---|---|
| 算法网关 | FastAPI | 0.115.0+ | 异步高性能,自动生成API文档 |
| 管理后台 | Django | 4.2.0+ | 完善的ORM和Admin |
| API框架 | Django REST Framework | 3.15.0+ | RESTful API 开发 |
| 数据库 | SQLite | 3.0+ | 开发/小规模默认 |
| 数据库 | PostgreSQL | 15+ | 生产环境推荐 |
| 数据库 | MySQL | 8.0+ | 备选方案 |
| 数据验证 | Pydantic | 2.5.0+ | FastAPI数据验证 |
| 算法 | OR-Tools | 9.10+ | 约束求解(可选) |
| Excel处理 | pandas | 2.0.0+ | 数据读写、清洗 |
| Excel处理 | numpy | 1.24.0+ | 数值计算 |
| Excel写入 | openpyxl | 3.1.0+ | Excel 文件写入 |
| 异步任务 | Celery | 5.3.0+ | 耗时任务处理(可选) |
| 缓存 | Redis | 7.0+ | 会话缓存(可选) |
前端
| 组件 | 技术 | 版本 | 说明 |
|---|---|---|---|
| 框架 | Vue 3 | 3.4.0+ | 组合式API |
| 构建工具 | Vite | 5.0.0+ | 快速冷启动,懒加载 |
| UI组件库(管理端) | Element Plus | 2.6.0+ | 美观后台组件 |
| UI组件库(学生端) | Vant | 4.8.0+ | 移动端组件 |
| 路由 | Vue Router | 4.2.0+ | 支持路由懒加载 |
| 状态管理 | Pinia | 2.1.0+ | 轻量状态管理 |
| HTTP客户端 | Axios | 1.6.0+ | 请求拦截、响应处理 |
| 图表 | ECharts | 5.5.0+ | 统计图表展示 |
| 日期处理 | dayjs | 1.11.0+ | 轻量日期库 |
部署
| 组件 | 技术 | 说明 |
|---|---|---|
| 容器化 | Docker | 应用容器化 |
| 容器编排 | Docker Compose | 多服务编排 |
| Web服务器 | Nginx | 静态资源、反向代理 |
| 进程管理 | Gunicorn | Django WSGI服务器 |
| 异步服务器 | Uvicorn | FastAPI ASGI服务器 |
2.3 私有部署架构特点
| 特性 | 说明 |
|---|---|
| 内网部署 | 部署在学校内部服务器,数据不外传 |
| 单机部署 | 一台服务器即可运行所有服务 |
| Docker 容器化 | 一键部署,环境隔离 |
| 数据备份 | 支持自动备份到本地存储 |
| 离线运行 | 无需互联网连接 |
| 支持 HTTPS | 可配置 SSL 证书 |
3. 用户角色与多端设计
3.1 角色定义
| 角色 | 标识 | 说明 | 使用端 | 典型用户 |
|---|---|---|---|---|
| 超级管理员 | super_admin | 系统全部权限 | 体育老师端 | 信息技术老师 |
| 体育老师 | teacher | 编排、成绩、报表 | 体育老师端 | 体育教研组 |
| 班主任 | class_teacher | 本班报名、查看成绩 | 班主任端 | 各班班主任 |
| 学生 | student | 查看个人赛程和成绩 | 学生端(可选) | 参赛学生 |
| 查看者 | viewer | 仅查看(预留) | Web | 其他教职工 |
3.2 多端功能对比
| 功能模块 | 体育老师端 | 班主任端 | 学生端 |
|---|---|---|---|
| 系统配置 | |||
| 基础规则配置 | ✅ | ❌ | ❌ |
| 项目管理(自定义) | ✅ | ❌(只读) | ❌(只读) |
| 号码簿规则配置 | ✅ | ❌ | ❌ |
| 编排规则配置 | ✅ | ❌ | ❌ |
| Excel别名配置 | ✅ | ❌ | ❌ |
| 积分规则配置 | ✅ | ❌ | ❌ |
| 用户管理 | ✅ | ❌ | ❌ |
| 班级/运动员管理 | |||
| 班级管理 | ✅ | ❌ | ❌ |
| 运动员管理(全校) | ✅ | ❌ | ❌ |
| 运动员管理(本班) | ✅ | ✅ | ❌ |
| 报名管理 | |||
| 查看可报项目 | ✅ | ✅ | ✅ |
| 本班学生报名 | ✅ | ✅ | ❌ |
| 个人报名 | ✅ | ❌ | ❌(可选) |
| 报名审核 | ✅ | ❌ | ❌ |
| 导出班级报名表 | ✅ | ✅ | ❌ |
| 导出各项目报名表 | ✅ | ❌ | ❌ |
| 编排管理 | |||
| 执行编排 | ✅ | ❌ | ❌ |
| 查看道次表 | ✅ | ✅ | ✅ |
| 手动调整 | ✅ | ❌ | ❌ |
| 导出道次表 | ✅ | ❌ | ❌ |
| 成绩管理 | |||
| 成绩录入 | ✅ | ❌ | ❌ |
| 查看本班成绩 | ✅ | ✅ | ❌ |
| 查看个人成绩 | ✅ | ✅ | ✅ |
| 排名积分 | |||
| 查看排名榜 | ✅ | ✅ | ✅ |
| 查看团体总分 | ✅ | ✅ | ✅ |
| 统计报表 | |||
| 报名统计 | ✅ | ✅(本班) | ❌ |
| 成绩统计 | ✅ | ✅(本班) | ✅(个人) |
| 导出报表 | ✅ | ✅(本班) | ❌ |
3.3 登录界面设计
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ 🏃 运动会智能编排系统 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 体育老师端 │ │ 班主任端 │ │ 学生端 │ │ │
│ │ │ (管理端) │ │ (报名端) │ │ (查看端) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ 账号:[________________] │ │
│ │ 密码:[________________] │ │
│ │ │ │
│ │ [登录] [忘记密码] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 私有部署版本 v1.0 | 学校内部系统 │
└─────────────────────────────────────────────────────────────────────────────┘
4. 功能需求详细
4.1 系统配置模块
4.1.1 基础规则配置
需求描述
配置运动会的基础参数,这些参数将影响整个系统的运行逻辑。
配置项详细
| 配置项 | 类型 | 默认值 | 说明 | 取值范围 |
|---|---|---|---|---|
| 全局跑道数 | 整数 | 8 | 项目默认跑道数 | 1-12 |
| 每班每项目最大人数 | 整数 | 3 | 报名人数上限 | 0-10 |
| 每名运动员最大报项数 | 整数 | 3 | 个人报项上限 | 1-8 |
| 短跑距离阈值 | 整数 | 400 | 小于此值为短跑 | 100-800 |
| 成绩小数位数 | 整数 | 2 | 成绩显示精度 | 0-3 |
| 报名截止时间 | 日期时间 | - | 班主任报名截止 | - |
| 是否允许学生自行报名 | 布尔 | 否 | 学生端报名开关 | 是/否 |
| 是否开放学生端 | 布尔 | 否 | 学生端功能开关 | 是/否 |
| 短跑成绩格式 | 字符串 | ss.xx | 秒.毫秒 | - |
| 长跑成绩格式 | 字符串 | mm:ss.xx | 分:秒.毫秒 | - |
| 是否自动编排 | 布尔 | 否 | 报名完成后自动编排 | 是/否 |
| 是否允许手动调整编排 | 布尔 | 是 | 编排后允许手动调整 | 是/否 |
| 默认日期 | 日期 | 当前日期 | 运动会举办日期 | - |
| 运动会名称 | 字符串 | 第X届运动会 | 显示在报表上 | - |
配置界面原型
┌─────────────────────────────────────────────────────────────────────────────┐
│ 系统基础配置 [保存] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 基本信息 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ 运动会名称: [第35届秋季田径运动会________________] │ │
│ │ 举办日期: [2026-10-15 ] 至 [2026-10-17 ] │ │
│ │ 举办地点: [学校田径场______________________] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 全局配置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ 全局默认跑道数: [8] ▼ 道 │ │
│ │ 每班每项目最多人数:[3] ▼ 人 │ │
│ │ 每名运动员最多报项数:[3] ▼ 项 │ │
│ │ 短跑距离阈值: [400] ▼ 米(含以下为短跑) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 成绩格式配置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ 成绩小数位数: [2] ▼ 位 │ │
│ │ 短跑成绩示例: 12.34 秒 │ │
│ │ 长跑成绩示例: 2:35.67(分:秒.毫秒) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 报名与编排配置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ 报名截止时间: [2026-10-01 23:59:59] │ │
│ │ ☑ 是否允许学生自行报名(需开启学生端) │ │
│ │ ☑ 是否开放学生端 │ │
│ │ ☐ 报名完成后自动编排 │ │
│ │ ☑ 允许手动调整编排结果 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [恢复默认] [保存配置] │
└─────────────────────────────────────────────────────────────────────────────┘
4.1.2 号码簿规则配置
需求描述
支持自定义号码簿生成规则,系统自动为运动员生成唯一号码簿。号码簿是运动员的唯一标识,在整个运动会中不可重复。
规则变量定义
| 变量 | 说明 | 示例值 | 格式选项 |
|---|---|---|---|
{grade} |
年级编号 | 1, 2, 3 | 可映射(一年级→1) |
{grade_name} |
年级名称 | 一年级, 二年级 | 直接使用名称 |
{class} |
班级编号 | 1, 2, 3 | 可映射 |
{class_name} |
班级名称 | 1班, 2班 | 直接使用名称 |
{seq} |
序号 | 1, 2, 3 | 可格式化 |
{seq:02d} |
2位序号 | 01, 02, 03 | 补零 |
{seq:03d} |
3位序号 | 001, 002 | 补零 |
{seq:04d} |
4位序号 | 0001, 0002 | 补零 |
{gender} |
性别代码 | M, F | 映射 |
{gender_cn} |
性别中文 | 男, 女 | 中文 |
{year} |
年份后两位 | 24, 25, 26 | 当前年份 |
{school_code} |
学校代码 | 001 | 固定前缀 |
规则模板库
| 模板名称 | 规则公式 | 示例输出 | 适用场景 |
|---|---|---|---|
| 年级+班级+序号 | {grade}{class}{seq:02d} |
1101 | 最常用 |
| 班级+序号 | {class}{seq:02d} |
101 | 不分年级 |
| 年级+序号 | {grade}{seq:03d} |
1001 | 班级不区分 |
| 纯序号 | {seq:04d} |
0001 | 全校统一编号 |
| 年级+班级+性别+序号 | {grade}{class}{gender}{seq:02d} |
11M01 | 区分性别 |
| 年份+班级+序号 | {year}{class}{seq:02d} |
24101 | 按年份区分 |
| 学校码+年级+班级+序号 | {school_code}{grade}{class}{seq:02d} |
0011101 | 多学校联合 |
年级/班级映射配置
用户可以自定义年级和班级到数字的映射关系:
| 原始值 | 映射值 | 说明 |
|---|---|---|
| 一年级 | 1 | 映射为数字1 |
| 二年级 | 2 | 映射为数字2 |
| 三年级 | 3 | 映射为数字3 |
| 四年级 | 4 | 映射为数字4 |
| 五年级 | 5 | 映射为数字5 |
| 六年级 | 6 | 映射为数字6 |
| 初一 | 1 | 映射为数字1 |
| 初二 | 2 | 映射为数字2 |
| 初三 | 3 | 映射为数字3 |
| 高一 | 1 | 映射为数字1 |
| 高二 | 2 | 映射为数字2 |
| 高三 | 3 | 映射为数字3 |
班级映射:用户可自定义,如"1班"→"01","2班"→"02"
规则配置界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 号码簿规则配置 [保存] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 规则模板选择 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │年级+班级+序号│ │ 班级+序号 │ │ 年级+序号 │ │ 纯序号 │ │ │
│ │ │ (推荐) │ │ │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ● 自定义公式 │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ {grade}{class}{seq:02d} │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 可用变量: │ │
│ │ [grade] [grade_name] [class] [class_name] [seq] [gender] [year] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 预览示例 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 年级:一年级 → 1 班级:1班 → 1 序号:1 → 01 │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 生成号码簿:1101 │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 更多预览: │ │
│ │ 一年级1班 张三 → 1101 │ │
│ │ 一年级1班 李四 → 1102 │ │
│ │ 一年级2班 王五 → 1201 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 年级映射配置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ┌──────────────┬──────────┬─────────────────────────────┐ │ │
│ │ │ 年级名称 │ 映射值 │ 操作 │ │ │
│ │ ├──────────────┼──────────┼─────────────────────────────┤ │ │
│ │ │ 一年级 │ [1 ] │ 编辑 │ │ │
│ │ │ 二年级 │ [2 ] │ 编辑 │ │ │
│ │ │ 三年级 │ [3 ] │ 编辑 │ │ │
│ │ │ 四年级 │ [4 ] │ 编辑 │ │ │
│ │ │ 五年级 │ [5 ] │ 编辑 │ │ │
│ │ │ 六年级 │ [6 ] │ 编辑 │ │ │
│ │ │ 初一年级 │ [7 ] │ 编辑 │ │ │
│ │ │ 初二年级 │ [8 ] │ 编辑 │ │ │
│ │ │ 初三年级 │ [9 ] │ 编辑 │ │ │
│ │ │ 高一年级 │ [10 ] │ 编辑 │ │ │
│ │ │ 高二年级 │ [11 ] │ 编辑 │ │ │
│ │ │ 高三年级 │ [12 ] │ 编辑 │ │ │
│ │ └──────────────┴──────────┴─────────────────────────────┘ │ │
│ │ │ │
│ │ [+ 添加自定义年级] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 班级映射配置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 规则类型: │ │
│ │ ● 自动提取数字(1班 → 1,2班 → 2) │ │
│ │ ○ 自定义映射 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 高级选项 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ☑ 自动补零(序号不足位数时补零) │ │
│ │ ☑ 全局唯一性校验(号码簿不可重复) │ │
│ │ ☑ 生成后允许手动修改 │ │
│ │ ☑ 导入时自动生成缺失的号码簿 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [重置默认] [测试生成] [保存配置] │
└─────────────────────────────────────────────────────────────────────────────┘
号码簿生成算法
python
def generate_number(grade, class_name, seq, config):
"""
根据配置规则生成号码簿
Args:
grade: 年级名称,如"一年级"
class_name: 班级名称,如"1班"
seq: 序号,如1
config: 规则配置 {
"template": "{grade}{class}{seq:02d}",
"grade_mapping": {"一年级": 1, "二年级": 2, ...},
"class_mapping": {"1班": 1, "2班": 2, ...} # 可选
}
Returns:
号码簿字符串
"""
# 获取年级映射值
grade_value = config['grade_mapping'].get(grade, grade)
# 获取班级映射值
if config.get('class_mapping'):
class_value = config['class_mapping'].get(class_name, class_name)
else:
# 自动提取数字
import re
numbers = re.findall(r'\d+', class_name)
class_value = numbers[0] if numbers else class_name
# 格式化序号
seq_formatted = seq
# 应用模板
template = config['template']
result = template.format(
grade=grade_value,
class=class_value,
seq=seq_formatted
)
return result
4.1.3 编排规则配置
需求描述
配置编排算法的约束条件,包括硬约束(必须遵守)和软约束(可配置开关)。
硬约束(不可违反)
| 规则 | 说明 | 违反时处理 |
|---|---|---|
| 禁止跨年级 | 同一组/跑道的运动员必须同年级 | 编排失败,提示用户 |
| 性别分离 | 男女生项目完全分开编排 | 编排失败,提示用户 |
软约束(可配置开关)
| 规则 | 选项 | 默认值 | 优先级 | 说明 |
|---|---|---|---|---|
| 同班是否允许同跑道 | 禁止/允许 | 禁止 | 高 | 禁止:同班不得在同一跑道;允许:优先尝试不同道,无法满足时允许 |
| 同班是否尽量不同组 | 尽量/无限制 | 尽量 | 中 | 尽量:同一班级运动员分散到不同轮次/组别 |
| 同班是否尽量不同道次 | 尽量/无限制 | 尽量 | 中 | 尽量:同一班级运动员分配不同道次 |
| 同年级是否打散 | 是/否 | 否 | 低 | 同年级不同班尽量分散 |
| 成绩优秀者是否居中 | 是/否 | 否 | 低 | 按历史成绩安排中间道次 |
编排规则配置界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 编排规则配置 [保存] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 硬约束(不可违反,系统强制执行) │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ☑ 禁止跨年级编排 │ │
│ │ (同一组/跑道的运动员必须同年级) │ │
│ │ │ │
│ │ ☑ 性别分离 │ │
│ │ (男子项目和女子项目完全分开编排) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 软约束(可配置,系统优先尝试满足) │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 1. 同班跑道规则: │ │
│ │ │ │
│ │ ● 禁止同班同跑道(推荐) │ │
│ │ 说明:同一班级的运动员不得出现在同一跑道 │ │
│ │ │ │
│ │ ○ 允许同班同跑道 │ │
│ │ 说明:优先尝试不同跑道,无法满足时允许同跑道 │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 2. 同班组别规则: │ │
│ │ │ │
│ │ ● 尽量分散到不同组 │ │
│ │ 说明:同一班级运动员优先安排到不同轮次/组别 │ │
│ │ │ │
│ │ ○ 无限制 │ │
│ │ 说明:不主动分散同一班级运动员 │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 3. 同班道次规则: │ │
│ │ │ │
│ │ ● 尽量分配不同道次 │ │
│ │ 说明:同一班级运动员尽量不在同一道次 │ │
│ │ │ │
│ │ ○ 无限制 │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 4. 同年级打散: │ │
│ │ │ │
│ │ ○ 是 ● 否 │ │
│ │ 说明:是否将同年级不同班级的运动员打散 │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 5. 优秀运动员居中(需历史成绩): │ │
│ │ │ │
│ │ ○ 是 ● 否 │ │
│ │ 说明:成绩优秀的运动员安排在中间道次(3-6道) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 编排参数 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 最大尝试次数: [1000] 次 │ │
│ │ (当无法满足约束时,算法尝试的最大次数) │ │
│ │ │ │
│ │ 编排超时时间: [30] 秒 │ │
│ │ (编排算法最大运行时间) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [恢复默认] [测试编排] [保存配置] │
└─────────────────────────────────────────────────────────────────────────────┘
编排规则数据结构
python
# 编排规则配置数据结构
ARRANGE_RULE_CONFIG = {
# 硬约束
"hard_constraints": {
"ban_cross_grade": True, # 禁止跨年级
"gender_separate": True, # 性别分离
},
# 软约束
"soft_constraints": {
"ban_same_class_same_lane": True, # 禁止同班同跑道
"prefer_diff_heat": True, # 尽量不同组
"prefer_diff_lane": True, # 尽量不同道次
"scramble_across_classes": False, # 同年级打散
"center_best_athletes": False, # 优秀运动员居中
},
# 算法参数
"algorithm_params": {
"max_attempts": 1000, # 最大尝试次数
"timeout_seconds": 30, # 超时时间(秒)
}
}
4.1.4 Excel 列别名配置
需求描述
用户上传的 Excel 文件列名可能与系统标准字段不一致,支持用户配置列名映射,无需修改模板即可导入。
标准字段与默认别名
| 标准字段 | 必填 | 数据类型 | 默认别名(逗号分隔) |
|---|---|---|---|
| 姓名 | ✅ | 字符串 | 姓名, 名字, name, 运动员, 运动员名称 |
| 性别 | ✅ | 枚举 | 性别, sex, gender, 男女 |
| 年级 | ✅ | 字符串 | 年级, grade, 年段 |
| 班级 | ✅ | 字符串 | 班级, class, 班别, 班级名称, 班 |
| 号码簿 | ✅ | 字符串 | 号码簿, 号码布, 号码, number, 参赛号, 编号, 运动员编号 |
| 参赛项目 | ❌ | 字符串 | 参赛项目, 项目, event, 报名项目, 项目名称 |
| 成绩 | ❌ | 字符串 | 成绩, 时间, result, time, 比赛成绩, 用时 |
| 跑道 | ❌ | 整数 | 跑道, lane, 道次 |
| 组别 | ❌ | 整数 | 组别, heat, 组, 轮次 |
| 名次 | ❌ | 整数 | 名次, rank, 排名 |
| 积分 | ❌ | 整数 | 积分, score, point |
| 班级编号 | ❌ | 字符串 | 班级编号, class_code |
| 学号 | ❌ | 字符串 | 学号, student_id, 学籍号 |
| 联系电话 | ❌ | 字符串 | 联系电话, 电话, phone, mobile |
别名配置界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ Excel 列别名配置 [保存] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 列名映射配置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ┌────────────┬────────────────────────────────────┬────────┐ │ │
│ │ │ 标准字段 │ 别名(多个用英文逗号分隔) │ 必填 │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 姓名 * │ [姓名, 名字, name, 运动员名称 ] │ ✅ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 性别 * │ [性别, sex, gender, 男女 ] │ ✅ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 年级 * │ [年级, grade, 年段 ] │ ✅ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 班级 * │ [班级, class, 班别, 班级名称, 班] │ ✅ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 号码簿 * │ [号码簿, 号码布, 号码, number, 参赛号]│ ✅ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 参赛项目 │ [参赛项目, 项目, event, 报名项目] │ ❌ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 成绩 │ [成绩, 时间, result, time, 比赛成绩]│ ❌ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 跑道 │ [跑道, lane, 道次 ] │ ❌ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 组别 │ [组别, heat, 组, 轮次 ] │ ❌ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 名次 │ [名次, rank, 排名 ] │ ❌ │ │ │
│ │ ├────────────┼────────────────────────────────────┼────────┤ │ │
│ │ │ 积分 │ [积分, score, point ] │ ❌ │ │ │
│ │ └────────────┴────────────────────────────────────┴────────┘ │ │
│ │ │ │
│ │ [+ 添加自定义字段] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 高级选项 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ☑ 启用智能列名匹配(自动识别常见别名) │ │
│ │ ☑ 导入时显示列名映射预览 │ │
│ │ ☑ 保存用户自定义映射(不同用户独立) │ │
│ │ ☐ 区分大小写 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [重置默认] [测试导入] [保存配置] │
└─────────────────────────────────────────────────────────────────────────────┘
列名映射算法
python
class ColumnMapper:
"""Excel 列名映射器"""
def __init__(self, alias_config: Dict[str, list]):
self.alias_config = alias_config
self.reverse_index = self._build_reverse_index()
def _build_reverse_index(self) -> Dict[str, str]:
"""构建别名 → 标准字段 反向索引"""
reverse = {}
for standard_field, aliases in self.alias_config.items():
for alias in aliases:
# 标准化别名(小写,去空格)
normalized = alias.lower().strip()
reverse[normalized] = standard_field
return reverse
def detect_columns(self, excel_columns: list) -> Dict[str, str]:
"""
检测 Excel 列名对应关系
Args:
excel_columns: Excel 文件中的列名列表
Returns:
{标准字段: 实际列名} 的映射字典
"""
mapping = {}
for col in excel_columns:
normalized = col.lower().strip()
if normalized in self.reverse_index:
standard = self.reverse_index[normalized]
mapping[standard] = col
return mapping
def map_dataframe(self, df: pd.DataFrame, user_mapping: Optional[Dict] = None) -> pd.DataFrame:
"""
根据映射重命名 DataFrame 列
Args:
df: 原始 DataFrame
user_mapping: 用户手动确认的映射 {标准字段: 实际列名}
Returns:
重命名后的 DataFrame
"""
if user_mapping:
# 使用用户手动确认的映射
rename_dict = {actual: standard for standard, actual in user_mapping.items()}
df = df.rename(columns=rename_dict)
else:
# 自动识别
detected = self.detect_columns(df.columns.tolist())
rename_dict = {actual: standard for standard, actual in detected.items()}
df = df.rename(columns=rename_dict)
return df
def get_unmatched_columns(self, excel_columns: list) -> list:
"""获取未匹配的列名"""
unmatched = []
for col in excel_columns:
normalized = col.lower().strip()
if normalized not in self.reverse_index:
unmatched.append(col)
return unmatched
def get_missing_required_fields(self, excel_columns: list, required_fields: list) -> list:
"""获取缺失的必填字段"""
detected = self.detect_columns(excel_columns)
missing = [field for field in required_fields if field not in detected]
return missing
导入时列名识别流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 导入时列名识别流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤1:用户上传 Excel 文件 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤2:系统读取 Excel 列名 │ │
│ │ 实际列名:["姓名", "性别", "年级", "班级", "号码布", "项目"] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤3:智能匹配 │ │
│ │ │ │
│ │ "姓名" → 匹配到 "姓名" │ │
│ │ "性别" → 匹配到 "性别" │ │
│ │ "年级" → 匹配到 "年级" │ │
│ │ "班级" → 匹配到 "班级" │ │
│ │ "号码布" → 匹配到 "号码簿"(别名包含"号码布") │ │
│ │ "项目" → 匹配到 "参赛项目"(别名包含"项目") │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤4:展示匹配结果预览(用户确认) │ │
│ │ │ │
│ │ ┌────────────┬──────────────┬────────┐ │ │
│ │ │ 标准字段 │ 识别到的列名 │ 状态 │ │ │
│ │ ├────────────┼──────────────┼────────┤ │ │
│ │ │ 姓名 │ 姓名 │ ✅ 已匹配 │ │ │
│ │ │ 性别 │ 性别 │ ✅ 已匹配 │ │ │
│ │ │ 年级 │ 年级 │ ✅ 已匹配 │ │ │
│ │ │ 班级 │ 班级 │ ✅ 已匹配 │ │ │
│ │ │ 号码簿 │ 号码布 │ ✅ 已匹配 │ │ │
│ │ │ 参赛项目 │ 项目 │ ✅ 已匹配 │ │ │
│ │ └────────────┴──────────────┴────────┘ │ │
│ │ │ │
│ │ [确认导入] [修改映射] [取消] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.1.5 积分规则配置
需求描述
配置比赛项目的积分规则,用于计算运动员个人积分和班级团体总分。
默认积分规则
| 名次 | 积分 | 说明 |
|---|---|---|
| 第1名 | 9 | 冠军 |
| 第2名 | 7 | 亚军 |
| 第3名 | 6 | 季军 |
| 第4名 | 5 | 第四名 |
| 第5名 | 4 | 第五名 |
| 第6名 | 3 | 第六名 |
| 第7名 | 2 | 第七名 |
| 第8名 | 1 | 第八名 |
| 8名以后 | 0 | 无积分 |
积分规则配置界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 积分规则配置 [保存] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 名次积分对照表 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ┌──────────────┬──────────┬─────────────────────────────┐ │ │
│ │ │ 名次 │ 积分 │ 操作 │ │ │
│ │ ├──────────────┼──────────┼─────────────────────────────┤ │ │
│ │ │ 第1名 │ [9 ] │ [删除] │ │ │
│ │ │ 第2名 │ [7 ] │ [删除] │ │ │
│ │ │ 第3名 │ [6 ] │ [删除] │ │ │
│ │ │ 第4名 │ [5 ] │ [删除] │ │ │
│ │ │ 第5名 │ [4 ] │ [删除] │ │ │
│ │ │ 第6名 │ [3 ] │ [删除] │ │ │
│ │ │ 第7名 │ [2 ] │ [删除] │ │ │
│ │ │ 第8名 │ [1 ] │ [删除] │ │ │
│ │ └──────────────┴──────────┴─────────────────────────────┘ │ │
│ │ │ │
│ │ [+ 添加名次积分] │ │
│ │ │ │
│ │ 💡 提示:名次支持范围输入,如 "1-8" 表示第1到第8名 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 特殊规则 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ☑ 成绩并列时名次相同,积分相同 │ │
│ │ (例如:两名并列第1名,都获得第1名积分) │ │
│ │ │ │
│ │ ☑ 成绩并列时后续名次顺延 │ │
│ │ (例如:1,1,3,4 - 两个第1名后下一个是第3名) │ │
│ │ │ │
│ │ ☐ 成绩并列时后续名次不顺延 │ │
│ │ (例如:1,1,2,3 - 两个第1名后下一个是第2名) │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 破纪录加分: │ │
│ │ │ │
│ │ ☑ 启用破纪录加分 │ │
│ │ 破纪录加分分值:[10] 分 │ │
│ │ 破纪录标准: ○ 打破校纪录 ○ 打破年级纪录 ● 打破运动会纪录 │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 参与分: │ │
│ │ │ │
│ │ ☐ 启用参与分 │ │
│ │ 参与分分值:[1] 分 │ │
│ │ 说明:所有参赛且未获奖的运动员获得基础分 │ │
│ │ │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 接力项目规则: │ │
│ │ │ │
│ │ 接力项目积分倍数:[2] 倍 │ │
│ │ 说明:接力项目的积分按此倍数计算 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 团体总分规则 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 团体总分计算方式: │ │
│ │ ● 按班级汇总 │ │
│ │ ○ 按年级汇总 │ │
│ │ │ │
│ │ 团体总分排序方式: │ │
│ │ ● 按总分降序(分高者在前) │ │
│ │ ○ 按金牌数优先 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [恢复默认] [保存配置] │
└─────────────────────────────────────────────────────────────────────────────┘
积分计算算法
python
def calculate_scores(results_df, scoring_rules, event_type='individual'):
"""
计算运动员积分
Args:
results_df: 包含 athlete_id, rank, event_id 的 DataFrame
scoring_rules: 积分规则配置
event_type: 项目类型(individual/relay)
Returns:
添加了积分的 DataFrame
"""
# 获取名次积分映射
rank_scores = scoring_rules.get('rank_scores', {})
# 默认积分映射
default_scores = {1: 9, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1}
rank_scores = {**default_scores, **rank_scores}
# 计算积分
scores = []
for _, row in results_df.iterrows():
rank = row['rank']
score = rank_scores.get(rank, 0)
# 接力项目倍数
if event_type == 'relay':
multiplier = scoring_rules.get('relay_multiplier', 2)
score = score * multiplier
# 参与分
if scoring_rules.get('participation_score_enabled', False) and score == 0:
score = scoring_rules.get('participation_score', 1)
scores.append(score)
results_df['score'] = scores
return results_df
def calculate_team_scores(individual_scores_df, scoring_rules):
"""
计算团体总分
Args:
individual_scores_df: 个人积分数据
scoring_rules: 积分规则配置
Returns:
团体总分 DataFrame
"""
# 按班级汇总
team_scores = individual_scores_df.groupby(['grade', 'class'])['score'].sum().reset_index()
team_scores = team_scores.sort_values('score', ascending=False)
team_scores['rank'] = range(1, len(team_scores) + 1)
return team_scores
4.2 班级管理模块
4.2.1 需求描述
班级是运动会组织的基本单位,班主任负责本班的报名工作。班级管理模块需要支持年级分组、班级排序、班主任账号关联等功能。
4.2.2 班级信息字段
| 字段名 | 类型 | 必填 | 唯一 | 说明 | 示例 |
|---|---|---|---|---|---|
| 班级名称 | 字符串 | ✅ | ✅ | 班级显示名称 | 高一1班 |
| 年级 | 字符串 | ✅ | ❌ | 所属年级 | 高一年级 |
| 年级排序 | 整数 | ❌ | ❌ | 年级显示顺序 | 10 |
| 班级排序 | 整数 | ❌ | ❌ | 班级内显示顺序 | 1 |
| 班级编号 | 字符串 | ❌ | ✅ | 用于号码簿生成 | 101 |
| 班主任姓名 | 字符串 | ✅ | ❌ | 班主任姓名 | 张三 |
| 班主任账号 | 外键 | ✅ | ✅ | 关联Django用户 | zhangsan |
| 联系电话 | 字符串 | ❌ | ❌ | 班主任电话 | 138****0000 |
| 学生人数 | 整数 | ❌ | ❌ | 班级总人数 | 45 |
| 是否参赛 | 布尔 | ❌ | ❌ | 是否参与本届运动会 | 是 |
| 备注 | 文本 | ❌ | ❌ | 其他说明 | - |
4.2.3 班级管理功能列表
| 功能 | 说明 | 权限 | 技术实现 |
|---|---|---|---|
| 班级列表 | 按年级分组展示,支持搜索和筛选 | 体育老师/班主任(仅本班) | Vue3 + Element Plus Table |
| 新增班级 | 单个添加班级 | 体育老师 | Django REST API |
| 批量导入 | Excel批量导入班级 | 体育老师 | pandas读取Excel |
| 编辑班级 | 修改班级信息 | 体育老师 | PUT请求 |
| 删除班级 | 删除班级(有运动员时禁止) | 体育老师 | DELETE请求 |
| 排序调整 | 拖拽调整班级显示顺序 | 体育老师 | 拖拽组件 + 批量更新 |
| 导出班级 | 导出班级列表为Excel | 体育老师 | pandas导出 |
| 班主任账号管理 | 创建/重置班主任登录账号 | 体育老师 | Django用户管理 |
4.2.4 班级列表界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 班级管理 [+ 新增] [批量导入] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 搜索:[________________] [搜索] [导出] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ▼ 高一年级(6个班) [拖拽排序] │
│ ┌─────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 排序 │ 班级名称 │ 班级编号 │ 班主任 │ 班主任账号│ 运动员数 │ 操作 │ │
│ ├─────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 1 │ 高一1班 │ 101 │ 张三 │ zhangsan │ 12 │ 编辑 删除 │ │
│ │ 2 │ 高一2班 │ 102 │ 李四 │ lisi │ 10 │ 编辑 删除 │ │
│ │ 3 │ 高一3班 │ 103 │ 王五 │ wangwu │ 11 │ 编辑 删除 │ │
│ │ 4 │ 高一4班 │ 104 │ 赵六 │ zhaoliu │ 9 │ 编辑 删除 │ │
│ │ 5 │ 高一5班 │ 105 │ 钱七 │ [创建] │ 0 │ 编辑 删除 │ │
│ │ 6 │ 高一6班 │ 106 │ 孙八 │ [创建] │ 0 │ 编辑 删除 │ │
│ └─────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ ▼ 高二年级(5个班) │
│ ┌─────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 1 │ 高二1班 │ 201 │ 周九 │ zhoujiu │ 15 │ 编辑 删除 │ │
│ │ 2 │ 高二2班 │ 202 │ 吴十 │ wushi │ 13 │ 编辑 删除 │ │
│ │ ... │ │
│ └─────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ 共 11 条记录 │
└─────────────────────────────────────────────────────────────────────────────┘
4.2.5 新增/编辑班级表单
┌─────────────────────────────────────────────────────────────────────────────┐
│ 新增班级 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 班级基本信息 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 班级名称: [高一7班________________] * │ │
│ │ │ │
│ │ 所属年级: [高一年级 ▼] * │ │
│ │ │ │
│ │ 班级编号: [107________________] │ │
│ │ 用于号码簿生成,建议使用数字 │ │
│ │ │ │
│ │ 年级排序: [10________________] │ │
│ │ 数字越小越靠前 │ │
│ │ │ │
│ │ 班级排序: [7_________________] │ │
│ │ 同年级内数字越小越靠前 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 班主任信息 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 班主任姓名: [赵老师________________] * │ │
│ │ │ │
│ │ 联系电话: [13812345678____________] │ │
│ │ │ │
│ │ 班主任账号: [zhaolaoshi_____________] │ │
│ │ ☑ 自动创建登录账号,密码默认:123456 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 其他设置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ☑ 是否参赛 │ │
│ │ │ │
│ │ 备注: [________________________] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [取消] [保存] │
└─────────────────────────────────────────────────────────────────────────────┘
4.2.6 班主任账号管理
┌─────────────────────────────────────────────────────────────────────────────┐
│ 班主任账号管理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 序号 │ 班级名称 │ 班主任 │ 登录账号 │ 账号状态 │ 操作 │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 1 │ 高一1班 │ 张三 │ zhangsan │ ✅ 已激活 │ 重置密码 │ │
│ │ 2 │ 高一2班 │ 李四 │ lisi │ ✅ 已激活 │ 重置密码 │ │
│ │ 3 │ 高一3班 │ 王五 │ wangwu │ ⚠ 未登录 │ 重置密码 │ │
│ │ 4 │ 高一4班 │ 赵六 │ zhaoliu │ 🔒 已锁定 │ 解锁账号 │ │
│ └────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ [批量创建账号] [导出账号信息] [发送通知短信] │
│ │
│ 💡 提示:班主任可使用账号登录班主任端进行本班报名 │
└─────────────────────────────────────────────────────────────────────────────┘
4.3 运动员管理模块
4.3.1 需求描述
运动员是运动会的参赛主体,每个运动员需要分配唯一的号码簿。体育老师可以管理全校运动员,班主任只能管理本班运动员。
4.3.2 运动员信息字段
| 字段名 | 类型 | 必填 | 唯一 | 说明 | 示例 |
|---|---|---|---|---|---|
| 姓名 | 字符串 | ✅ | ❌ | 运动员姓名 | 张三 |
| 性别 | 枚举 | ✅ | ❌ | 男/女 | 男 |
| 年级 | 字符串 | ✅ | ❌ | 所属年级 | 高一年级 |
| 班级 | 外键 | ✅ | ❌ | 所属班级ID | 1 |
| 班级名称 | 字符串 | ✅ | ❌ | 班级显示名 | 高一1班 |
| 号码簿 | 字符串 | ✅ | ✅ | 参赛号码 | 1101 |
| 学号 | 字符串 | ❌ | ✅ | 学籍号(可选) | 20240001 |
| 身份证号 | 字符串 | ❌ | ✅ | 身份标识 | 110101200001011234 |
| 出生日期 | 日期 | ❌ | ❌ | 用于年龄分组 | 2010-01-01 |
| 联系电话 | 字符串 | ❌ | ❌ | 家长电话 | 138****0000 |
| 紧急联系人 | 字符串 | ❌ | ❌ | 紧急情况联系人 | 李四 |
| 紧急联系电话 | 字符串 | ❌ | ❌ | 紧急联系电话 | 139****0000 |
| 健康状况 | 文本 | ❌ | ❌ | 特殊病史/过敏等 | - |
| 照片 | 图片URL | ❌ | ❌ | 运动员照片 | /uploads/photos/1101.jpg |
| 状态 | 枚举 | ❌ | ❌ | 正常/受伤/退赛 | 正常 |
| 备注 | 文本 | ❌ | ❌ | 其他说明 | - |
4.3.3 运动员管理功能列表
| 功能 | 说明 | 体育老师 | 班主任 |
|---|---|---|---|
| 运动员列表 | 查看所有运动员 | ✅ | ✅(仅本班) |
| 新增运动员 | 单个添加 | ✅ | ✅ |
| 批量导入 | Excel批量导入 | ✅ | ❌ |
| 批量导出 | 导出运动员列表 | ✅ | ✅(仅本班) |
| 编辑运动员 | 修改信息 | ✅ | ✅ |
| 删除运动员 | 删除记录 | ✅ | ✅ |
| 号码簿生成 | 批量生成号码簿 | ✅ | ❌ |
| 模板下载 | 下载导入模板 | ✅ | ✅ |
4.3.4 运动员列表界面(体育老师端)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 运动员管理 [+ 新增] [导入] [导出] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 筛选: │
│ 年级:[全部年级 ▼] 班级:[全部班级 ▼] 性别:[全部 ▼] 状态:[全部 ▼] │
│ 搜索:[________________] [搜索] [重置] │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌───┬────────┬────────┬────┬──────────┬──────────┬──────────────┬──────┐ │
│ │ ☐ │ 号码簿 │ 姓名 │ 性别 │ 年级 │ 班级 │ 参赛项目 │ 操作 │ │
│ ├───┼────────┼────────┼────┼──────────┼──────────┼──────────────┼──────┤ │
│ │ ☐ │ 1101 │ 张三 │ 男 │ 高一年级 │ 高一1班 │ 100米,800米 │ 编辑 │ │
│ │ │ │ │ │ │ │ │ 删除 │ │
│ ├───┼────────┼────────┼────┼──────────┼──────────┼──────────────┼──────┤ │
│ │ ☐ │ 1102 │ 李四 │ 女 │ 高一年级 │ 高一1班 │ 50米 │ 编辑 │ │
│ │ │ │ │ │ │ │ │ 删除 │ │
│ ├───┼────────┼────────┼────┼──────────┼──────────┼──────────────┼──────┤ │
│ │ ☐ │ 1103 │ 王五 │ 男 │ 高一年级 │ 高一1班 │ 100米 │ 编辑 │ │
│ │ │ │ │ │ │ │ │ 删除 │ │
│ ├───┼────────┼────────┼────┼──────────┼──────────┼──────────────┼──────┤ │
│ │ ☐ │ 1201 │ 赵六 │ 男 │ 高一年级 │ 高一2班 │ 100米,跳远 │ 编辑 │ │
│ │ │ │ │ │ │ │ │ 删除 │ │
│ └───┴────────┴────────┴────┴──────────┴──────────┴──────────────┴──────┘ │
│ │
│ 已选择 0 条记录 [批量删除] [批量生成号码簿] [批量修改班级] │
│ │
│ [<] 1 2 3 4 5 [>] 共 156 条记录,每页显示 20 条 │
└─────────────────────────────────────────────────────────────────────────────┘
4.3.5 运动员列表界面(班主任端)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 本班运动员 - 高一1班 [+ 新增] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 班级信息:班主任:张三 | 联系电话:138****0000 | 运动员数:12人 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 搜索:[________________] [搜索] [导出本班] │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌────┬────────┬────────┬────┬──────────────┬──────────────┬──────┐ │
│ │ 序号 │ 号码簿 │ 姓名 │ 性别 │ 参赛项目 │ 状态 │ 操作 │ │
│ ├────┼────────┼────────┼────┼──────────────┼──────────────┼──────┤ │
│ │ 1 │ 1101 │ 张三 │ 男 │ 100米,800米 │ 正常 │ 编辑 │ │
│ │ │ │ │ │ │ │ 删除 │ │
│ ├────┼────────┼────────┼────┼──────────────┼──────────────┼──────┤ │
│ │ 2 │ 1102 │ 李四 │ 女 │ 50米 │ 正常 │ 编辑 │ │
│ │ │ │ │ │ │ │ 删除 │ │
│ ├────┼────────┼────────┼────┼──────────────┼──────────────┼──────┤ │
│ │ 3 │ 1103 │ 王五 │ 男 │ 100米 │ 受伤 │ 编辑 │ │
│ │ │ │ │ │ │ │ 删除 │ │
│ └────┴────────┴────────┴────┴──────────────┴──────────────┴──────┘ │
│ │
│ ⚠ 提示:运动员报名请到【报名管理】模块 │
└─────────────────────────────────────────────────────────────────────────────┘
4.3.6 新增/编辑运动员表单
┌─────────────────────────────────────────────────────────────────────────────┐
│ 新增运动员 - 高一1班 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 基本信息 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 姓名: [张三________________] * │ │
│ │ │ │
│ │ 性别: (●) 男 ( ) 女 ( ) 混合 * │ │
│ │ │ │
│ │ 年级/班级: 高一年级 ▼ 高一1班 ▼ * │ │
│ │ │ │
│ │ 号码簿: [1101______________] * │ │
│ │ ☐ 自动生成(根据号码簿规则) │ │
│ │ │ │
│ │ 学号: [20240001__________] │ │
│ │ │ │
│ │ 身份证号: [__________________] │ │
│ │ │ │
│ │ 出生日期: [2010-01-15______] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 联系方式 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 联系电话: [13812345678________] │ │
│ │ 紧急联系人: [李四________________] │ │
│ │ 紧急电话: [13912345678________] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 其他信息 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 健康状况: [________________________] │ │
│ │ 如有特殊病史或过敏请注明 │ │
│ │ │ │
│ │ 状态: ● 正常 ○ 受伤 ○ 退赛 │ │
│ │ │ │
│ │ 备注: [________________________] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [取消] [保存] │
└─────────────────────────────────────────────────────────────────────────────┘
4.4 项目管理模块(可自定义)
4.4.1 需求描述
体育老师可以在管理端自定义任何比赛项目,包括项目名称、类别、性别限制、跑道数、计分规则等。支持预设模板和完全自定义。
4.4.2 项目信息字段
| 字段名 | 类型 | 必填 | 唯一 | 说明 | 示例 |
|---|---|---|---|---|---|
| 项目名称 | 字符串 | ✅ | ❌ | 比赛项目名称 | 100米 |
| 项目代码 | 字符串 | ✅ | ✅ | 唯一标识符 | M100 |
| 项目类别 | 枚举 | ✅ | ❌ | 径赛/田赛/趣味 | 径赛 |
| 距离/类型 | 字符串 | ❌ | ❌ | 具体距离或类型 | 100米 |
| 性别限制 | 枚举 | ✅ | ❌ | 男子/女子/混合 | 男子 |
| 默认跑道数 | 整数 | ❌ | ❌ | 径赛专用 | 8 |
| 是否需要分组 | 布尔 | ❌ | ❌ | 是否分组预赛 | 是 |
| 每组最大人数 | 整数 | ❌ | ❌ | 分组时每组最多人数 | 8 |
| 晋级人数 | 整数 | ❌ | ❌ | 预赛晋级决赛人数 | 8 |
| 计分规则类型 | 枚举 | ❌ | ❌ | 全局/自定义 | 自定义 |
| 计分规则 | JSON | ❌ | ❌ | 自定义积分映射 | {"1":9,"2":7} |
| 排序顺序 | 整数 | ❌ | ❌ | 显示顺序 | 10 |
| 是否启用 | 布尔 | ❌ | ❌ | 是否在本届使用 | 是 |
| 报名开始时间 | 日期时间 | ❌ | ❌ | 报名起止 | - |
| 报名结束时间 | 日期时间 | ❌ | ❌ | 报名起止 | - |
| 项目纪录 | 字符串 | ❌ | ❌ | 当前纪录 | 11.23秒 |
| 备注 | 文本 | ❌ | ❌ | 其他说明 | - |
4.4.3 项目类别预设
| 类别 | 预设项目 | 可自定义 |
|---|---|---|
| 短跑 | 50米、100米、200米、400米 | ✅ |
| 长跑 | 800米、1000米、1500米、3000米 | ✅ |
| 接力 | 4×100米、4×400米 | ✅ |
| 田赛 | 跳远、三级跳远、跳高、铅球、实心球、标枪、铁饼 | ✅ |
| 趣味 | 袋鼠跳、两人三足、迎面接力、拔河 | ✅ |
4.4.4 项目管理界面(体育老师端)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 项目管理 - 体育老师端 [+ 新增] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 搜索:[________________] [按类别:全部 ▼] [启用状态:全部 ▼] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────┬──────────┬──────────┬──────────┬──────┬──────┬──────┬──────────┐ │
│ │ 排序│ 项目名称 │ 项目代码 │ 类别 │ 性别 │ 跑道数│ 分组 │ 操作 │ │
│ ├────┼──────────┼──────────┼──────────┼──────┼──────┼──────┼──────────┤ │
│ │ 1 │ 50米 │ M50 │ 短跑 │ 男子 │ 8 │ 是 │ 编辑 删除 │ │
│ │ 2 │ 50米 │ W50 │ 短跑 │ 女子 │ 8 │ 是 │ 编辑 删除 │ │
│ │ 3 │ 100米 │ M100 │ 短跑 │ 男子 │ 8 │ 是 │ 编辑 删除 │ │
│ │ 4 │ 100米 │ W100 │ 短跑 │ 女子 │ 8 │ 是 │ 编辑 删除 │ │
│ │ 5 │ 200米 │ M200 │ 短跑 │ 男子 │ 8 │ 是 │ 编辑 删除 │ │
│ │ 6 │ 400米 │ M400 │ 短跑 │ 男子 │ 8 │ 是 │ 编辑 删除 │ │
│ │ 7 │ 800米 │ M800 │ 长跑 │ 男子 │ 8 │ 否 │ 编辑 删除 │ │
│ │ 8 │ 1000米 │ M1000 │ 长跑 │ 男子 │ 8 │ 否 │ 编辑 删除 │ │
│ │ 9 │ 1500米 │ M1500 │ 长跑 │ 男子 │ 8 │ 否 │ 编辑 删除 │ │
│ │ 10 │ 4×100米 │ R4100 │ 接力 │ 混合 │ 8 │ 是 │ 编辑 删除 │ │
│ │ 11 │ 跳远 │ LJ │ 田赛 │ 男子 │ - │ 是 │ 编辑 删除 │ │
│ │ 12 │ 跳高 │ HJ │ 田赛 │ 女子 │ - │ 是 │ 编辑 删除 │ │
│ │ 13 │ 铅球 │ SP │ 田赛 │ 男子 │ - │ 是 │ 编辑 删除 │ │
│ └────┴──────────┴──────────┴──────────┴──────┴──────┴──────┴──────────┘ │
│ │
│ [+ 新增自定义项目] [从模板导入] [批量启用] [批量禁用] [导出项目] │
└─────────────────────────────────────────────────────────────────────────────┘
4.4.5 新增/编辑项目表单
┌─────────────────────────────────────────────────────────────────────────────┐
│ 新增项目 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 基本信息 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 项目名称: [100米________________] * │ │
│ │ │ │
│ │ 项目代码: [M100________________] * │ │
│ │ 唯一标识,建议使用字母+数字 │ │
│ │ │ │
│ │ 项目类别: (●) 径赛 ( ) 田赛 ( ) 趣味 │ │
│ │ │ │
│ │ 距离/类型: [100米________________] │ │
│ │ 如:100米、跳远、4×100米接力 │ │
│ │ │ │
│ │ 性别限制: (●) 男子 ( ) 女子 ( ) 混合 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 径赛专用设置(田赛/趣味项目可跳过) │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 距离类型: (●) 短跑 ( ) 长跑 │ │
│ │ │ │
│ │ 默认跑道数: [8] ▼ 道 │ │
│ │ │ │
│ │ 是否需要分组: (●) 是 ( ) 否 │ │
│ │ │ │
│ │ 每组最大人数: [8] 人 │ │
│ │ │ │
│ │ 晋级人数: [8] 人 │ │
│ │ (预赛→决赛,0表示不设决赛) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 计分规则 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ ○ 使用全局计分规则 │ │
│ │ ● 自定义计分规则 │ │
│ │ │ │
│ │ ┌──────────────┬──────────┬─────────────────────────┐ │ │
│ │ │ 名次范围 │ 积分 │ 操作 │ │ │
│ │ ├──────────────┼──────────┼─────────────────────────┤ │ │
│ │ │ 第1名 │ [9 ] │ [删除] │ │ │
│ │ │ 第2名 │ [7 ] │ [删除] │ │ │
│ │ │ 第3名 │ [6 ] │ [删除] │ │ │
│ │ │ 第4-8名 │ [5-1] │ [编辑] │ │ │
│ │ └──────────────┴──────────┴─────────────────────────┘ │ │
│ │ │ │
│ │ [+ 添加名次范围] │ │
│ │ │ │
│ │ 💡 接力项目积分倍数:[2] 倍 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 其他设置 │ │
│ │ ─────────────────────────────────────────────────────────────────── │ │
│ │ │ │
│ │ 排序顺序: [14________________] │ │
│ │ 数字越小显示越靠前 │ │
│ │ │ │
│ │ 是否启用: ● 是 ○ 否 │ │
│ │ │ │
│ │ 报名开始时间:[2026-03-01 00:00] │ │
│ │ 报名结束时间:[2026-03-15 23:59] │ │
│ │ │ │
│ │ 项目纪录: [11.23秒______________] │ │
│ │ │ │
│ │ 备注: [________________________] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [取消] [保存] │
└─────────────────────────────────────────────────────────────────────────────┘
4.4.6 项目模板库
┌─────────────────────────────────────────────────────────────────────────────┐
│ 从模板导入项目 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 选择模板类别: │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ 径赛-短跑 │ 径赛-长跑 │ 田赛 │ 接力 │ │
│ │ 模板 │ 模板 │ 模板 │ 模板 │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
│ │
│ 径赛-短跑模板(点击展开): │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ☑ 50米(男子) ☑ 50米(女子) ☑ 100米(男子) ☑ 100米(女子)│ │
│ │ ☑ 200米(男子) ☑ 200米(女子) ☑ 400米(男子) ☑ 400米(女子)│ │
│ │ ☐ 60米(男子) ☐ 60米(女子) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 径赛-长跑模板: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ☑ 800米(男子) ☑ 800米(女子) ☑ 1000米(男子) ☑ 1000米(女子)│ │
│ │ ☑ 1500米(男子) ☑ 1500米(女子) ☐ 3000米(男子) ☐ 3000米(女子)│ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 田赛模板: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ☑ 跳远(男子) ☑ 跳远(女子) ☑ 跳高(男子) ☑ 跳高(女子)│ │
│ │ ☑ 铅球(男子) ☑ 铅球(女子) ☐ 实心球(男子) ☐ 实心球(女子)│ │
│ │ ☐ 标枪(男子) ☐ 标枪(女子) ☐ 铁饼(男子) ☐ 铁饼(女子)│ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 接力模板: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ☑ 4×100米接力(混合) ☑ 4×400米接力(混合) │ │
│ │ ☐ 4×100米接力(男子) ☐ 4×100米接力(女子) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [全选] [清空] [导入选中项目] │
└─────────────────────────────────────────────────────────────────────────────┘
4.5 报名管理模块
4.5.1 需求描述
班主任为本班学生报名参赛项目,体育老师可以审核和汇总报名信息。支持批量报名、个人报名、导出报名表等功能。
4.5.2 报名信息字段
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 运动员 | 外键 | ✅ | 关联运动员 |
| 项目 | 外键 | ✅ | 关联比赛项目 |
| 报名状态 | 枚举 | ❌ | 已报名/已参赛/弃权 |
| 报名时间 | 日期时间 | ❌ | 自动记录 |
| 报名人 | 字符串 | ❌ | 谁报的名 |
| 备注 | 文本 | ❌ | 特殊说明 |
4.5.3 报名管理功能列表
| 功能 | 说明 | 体育老师 | 班主任 |
|---|---|---|---|
| 查看可报项目 | 查看当前开放报名的项目 | ✅ | ✅ |
| 批量报名 | 按班级/项目批量报名 | ✅ | ✅ |
| 个人报名 | 为单个运动员报名 | ✅ | ✅ |
| 报名列表 | 按项目/班级查看报名 | ✅ | ✅ |
| 导出班级报名表 | 导出本班报名表 | ✅ | ✅ |
| 导出各项目报名表 | 按项目导出报名表 | ✅ | ❌ |
| 取消报名 | 删除报名记录 | ✅ | ✅ |
| 报名审核 | 审核班主任提交的报名 | ✅ | ❌ |
| 人数统计 | 实时统计各项目报名人数 | ✅ | ✅ |
4.5.4 班主任端-报名界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 本班报名 - 高一1班 [帮助] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 班级信息:班主任:张三 | 运动员数:12人 | 已报名人次:18人次 │
│ 报名时间:2026-03-01 00:00 至 2026-03-15 23:59 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📋 报名规则提醒: │
│ • 每班每项目最多报名 3 人 │
│ • 每名运动员最多报名 3 个项目 │
│ • 请确认运动员资格和健康状况 │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ 快速报名:[选择运动员 ▼] → [选择项目 ▼] → [报名] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ▼ 运动员报名表 │
│ ┌────┬────────┬────────┬────┬────────────────────────────────┬──────┐ │
│ │ ☐ │ 号码簿 │ 姓名 │ 性别 │ 报名项目 │ 操作 │ │
│ ├────┼────────┼────────┼────┼────────────────────────────────┼──────┤ │
│ │ ☐ │ 1101 │ 张三 │ 男 │ ☑100米 ☑800米 ☐跳远 [+ 添加] │ 编辑 │ │
│ ├────┼────────┼────────┼────┼────────────────────────────────┼──────┤ │
│ │ ☐ │ 1102 │ 李四 │ 女 │ ☑50米 ☐100米 ☐跳高 [+ 添加] │ 编辑 │ │
│ ├────┼────────┼────────┼────┼────────────────────────────────┼──────┤ │
│ │ ☐ │ 1103 │ 王五 │ 男 │ ☑100米 ☐800米 ☐跳远 [+ 添加] │ 编辑 │ │
│ ├────┼────────┼────────┼────┼────────────────────────────────┼──────┤ │
│ │ ☐ │ 1104 │ 赵六 │ 男 │ ☐100米 ☑4×100米 ☐铅球 [+ 添加] │ 编辑 │ │
│ └────┴────────┴────────┴────┴────────────────────────────────┴──────┘ │
│ │
│ 已选择 0 条记录 [批量取消报名] [提交报名] [保存草稿] │
│ │
│ 💡 提示:请确认报名信息无误后点击"提交报名",提交后需体育老师审核 │
└─────────────────────────────────────────────────────────────────────────────┘
4.5.5 体育老师端-报名汇总界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 报名汇总 - 体育老师端 [导出] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 项目筛选:[全部项目 ▼] 班级筛选:[全部班级 ▼] 状态:[全部 ▼] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 报名统计看板 │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ 总报名人次 │ 已审核人次 │ 待审核人次 │ 超限班级数 │ │
│ │ 186 │ 156 │ 30 │ 3 │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
│ │
│ ▼ 各项目报名情况 │
│ ┌────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 序号 │ 项目名称 │ 性别 │ 报名人数 │ 班级数 │ 超限情况 │ 操作 │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 1 │ 100米 │ 男子 │ 32 │ 8 │ ⚠ 高一1班│ 查看详情 │ │
│ │ │ │ │ │ │ 超1人 │ 导出名单 │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 2 │ 100米 │ 女子 │ 28 │ 7 │ ✅ 正常 │ 查看详情 │ │
│ │ │ │ │ │ │ │ 导出名单 │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 3 │ 800米 │ 男子 │ 16 │ 6 │ ✅ 正常 │ 查看详情 │ │
│ │ │ │ │ │ │ │ 导出名单 │ │
│ └────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ ▼ 待审核报名列表(30条) │
│ ┌────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ ☐ │ 班级 │ 运动员 │ 项目 │ 报名时间 │ 状态 │ 操作 │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ ☐ │ 高一1班 │ 张三 │ 100米 │ 03-10 │ 待审核 │ [通过] │ │
│ │ │ │ │ │ │ │ [拒绝] │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ ☐ │ 高一1班 │ 李四 │ 50米 │ 03-10 │ 待审核 │ [通过] │ │
│ │ │ │ │ │ │ │ [拒绝] │ │
│ └────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ [批量通过] [批量拒绝] [导出报名总表] [导出各项目表] │
└─────────────────────────────────────────────────────────────────────────────┘
4.5.6 导出各项目报名表功能
┌─────────────────────────────────────────────────────────────────────────────┐
│ 导出报名表 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 导出选项: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 导出格式: │ │
│ │ (●) Excel (.xlsx) │ │
│ │ ( ) CSV (.csv) │ │
│ │ ( ) PDF (.pdf) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 导出内容: │ │
│ │ │ │
│ │ ☑ 项目报名表(按项目分Sheet) │ │
│ │ ☑ 班级报名表(按班级分Sheet) │ │
│ │ ☑ 报名统计汇总表 │ │
│ │ ☐ 运动员信息表 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 项目筛选(选择要导出的项目): │ │
│ │ │ │
│ │ ☑ 全选 ☐ 清空 │ │
│ │ ☑ 100米(男子) ☑ 100米(女子) ☑ 800米(男子) │ │
│ │ ☑ 800米(女子) ☑ 跳远(男子) ☑ 跳远(女子) │ │
│ │ ☐ 4×100米接力 ☐ 铅球(男子) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 导出文件预览: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 运动会报名表_20260315.xlsx │ │
│ │ ├── 报名统计汇总 │ │
│ │ ├── 100米(男子) │ │
│ │ ├── 100米(女子) │ │
│ │ ├── 800米(男子) │ │
│ │ ├── 高一1班报名表 │ │
│ │ ├── 高一2班报名表 │ │
│ │ └── ... │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [取消] [确认导出] │
└─────────────────────────────────────────────────────────────────────────────┘
4.5.7 导出的各项目报名表格式
Sheet: 100米(男子)
| 序号 | 号码簿 | 姓名 | 班级 | 年级 | 报名时间 | 状态 |
|---|---|---|---|---|---|---|
| 1 | 1101 | 张三 | 高一1班 | 高一年级 | 2026-03-10 | 已审核 |
| 2 | 1103 | 王五 | 高一1班 | 高一年级 | 2026-03-10 | 已审核 |
| 3 | 1201 | 赵六 | 高一2班 | 高一年级 | 2026-03-11 | 已审核 |
| 4 | 1203 | 钱七 | 高一2班 | 高一年级 | 2026-03-11 | 已审核 |
| ... | ... | ... | ... | ... | ... | ... |
统计行:报名总人数:32人 | 涉及班级:8个 | 超限班级:1个
4.6 编排算法模块
4.6.1 需求描述
编排算法是本系统的核心功能,需要根据报名情况自动分配运动员的道次和组别,满足禁止跨年级、同班尽量不同道等约束条件。
4.6.2 编排输入
| 输入项 | 类型 | 说明 |
|---|---|---|
| 项目ID | 整数 | 要编排的比赛项目 |
| 年级 | 字符串 | 指定年级(不跨年级) |
| 性别 | 枚举 | 男子/女子 |
| 报名运动员列表 | 列表 | 该项目的所有报名运动员 |
| 规则配置 | 对象 | 编排规则配置 |
4.6.3 编排输出
| 输出项 | 类型 | 说明 |
|---|---|---|
| 组别列表 | 列表 | 每个组的信息(组号、跑道分配) |
| 道次分配 | 二维数组 | 组×跑道的运动员分配 |
| 编排统计 | 对象 | 总人数、组数、违规情况 |
| 警告信息 | 列表 | 无法满足的约束警告 |
4.6.4 编排算法流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 编排算法主流程 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 1: 数据准备 │
│ - 获取该项目报名运动员列表 │
│ - 按班级分组 │
│ - 验证硬约束(同年级、同性别) │
│ - 如果验证失败,返回错误信息 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 2: 计算组数 │
│ - 最大班级人数 = max(各班报名人数) │
│ - 需要组数 = ceil(最大班级人数 / 跑道数) │
│ - 如果组数 == 0: 组数 = 1 │
│ - 如果组数 > 最大组数限制: 返回警告 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 3: 初始化组结构 │
│ - 创建 heats 数组,长度为组数 │
│ - 每个组包含 lanes 数组,长度为跑道数 │
│ - 所有位置初始化为 None │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 4: 按班级人数降序排序 │
│ - 将班级按报名人数从多到少排序 │
│ - 人数多的班级优先分配,便于满足约束 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 5: 贪心分配运动员 │
│ For each 班级: │
│ For each 运动员: │
│ 1. 选择最优组(同班人数最少的组) │
│ 2. 选择最优跑道(同班未占用的跑道) │
│ 3. 如果约束无法满足,根据配置决定是警告还是退化为任意位置 │
│ 4. 分配运动员到该位置 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 6: 局部优化(可选) │
│ - 交换运动员位置,优化同班分散度 │
│ - 平衡各组的班级分布 │
│ - 优化跑道使用率 │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Step 7: 输出结果 │
│ - 返回编排结果(组别、跑道、运动员) │
│ - 生成警告列表(无法完全满足的约束) │
│ - 记录编排日志 │
└─────────────────────────────────────────────────────────────────────────────┘
4.6.5 核心算法伪代码
python
def arrange_event(event_id, grade, gender, rule_config):
"""
编排算法核心函数
Args:
event_id: 项目ID
grade: 年级
gender: 性别
rule_config: 规则配置 {
'ban_same_class_same_lane': True, # 禁止同班同跑道
'prefer_diff_heat': True, # 尽量不同组
'prefer_diff_lane': True, # 尽量不同道次
'max_attempts': 1000, # 最大尝试次数
}
Returns:
heats: 编排结果
warnings: 警告信息
"""
# 1. 获取运动员数据
athletes = get_athletes_by_event(event_id, grade, gender)
if not athletes:
return None, "无报名运动员"
# 2. 验证硬约束
if not validate_hard_constraints(athletes, grade, gender):
return None, "硬约束验证失败"
# 3. 按班级分组
classes = group_by_class(athletes)
max_class_size = max(len(c) for c in classes.values())
# 4. 计算需要的组数
lanes_count = get_event_lanes(event_id)
heats_needed = max(1, (max_class_size + lanes_count - 1) // lanes_count)
# 5. 初始化组结构
heats = [Heat(heat_id=i, lanes=[None] * lanes_count)
for i in range(heats_needed)]
# 6. 记录每个班级已分配的组和跑道
class_assigned = {} # {class_id: {'heats': set, 'lanes': set}}
# 7. 按班级人数降序排序
sorted_classes = sorted(classes.items(),
key=lambda x: len(x[1]),
reverse=True)
warnings = []
for class_id, members in sorted_classes:
class_assigned[class_id] = {'heats': set(), 'lanes': set()}
for athlete in members:
# 选择最优组
best_heat = select_best_heat(
heats,
class_id,
rule_config.get('prefer_diff_heat', True)
)
# 选择最优跑道
best_lane = select_best_lane(
heats[best_heat],
class_id,
class_assigned,
rule_config.get('ban_same_class_same_lane', True)
)
if best_lane is None:
# 无法满足约束,记录警告
warnings.append(f"运动员 {athlete.name} 无法满足同班不同道约束")
# 退化为任意可用跑道
best_lane = find_any_free_lane(heats[best_heat])
# 分配
heats[best_heat].lanes[best_lane] = athlete
class_assigned[class_id]['heats'].add(best_heat)
class_assigned[class_id]['lanes'].add(best_lane)
# 8. 局部优化(可选)
if rule_config.get('enable_optimization', True):
heats = optimize_arrangement(heats, rule_config)
return heats, warnings
def select_best_heat(heats, class_id, prefer_diff_heat):
"""选择最优的组(尽量不同组)"""
if not prefer_diff_heat:
# 选择运动员最少的组
return min(range(len(heats)),
key=lambda i: count_athletes_in_heat(heats[i]))
# 优先选择班级人数最少的组
heat_scores = []
for i, heat in enumerate(heats):
class_count = count_class_in_heat(heat, class_id)
heat_scores.append((class_count, count_athletes_in_heat(heat), i))
# 按班级人数升序,总人数升序排序
heat_scores.sort()
return heat_scores[0][2]
4.6.6 编排结果展示界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 编排结果 - 100米(男子) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 项目信息: │
│ 年级:高一年级 | 性别:男子 | 跑道数:8道 | 报名人数:24人 │
│ 编排时间:2026-03-16 10:30:00 | 状态:✅ 编排完成 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ▼ 第1组 │
│ ┌─────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 跑道 │ 号码簿 │ 姓名 │ 班级 │ 成绩 │ 状态 │ │
│ ├─────────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 道次1 │ 1101 │ 张三 │ 高一1班 │ - │ 待比赛 │ │
│ │ 道次2 │ 1201 │ 赵六 │ 高一2班 │ - │ 待比赛 │ │
│ │ 道次3 │ 1301 │ 王七 │ 高一3班 │ - │ 待比赛 │ │
│ │ 道次4 │ 1401 │ 李八 │ 高一4班 │ - │ 待比赛 │ │
│ │ 道次5 │ 1102 │ 李四 │ 高一1班 │ - │ 待比赛 │ │
│ │ 道次6 │ 1202 │ 钱九 │ 高一2班 │ - │ 待比赛 │ │
│ │ 道次7 │ 1302 │ 孙十 │ 高一3班 │ - │ 待比赛 │ │
│ │ 道次8 │ 1402 │ 周一 │ 高一4班 │ - │ 待比赛 │ │
│ └─────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ ▼ 第2组 │
│ ┌─────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 道次1 │ 1103 │ 王五 │ 高一1班 │ - │ 待比赛 │ │
│ │ 道次2 │ 1203 │ 郑十 │ 高一2班 │ - │ 待比赛 │ │
│ │ 道次3 │ 1303 │ 陈一 │ 高一3班 │ - │ 待比赛 │ │
│ │ 道次4 │ 1403 │ 刘二 │ 高一4班 │ - │ 待比赛 │ │
│ │ 道次5 │ 1104 │ 林三 │ 高一1班 │ - │ 待比赛 │ │
│ │ 道次6 │ 1204 │ 郭四 │ 高一2班 │ - │ 待比赛 │ │
│ │ 道次7 │ 1304 │ 唐五 │ 高一3班 │ - │ 待比赛 │ │
│ │ 道次8 │ 1404 │ 宋六 │ 高一4班 │ - │ 待比赛 │ │
│ └─────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ ▼ 第3组 │
│ ┌─────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 道次1 │ 1105 │ 吴七 │ 高一1班 │ - │ 待比赛 │ │
│ │ 道次2 │ 1205 │ 郑八 │ 高一2班 │ - │ 待比赛 │ │
│ │ 道次3 │ 1305 │ 王九 │ 高一3班 │ - │ 待比赛 │ │
│ │ 道次4 │ 1405 │ 李十 │ 高一4班 │ - │ 待比赛 │ │
│ │ 道次5 │ - │ 空位 │ - │ - │ 空位 │ │
│ │ 道次6 │ - │ 空位 │ - │ - │ 空位 │ │
│ │ 道次7 │ - │ 空位 │ - │ - │ 空位 │ │
│ │ 道次8 │ - │ 空位 │ - │ - │ 空位 │ │
│ └─────────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ 📊 编排统计: │
│ 总运动员:24人 | 总组数:3组 | 平均每组:8人 | 空跑道:8个 │
│ │
│ ⚠ 警告信息: │
│ • 高一1班有5人报名,无法完全满足"同班不同组"约束,已尽量分散 │
│ │
│ [导出Excel] [导出PDF] [打印道次表] [手动调整] [重新编排] │
└─────────────────────────────────────────────────────────────────────────────┘
4.6.7 手动调整功能
┌─────────────────────────────────────────────────────────────────────────────┐
│ 手动调整编排 - 100米(男子) [保存] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 调整说明: │
│ • 拖拽运动员卡片可以移动到其他跑道或组别 │
│ • 系统会实时检查是否违反硬约束 │
│ • 违反约束时会显示红色警告 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 第1组 第2组 第3组 │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │道次1│ │道次2│ │道次3│ │道次1│ │道次2│ │道次3│ │ │
│ │ │张三 │ │赵六 │ │王七 │ │王五 │ │郑十 │ │陈一 │ │ │
│ │ │1101 │ │1201 │ │1301 │ │1103 │ │1203 │ │1303 │ │ │
│ │ │高一1│ │高一2│ │高一3│ │高一1│ │高一2│ │高一3│ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │道次4│ │道次5│ │道次6│ │道次4│ │道次5│ │道次6│ │ │
│ │ │李八 │ │李四 │ │钱九 │ │刘二 │ │林三 │ │郭四 │ │ │
│ │ │1401 │ │1102 │ │1202 │ │1403 │ │1104 │ │1204 │ │ │
│ │ │高一4│ │高一1│ │高一2│ │高一4│ │高一1│ │高一2│ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │道次7│ │道次8│ │道次7│ │道次8│ │ │
│ │ │孙十 │ │周一 │ │唐五 │ │宋六 │ │ │
│ │ │1302 │ │1402 │ │1304 │ │1404 │ │ │
│ │ │高一3│ │高一4│ │高一3│ │高一4│ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 约束检查: │
│ ✅ 禁止跨年级:通过 │
│ ✅ 性别分离:通过 │
│ ⚠ 同班不同道:高一1班有2人在同一组但不同跑道(允许) │
│ │
│ [撤销] [重做] [重置] [保存调整] │
└─────────────────────────────────────────────────────────────────────────────┘
4.7 成绩管理模块
4.7.1 需求描述
成绩管理是运动会核心环节,支持成绩录入、批量导入、修改、审核等功能。成绩录入后自动触发排名和积分计算。
4.7.2 成绩信息字段
| 字段名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| 运动员 | 外键 | ✅ | 关联运动员 | - |
| 项目 | 外键 | ✅ | 关联项目 | - |
| 组别 | 整数 | ✅ | 第几组 | 1 |
| 跑道 | 整数 | ✅ | 跑道号 | 3 |
| 成绩原始值 | 字符串 | ✅ | 原始输入成绩 | 12.34 或 2:35.67 |
| 成绩秒数 | 浮点数 | ❌ | 转换为秒便于排序 | 12.34 或 155.67 |
| 名次 | 整数 | ❌ | 组内名次 | 1 |
| 总名次 | 整数 | ❌ | 项目总名次 | 3 |
| 积分 | 整数 | ❌ | 所得积分 | 9 |
| 是否破纪录 | 布尔 | ❌ | 是否打破纪录 | 否 |
| 风速 | 浮点数 | ❌ | 径赛风速(可选) | +1.2 |
| 成绩状态 | 枚举 | ❌ | 有效/无效/弃权/DQ | 有效 |
| 录入时间 | 日期时间 | ❌ | 自动记录 | - |
| 录入人 | 字符串 | ❌ | 谁录入的 | admin |
| 审核状态 | 枚举 | ❌ | 待审核/已审核 | 已审核 |
| 备注 | 文本 | ❌ | 特殊说明 | - |
4.7.3 成绩管理功能列表
| 功能 | 说明 | 体育老师 | 班主任 | 学生 |
|---|---|---|---|---|
| 成绩录入 | 手动输入成绩 | ✅ | ❌ | ❌ |
| 批量导入 | Excel批量导入成绩 | ✅ | ❌ | ❌ |
| 成绩修改 | 修改已录入成绩 | ✅ | ❌ | ❌ |
| 成绩审核 | 确认成绩有效 | ✅ | ❌ | ❌ |
| 成绩查询 | 按项目/班级/运动员查询 | ✅ | ✅ | ✅ |
| 成绩对比 | 同一项目不同组成绩对比 | ✅ | ✅ | ✅ |
| 导出成绩单 | 导出成绩表 | ✅ | ✅ | ❌ |
4.7.4 成绩录入界面(体育老师端)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 成绩录入 - 100米(男子) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 项目信息:高一年级 男子 100米 | 赛道数:8道 | 总组数:3组 │
│ 已录入:0/24人 | 录入进度:0% │
├─────────────────────────────────────────────────────────────────────────────┤
│ 选择组别:[第1组 ▼] 成绩格式:秒.毫秒(如:12.34) [批量录入] [导入Excel] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────┬────┐ │
│ │ 跑道 │ 号码簿 │ 姓名 │ 班级 │ 成绩 │ 风速 │ 状态 │操作│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 1 │ 1101 │ 张三 │ 高一1班 │ [12.34] │ [+1.2] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 2 │ 1201 │ 赵六 │ 高一2班 │ [12.56] │ [+1.2] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 3 │ 1301 │ 王七 │ 高一3班 │ [12.89] │ [+1.2] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 4 │ 1401 │ 李八 │ 高一4班 │ [13.12] │ [+1.2] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 5 │ 1102 │ 李四 │ 高一1班 │ [12.45] │ [+1.2] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 6 │ 1202 │ 钱九 │ 高一2班 │ [12.78] │ [+1.2] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 7 │ 1302 │ 孙十 │ 高一3班 │ [13.01] │ [+1.2] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┼────┤ │
│ │ 8 │ 1402 │ 周一 │ 高一4班 │ [13.23] │ [+1.2] │ 有效 ▼ │保存│ │
│ └────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────┴────┘ │
│ │
│ [一键保存全部] [自动计算排名] [查看排名] │
│ │
│ 💡 快捷键:Enter 保存当前成绩,Tab 切换到下一个输入框 │
└─────────────────────────────────────────────────────────────────────────────┘
4.7.5 长跑成绩录入界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 成绩录入 - 1000米(男子) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 项目信息:高一年级 男子 1000米 | 赛道数:8道 | 总组数:2组 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 选择组别:[第1组 ▼] 成绩格式:分:秒.毫秒(如:2:35.67) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────┬──────┬──────────┬──────────┬──────────────────┬──────────┬────┐ │
│ │ 跑道 │ 号码簿 │ 姓名 │ 班级 │ 成绩 │ 状态 │操作│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 1 │ 1105 │ 吴七 │ 高一1班 │ [2:35.67______] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 2 │ 1205 │ 郑八 │ 高一2班 │ [2:38.23______] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 3 │ 1305 │ 王九 │ 高一3班 │ [2:42.15______] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 4 │ 1405 │ 李十 │ 高一4班 │ [2:45.89______] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 5 │ 1106 │ 周十一 │ 高一1班 │ [2:39.01______] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 6 │ 1206 │ 吴十二 │ 高一2班 │ [2:41.33______] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 7 │ 1306 │ 郑十三 │ 高一3班 │ [2:44.67______] │ 有效 ▼ │保存│ │
│ ├────┼──────┼──────────┼──────────┼──────────────────┼──────────┼────┤ │
│ │ 8 │ 1406 │ 王十四 │ 高一4班 │ [2:48.22______] │ 有效 ▼ │保存│ │
│ └────┴──────┴──────────┴──────────┴──────────────────┴──────────┴────┘ │
│ │
│ 💡 提示:支持输入分:秒.毫秒格式,如 2:35.67 表示 2分35秒67毫秒 │
└─────────────────────────────────────────────────────────────────────────────┘
4.7.6 成绩批量导入功能(支持别名映射)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 批量导入成绩 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 步骤1:上传文件 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ 📁 拖拽或点击上传 Excel 文件 │ │ │
│ │ │ │ │ │
│ │ │ 支持格式:.xlsx, .xls │ │ │
│ │ │ 文件大小限制:10MB │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 步骤2:列名映射(系统自动识别) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌────────────────┬────────────────┬──────────────────┬──────────┐ │ │
│ │ │ 标准字段 │ 识别的列名 │ Excel列名示例 │ 状态 │ │ │
│ │ ├────────────────┼────────────────┼──────────────────┼──────────┤ │ │
│ │ │ 号码簿 * │ 号码布 │ 1101,1102,... │ ✅ 已匹配 │ │ │
│ │ │ 项目名称 * │ 比赛项目 │ 100米,800米 │ ✅ 已匹配 │ │ │
│ │ │ 成绩 * │ 成绩 │ 12.34,2:35.67 │ ✅ 已匹配 │ │ │
│ │ │ 组别 │ 组别 │ 1,2,3 │ ✅ 已匹配 │ │ │
│ │ │ 跑道 │ 跑道 │ 1,2,3,... │ ✅ 已匹配 │ │ │
│ │ │ 风速 │ [未匹配] │ - │ ⚠ 手动映射│ │ │
│ │ └────────────────┴────────────────┴──────────────────┴──────────┘ │ │
│ │ │ │
│ │ [修改映射] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 步骤3:数据预览 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌────────┬──────────┬──────────┬──────┬──────┬──────────┬──────┐ │ │
│ │ │ 号码簿 │ 姓名 │ 项目 │ 组别 │ 跑道 │ 成绩 │ 状态 │ │ │
│ │ ├────────┼──────────┼──────────┼──────┼──────┼──────────┼──────┤ │ │
│ │ │ 1101 │ 张三 │ 100米 │ 1 │ 1 │ 12.34 │ ✅ │ │ │
│ │ │ 1201 │ 赵六 │ 100米 │ 1 │ 2 │ 12.56 │ ✅ │ │ │
│ │ │ 1301 │ 王七 │ 100米 │ 1 │ 3 │ 12.89 │ ✅ │ │ │
│ │ │ 1102 │ 李四 │ 100米 │ 1 │ 5 │ 12.45 │ ✅ │ │ │
│ │ │ 1105 │ 吴七 │ 1000米 │ 1 │ 1 │ 2:35.67 │ ✅ │ │ │
│ │ │ 1205 │ 郑八 │ 1000米 │ 1 │ 2 │ 2:38.23 │ ⚠ │ │ │
│ │ │ │ │ │ │ │ │格式异常│ │ │
│ │ └────────┴──────────┴──────────┴──────┴──────┴──────────┴──────┘ │ │
│ │ │ │
│ │ 共发现 2 条格式异常记录,建议检查后再导入 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 步骤4:导入选项 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ☑ 遇到重复成绩时覆盖 │ │
│ │ ☐ 跳过格式错误的记录 │ │
│ │ ☑ 导入后自动计算排名 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [取消] [确认导入] │
└─────────────────────────────────────────────────────────────────────────────┘
4.7.7 成绩导入模板格式
成绩导入模板(Excel):
| 号码簿 | 姓名 | 项目 | 组别 | 跑道 | 成绩 | 风速 | 备注 |
|---|---|---|---|---|---|---|---|
| 1101 | 张三 | 100米 | 1 | 1 | 12.34 | +1.2 | |
| 1102 | 李四 | 100米 | 1 | 5 | 12.45 | +1.2 | |
| 1105 | 吴七 | 1000米 | 1 | 1 | 2:35.67 | ||
| 1205 | 郑八 | 1000米 | 1 | 2 | 2:38.23 |
模板说明:
- 必填列:号码簿、项目、成绩
- 项目名称必须与系统中定义的项目名称完全一致
- 成绩格式:短跑用"秒.毫秒",长跑用"分:秒.毫秒"
4.8 排名与积分模块
4.8.1 需求描述
根据录入的成绩自动计算组内排名、项目总排名,并根据积分规则计算个人积分和团体总分。
4.8.2 排名功能
| 功能 | 说明 | 算法 |
|---|---|---|
| 组内排名 | 同组内按成绩排序 | 成绩越好排名越靠前(数值越小) |
| 项目总排名 | 所有组合并排序 | 成绩越好排名越靠前 |
| 并列处理 | 成绩相同时的处理 | 可配置:同名次/名次顺延 |
| 晋级排名 | 预赛晋级决赛排名 | 取前N名 |
| 按班级排名 | 班级内运动员排名 | 按成绩排序 |
| 按年级排名 | 年级内运动员排名 | 按成绩排序 |
4.8.3 积分功能
| 功能 | 说明 |
|---|---|
| 个人积分 | 根据项目名次计算个人积分 |
| 团体总分 | 按班级/年级汇总积分 |
| 破纪录加分 | 破纪录额外加分 |
| 参与分 | 参赛未获奖的基础分 |
| 积分榜 | 实时更新的积分排名 |
| 多项目积分汇总 | 运动员所有项目积分累加 |
4.8.4 排名计算算法
python
def calculate_ranking_and_scores(event_id, scoring_rules):
"""
计算项目排名和积分
Args:
event_id: 项目ID
scoring_rules: 积分规则配置
Returns:
rankings: 排名结果列表
team_scores: 团体总分
"""
# 1. 获取所有成绩记录
results = get_results_by_event(event_id)
# 2. 转换成绩为秒数
for result in results:
result['time_seconds'] = convert_to_seconds(result['raw_time'])
# 3. 按组成绩排序,计算组内名次
for heat in results.groupby('heat'):
heat['heat_rank'] = heat.sort_values('time_seconds').reset_index(drop=True).index + 1
# 4. 合并所有组成绩,计算总名次
all_results = results.sort_values('time_seconds').reset_index(drop=True)
# 5. 处理并列名次
if scoring_rules.get('tie_handling') == 'same_rank':
# 成绩并列时名次相同
all_results['total_rank'] = all_results['time_seconds'].rank(method='min').astype(int)
else:
# 成绩并列时名次顺延
all_results['total_rank'] = all_results['time_seconds'].rank(method='dense').astype(int)
# 6. 计算积分
rank_scores = scoring_rules.get('rank_scores', {})
default_scores = {1: 9, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1}
rank_scores = {**default_scores, **rank_scores}
all_results['score'] = all_results['total_rank'].map(rank_scores).fillna(0)
# 7. 破纪录加分
if scoring_rules.get('record_bonus_enabled', False):
record = get_event_record(event_id)
for idx, row in all_results.iterrows():
if row['time_seconds'] < record['time_seconds']:
all_results.loc[idx, 'score'] += scoring_rules.get('record_bonus', 10)
all_results.loc[idx, 'is_record'] = True
# 8. 参与分
if scoring_rules.get('participation_score_enabled', False):
participation_score = scoring_rules.get('participation_score', 1)
all_results['score'] = all_results['score'].apply(
lambda x: x if x > 0 else participation_score
)
# 9. 计算团体总分
team_scores = all_results.groupby(['grade', 'class'])['score'].sum().reset_index()
team_scores = team_scores.sort_values('score', ascending=False)
team_scores['rank'] = range(1, len(team_scores) + 1)
return all_results, team_scores
def convert_to_seconds(time_str: str) -> float:
"""
将成绩字符串转换为秒数(浮点数)
支持格式:
- 短跑:12.34 -> 12.34
- 长跑:2:35.67 -> 155.67
- 分:秒:毫秒:1:23:45.67 -> 5025.67
"""
if not time_str:
return None
time_str = time_str.strip()
# 判断是否包含冒号(长跑格式)
if ':' in time_str:
parts = time_str.split(':')
if len(parts) == 2: # 分:秒.毫秒
minutes = float(parts[0])
seconds = float(parts[1])
return minutes * 60 + seconds
elif len(parts) == 3: # 时:分:秒.毫秒
hours = float(parts[0])
minutes = float(parts[1])
seconds = float(parts[2])
return hours * 3600 + minutes * 60 + seconds
else:
# 短跑格式
return float(time_str)
return None
4.8.5 成绩排名展示界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 成绩排名 - 100米(男子) │
├─────────────────────────────────────────────────────────────────────────────┤
│ 项目信息:高一年级 男子 100米 │
│ 总参赛人数:24人 | 已录入成绩:24人 | 状态:✅ 排名已计算 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 视图切换:[总排名 ▼] [组内排名] [班级排名] [导出成绩单] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 🏆 前三名 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🥇 金牌 🥈 银牌 🥉 铜牌 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ 1101 │ │ 1102 │ │ 1201 │ │ │
│ │ │ 张三 │ │ 李四 │ │ 赵六 │ │ │
│ │ │ 12.34秒 │ │ 12.45秒 │ │ 12.56秒 │ │ │
│ │ │ 9分 │ │ 7分 │ │ 6分 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 完整排名列表 │
│ ┌────┬────────┬──────────┬──────────┬──────────┬──────┬──────┬──────────┐ │
│ │ 名次 │ 号码簿 │ 姓名 │ 班级 │ 成绩 │ 组别 │ 跑道 │ 积分 │ │
│ ├────┼────────┼──────────┼──────────┼──────────┼──────┼──────┼──────────┤ │
│ │ 🥇1 │ 1101 │ 张三 │ 高一1班 │ 12.34 │ 1 │ 1 │ 9 │ │
│ │ 🥈2 │ 1102 │ 李四 │ 高一1班 │ 12.45 │ 1 │ 5 │ 7 │ │
│ │ 🥉3 │ 1201 │ 赵六 │ 高一2班 │ 12.56 │ 1 │ 2 │ 6 │ │
│ │ 4 │ 1202 │ 钱九 │ 高一2班 │ 12.78 │ 1 │ 6 │ 5 │ │
│ │ 5 │ 1301 │ 王七 │ 高一3班 │ 12.89 │ 1 │ 3 │ 4 │ │
│ │ 6 │ 1103 │ 王五 │ 高一1班 │ 13.01 │ 2 │ 1 │ 3 │ │
│ │ 7 │ 1302 │ 孙十 │ 高一3班 │ 13.12 │ 1 │ 7 │ 2 │ │
│ │ 8 │ 1401 │ 李八 │ 高一4班 │ 13.23 │ 1 │ 4 │ 1 │ │
│ │ 9 │ 1402 │ 周一 │ 高一4班 │ 13.45 │ 1 │ 8 │ 0 │ │
│ │ ...│ ... │ ... │ ... │ ... │ ... │ ... │ ... │ │
│ └────┴────────┴──────────┴──────────┴──────────┴──────┴──────┴──────────┘ │
│ │
│ [导出排名] [打印奖状] [查看详情] │
└─────────────────────────────────────────────────────────────────────────────┘
4.8.6 团体总分榜界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 团体总分榜 [导出] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 统计范围:全部项目 | 统计时间:2026-03-18 | 状态:✅ 实时更新 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 🏆 团体总分前三名 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 🥇 冠军 🥈 亚军 🥉 季军 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ 高一1班 │ │ 高一2班 │ │ 高一3班 │ │ │
│ │ │ 156分 │ │ 142分 │ │ 128分 │ │ │
│ │ │ 🥇×5 │ │ 🥇×3 │ │ 🥇×2 │ │ │
│ │ │ 🥈×4 │ │ 🥈×5 │ │ 🥈×3 │ │ │
│ │ │ 🥉×3 │ │ 🥉×2 │ │ 🥉×4 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 完整团体总分榜 │
│ ┌────┬──────────┬──────────┬──────────┬──────┬──────┬──────┬──────────┐ │
│ │ 排名 │ 班级 │ 年级 │ 总分 │ 金牌 │ 银牌 │ 铜牌 │ 详情 │ │
│ ├────┼──────────┼──────────┼──────────┼──────┼──────┼──────┼──────────┤ │
│ │ 1 │ 高一1班 │ 高一年级 │ 156 │ 5 │ 4 │ 3 │ 查看详情 │ │
│ │ 2 │ 高一2班 │ 高一年级 │ 142 │ 3 │ 5 │ 2 │ 查看详情 │ │
│ │ 3 │ 高一3班 │ 高一年级 │ 128 │ 2 │ 3 │ 4 │ 查看详情 │ │
│ │ 4 │ 高一4班 │ 高一年级 │ 98 │ 1 │ 2 │ 3 │ 查看详情 │ │
│ │ 5 │ 高二1班 │ 高二年级 │ 87 │ 1 │ 2 │ 1 │ 查看详情 │ │
│ │ 6 │ 高二2班 │ 高二年级 │ 76 │ 0 │ 2 │ 3 │ 查看详情 │ │
│ │ 7 │ 高二3班 │ 高二年级 │ 65 │ 0 │ 1 │ 2 │ 查看详情 │ │
│ │ 8 │ 高二4班 │ 高二年级 │ 54 │ 0 │ 1 │ 1 │ 查看详情 │ │
│ └────┴──────────┴──────────┴──────────┴──────┴──────┴──────┴──────────┘ │
│ │
│ [导出总分榜] [按年级筛选] [按项目筛选] [打印] │
└─────────────────────────────────────────────────────────────────────────────┘
4.9 统计报表模块
4.9.1 需求描述
提供多维度的统计报表,支持报名统计、成绩统计、积分统计等,并支持导出为 Excel/PDF。
4.9.2 统计报表类型
| 报表类型 | 说明 | 体育老师 | 班主任 | 学生 |
|---|---|---|---|---|
| 报名统计表 | 各项目报名人数统计 | ✅ | ✅ | ❌ |
| 参赛人数统计 | 各班/年级参赛人数 | ✅ | ✅ | ❌ |
| 成绩统计表 | 各项目成绩汇总 | ✅ | ✅ | ✅ |
| 积分统计表 | 个人/班级积分汇总 | ✅ | ✅ | ✅ |
| 破纪录统计 | 破纪录运动员列表 | ✅ | ✅ | ✅ |
| 道次表 | 各项目道次安排 | ✅ | ✅ | ✅ |
| 秩序册 | 完整秩序册 | ✅ | ❌ | ❌ |
| 成绩册 | 完整成绩册 | ✅ | ❌ | ❌ |
4.9.3 统计报表界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 统计报表 [导出] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 报表类型:[报名统计 ▼] 统计范围:[全部项目 ▼] 时间:[2026运动会 ▼] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 各项目报名人数统计 │
│ │
│ ┌────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 序号 │ 项目名称 │ 性别 │ 报名人数 │ 班级数 │ 超限情况 │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 1 │ 50米 │ 男子 │ 24 │ 6 │ ✅ │ │
│ │ 2 │ 50米 │ 女子 │ 20 │ 5 │ ✅ │ │
│ │ 3 │ 100米 │ 男子 │ 32 │ 8 │ ⚠ 高一1班│ │
│ │ 4 │ 100米 │ 女子 │ 28 │ 7 │ ✅ │ │
│ │ 5 │ 800米 │ 男子 │ 16 │ 4 │ ✅ │ │
│ │ 6 │ 800米 │ 女子 │ 14 │ 4 │ ✅ │ │
│ │ 7 │ 1000米 │ 男子 │ 12 │ 3 │ ✅ │ │
│ │ 8 │ 跳远 │ 男子 │ 18 │ 5 │ ✅ │ │
│ │ 9 │ 跳远 │ 女子 │ 15 │ 4 │ ✅ │ │
│ │ 10 │ 4×100米 │ 混合 │ 8队 │ 8 │ ✅ │ │
│ └────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ 📊 报名趋势图(柱状图) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 报名人数 │ │
│ │ 40 ┤ │ │
│ │ 30 ┤ ████ │ │
│ │ 20 ┤ ████ ████ │ │
│ │ 10 ┤ ████ ████ ████ │ │
│ │ 0 └────┴────┴────┴────┴────┴──── │ │
│ │ 50米 100米 800米 1000米 跳远 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [导出统计表] [生成图表] [打印] │
└─────────────────────────────────────────────────────────────────────────────┘
4.9.4 秩序册导出功能
┌─────────────────────────────────────────────────────────────────────────────┐
│ 导出秩序册 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 秩序册内容: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ☑ 封面(运动会名称、日期、地点) │ │
│ │ ☑ 目录 │ │
│ │ ☑ 竞赛规程 │ │
│ │ ☑ 组织委员会名单 │ │
│ │ ☑ 裁判员名单 │ │
│ │ ☑ 代表队名单(按班级) │ │
│ │ ☑ 运动员名单(按班级,含号码簿) │ │
│ │ ☑ 竞赛日程表 │ │
│ │ ☑ 各项目道次表(按项目) │ │
│ │ ☑ 田径纪录表 │ │
│ │ ☐ 场地示意图 │ │
│ │ ☐ 注意事项 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 导出格式: │
│ (●) Excel (.xlsx) ( ) PDF (.pdf) ( ) Word (.docx) │
│ │
│ 页面设置: │
│ 纸张大小:[A4 ▼] 方向:[纵向 ▼] 页边距:[普通 ▼] │
│ │
│ [预览] [取消] [确认导出] │
└─────────────────────────────────────────────────────────────────────────────┘
4.10 Excel 导入导出模块
4.10.1 需求描述
支持各类数据的 Excel 导入导出,使用 pandas + numpy 处理,支持列别名映射、数据校验、错误提示等功能。
4.10.2 导入导出功能总览
| 数据类型 | 导入 | 导出 | 支持别名 | 必填字段 | 模板下载 |
|---|---|---|---|---|---|
| 班级信息 | ✅ | ✅ | ✅ | 班级名称、年级 | ✅ |
| 运动员信息 | ✅ | ✅ | ✅ | 姓名、性别、年级、班级、号码簿 | ✅ |
| 项目信息 | ✅ | ✅ | ✅ | 项目名称、项目代码 | ✅ |
| 报名信息 | ✅ | ✅ | ✅ | 号码簿、项目 | ✅ |
| 成绩信息 | ✅ | ✅ | ✅ | 号码簿、项目、成绩 | ✅ |
| 编排结果 | ❌ | ✅ | - | - | - |
| 统计报表 | ❌ | ✅ | - | - | - |
4.10.3 导入流程详细
┌─────────────────────────────────────────────────────────────────────────────┐
│ Excel 导入流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤1:用户上传 Excel 文件 │ │
│ │ - 支持格式:.xlsx, .xls │ │
│ │ - 文件大小限制:10MB │ │
│ │ - 编码:UTF-8 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤2:读取文件,检测列名 │ │
│ │ - 使用 pandas.read_excel() 读取 │ │
│ │ - 获取列名列表 │ │
│ │ - 调用列名映射器进行智能匹配 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤3:展示列名映射预览(用户确认) │ │
│ │ - 显示标准字段与Excel列的对应关系 │ │
│ │ - 用户可手动调整映射 │ │
│ │ - 检查必填字段是否都已匹配 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤4:数据清洗和验证 │ │
│ │ - 处理空值(必填字段报错) │ │
│ │ - 格式验证(性别、成绩格式等) │ │
│ │ - 唯一性验证(号码簿) │ │
│ │ - 业务规则验证(班级存在、项目存在等) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤5:展示验证结果和预览数据 │ │
│ │ - 成功记录数、失败记录数 │ │
│ │ - 错误详情列表(行号+错误原因) │ │
│ │ - 预览前10条数据 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 步骤6:用户确认后执行导入 │ │
│ │ - 批量插入数据库 │ │
│ │ - 记录导入日志 │ │
│ │ - 返回导入结果(成功数、失败数) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.10.4 导入校验规则详细
| 数据类型 | 字段 | 校验规则 | 错误提示 |
|---|---|---|---|
| 运动员 | 姓名 | 不能为空,长度≤20 | "第X行:姓名不能为空" |
| 运动员 | 性别 | 只能是"男"/"女"/"M"/"F" | "第X行:性别格式错误" |
| 运动员 | 年级 | 必须在系统配置中存在 | "第X行:年级不存在" |
| 运动员 | 班级 | 必须在系统配置中存在 | "第X行:班级不存在" |
| 运动员 | 号码簿 | 不能为空,全局唯一 | "第X行:号码簿重复或为空" |
| 成绩 | 成绩 | 格式正确(秒.毫秒 或 分:秒.毫秒) | "第X行:成绩格式错误" |
| 报名 | 号码簿 | 必须在运动员表中存在 | "第X行:号码簿不存在" |
| 报名 | 项目 | 必须在项目表中存在 | "第X行:项目不存在" |
| 报名 | 报名人数 | 不超过班级每项目限制 | "第X行:本班该项目报名已超限" |
4.10.5 导入结果反馈界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 导入结果 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ✅ 导入完成 │ │
│ │ │ │
│ │ 成功导入:156 条 │ │
│ │ 失败:5 条 │ │
│ │ 跳过:0 条 │ │
│ │ │ │
│ │ 总耗时:2.3 秒 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 错误详情: │
│ ┌────┬──────────┬────────────────────────────────────────────────────┐ │
│ │ 行号 │ 错误类型 │ 错误信息 │ │
│ ├────┼──────────┼────────────────────────────────────────────────────┤ │
│ │ 3 │ 格式错误 │ 性别格式错误,应为"男"或"女" │ │
│ │ 7 │ 必填缺失 │ 号码簿不能为空 │ │
│ │ 12 │ 重复 │ 号码簿 1101 已存在 │ │
│ │ 15 │ 业务规则 │ 高一1班 100米 报名人数超过限制(最多3人) │ │
│ │ 18 │ 数据验证 │ 成绩格式错误,应为"秒.毫秒"或"分:秒.毫秒" │ │
│ └────┴──────────┴────────────────────────────────────────────────────┘ │
│ │
│ [导出错误报告] [继续导入] [关闭] │
└─────────────────────────────────────────────────────────────────────────────┘
4.10.6 导入模板下载
┌─────────────────────────────────────────────────────────────────────────────┐
│ 下载导入模板 [关闭] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 选择模板类型: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 📁 运动员导入模板 │ │ │
│ │ │ 包含字段:姓名、性别、年级、班级、号码簿、学号、联系电话 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 📁 成绩导入模板 │ │ │
│ │ │ 包含字段:号码簿、项目、组别、跑道、成绩、风速 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 📁 报名导入模板 │ │ │
│ │ │ 包含字段:号码簿、项目 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 模板说明: │
│ • 第一行为列名,请不要修改列名格式 │
│ • 红色*列为必填列 │
│ • 性别请填写"男"或"女" │
│ • 成绩格式:短跑用"秒.毫秒",长跑用"分:秒.毫秒" │
│ • 项目名称必须与系统中定义的项目名称完全一致 │
│ │
│ [下载运动员模板] [下载成绩模板] [下载报名模板] [下载班级模板] │
└─────────────────────────────────────────────────────────────────────────────┘
4.10.7 Excel 处理核心代码
python
import pandas as pd
import numpy as np
from io import BytesIO
from fastapi import UploadFile
class ExcelHandler:
"""Excel 文件处理器"""
@staticmethod
async def read_excel(file: UploadFile) -> pd.DataFrame:
"""读取 Excel 文件为 DataFrame"""
contents = await file.read()
df = pd.read_excel(BytesIO(contents), dtype=str)
# 去除首尾空格
df = df.apply(lambda x: x.str.strip() if x.dtype == "object" else x)
return df
@staticmethod
def export_to_excel(dataframes: dict, filename: str) -> BytesIO:
"""
导出多个 DataFrame 到 Excel 的不同 Sheet
Args:
dataframes: {"Sheet名称": DataFrame}
filename: 文件名
Returns:
BytesIO 对象
"""
output = BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
for sheet_name, df in dataframes.items():
df.to_excel(writer, sheet_name=sheet_name, index=False)
output.seek(0)
return output
@staticmethod
def validate_required_columns(df: pd.DataFrame, required_cols: list) -> list:
"""验证必填列是否存在"""
missing = [col for col in required_cols if col not in df.columns]
return missing
@staticmethod
def validate_not_null(df: pd.DataFrame, column: str) -> pd.Series:
"""验证非空"""
is_null = df[column].isna() | (df[column].astype(str).str.strip() == '')
return is_null
@staticmethod
def validate_unique(df: pd.DataFrame, column: str) -> dict:
"""验证唯一性"""
duplicates = df[df[column].duplicated(keep=False)]
return {
"is_valid": len(duplicates) == 0,
"duplicates": duplicates[column].tolist()
}
@staticmethod
def generate_template(template_type: str) -> BytesIO:
"""生成导入模板"""
templates = {
"athlete": pd.DataFrame({
"姓名": ["张三"],
"性别": ["男"],
"年级": ["高一年级"],
"班级": ["高一1班"],
"号码簿": ["1101"],
"学号": ["20240001"],
"联系电话": ["13812345678"]
}),
"score": pd.DataFrame({
"号码簿": ["1101"],
"项目": ["100米"],
"组别": [1],
"跑道": [1],
"成绩": ["12.34"],
"风速": ["+1.2"]
}),
"registration": pd.DataFrame({
"号码簿": ["1101"],
"项目": ["100米"]
}),
"class": pd.DataFrame({
"班级名称": ["高一1班"],
"年级": ["高一年级"],
"班主任": ["张三"],
"联系电话": ["13812345678"]
})
}
df = templates.get(template_type)
output = BytesIO()
df.to_excel(output, index=False)
output.seek(0)
return output
5. 多端功能详细
5.1 体育老师端
5.1.1 功能概述
体育老师端是系统的管理核心,拥有全部功能权限,负责系统配置、项目管理、编排执行、成绩录入、报表导出等。
5.1.2 菜单结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 体育老师端 - 菜单结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 仪表盘 │
│ ├── 数据看板(报名人数、完成度、排名预警) │
│ ├── 快捷操作(快速编排、快速录入) │
│ └── 通知公告 │
│ │
│ ⚙️ 系统配置 │
│ ├── 基础规则配置 │
│ ├── 号码簿规则配置 │
│ ├── 编排规则配置 │
│ ├── Excel别名配置 │
│ └── 积分规则配置 │
│ │
│ 🏫 基础数据 │
│ ├── 班级管理 │
│ ├── 运动员管理 │
│ └── 项目管理(可自定义) │
│ │
│ 📝 报名管理 │
│ ├── 报名汇总 │
│ ├── 报名审核 │
│ ├── 导出报名表 │
│ └── 报名统计 │
│ │
│ 🎯 编排管理 │
│ ├── 执行编排 │
│ ├── 编排结果查看 │
│ ├── 手动调整 │
│ └── 导出道次表 │
│ │
│ 🏆 成绩管理 │
│ ├── 成绩录入 │
│ ├── 成绩导入 │
│ ├── 成绩查询 │
│ └── 成绩审核 │
│ │
│ 📈 排名积分 │
│ ├── 成绩排名 │
│ ├── 团体总分榜 │
│ ├── 个人积分榜 │
│ └── 破纪录榜 │
│ │
│ 📄 统计报表 │
│ ├── 报名统计表 │
│ ├── 成绩统计表 │
│ ├── 秩序册生成 │
│ ├── 成绩册生成 │
│ └── 自定义报表 │
│ │
│ 👥 用户管理 │
│ ├── 班主任账号管理 │
│ ├── 学生账号管理 │
│ └── 角色权限管理 │
│ │
│ 📋 系统日志 │
│ ├── 操作日志 │
│ ├── 导入导出日志 │
│ └── 登录日志 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.1.3 仪表盘界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 仪表盘 2026运动会 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 数据概览 │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ 参赛班级数 │ 运动员总数 │ 报名总人次 │ 已完成项目 │ │
│ │ 12 │ 186 │ 324 │ 8/12 │ │
│ │ ↑2 较上届 │ ↑15% │ ↑8% │ 67% │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
│ │
│ ⚠️ 待办提醒 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 3个班级未提交报名表(高一5班、高一6班、高二3班) │ │
│ │ • 4个项目未编排(100米女子、800米男子、跳远女子、铅球男子) │ │
│ │ • 2个班级报名超限(高一1班100米超1人,高一2班跳远超2人) │ │
│ │ • 6个项目的成绩未录入 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 🚀 快捷操作 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [执行编排] [成绩录入] [导入数据] [导出报表] [查看排名] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📈 报名趋势图 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 100 ┤ │ │
│ │ 80 ┤ ████ │ │
│ │ 60 ┤ ████ ████ │ │
│ │ 40 ┤ ████ ████ ████ │ │
│ │ 20 ┤ ████ ████ ████ ████ │ │
│ │ 0 └────┴────┴────┴────┴────┴──── │ │
│ │ 第1周 第2周 第3周 第4周 第5周 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.2 班主任端
5.2.1 功能概述
班主任端主要用于本班运动员管理和报名工作,可以查看本班信息、管理本班运动员、为本班学生报名参赛。
5.2.2 菜单结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 班主任端 - 菜单结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 班级看板 │
│ ├── 本班概况(运动员数、报名人次) │
│ ├── 报名进度 │
│ └── 通知公告 │
│ │
│ 👥 本班运动员 │
│ ├── 运动员列表 │
│ ├── 新增运动员 │
│ ├── 编辑运动员 │
│ └── 导出本班运动员 │
│ │
│ 📝 本班报名 │
│ ├── 报名操作 │
│ ├── 报名列表 │
│ ├── 导出报名表 │
│ └── 报名统计 │
│ │
│ 🎯 赛程查看 │
│ ├── 本班道次表 │
│ ├── 本班赛程日历 │
│ └── 运动员个人赛程 │
│ │
│ 🏆 成绩查看 │
│ ├── 本班成绩榜 │
│ ├── 个人成绩查询 │
│ └── 本班积分榜 │
│ │
│ 📄 报表查看 │
│ ├── 本班报名统计 │
│ └── 本班成绩统计 │
│ │
│ ⚙️ 班级设置 │
│ ├── 班级信息修改 │
│ └── 修改密码 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.2.3 班级看板界面(班主任端)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 班级看板 - 高一1班 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 🏫 班级信息 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 班主任:张三 联系电话:138****0000 │ │
│ │ 班级人数:45人 运动员数:12人 │ │
│ │ 报名状态:✅ 已提交 提交时间:2026-03-12 14:30 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 报名统计 │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ 报名总人次 │ 男子项目 │ 女子项目 │ 人均项目数 │ │
│ │ 18 │ 10 │ 8 │ 1.5 │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┘ │
│ │
│ ⚠️ 提醒 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 报名截止时间:2026-03-15 23:59,请及时提交 │ │
│ │ • 张三(1101)已报2个项目,还可报1个 │ │
│ │ • 王五(1103)未报名任何项目 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 🏆 本班成绩快报(已有成绩的项目) │
│ ┌────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ 序号 │ 运动员 │ 项目 │ 成绩 │ 名次 │ 积分 │ │
│ ├────┼──────────┼──────────┼──────────┼──────────┼──────────┤ │
│ │ 1 │ 张三 │ 100米 │ 12.34 │ 第1名 │ 9 │ │
│ │ 2 │ 李四 │ 50米 │ 7.89 │ 第2名 │ 7 │ │
│ │ 3 │ 赵六 │ 跳远 │ 5.23米 │ 第3名 │ 6 │ │
│ └────┴──────────┴──────────┴──────────┴──────────┴──────────┘ │
│ │
│ [前往报名] [查看赛程] [查看成绩榜] │
└─────────────────────────────────────────────────────────────────────────────┘
5.2.4 本班赛程日历界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 本班赛程日历 - 高一1班 [导出] │
├─────────────────────────────────────────────────────────────────────────────┤
│ 日期选择:[2026-10-15 ▼] [2026-10-16] [2026-10-17] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📅 2026年10月15日 上午 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 时间 │ 项目 │ 运动员 │ 组别 │ 跑道 │ 状态 │ │
│ ├─────────────┼────────────┼──────────┼──────┼──────┼──────────────┤ │
│ │ 09:00 │ 100米预赛 │ 张三 │ 1 │ 1 │ ✅ 已比赛 │ │
│ │ 09:08 │ 100米预赛 │ 王五 │ 2 │ 5 │ ⏰ 即将开始 │ │
│ │ 10:00 │ 跳远决赛 │ 赵六 │ 1 │ 3 │ ⏰ 即将开始 │ │
│ │ 10:30 │ 4×100米接力 │ 高一1班 │ 1 │ 2 │ 📋 待比赛 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📅 2026年10月15日 下午 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 时间 │ 项目 │ 运动员 │ 组别 │ 跑道 │ 状态 │ │
│ ├─────────────┼────────────┼──────────┼──────┼──────┼──────────────┤ │
│ │ 14:00 │ 100米决赛 │ 张三 │ 1 │ 4 │ 📋 待比赛 │ │
│ │ 15:00 │ 800米决赛 │ 李四 │ 1 │ 2 │ 📋 待比赛 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ [打印赛程] [添加到日历] │
└─────────────────────────────────────────────────────────────────────────────┘
5.3 学生端(可选)
5.3.1 功能概述
学生端为可选模块,供学生查看个人赛程和成绩。支持移动端适配,使用 Vant UI 组件库。
5.3.2 菜单结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 学生端 - 菜单结构(移动端) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 🏠 首页 │
│ ├── 我的赛程(今日比赛) │
│ ├── 成绩快报 │
│ └── 通知公告 │
│ │
│ 🎯 我的赛程 │
│ ├── 全部赛程 │
│ ├── 按日期查看 │
│ └── 比赛日历 │
│ │
│ 🏆 我的成绩 │
│ ├── 个人成绩列表 │
│ ├── 个人积分 │
│ └── 获奖记录 │
│ │
│ 📊 赛事信息 │
│ ├── 项目列表 │
│ ├── 道次表查看 │
│ ├── 成绩排名 │
│ └── 团体总分 │
│ │
│ 👤 个人中心 │
│ ├── 个人资料 │
│ ├── 修改密码 │
│ └── 退出登录 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5.3.3 学生端首页界面(移动端)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 📱 学生端 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 👋 你好,张三 🔔 📅 │ │
│ │ 号码簿:1101 高一1班 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 我的数据 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 参赛项目 │ │ 已完赛 │ │ 获得积分 │ │
│ │ 3 │ │ 2 │ │ 16 │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ 🎯 今日赛程(2026-10-15) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 🏃 100米决赛 │ │ │
│ │ │ 时间:14:00 状态:⏰ 即将开始 │ │ │
│ │ │ 跑道:第4道 组别:第1组 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 🏃 4×100米接力 │ │ │
│ │ │ 时间:15:30 状态:📋 待比赛 │ │ │
│ │ │ 跑道:第2道 组别:第1组 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 🏆 我的成绩 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 🥇 100米预赛 │ │ │
│ │ │ 成绩:12.34秒 名次:第1名 │ │ │
│ │ │ 积分:9分 状态:✅ 已完赛 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 🥈 跳远决赛 │ │ │
│ │ │ 成绩:5.23米 名次:第2名 │ │ │
│ │ │ 积分:7分 状态:✅ 已完赛 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 底部导航栏: [🏠首页] [🎯赛程] [🏆成绩] [📊赛事] [👤我的] │
└─────────────────────────────────────────────────────────────────────────────┘
5.3.4 学生端赛程查看界面
┌─────────────────────────────────────────────────────────────────────────────┐
│ 我的赛程 📅 日历视图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 十月 2026 │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ 日 │ 一 │ 二 │ 三 │ 四 │ 五 │ 六 │ │
│ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │
│ │ │ │ │ │ 1 │ 2 │ 3 │ │
│ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │
│ │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ │
│ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │
│ │ 11 │ 12 │ 13 │ 14 │ ●15 │ 16 │ 17 │ ● 有比赛 │
│ ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ │
│ │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ │
│ 2026-10-15 的比赛: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 时间 │ 项目 │ 组别 │ 跑道 │ 状态 │ │
│ ├─────────────┼────────────┼──────┼──────┼────────────────────────────┤ │
│ │ 09:00 │ 100米预赛 │ 1 │ 1 │ ✅ 已完赛 12.34秒 第1名 │ │
│ │ 14:00 │ 100米决赛 │ 1 │ 4 │ ⏰ 即将开始 │ │
│ │ 15:30 │ 4×100米接力 │ 1 │ 2 │ 📋 待比赛 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
6. 非功能需求
6.1 性能需求
| 指标 | 要求 | 测量方法 |
|---|---|---|
| 页面加载时间 | < 2秒 | 浏览器开发者工具 |
| API响应时间 | < 500ms(95%请求) | 后端日志统计 |
| 编排算法执行时间 | < 30秒(1000人规模) | 计时统计 |
| Excel导入速度 | > 1000条/秒 | 批量导入测试 |
| 并发用户数 | 支持50人同时在线 | 压力测试 |
| 数据库查询 | 带索引查询 < 100ms | 慢查询日志 |
6.2 安全需求
| 需求 | 说明 |
|---|---|
| 身份认证 | JWT Token,有效期24小时 |
| 密码安全 | BCrypt加密存储,强制复杂度要求 |
| 权限控制 | RBAC模型,接口级权限校验 |
| SQL注入防护 | Django ORM + 参数化查询 |
| XSS防护 | 前端输出转义,CSP策略 |
| CSRF防护 | Django CSRF中间件 |
| 操作日志 | 记录关键操作(导入、编排、删除) |
| 数据备份 | 每日自动备份,保留30天 |
6.3 可用性需求
| 需求 | 说明 |
|---|---|
| 界面响应式 | 支持1920x1080、1366x768分辨率 |
| 移动端适配 | 学生端支持手机浏览器 |
| 操作可撤销 | 编排结果支持回滚 |
| 错误提示 | 友好的错误提示,含解决方案 |
| 操作引导 | 首次使用新手引导 |
| 快捷键支持 | 成绩录入支持Enter/Tab快捷键 |
6.4 可扩展性需求
| 需求 | 说明 |
|---|---|
| 项目可自定义 | 支持添加任意比赛项目 |
| 规则可配置 | 编排规则、积分规则可配置 |
| 字段可扩展 | 运动员信息支持自定义字段 |
| API版本管理 | 支持多版本API共存 |
| 插件机制 | 预留计时设备对接接口 |
6.5 可维护性需求
| 需求 | 说明 |
|---|---|
| 代码规范 | 遵循PEP8、ESLint规范 |
| 注释覆盖率 | 核心代码注释 > 30% |
| 日志记录 | 分级日志(DEBUG/INFO/WARNING/ERROR) |
| 监控告警 | 关键指标监控,异常告警 |
| 一键部署 | Docker Compose一键启动 |
7. 数据模型详细
7.1 Django Models 设计
python
# models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.core.validators import MinValueValidator, MaxValueValidator
class User(AbstractUser):
"""用户表"""
ROLE_CHOICES = (
('super_admin', '超级管理员'),
('teacher', '体育老师'),
('class_teacher', '班主任'),
('student', '学生'),
('viewer', '查看者'),
)
role = models.CharField('角色', max_length=20, choices=ROLE_CHOICES, default='viewer')
phone = models.CharField('电话', max_length=20, blank=True)
avatar = models.ImageField('头像', upload_to='avatars/', blank=True, null=True)
school = models.CharField('学校', max_length=100, blank=True, default='')
class Meta:
db_table = 'users'
verbose_name = '用户'
verbose_name_plural = '用户'
class Class(models.Model):
"""班级表"""
name = models.CharField('班级名称', max_length=50, unique=True)
grade = models.CharField('年级', max_length=20, db_index=True)
grade_order = models.IntegerField('年级排序', default=0)
class_order = models.IntegerField('班级排序', default=0)
code = models.CharField('班级编号', max_length=20, blank=True, unique=True)
teacher_name = models.CharField('班主任姓名', max_length=50)
teacher_user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='class_managed')
phone = models.CharField('联系电话', max_length=20, blank=True)
student_count = models.IntegerField('学生人数', default=0)
is_participating = models.BooleanField('是否参赛', default=True)
remark = models.TextField('备注', blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
db_table = 'classes'
verbose_name = '班级'
verbose_name_plural = '班级'
ordering = ['grade_order', 'class_order']
def __str__(self):
return self.name
class Athlete(models.Model):
"""运动员表"""
GENDER_CHOICES = (
('M', '男'),
('F', '女'),
)
STATUS_CHOICES = (
('normal', '正常'),
('injured', '受伤'),
('withdrawn', '退赛'),
)
name = models.CharField('姓名', max_length=50, db_index=True)
gender = models.CharField('性别', max_length=1, choices=GENDER_CHOICES)
grade = models.CharField('年级', max_length=20, db_index=True)
class_obj = models.ForeignKey(Class, on_delete=models.CASCADE, related_name='athletes', verbose_name='班级')
number = models.CharField('号码簿', max_length=20, unique=True, db_index=True)
student_id = models.CharField('学号', max_length=50, blank=True, unique=True)
id_card = models.CharField('身份证号', max_length=18, blank=True, unique=True)
birth_date = models.DateField('出生日期', null=True, blank=True)
phone = models.CharField('联系电话', max_length=20, blank=True)
emergency_contact = models.CharField('紧急联系人', max_length=50, blank=True)
emergency_phone = models.CharField('紧急联系电话', max_length=20, blank=True)
health_status = models.TextField('健康状况', blank=True)
photo = models.ImageField('照片', upload_to='athletes/', blank=True, null=True)
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='normal')
remark = models.TextField('备注', blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
db_table = 'athletes'
verbose_name = '运动员'
verbose_name_plural = '运动员'
indexes = [
models.Index(fields=['grade', 'class_obj']),
]
def __str__(self):
return f"{self.number} - {self.name}"
class Event(models.Model):
"""项目表"""
CATEGORY_CHOICES = (
('track_sprint', '径赛-短跑'),
('track_distance', '径赛-长跑'),
('track_relay', '径赛-接力'),
('field_jump', '田赛-跳跃'),
('field_throw', '田赛-投掷'),
('fun', '趣味项目'),
)
GENDER_CHOICES = (
('M', '男子'),
('F', '女子'),
('mixed', '混合'),
)
name = models.CharField('项目名称', max_length=50, db_index=True)
code = models.CharField('项目代码', max_length=20, unique=True)
category = models.CharField('项目类别', max_length=20, choices=CATEGORY_CHOICES)
distance_type = models.CharField('距离类型', max_length=20, blank=True, help_text='短跑/长跑')
gender_limit = models.CharField('性别限制', max_length=10, choices=GENDER_CHOICES)
default_lanes = models.IntegerField('默认跑道数', default=8, validators=[MinValueValidator(1), MaxValueValidator(12)])
need_heats = models.BooleanField('是否需要分组', default=True)
max_per_heat = models.IntegerField('每组最大人数', default=8)
晋级人数 = models.IntegerField('晋级人数', default=8, help_text='预赛晋级决赛人数,0表示不设决赛')
scoring_type = models.CharField('计分规则类型', max_length=20, default='global', choices=(('global', '全局'), ('custom', '自定义')))
scoring_rules = models.JSONField('计分规则', default=dict, blank=True)
sort_order = models.IntegerField('排序顺序', default=0)
is_enabled = models.BooleanField('是否启用', default=True)
registration_start = models.DateTimeField('报名开始时间', null=True, blank=True)
registration_end = models.DateTimeField('报名结束时间', null=True, blank=True)
record = models.CharField('项目纪录', max_length=50, blank=True)
remark = models.TextField('备注', blank=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
db_table = 'events'
verbose_name = '项目'
verbose_name_plural = '项目'
ordering = ['sort_order']
def __str__(self):
return f"{self.name} - {self.get_gender_limit_display()}"
class Registration(models.Model):
"""报名表"""
STATUS_CHOICES = (
('pending', '待审核'),
('approved', '已审核'),
('rejected', '已拒绝'),
('withdrawn', '已弃权'),
)
athlete = models.ForeignKey(Athlete, on_delete=models.CASCADE, related_name='registrations')
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='registrations')
status = models.CharField('报名状态', max_length=20, choices=STATUS_CHOICES, default='pending')
registered_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='registrations')
registered_at = models.DateTimeField('报名时间', auto_now_add=True)
remark = models.TextField('备注', blank=True)
class Meta:
db_table = 'registrations'
verbose_name = '报名'
verbose_name_plural = '报名'
unique_together = [['athlete', 'event']]
indexes = [
models.Index(fields=['event', 'status']),
models.Index(fields=['athlete']),
]
def __str__(self):
return f"{self.athlete.name} - {self.event.name}"
class Arrangement(models.Model):
"""编排结果表"""
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='arrangements')
grade = models.CharField('年级', max_length=20, db_index=True)
gender = models.CharField('性别', max_length=1, choices=Athlete.GENDER_CHOICES)
heat = models.IntegerField('组别', db_index=True)
lane = models.IntegerField('跑道')
athlete = models.ForeignKey(Athlete, on_delete=models.CASCADE, related_name='arrangements')
created_at = models.DateTimeField('编排时间', auto_now_add=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
class Meta:
db_table = 'arrangements'
verbose_name = '编排结果'
verbose_name_plural = '编排结果'
unique_together = [['event', 'grade', 'gender', 'heat', 'lane']]
indexes = [
models.Index(fields=['event', 'grade', 'gender']),
]
def __str__(self):
return f"{self.event.name} - {self.grade} - 第{self.heat}组 - 第{self.lane}道"
class Result(models.Model):
"""成绩表"""
STATUS_CHOICES = (
('valid', '有效'),
('invalid', '无效'),
('dq', '取消成绩'),
('dns', '未参赛'),
('dnf', '未完赛'),
)
athlete = models.ForeignKey(Athlete, on_delete=models.CASCADE, related_name='results')
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='results')
arrangement = models.ForeignKey(Arrangement, on_delete=models.SET_NULL, null=True, blank=True)
heat = models.IntegerField('组别')
lane = models.IntegerField('跑道')
raw_time = models.CharField('原始成绩', max_length=50)
time_seconds = models.FloatField('成绩秒数', null=True, blank=True, db_index=True)
heat_rank = models.IntegerField('组内名次', null=True, blank=True)
total_rank = models.IntegerField('总名次', null=True, blank=True, db_index=True)
score = models.IntegerField('积分', default=0)
wind_speed = models.FloatField('风速', null=True, blank=True)
is_record = models.BooleanField('是否破纪录', default=False)
status = models.CharField('成绩状态', max_length=20, choices=STATUS_CHOICES, default='valid')
entered_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='entered_results')
entered_at = models.DateTimeField('录入时间', auto_now_add=True)
reviewed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='reviewed_results')
reviewed_at = models.DateTimeField('审核时间', null=True, blank=True)
remark = models.TextField('备注', blank=True)
class Meta:
db_table = 'results'
verbose_name = '成绩'
verbose_name_plural = '成绩'
indexes = [
models.Index(fields=['event', 'total_rank']),
models.Index(fields=['athlete', 'event']),
]
def __str__(self):
return f"{self.athlete.name} - {self.event.name} - {self.raw_time}"
class SystemConfig(models.Model):
"""系统配置表"""
key = models.CharField('配置键', max_length=100, unique=True, db_index=True)
value = models.JSONField('配置值')
description = models.TextField('描述', blank=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
db_table = 'system_configs'
verbose_name = '系统配置'
verbose_name_plural = '系统配置'
def __str__(self):
return self.key
class OperationLog(models.Model):
"""操作日志表"""
ACTION_CHOICES = (
('create', '创建'),
('update', '更新'),
('delete', '删除'),
('import', '导入'),
('export', '导出'),
('arrange', '编排'),
('login', '登录'),
('logout', '登出'),
)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='logs')
action = models.CharField('操作类型', max_length=20, choices=ACTION_CHOICES)
module = models.CharField('模块', max_length=50)
description = models.TextField('描述')
ip_address = models.GenericIPAddressField('IP地址', null=True, blank=True)
user_agent = models.TextField('User Agent', blank=True)
created_at = models.DateTimeField('操作时间', auto_now_add=True)
class Meta:
db_table = 'operation_logs'
verbose_name = '操作日志'
verbose_name_plural = '操作日志'
indexes = [
models.Index(fields=['user', 'created_at']),
models.Index(fields=['action', 'created_at']),
]
7.2 预置系统配置
python
# 预置配置数据
PRESET_CONFIGS = {
# 基础配置
"basic_config": {
"global_lanes": 8,
"max_per_class_per_event": 3,
"max_events_per_athlete": 3,
"sprint_threshold": 400,
"score_decimal_places": 2,
"registration_deadline": None,
"allow_self_registration": False,
"student_portal_enabled": False,
"auto_arrange": False,
"allow_manual_adjust": True,
"sports_name": "第X届田径运动会",
"start_date": None,
"end_date": None,
"location": "",
},
# 号码簿规则
"number_rule": {
"template": "{grade}{class}{seq:02d}",
"grade_mapping": {
"一年级": 1, "二年级": 2, "三年级": 3,
"四年级": 4, "五年级": 5, "六年级": 6,
"初一": 7, "初二": 8, "初三": 9,
"高一": 10, "高二": 11, "高三": 12,
},
"auto_extract_class_number": True,
"auto_pad_zero": True,
"unique_global": True,
"allow_manual_edit": True,
},
# 编排规则
"arrange_rule": {
"hard_constraints": {
"ban_cross_grade": True,
"gender_separate": True,
},
"soft_constraints": {
"ban_same_class_same_lane": True,
"prefer_diff_heat": True,
"prefer_diff_lane": True,
"scramble_across_classes": False,
"center_best_athletes": False,
},
"algorithm_params": {
"max_attempts": 1000,
"timeout_seconds": 30,
},
},
# Excel列别名
"column_alias": {
"name": ["姓名", "名字", "name", "运动员名称"],
"gender": ["性别", "sex", "gender", "男女"],
"grade": ["年级", "grade", "年段"],
"class": ["班级", "class", "班别", "班级名称", "班"],
"number": ["号码簿", "号码布", "号码", "number", "参赛号", "编号"],
"events": ["参赛项目", "项目", "event", "报名项目"],
"result": ["成绩", "时间", "result", "time", "比赛成绩"],
"lane": ["跑道", "lane", "道次"],
"heat": ["组别", "heat", "组", "轮次"],
"rank": ["名次", "rank", "排名"],
"score": ["积分", "score", "point"],
},
# 积分规则
"scoring_rule": {
"rank_scores": {1: 9, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1},
"tie_handling": "same_rank", # same_rank / sequential
"record_bonus_enabled": False,
"record_bonus": 10,
"participation_score_enabled": False,
"participation_score": 1,
"relay_multiplier": 2,
"team_score_type": "class", # class / grade
"team_score_sort": "total_score", # total_score / gold_first
},
}
8. API 接口详细
8.1 接口设计规范
8.1.1 通用规范
| 规范项 | 说明 |
|---|---|
| 基础路径 | /api/v1/ |
| 请求格式 | application/json |
| 响应格式 | application/json |
| 字符编码 | UTF-8 |
| 时间格式 | ISO 8601 (YYYY-MM-DDTHH:mm:ssZ) |
| 分页参数 | page(页码,默认1)、page_size(每页条数,默认20) |
8.1.2 统一响应格式
成功响应:
json
{
"code": 200,
"message": "success",
"data": {},
"timestamp": "2026-01-20T10:30:00Z"
}
分页响应:
json
{
"code": 200,
"message": "success",
"data": {
"items": [],
"total": 100,
"page": 1,
"page_size": 20,
"total_pages": 5
},
"timestamp": "2026-01-20T10:30:00Z"
}
错误响应:
json
{
"code": 400,
"message": "参数错误",
"errors": [
{
"field": "name",
"message": "姓名不能为空"
}
],
"timestamp": "2026-01-20T10:30:00Z"
}
8.1.3 HTTP状态码
| 状态码 | 说明 |
|---|---|
| 200 | 成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 401 | 未认证 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 409 | 数据冲突 |
| 422 | 验证失败 |
| 500 | 服务器内部错误 |
8.2 认证接口(Django + JWT)
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| POST | /api/auth/login/ |
用户登录 | 公开 |
| POST | /api/auth/logout/ |
用户登出 | 已认证 |
| POST | /api/auth/refresh/ |
刷新Token | 已认证 |
| POST | /api/auth/change-password/ |
修改密码 | 已认证 |
| POST | /api/auth/reset-password/ |
重置密码(申请) | 公开 |
| GET | /api/auth/profile/ |
获取当前用户信息 | 已认证 |
| PUT | /api/auth/profile/ |
更新用户信息 | 已认证 |
8.2.1 登录接口
请求:
http
POST /api/auth/login/
Content-Type: application/json
{
"username": "zhangsan",
"password": "123456",
"role_type": "teacher" // teacher / class_teacher / student
}
响应:
json
{
"code": 200,
"message": "登录成功",
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 86400,
"user": {
"id": 1,
"username": "zhangsan",
"name": "张三",
"role": "teacher",
"avatar": "/media/avatars/zhangsan.jpg",
"class_id": null,
"class_name": null,
"permissions": ["arrange_event", "import_athletes", "enter_scores"]
}
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.3 班级管理接口
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| GET | /api/classes/ |
获取班级列表 | 所有角色 |
| GET | /api/classes/{id}/ |
获取班级详情 | 所有角色 |
| POST | /api/classes/ |
创建班级 | 体育老师 |
| PUT | /api/classes/{id}/ |
更新班级 | 体育老师 |
| DELETE | /api/classes/{id}/ |
删除班级 | 体育老师 |
| POST | /api/classes/import/ |
批量导入班级 | 体育老师 |
| GET | /api/classes/export/ |
导出班级列表 | 体育老师 |
| POST | /api/classes/{id}/create-teacher-account/ |
创建班主任账号 | 体育老师 |
8.3.1 获取班级列表
请求:
http
GET /api/classes/?grade=高一年级&page=1&page_size=20
响应:
json
{
"code": 200,
"message": "success",
"data": {
"items": [
{
"id": 1,
"name": "高一1班",
"grade": "高一年级",
"grade_order": 10,
"class_order": 1,
"code": "101",
"teacher_name": "张三",
"teacher_user": {
"id": 10,
"username": "zhangsan",
"last_login": null
},
"phone": "13812345678",
"student_count": 12,
"athlete_count": 12,
"is_participating": true,
"remark": "",
"created_at": "2026-03-01T00:00:00Z"
}
],
"total": 12,
"page": 1,
"page_size": 20,
"total_pages": 1
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.4 运动员管理接口
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| GET | /api/athletes/ |
获取运动员列表 | 所有角色 |
| GET | /api/athletes/{id}/ |
获取运动员详情 | 所有角色 |
| POST | /api/athletes/ |
创建运动员 | 体育老师/班主任 |
| PUT | /api/athletes/{id}/ |
更新运动员 | 体育老师/班主任(本班) |
| DELETE | /api/athletes/{id}/ |
删除运动员 | 体育老师/班主任(本班) |
| POST | /api/athletes/import/ |
批量导入运动员 | 体育老师 |
| GET | /api/athletes/export/ |
导出运动员列表 | 体育老师 |
| POST | /api/athletes/batch-generate-numbers/ |
批量生成号码簿 | 体育老师 |
| GET | /api/athletes/template/ |
下载导入模板 | 所有角色 |
8.4.1 获取运动员列表(带筛选)
请求:
http
GET /api/athletes/?grade=高一年级&class_id=1&gender=M&status=normal&search=张三&page=1&page_size=20
响应:
json
{
"code": 200,
"message": "success",
"data": {
"items": [
{
"id": 1,
"name": "张三",
"gender": "M",
"gender_display": "男",
"grade": "高一年级",
"class_id": 1,
"class_name": "高一1班",
"number": "1101",
"student_id": "20240001",
"phone": "13812345678",
"status": "normal",
"status_display": "正常",
"events": [
{
"id": 3,
"name": "100米"
},
{
"id": 7,
"name": "800米"
}
],
"created_at": "2026-03-01T00:00:00Z"
}
],
"total": 156,
"page": 1,
"page_size": 20,
"total_pages": 8
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.4.2 批量生成号码簿
请求:
http
POST /api/athletes/batch-generate-numbers/
Content-Type: application/json
{
"athlete_ids": [1, 2, 3, 4, 5],
"dry_run": false // 是否仅预览不保存
}
响应:
json
{
"code": 200,
"message": "success",
"data": {
"generated": 5,
"failed": 0,
"results": [
{
"athlete_id": 1,
"name": "张三",
"old_number": null,
"new_number": "1101",
"status": "success"
}
]
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.5 项目管理接口
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| GET | /api/events/ |
获取项目列表 | 所有角色 |
| GET | /api/events/{id}/ |
获取项目详情 | 所有角色 |
| POST | /api/events/ |
创建项目 | 体育老师 |
| PUT | /api/events/{id}/ |
更新项目 | 体育老师 |
| DELETE | /api/events/{id}/ |
删除项目 | 体育老师 |
| POST | /api/events/import/ |
批量导入项目 | 体育老师 |
| GET | /api/events/export/ |
导出项目列表 | 体育老师 |
| POST | /api/events/{id}/toggle/ |
启用/禁用项目 | 体育老师 |
| GET | /api/events/templates/ |
获取项目模板 | 体育老师 |
8.5.1 获取项目列表
请求:
http
GET /api/events/?category=track_sprint&gender=M&is_enabled=true
响应:
json
{
"code": 200,
"message": "success",
"data": {
"items": [
{
"id": 1,
"name": "100米",
"code": "M100",
"category": "track_sprint",
"category_display": "径赛-短跑",
"distance_type": "短跑",
"gender_limit": "M",
"gender_display": "男子",
"default_lanes": 8,
"need_heats": true,
"max_per_heat": 8,
"晋级人数": 8,
"scoring_type": "global",
"scoring_rules": {},
"sort_order": 10,
"is_enabled": true,
"registration_start": "2026-03-01T00:00:00Z",
"registration_end": "2026-03-15T23:59:59Z",
"record": "11.23秒",
"registration_count": 32,
"arranged": true
}
],
"total": 15
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.6 报名管理接口
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| GET | /api/registrations/ |
获取报名列表 | 所有角色 |
| POST | /api/registrations/ |
添加报名 | 体育老师/班主任 |
| DELETE | /api/registrations/{id}/ |
取消报名 | 体育老师/班主任 |
| POST | /api/registrations/batch/ |
批量报名 | 体育老师/班主任 |
| PUT | /api/registrations/{id}/approve/ |
审核报名 | 体育老师 |
| GET | /api/registrations/statistics/ |
报名统计 | 所有角色 |
| POST | /api/registrations/import/ |
批量导入报名 | 体育老师 |
| GET | /api/registrations/export/ |
导出报名表 | 体育老师/班主任 |
8.6.1 批量报名
请求:
http
POST /api/registrations/batch/
Content-Type: application/json
{
"class_id": 1,
"athlete_ids": [1, 2, 3, 4],
"event_ids": [3, 7]
}
响应:
json
{
"code": 200,
"message": "success",
"data": {
"success": 8,
"failed": 0,
"warnings": [],
"details": [
{
"athlete_id": 1,
"athlete_name": "张三",
"event_id": 3,
"event_name": "100米",
"status": "success"
}
]
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.6.2 报名统计
响应:
json
{
"code": 200,
"message": "success",
"data": {
"total_registrations": 324,
"total_athletes": 186,
"total_events": 15,
"pending_review": 30,
"approved": 156,
"rejected": 0,
"by_event": [
{
"event_id": 3,
"event_name": "100米",
"gender": "男子",
"count": 32,
"class_count": 8,
"violations": [
{
"class_name": "高一1班",
"count": 4,
"limit": 3,
"exceed": 1
}
]
}
],
"by_class": [
{
"class_id": 1,
"class_name": "高一1班",
"athlete_count": 12,
"registration_count": 18,
"avg_per_athlete": 1.5
}
]
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.7 编排算法接口(FastAPI)
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| POST | /api/arrange/{event_id}/ |
执行编排 | 体育老师 |
| POST | /api/arrange/preview/ |
预览编排(不保存) | 体育老师 |
| GET | /api/arrange/{event_id}/ |
获取编排结果 | 所有角色 |
| PUT | /api/arrange/{event_id}/ |
更新编排结果(手动调整) | 体育老师 |
| DELETE | /api/arrange/{event_id}/ |
清空编排结果 | 体育老师 |
| GET | /api/arrange/export/{event_id}/ |
导出道次表 | 体育老师 |
| POST | /api/arrange/batch/ |
批量编排多个项目 | 体育老师 |
8.7.1 执行编排
请求:
http
POST /api/arrange/3/
Content-Type: application/json
{
"grade": "高一年级",
"gender": "M",
"rule_config": {
"ban_same_class_same_lane": true,
"prefer_diff_heat": true,
"prefer_diff_lane": true
},
"force": false // 是否覆盖已有编排结果
}
响应:
json
{
"code": 200,
"message": "编排成功",
"data": {
"event_id": 3,
"event_name": "100米",
"grade": "高一年级",
"gender": "M",
"heats": [
{
"heat_no": 1,
"lanes": [
{
"lane": 1,
"athlete": {
"id": 1,
"name": "张三",
"number": "1101",
"class_name": "高一1班"
}
},
{
"lane": 2,
"athlete": null
}
]
}
],
"statistics": {
"total_athletes": 24,
"total_heats": 3,
"avg_per_heat": 8,
"empty_lanes": 0
},
"warnings": [
"高一1班有5人报名,无法完全满足'同班不同组'约束,已尽量分散"
],
"execution_time_ms": 234
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.7.2 批量编排
请求:
http
POST /api/arrange/batch/
Content-Type: application/json
{
"event_ids": [1, 2, 3, 4, 5],
"rule_config": {
"ban_same_class_same_lane": true,
"prefer_diff_heat": true
}
}
响应:
json
{
"code": 200,
"message": "批量编排完成",
"data": {
"total": 5,
"success": 4,
"failed": 1,
"results": [
{
"event_id": 1,
"event_name": "50米",
"status": "success",
"message": "编排成功"
},
{
"event_id": 2,
"event_name": "50米",
"status": "skipped",
"message": "无报名运动员"
}
]
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.8 成绩管理接口
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| GET | /api/results/ |
获取成绩列表 | 所有角色 |
| POST | /api/results/ |
录入成绩 | 体育老师 |
| PUT | /api/results/{id}/ |
修改成绩 | 体育老师 |
| DELETE | /api/results/{id}/ |
删除成绩 | 体育老师 |
| POST | /api/results/import/ |
批量导入成绩 | 体育老师 |
| GET | /api/results/export/{event_id}/ |
导出成绩单 | 体育老师 |
| POST | /api/results/calculate-ranking/{event_id}/ |
计算排名 | 体育老师 |
| GET | /api/results/ranking/{event_id}/ |
获取排名 | 所有角色 |
8.8.1 录入成绩
请求:
http
POST /api/results/
Content-Type: application/json
{
"athlete_id": 1,
"event_id": 3,
"heat": 1,
"lane": 1,
"raw_time": "12.34",
"wind_speed": 1.2,
"status": "valid"
}
响应:
json
{
"code": 200,
"message": "成绩录入成功",
"data": {
"id": 1,
"athlete_id": 1,
"athlete_name": "张三",
"event_id": 3,
"event_name": "100米",
"heat": 1,
"lane": 1,
"raw_time": "12.34",
"time_seconds": 12.34,
"status": "valid",
"entered_at": "2026-01-20T10:30:00Z"
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.8.2 获取成绩排名
响应:
json
{
"code": 200,
"message": "success",
"data": {
"event_id": 3,
"event_name": "100米",
"grade": "高一年级",
"gender": "男子",
"rankings": [
{
"rank": 1,
"athlete_id": 1,
"name": "张三",
"number": "1101",
"class_name": "高一1班",
"raw_time": "12.34",
"time_seconds": 12.34,
"heat": 1,
"lane": 1,
"score": 9,
"is_record": false
},
{
"rank": 2,
"athlete_id": 2,
"name": "李四",
"number": "1102",
"class_name": "高一1班",
"raw_time": "12.45",
"time_seconds": 12.45,
"heat": 1,
"lane": 5,
"score": 7,
"is_record": false
}
],
"statistics": {
"total": 24,
"with_scores": 24,
"average_time": 13.02,
"best_time": 12.34,
"record_broken": false
}
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.9 报表统计接口
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| GET | /api/statistics/team-score/ |
团体总分榜 | 所有角色 |
| GET | /api/statistics/individual-score/ |
个人积分榜 | 所有角色 |
| GET | /api/statistics/registration/ |
报名统计 | 所有角色 |
| GET | /api/statistics/record/ |
破纪录统计 | 所有角色 |
| POST | /api/statistics/export/ |
导出统计报表 | 体育老师 |
| GET | /api/statistics/order-book/ |
生成秩序册 | 体育老师 |
| GET | /api/statistics/result-book/ |
生成成绩册 | 体育老师 |
8.9.1 团体总分榜
响应:
json
{
"code": 200,
"message": "success",
"data": {
"items": [
{
"rank": 1,
"class_id": 1,
"class_name": "高一1班",
"grade": "高一年级",
"total_score": 156,
"gold": 5,
"silver": 4,
"bronze": 3,
"details": {
"by_event": [
{
"event_name": "100米",
"score": 25,
"gold": 1,
"silver": 1,
"bronze": 0
}
]
}
}
],
"updated_at": "2026-01-20T10:30:00Z"
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.10 Excel 导入导出接口(FastAPI)
| 方法 | 端点 | 功能 | 权限 |
|---|---|---|---|
| POST | /api/excel/import/athletes/ |
导入运动员 | 体育老师 |
| POST | /api/excel/import/scores/ |
导入成绩 | 体育老师 |
| POST | /api/excel/import/registrations/ |
导入报名 | 体育老师 |
| POST | /api/excel/import/preview/ |
预览导入(列名检测) | 体育老师 |
| GET | /api/excel/template/{type}/ |
下载导入模板 | 所有角色 |
| POST | /api/excel/export/arrangement/ |
导出道次表 | 体育老师 |
| POST | /api/excel/export/order-book/ |
导出秩序册 | 体育老师 |
| POST | /api/excel/export/result-book/ |
导出成绩册 | 体育老师 |
8.10.1 预览导入(列名检测)
请求:
http
POST /api/excel/import/preview/
Content-Type: multipart/form-data
file: athletes.xlsx
import_type: athletes
响应:
json
{
"code": 200,
"message": "success",
"data": {
"excel_columns": ["姓名", "性别", "年级", "班级", "号码布", "参赛项目"],
"detected_mapping": {
"name": "姓名",
"gender": "性别",
"grade": "年级",
"class": "班级",
"number": "号码布",
"events": "参赛项目"
},
"missing_required": [],
"unmatched_columns": [],
"preview_data": [
{
"姓名": "张三",
"性别": "男",
"年级": "高一年级",
"班级": "高一1班",
"号码布": "1101",
"参赛项目": "100米,800米"
}
],
"total_rows": 156,
"validation_summary": {
"valid": true,
"errors": [],
"warnings": []
}
},
"timestamp": "2026-01-20T10:30:00Z"
}
8.10.2 确认导入
请求:
http
POST /api/excel/import/athletes/
Content-Type: application/json
{
"file_id": "temp_123456",
"mapping": {
"name": "姓名",
"gender": "性别",
"grade": "年级",
"class": "班级",
"number": "号码布",
"events": "参赛项目"
},
"options": {
"skip_on_error": false,
"auto_generate_number": true,
"validate_unique": true
}
}
响应:
json
{
"code": 200,
"message": "导入完成",
"data": {
"total": 156,
"success": 151,
"failed": 5,
"errors": [
{
"row": 3,
"field": "性别",
"message": "性别格式错误,应为'男'或'女'"
},
{
"row": 7,
"field": "号码簿",
"message": "号码簿不能为空"
}
],
"import_id": "imp_20260120_001",
"duration_ms": 2345
},
"timestamp": "2026-01-20T10:30:00Z"
}
9. 前端界面详细
9.1 路由配置(懒加载)
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
// 体育老师端路由
{
path: '/teacher',
component: () => import('@/layouts/TeacherLayout.vue'),
meta: { role: ['super_admin', 'teacher'], title: '体育老师端' },
children: [
{
path: 'dashboard',
name: 'TeacherDashboard',
component: () => import('@/views/teacher/Dashboard.vue'),
meta: { title: '仪表盘' }
},
{
path: 'classes',
name: 'ClassManagement',
component: () => import('@/views/teacher/ClassManagement.vue'),
meta: { title: '班级管理' }
},
{
path: 'athletes',
name: 'AthleteManagement',
component: () => import('@/views/teacher/AthleteManagement.vue'),
meta: { title: '运动员管理' }
},
{
path: 'events',
name: 'EventManagement',
component: () => import('@/views/teacher/EventManagement.vue'),
meta: { title: '项目管理' }
},
{
path: 'registrations',
name: 'RegistrationManagement',
component: () => import('@/views/teacher/RegistrationManagement.vue'),
meta: { title: '报名管理' }
},
{
path: 'arrange',
name: 'ArrangeCenter',
component: () => import('@/views/teacher/ArrangeCenter.vue'),
meta: { title: '编排中心' }
},
{
path: 'scores',
name: 'ScoreEntry',
component: () => import('@/views/teacher/ScoreEntry.vue'),
meta: { title: '成绩录入' }
},
{
path: 'ranking',
name: 'RankingView',
component: () => import('@/views/teacher/RankingView.vue'),
meta: { title: '成绩排名' }
},
{
path: 'statistics',
name: 'StatisticsReport',
component: () => import('@/views/teacher/StatisticsReport.vue'),
meta: { title: '统计报表' }
},
{
path: 'settings',
name: 'SystemSettings',
component: () => import('@/views/teacher/SystemSettings.vue'),
meta: { title: '系统设置' }
}
]
},
// 班主任端路由
{
path: '/class-teacher',
component: () => import('@/layouts/ClassTeacherLayout.vue'),
meta: { role: ['class_teacher'], title: '班主任端' },
children: [
{
path: 'dashboard',
name: 'ClassDashboard',
component: () => import('@/views/class-teacher/Dashboard.vue'),
meta: { title: '班级看板' }
},
{
path: 'athletes',
name: 'ClassAthletes',
component: () => import('@/views/class-teacher/Athletes.vue'),
meta: { title: '本班运动员' }
},
{
path: 'registration',
name: 'ClassRegistration',
component: () => import('@/views/class-teacher/Registration.vue'),
meta: { title: '本班报名' }
},
{
path: 'schedule',
name: 'ClassSchedule',
component: () => import('@/views/class-teacher/Schedule.vue'),
meta: { title: '本班赛程' }
},
{
path: 'results',
name: 'ClassResults',
component: () => import('@/views/class-teacher/Results.vue'),
meta: { title: '本班成绩' }
}
]
},
// 学生端路由(移动端)
{
path: '/student',
component: () => import('@/layouts/StudentLayout.vue'),
meta: { role: ['student'], title: '学生端' },
children: [
{
path: 'home',
name: 'StudentHome',
component: () => import('@/views/student/Home.vue'),
meta: { title: '首页' }
},
{
path: 'schedule',
name: 'StudentSchedule',
component: () => import('@/views/student/Schedule.vue'),
meta: { title: '我的赛程' }
},
{
path: 'results',
name: 'StudentResults',
component: () => import('@/views/student/Results.vue'),
meta: { title: '我的成绩' }
},
{
path: 'events',
name: 'StudentEvents',
component: () => import('@/views/student/Events.vue'),
meta: { title: '赛事信息' }
},
{
path: 'profile',
name: 'StudentProfile',
component: () => import('@/views/student/Profile.vue'),
meta: { title: '个人中心' }
}
]
},
// 公共路由
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录' }
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: { title: '页面不存在' }
},
{
path: '/:pathMatch(.*)*',
redirect: '/404'
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('access_token')
const userRole = localStorage.getItem('user_role')
if (to.path !== '/login' && !token) {
next('/login')
} else if (to.meta.role && !to.meta.role.includes(userRole)) {
next('/404')
} else {
next()
}
})
export default router
9.2 全局组件注册
javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')
9.3 主要UI组件设计
9.3.1 数据表格组件
vue
<!-- components/DataTable.vue -->
<template>
<div class="data-table">
<!-- 工具栏 -->
<div class="table-toolbar">
<div class="toolbar-left">
<slot name="toolbar-left"></slot>
</div>
<div class="toolbar-right">
<el-input
v-model="searchKeyword"
placeholder="搜索"
prefix-icon="Search"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
/>
<el-button @click="handleRefresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
<slot name="toolbar-right"></slot>
</div>
</div>
<!-- 表格 -->
<el-table
:data="tableData"
v-loading="loading"
border
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" v-if="showSelection" />
<el-table-column
v-for="col in columns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
:fixed="col.fixed"
:sortable="col.sortable"
>
<template #default="{ row }">
<slot :name="`col-${col.prop}`" :row="row">
{{ row[col.prop] }}
</slot>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right" v-if="showActions">
<template #default="{ row }">
<slot name="actions" :row="row"></slot>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="table-pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
columns: { type: Array, required: true },
fetchData: { type: Function, required: true },
showSelection: { type: Boolean, default: false },
showActions: { type: Boolean, default: true }
})
const emit = defineEmits(['selection-change'])
const tableData = ref([])
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
const searchKeyword = ref('')
const selectedRows = ref([])
const loadData = async () => {
loading.value = true
try {
const res = await props.fetchData({
page: currentPage.value,
page_size: pageSize.value,
search: searchKeyword.value
})
tableData.value = res.data.items
total.value = res.data.total
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = () => {
currentPage.value = 1
loadData()
}
const handleRefresh = () => {
loadData()
}
const handlePageChange = () => {
loadData()
}
const handleSizeChange = () => {
currentPage.value = 1
loadData()
}
const handleSelectionChange = (val) => {
selectedRows.value = val
emit('selection-change', val)
}
watch(() => props.fetchData, () => {
loadData()
}, { immediate: true })
</script>
<style scoped>
.data-table {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.table-toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.toolbar-right {
display: flex;
gap: 12px;
align-items: center;
}
.table-pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>
10. 私有部署方案
10.1 部署架构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 学校内网服务器 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Docker Host │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Nginx │ │ Django │ │ FastAPI │ │ │
│ │ │ Container │ │ Container │ │ Container │ │ │
│ │ │ Port:80/443 │ │ Port:8000 │ │ Port:8001 │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ │ │ │ │ │ │
│ │ └──────────────────┼──────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────┴────────┐ │ │
│ │ │ │ │ │
│ │ ┌──────┴──────┐ ┌──────┴──────┐ │ │
│ │ │ PostgreSQL │ │ Redis │ │ │
│ │ │ Container │ │ Container │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 数据卷(持久化) │ │ │
│ │ │ ./data/postgres - 数据库文件 │ │ │
│ │ │ ./data/uploads - 上传文件 │ │ │
│ │ │ ./data/exports - 导出文件 │ │ │
│ │ │ ./data/logs - 日志文件 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 学校内网客户端 │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 体育老师电脑 │ │ 班主任电脑 │ │ 学生手机 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
10.2 硬件配置要求
| 部署规模 | CPU | 内存 | 硬盘 | 带宽 | 适用场景 |
|---|---|---|---|---|---|
| 小型(<500人) | 2核 | 4GB | 50GB | 10Mbps | 小学、初中 |
| 中型(500-1500人) | 4核 | 8GB | 100GB | 20Mbps | 高中、完中 |
| 大型(>1500人) | 8核 | 16GB | 200GB | 50Mbps | 大型学校、区级 |
10.3 软件环境要求
| 软件 | 版本 | 说明 |
|---|---|---|
| Docker | 20.10+ | 容器运行时 |
| Docker Compose | 2.0+ | 容器编排 |
| Git | 2.0+ | 代码拉取 |
| 操作系统 | Ubuntu 20.04/22.04 或 CentOS 7+ | 推荐Ubuntu |
| 操作系统* | Windows | 临时运行+Python |
10.4 Docker Compose 配置
yaml
# docker-compose.yml
version: '3.8'
services:
# PostgreSQL 数据库
postgres:
image: postgres:15
container_name: sports_postgres
environment:
POSTGRES_DB: sports_meet
POSTGRES_USER: sports_admin
POSTGRES_PASSWORD: ${DB_PASSWORD}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- ./data/postgres:/var/lib/postgresql/data
- ./backups:/backups
ports:
- "5432:5432"
restart: unless-stopped
networks:
- sports_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sports_admin"]
interval: 10s
timeout: 5s
retries: 5
# Redis 缓存
redis:
image: redis:7-alpine
container_name: sports_redis
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- ./data/redis:/data
ports:
- "6379:6379"
restart: unless-stopped
networks:
- sports_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Django 后端
django:
build:
context: ./backend/django
dockerfile: Dockerfile
container_name: sports_django
environment:
- DJANGO_SETTINGS_MODULE=config.settings.production
- DATABASE_URL=postgresql://sports_admin:${DB_PASSWORD}@postgres:5432/sports_meet
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
- SECRET_KEY=${DJANGO_SECRET_KEY}
- DEBUG=False
- ALLOWED_HOSTS=${ALLOWED_HOSTS}
volumes:
- ./backend/django:/app
- ./data/uploads:/app/media/uploads
- ./data/exports:/app/media/exports
- ./data/logs:/app/logs
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- sports_network
command: >
sh -c "
python manage.py migrate &&
python manage.py collectstatic --noinput &&
gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 4 --threads 2
"
# FastAPI 算法网关
fastapi:
build:
context: ./backend/fastapi
dockerfile: Dockerfile
container_name: sports_fastapi
environment:
- DATABASE_URL=postgresql://sports_admin:${DB_PASSWORD}@postgres:5432/sports_meet
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
- SECRET_KEY=${FASTAPI_SECRET_KEY}
volumes:
- ./backend/fastapi:/app
- ./data/exports:/app/exports
ports:
- "8001:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- sports_network
command: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2
# Nginx 反向代理
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
container_name: sports_nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./frontend/dist:/usr/share/nginx/html
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/ssl:/etc/nginx/ssl
- ./data/uploads:/usr/share/nginx/html/uploads
- ./data/logs/nginx:/var/log/nginx
depends_on:
- django
- fastapi
restart: unless-stopped
networks:
- sports_network
# 备份服务(可选)
backup:
image: alpine:latest
container_name: sports_backup
volumes:
- ./data/postgres:/source/postgres
- ./data/uploads:/source/uploads
- ./backups:/backups
environment:
- BACKUP_RETENTION_DAYS=30
command: |
sh -c "
while true; do
BACKUP_DATE=\$(date +%Y%m%d_%H%M%S)
tar -czf /backups/backup_\${BACKUP_DATE}.tar.gz /source/postgres /source/uploads
find /backups -name 'backup_*.tar.gz' -mtime +${BACKUP_RETENTION_DAYS} -delete
sleep 86400
done
"
restart: unless-stopped
networks:
- sports_network
networks:
sports_network:
driver: bridge
volumes:
postgres_data:
redis_data:
uploads_data:
exports_data:
logs_data:
10.5 Dockerfile 配置
10.5.1 Django Dockerfile
dockerfile
# backend/django/Dockerfile
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
libpq-dev \
libjpeg-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . .
# 创建必要目录
RUN mkdir -p /app/media/uploads /app/media/exports /app/logs /app/static
# 收集静态文件
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2"]
10.5.2 FastAPI Dockerfile
dockerfile
# backend/fastapi/Dockerfile
FROM python:3.11-slim
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
g++ \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
10.5.3 Nginx Dockerfile
dockerfile
# nginx/Dockerfile
FROM nginx:alpine
# 复制配置文件
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf
# 创建SSL目录
RUN mkdir -p /etc/nginx/ssl
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
10.6 Nginx 配置
nginx
# nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 50M;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss;
# 上游服务
upstream django {
server django:8000 max_fails=3 fail_timeout=30s;
}
upstream fastapi {
server fastapi:8000 max_fails=3 fail_timeout=30s;
}
# 主配置
include /etc/nginx/conf.d/*.conf;
}
nginx
# nginx/conf.d/default.conf
server {
listen 80;
server_name _;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name sports.school.edu.cn;
# SSL配置
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 静态文件
location /static/ {
alias /usr/share/nginx/html/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# 媒体文件
location /media/ {
alias /usr/share/nginx/html/uploads/;
expires 7d;
add_header Cache-Control "public";
}
# 前端页面
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
expires 1h;
}
# Django API
location /api/ {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# FastAPI 算法网关
location /api/arrange/ {
proxy_pass http://fastapi;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
10.7 环境变量配置
bash
# .env 文件
# 数据库配置
DB_PASSWORD=StrongPassword123!
REDIS_PASSWORD=RedisPassword123!
# Django配置
DJANGO_SECRET_KEY=django-insecure-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ALLOWED_HOSTS=sports.school.edu.cn,localhost,127.0.0.1
# FastAPI配置
FASTAPI_SECRET_KEY=fastapi-secret-key-xxxxxxxxxxxxxxxx
# 管理员账号
ADMIN_USERNAME=admin
ADMIN_PASSWORD=Admin123!
ADMIN_EMAIL=admin@school.edu.cn
10.8 部署步骤
bash
#!/bin/bash
# deploy.sh - 一键部署脚本
set -e
echo "=========================================="
echo "运动会智能编排系统 - 一键部署脚本"
echo "=========================================="
# 1. 检查Docker
if ! command -v docker &> /dev/null; then
echo "错误: Docker未安装,请先安装Docker"
exit 1
fi
# 2. 创建目录结构
echo "创建目录结构..."
mkdir -p data/{postgres,redis,uploads,exports,logs}
mkdir -p backend/{django,fastapi}
mkdir -p frontend/dist
mkdir -p nginx/ssl
mkdir -p backups
# 3. 复制配置文件
echo "复制配置文件..."
cp -r templates/backend/django/* backend/django/
cp -r templates/backend/fastapi/* backend/fastapi/
cp -r templates/frontend/dist/* frontend/dist/
cp templates/nginx/nginx.conf nginx/
cp templates/nginx/default.conf nginx/conf.d/
# 4. 生成SSL证书(自签名,生产环境请使用正式证书))(可选)
if [ ! -f nginx/ssl/server.crt ]; then
echo "生成SSL证书..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout nginx/ssl/server.key \
-out nginx/ssl/server.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=School/CN=sports.school.edu.cn"
fi
# 5. 设置环境变量
if [ ! -f .env ]; then
echo "创建环境变量文件..."
cp .env.example .env
echo "请修改.env文件中的密码配置"
fi
# 6. 拉取镜像并启动
echo "启动服务..."
docker-compose pull
docker-compose up -d
# 7. 等待服务启动
echo "等待服务启动..."
sleep 10
# 8. 创建超级管理员
echo "创建超级管理员..."
docker exec sports_django python manage.py createsuperuser --noinput \
--username admin --email admin@school.edu.cn || true
# 9. 初始化数据
echo "初始化系统配置..."
docker exec sports_django python manage.py init_config
# 10. 检查服务状态
echo "检查服务状态..."
docker-compose ps
echo "=========================================="
echo "部署完成!"
echo "访问地址: https://sports.school.edu.cn"
echo "管理员账号: admin"
echo "=========================================="
10.9 数据备份与恢复
10.9.1 自动备份脚本
bash
#!/bin/bash
# backup.sh - 数据备份脚本
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
# 备份数据库
docker exec sports_postgres pg_dump -U sports_admin sports_meet > ${BACKUP_DIR}/db_${DATE}.sql
# 备份上传文件
tar -czf ${BACKUP_DIR}/uploads_${DATE}.tar.gz -C /data/uploads .
# 备份导出文件
tar -czf ${BACKUP_DIR}/exports_${DATE}.tar.gz -C /data/exports .
# 删除旧备份
find ${BACKUP_DIR} -name "db_*.sql" -mtime +${RETENTION_DAYS} -delete
find ${BACKUP_DIR} -name "uploads_*.tar.gz" -mtime +${RETENTION_DAYS} -delete
find ${BACKUP_DIR} -name "exports_*.tar.gz" -mtime +${RETENTION_DAYS} -delete
echo "Backup completed at $(date)"
10.9.2 数据恢复脚本
bash
#!/bin/bash
# restore.sh - 数据恢复脚本
BACKUP_FILE=$1
if [ -z "$BACKUP_FILE" ]; then
echo "Usage: ./restore.sh <backup_file>"
exit 1
fi
# 停止服务
docker-compose stop
# 恢复数据库
docker exec -i sports_postgres psql -U sports_admin sports_meet < ${BACKUP_FILE}
# 恢复上传文件
tar -xzf ${BACKUP_DIR}/${BACKUP_FILE} -C /
# 启动服务
docker-compose start
echo "Restore completed from ${BACKUP_FILE}"
10.10 监控与告警
10.10.1 健康检查端点
python
# health.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0",
"services": {
"database": check_database(),
"redis": check_redis(),
"storage": check_storage()
}
}
10.10.2 日志收集配置
python
# logging配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/app/logs/sports.log',
'maxBytes': 1024 * 1024 * 100, # 100MB
'backupCount': 10,
'formatter': 'verbose',
},
'error_file': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/app/logs/errors.log',
'maxBytes': 1024 * 1024 * 50, # 50MB
'backupCount': 5,
'formatter': 'verbose',
},
},
'root': {
'handlers': ['file', 'error_file'],
'level': 'INFO',
},
}
11. 附录
11.1 术语表
| 术语 | 说明 |
|---|---|
| 道次 | 跑道上的位置编号 |
| 组别 | 比赛的轮次/分组 |
| 径赛 | 跑道上的跑步比赛 |
| 田赛 | 跳跃、投掷类比赛 |
| 号码簿 | 运动员的参赛编号 |
| 编排 | 分配运动员的道次和组别 |
| 秩序册 | 比赛秩序手册,包含赛程、道次等 |
| 成绩册 | 比赛成绩汇总手册 |
| 破纪录 | 打破原有比赛纪录 |
| DQ | Disqualified,取消成绩 |
| DNS | Did Not Start,未参赛 |
| DNF | Did Not Finish,未完赛 |
11.2 常见问题解答(FAQ)
Q1: 系统支持多少个项目同时编排?
A: 系统支持无限个项目,编排算法的时间复杂度为O(n²),建议每个项目单独编排。
Q2: 如何处理两个运动员成绩完全相同的情况?
A: 系统支持两种并列处理方式:
- 同名次:两人并列第一,下一个是第三名
- 名次顺延:两人并列第一,下一个是第二名
Q3: 数据迁移如何进行?
A: 系统提供Excel导入导出功能,可以通过模板进行数据迁移。同时也支持数据库级别的备份恢复。
Q4: 系统是否支持多学校使用?
A: 当前版本为单学校设计。如需多学校支持,可在数据库层面增加school_id字段进行隔离。
Q5: 如何自定义报表模板?
A: 系统提供报表模板配置功能,可以在后台自定义报表的样式和内容。高级定制需要修改前端代码。