主页 > 人工智能  > 

Java全栈项目-田径运动会管理系统

Java全栈项目-田径运动会管理系统
项目介绍

本项目是一个基于Java全栈技术开发的田径运动会管理系统,旨在为学校、体育场馆提供一个完整的运动会管理解决方案。系统采用前后端分离架构,实现了运动员管理、比赛项目管理、成绩录入、数据统计等核心功能。

技术栈 后端技术 Spring Boot 2.7.0:核心框架Spring Security:安全框架MyBatis-Plus:ORM框架MySQL 8.0:数据库Redis:缓存服务JWT:身份认证 前端技术 Vue 3:前端框架Element Plus:UI组件库Axios:HTTP客户端Vuex:状态管理Vue Router:路由管理 核心功能 运动员管理 运动员信息录入与维护参赛项目分配号码簿生成运动员成绩查询 比赛项目管理 项目设置与编排赛程安排场地分配裁判员分配 成绩管理 实时成绩录入成绩审核成绩公示破纪录提醒 数据统计 团体总分排名单项成绩排名奖牌榜统计数据可视化展示 项目特点

实时数据更新

采用WebSocket技术实现成绩实时推送Redis缓存确保高并发场景下的数据一致性

高度可配置

灵活的项目设置可自定义计分规则支持多种赛制

安全性

基于RBAC的权限管理操作日志记录数据备份恢复

用户体验

响应式设计多终端适配直观的数据展示 项目部署 环境要求 JDK 1.8+Maven 3.6+MySQL 8.0+Redis 6.0+Node.js 14+ 部署步骤 后端部署 # 克隆项目 git clone github /username/sports-meet-manager.git # 进入项目目录 cd sports-meet-manager # 编译打包 mvn clean package # 运行项目 java -jar target/sports-meet-manager.jar 前端部署 # 进入前端项目目录 cd web # 安装依赖 npm install # 编译打包 npm run build # 部署到nginx cp -r dist/* /usr/share/nginx/html/ 项目展望

功能扩展

引入AI识别技术,实现自动计时增加移动端应用支持更多类型的运动会

性能优化

引入分布式架构优化数据处理算法提升系统并发能力

用户体验提升

完善数据可视化优化操作流程增加个性化配置 总结

本项目采用主流的Java全栈技术栈,实现了一个功能完善、性能稳定的运动会管理系统。通过前后端分离的架构设计,确保了系统的可维护性和扩展性。项目在实际应用中取得了良好的效果,为运动会的组织和管理提供了有力的技术支持。

运动会管理系统模块 1. 数据库设计 1.1 运动员管理相关表 -- 运动员基本信息表 CREATE TABLE t_athlete ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT NOT NULL COMMENT '性别:0-女,1-男', birth_date DATE NOT NULL COMMENT '出生日期', id_card VARCHAR(18) UNIQUE COMMENT '身份证号', team_id BIGINT NOT NULL COMMENT '代表队ID', phone VARCHAR(20) COMMENT '联系电话', photo_url VARCHAR(255) COMMENT '照片URL', status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (team_id) REFERENCES t_team(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动员信息表'; -- 代表队表 CREATE TABLE t_team ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '代表队名称', leader_name VARCHAR(50) COMMENT '领队姓名', phone VARCHAR(20) COMMENT '联系电话', status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代表队表'; -- 运动员参赛项目关联表 CREATE TABLE t_athlete_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, athlete_id BIGINT NOT NULL COMMENT '运动员ID', event_id BIGINT NOT NULL COMMENT '比赛项目ID', bib_number VARCHAR(20) NOT NULL COMMENT '号码簿编号', track_number INT COMMENT '道次号', group_number VARCHAR(20) COMMENT '分组编号', status TINYINT DEFAULT 1 COMMENT '状态:0-弃权,1-正常', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (athlete_id) REFERENCES t_athlete(id), FOREIGN KEY (event_id) REFERENCES t_event(id), UNIQUE KEY uk_athlete_event (athlete_id, event_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动员参赛项目关联表'; -- 运动员成绩表 CREATE TABLE t_athlete_result ( id BIGINT PRIMARY KEY AUTO_INCREMENT, athlete_event_id BIGINT NOT NULL COMMENT '运动员参赛关联ID', result VARCHAR(20) NOT NULL COMMENT '成绩', ranking INT COMMENT '名次', is_broken_record TINYINT DEFAULT 0 COMMENT '是否破记录:0-否,1-是', status TINYINT DEFAULT 1 COMMENT '状态:0-无效,1-有效', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (athlete_event_id) REFERENCES t_athlete_event(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动员成绩表'; 1.2 比赛项目管理相关表 -- 比赛项目表 CREATE TABLE t_event ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '项目名称', type TINYINT NOT NULL COMMENT '项目类型:1-田赛,2-径赛', gender_limit TINYINT NOT NULL COMMENT '性别限制:0-女,1-男', age_min INT COMMENT '最小年龄限制', age_max INT COMMENT '最大年龄限制', max_participants INT COMMENT '最大参赛人数', current_record VARCHAR(20) COMMENT '当前记录', record_holder VARCHAR(50) COMMENT '记录保持者', status TINYINT DEFAULT 1 COMMENT '状态:0-关闭,1-开放', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='比赛项目表'; -- 赛程表 CREATE TABLE t_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id BIGINT NOT NULL COMMENT '比赛项目ID', venue_id BIGINT NOT NULL COMMENT '场地ID', start_time DATETIME NOT NULL COMMENT '开始时间', end_time DATETIME NOT NULL COMMENT '结束时间', round_type TINYINT NOT NULL COMMENT '轮次:1-预赛,2-决赛', status TINYINT DEFAULT 1 COMMENT '状态:0-取消,1-正常', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (event_id) REFERENCES t_event(id), FOREIGN KEY (venue_id) REFERENCES t_venue(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='赛程表'; -- 场地表 CREATE TABLE t_venue ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '场地名称', type TINYINT NOT NULL COMMENT '场地类型:1-田赛场地,2-径赛场地', location VARCHAR(255) COMMENT '位置描述', status TINYINT DEFAULT 1 COMMENT '状态:0-维护中,1-可用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地表'; -- 裁判员表 CREATE TABLE t_referee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT NOT NULL COMMENT '性别:0-女,1-男', phone VARCHAR(20) COMMENT '联系电话', level VARCHAR(20) COMMENT '裁判等级', status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='裁判员表'; -- 裁判员分配表 CREATE TABLE t_referee_assignment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, referee_id BIGINT NOT NULL COMMENT '裁判员ID', schedule_id BIGINT NOT NULL COMMENT '赛程ID', role_type TINYINT NOT NULL COMMENT '角色:1-主裁判,2-副裁判,3-检录员', status TINYINT DEFAULT 1 COMMENT '状态:0-取消,1-正常', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (referee_id) REFERENCES t_referee(id), FOREIGN KEY (schedule_id) REFERENCES t_schedule(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='裁判员分配表'; 2. 后端实现 2.1 项目结构 src/main/java/com/sports ├── common │ ├── config │ ├── exception │ └── util ├── controller ├── service ├── mapper └── model ├── entity ├── dto └── vo 2.2 核心代码实现 2.2.1 运动员管理 // AthleteController.java @RestController @RequestMapping("/api/athletes") @Slf4j public class AthleteController { @Autowired private AthleteService athleteService; @PostMapping public ResponseEntity<AthleteVO> createAthlete(@RequestBody @Valid AthleteDTO athleteDTO) { return ResponseEntity.ok(athleteService.createAthlete(athleteDTO)); } #### 3.2.2 比赛项目管理页面 ```vue <!-- views/event/EventList.vue --> <template> <div class="event-list"> <div class="header"> <h1>比赛项目管理</h1> <el-button type="primary" @click="showCreateDialog">新增项目</el-button> </div> <!-- 搜索表单 --> <el-form :model="queryForm" :inline="true" class="search-form"> <el-form-item label="项目名称"> <el-input v-model="queryForm.name" placeholder="请输入项目名称"></el-input> </el-form-item> <el-form-item label="项目类型"> <el-select v-model="queryForm.type" placeholder="请选择项目类型"> <el-option label="田赛" :value="1"></el-option> <el-option label="径赛" :value="2"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> <!-- 项目列表 --> <el-table :data="events" border stripe> <el-table-column prop="name" label="项目名称"></el-table-column> <el-table-column prop="type" label="项目类型"> @PutMapping("/{id}") public ResponseEntity<AthleteVO> updateAthlete(@PathVariable Long id, @RequestBody @Valid AthleteDTO athleteDTO) { return ResponseEntity.ok(athleteService.updateAthlete(id, athleteDTO)); } @PostMapping("/batch") public ResponseEntity<List<AthleteVO>> batchImport(@RequestParam("file") MultipartFile file) { return ResponseEntity.ok(athleteService.batchImport(file)); } @GetMapping public ResponseEntity<Page<AthleteVO>> getAthletes(AthleteQueryDTO queryDTO, @PageableDefault Pageable pageable) { return ResponseEntity.ok(athleteService.getAthletes(queryDTO, pageable)); } } // AthleteServiceImpl.java @Service @Slf4j public class AthleteServiceImpl implements AthleteService { @Autowired private AthleteMapper athleteMapper; @Autowired private FileService fileService; @Override @Transactional public AthleteVO createAthlete(AthleteDTO athleteDTO) { // 验证身份证号 validateIdCard(athleteDTO.getIdCard()); // 上传照片 String photoUrl = fileService.uploadPhoto(athleteDTO.getPhoto()); // 保存运动员信息 Athlete athlete = convertToEntity(athleteDTO); athlete.setPhotoUrl(photoUrl); athleteMapper.insert(athlete); return convertToVO(athlete); } @Override @Transactional(readOnly = true) public Page<AthleteVO> getAthletes(AthleteQueryDTO queryDTO, Pageable pageable) { Page<Athlete> athletePage = athleteMapper.selectPage(queryDTO, pageable); return athletePage.map(this::convertToVO); } } 2.2.2 比赛项目管理 // EventController.java @RestController @RequestMapping("/api/events") @Slf4j public class EventController { @Autowired private EventService eventService; @PostMapping public ResponseEntity<EventVO> createEvent(@RequestBody @Valid EventDTO eventDTO) { return ResponseEntity.ok(eventService.createEvent(eventDTO)); } @PostMapping("/{eventId}/schedule") public ResponseEntity<ScheduleVO> createSchedule(@PathVariable Long eventId, @RequestBody @Valid ScheduleDTO scheduleDTO) { return ResponseEntity.ok(eventService.createSchedule(eventId, scheduleDTO)); } @PostMapping("/{eventId}/referee-assignments") public ResponseEntity<List<RefereeAssignmentVO>> assignReferees( @PathVariable Long eventId, @RequestBody @Valid List<RefereeAssignmentDTO> assignments) { return ResponseEntity.ok(eventService.assignReferees(eventId, assignments)); } } // EventServiceImpl.java @Service @Slf4j public class EventServiceImpl implements EventService { @Autowired private EventMapper eventMapper; @Autowired private ScheduleMapper scheduleMapper; @Override @Transactional public EventVO createEvent(EventDTO eventDTO) { // 验证项目信息 validateEventInfo(eventDTO); // 保存项目信息 Event event = convertToEntity(eventDTO); eventMapper.insert(event); return convertToVO(event); } @Override @Transactional public ScheduleVO createSchedule(Long eventId, ScheduleDTO scheduleDTO) { // 检查时间冲突 checkTimeConflict(scheduleDTO); // 检查场地可用性 checkVenueAvailability(scheduleDTO.getVenueId(), scheduleDTO.getStartTime(), scheduleDTO.getEndTime()); // 保存赛程信息 Schedule schedule = convertToEntity(scheduleDTO); schedule.setEventId(eventId); scheduleMapper.insert(schedule); return convertToVO(schedule); } } 3. 前端实现 3.1 项目结构 src ├── api ├── assets ├── components ├── layouts ├── router ├── store └── views ├── athlete └── event 3.2 核心代码实现 3.2.1 运动员管理页面 <!-- views/athlete/AthleteList.vue --> <template> <div class="athlete-list"> <div class="header"> <h1>运动员管理</h1> <div class="actions"> <el-button type="primary" @click="showCreateDialog">新增运动员</el-button> <el-upload class="upload-btn" action="/api/athletes/batch" :on-success="handleUploadSuccess" > <el-button type="primary">批量导入</el-button> </el-upload> </div> </div> <!-- 搜索表单 --> <el-form :model="queryForm" :inline="true" class="search-form"> <el-form-item label="姓名"> <el-input v-model="queryForm.name" placeholder="请输入姓名"></el-input> </el-form-item> <el-form-item label="代表队"> <el-select v-model="queryForm.teamId" placeholder="请选择代表队"> <el-option v-for="team in teams" :key="team.id" :label="team.name" :value="team.id" ></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> <!-- 运动员列表 --> <el-table :data="athletes" border stripe> <el-table-column prop="name" label="姓名"></el-table-column> <el-table-column prop="gender" label="性别"> <template #default="scope"> {{ scope.row.gender === 1 ? '男' : '女' }} </template> </el-table-column> <el-table-column prop="birthDate" label="出生日期"></el-table-column> <el-table-column prop="teamName" label="代表队"></el-table-column> <el-table-column prop="phone" label="联系电话"></el-table-column> <el-table-column label="照片"> <template #default="scope"> <el-image :src="scope.row.photoUrl" :preview-src-list="[scope.row.photoUrl]" fit="cover" class="athlete-photo" ></el-image> </template> </el-table-column> <el-table-column label="操作" width="250"> <template #default="scope"> <el-button type="primary" size="small" @click="showEditDialog(scope.row)"> 编辑 </el-button> <el-button type="success" size="small" @click="showAssignEvents(scope.row)"> 分配项目 </el-button> <el-button type="danger" size="small" @click="handleDelete(scope.row)"> 删除 </el-button> </template> </el-table-column> </el-table> <!-- 分页器 --> <el-pagination :current-page="page.current" :page-size="page.size" :total="page.total" @current-change="handlePageChange" layout="total, prev, pager, next" ></el-pagination> <!-- 新增/编辑对话框 --> <el-dialog :title="dialogType === 'create' ? '新增运动员' : '编辑运动员'" v-model="dialogVisible" width="600px" > <el-form ref="athleteForm" :model="athleteForm" :rules="rules" label-width="100px" > <el-form-item label="姓名" prop="name"> <el-input v-model="athleteForm.name"></el-input> </el-form-item> <el-form-item label="性别" prop="gender"> <el-radio-group v-model="athleteForm.gender"> <el-radio :label="1">男</el-radio> <el-radio :label="0">女</el-radio> </el-radio-group> </el-form-item> <el-form-item label="出生日期" prop="birthDate"> <el-date-picker v-model="athleteForm.birthDate" type="date" placeholder="选择日期" ></el-date-picker> </el-form-item> <el-form-item label="身份证号" prop="idCard"> <el-input v-model="athleteForm.idCard"></el-input> </el-form-item> <el-form-item label="代表队" prop="teamId"> <el-select v-model="athleteForm.teamId" placeholder="请选择代表队"> <el-option v-for="team in teams" :key="team.id" :label="team.name" :value="team.id" ></el-option> </el-select> </el-form-item> <el-form-item label="联系电话" prop="phone"> <el-input v-model="athleteForm.phone"></el-input> </el-form-item> <el-form-item label="照片" prop="photo"> <el-upload class="avatar-uploader" action="/api/upload" :show-file-list="false" :before-upload="beforePhotoUpload" :on-success="handlePhotoSuccess" > <img v-if="athleteForm.photoUrl" :src="athleteForm.photoUrl" class="avatar"> <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon> </el-upload> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="handleSubmit">确定</el-button> </template> </el-dialog> <!-- 项目分配对话框 --> <el-dialog title="分配比赛项目" v-model="eventDialogVisible" width="800px" > <el-transfer v-model="selectedEvents" :data="allEvents" :titles="['可选项目', '已选项目']" :props="{ key: 'id', label: 'name' }" ></el-transfer> <template #footer> <el-button @click="eventDialogVisible = false">取消</el-button> <el-button type="primary" @click="handleEventAssign">确定</el-button> </template> </el-dialog> </div> </template> <script setup> import { ref, onMounted, reactive } from 'vue' import { getAthletes, createAthlete, updateAthlete, deleteAthlete } from '@/api/athlete' import { getTeams } from '@/api/team' import { getEvents, assignEvents } from '@/api/event' import { ElMessage } from 'element-plus' // 数据定义 const athletes = ref([]) const teams = ref([]) const allEvents = ref([]) const page = reactive({ current: 1, size: 10, total: 0 }) const queryForm = reactive({ name: '', teamId: '' }) const athleteForm = reactive({ name: '', gender: 1, birthDate: '', idCard: '', teamId: '', phone: '', photoUrl: '' }) // 校验规则 const rules = { name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], gender: [{ required: true, message: '请选择性别', trigger: 'change' }], birthDate: [{ required: true, message: '请选择出生日期', trigger: 'change' }], idCard: [ { required: true, message: '请输入身份证号', trigger: 'blur' }, { pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '请输入正确的身份证号', trigger: 'blur' } ], teamId: [{ required: true, message: '请选择代表队', trigger: 'change' }], phone: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }] } // 对话框控制 const dialogVisible = ref(false) const dialogType = ref('create') const eventDialogVisible = ref(false) const selectedEvents = ref([]) const currentAthlete = ref(null) // 初始化数据 onMounted(async () => { await Promise.all([ loadAthletes(), loadTeams(), loadEvents() ]) }) // 方法定义 const loadAthletes = async () => { try { const res = await getAthletes({ ...queryForm, page: page.current, size: page.size }) athletes.value = res.data.records page.total = res.data.total } catch (error) { console.error('加载运动员列表失败:', error) ElMessage.error('加载运动员列表失败') } } const loadTeams = async () => { try { const res = await getTeams() teams.value = res.data } catch (error) { console.error('加载代表队列表失败:', error) ElMessage.error('加载代表队列表失败') } } const loadEvents = async () => { try { const res = await getEvents() allEvents.value = res.data } catch (error) { console.error('加载比赛项目失败:', error) ElMessage.error('加载比赛项目失败') } } // 处理搜索 const handleSearch = () => { page.current = 1 loadAthletes() } // 重置表单 const resetForm = () => { queryForm.name = '' queryForm.teamId = '' handleSearch() } // 显示新增对话框 const showCreateDialog = () => { dialogType.value = 'create' Object.keys(athleteForm).forEach(key => { athleteForm[key] = '' }) dialogVisible.value = true } // 显示编辑对话框 const showEditDialog = (row) => { dialogType.value = 'edit' Object.assign(athleteForm, row) dialogVisible.value = true } // 显示项目分配对话框 const showAssignEvents = async (row) => { currentAthlete.value = row selectedEvents.value = row.eventIds || [] eventDialogVisible.value = true } // 提交表单 const handleSubmit = async () => { try { if (dialogType.value === 'create') { await createAthlete(athleteForm) ElMessage.success('新增运动员成功') } else { await updateAthlete(currentAthlete.value.id, athleteForm) ElMessage.success('更新运动员成功') } dialogVisible.value = false loadAthletes() } catch (error) { console.error('保存运动员失败:', error) ElMessage.error('保存运动员失败') } } // 处理删除 const handleDelete = async (row) => { try { await ElMessageBox.confirm('确认删除该运动员?', '提示', { type: 'warning' }) await deleteAthlete(row.id) ElMessage.success('删除成功') loadAthletes() } catch (error) { if (error !== 'cancel') { console.error('删除运动员失败:', error) ElMessage.error('删除运动员失败') } } } // 处理项目分配 const handleEventAssign = async () => { try { await assignEvents(currentAthlete.value.id, selectedEvents.value) ElMessage.success('项目分配成功') eventDialogVisible.value = false loadAthletes() } catch (error) { console.error('项目分配失败:', error) ElMessage.error('项目分配失败') } } </script> <style scoped> .athlete-list { padding: 20px; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .search-form { margin-bottom: 20px; } .athlete-photo { width: 60px; height: 60px; border-radius: 4px; } .avatar-uploader { width: 148px; height: 148px; border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; } .avatar-uploader:hover { border-color: #409EFF; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 148px; height: 148px; text-align: center; line-height: 148px; } .avatar { width: 148px; height: 148px; display: block; } 成绩管理与数据统计模块 1. 数据库设计 1.1 成绩管理相关表 -- 成绩记录表 CREATE TABLE t_score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, athlete_event_id BIGINT NOT NULL COMMENT '运动员参赛ID', round_type TINYINT NOT NULL COMMENT '轮次:1-预赛,2-决赛', score VARCHAR(20) NOT NULL COMMENT '成绩', score_status TINYINT DEFAULT 0 COMMENT '成绩状态:0-待审核,1-已审核,2-已公示', is_broken_record TINYINT DEFAULT 0 COMMENT '是否破记录:0-否,1-是', recorder_id BIGINT NOT NULL COMMENT '记录员ID', auditor_id BIGINT COMMENT '审核员ID', audit_time DATETIME COMMENT '审核时间', audit_remark VARCHAR(255) COMMENT '审核备注', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (athlete_event_id) REFERENCES t_athlete_event(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩记录表'; -- 破纪录记录表 CREATE TABLE t_record_breaking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id BIGINT NOT NULL COMMENT '比赛项目ID', athlete_id BIGINT NOT NULL COMMENT '运动员ID', old_record VARCHAR(20) COMMENT '原记录', new_record VARCHAR(20) NOT NULL COMMENT '新记录', breaking_time DATETIME NOT NULL COMMENT '破记录时间', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (event_id) REFERENCES t_event(id), FOREIGN KEY (athlete_id) REFERENCES t_athlete(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='破纪录记录表'; 1.2 数据统计相关表 -- 团体总分表 CREATE TABLE t_team_score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, team_id BIGINT NOT NULL COMMENT '代表队ID', total_score DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '总分', gold_count INT NOT NULL DEFAULT 0 COMMENT '金牌数', silver_count INT NOT NULL DEFAULT 0 COMMENT '银牌数', bronze_count INT NOT NULL DEFAULT 0 COMMENT '铜牌数', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (team_id) REFERENCES t_team(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='团体总分表'; -- 单项排名表 CREATE TABLE t_event_ranking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id BIGINT NOT NULL COMMENT '比赛项目ID', athlete_id BIGINT NOT NULL COMMENT '运动员ID', rank_num INT NOT NULL COMMENT '名次', score VARCHAR(20) NOT NULL COMMENT '成绩', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (event_id) REFERENCES t_event(id), FOREIGN KEY (athlete_id) REFERENCES t_athlete(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单项排名表'; 2. 后端实现 2.1 成绩录入与管理 // ScoreController.java @RestController @RequestMapping("/api/scores") @Slf4j public class ScoreController { @Autowired private ScoreService scoreService; @PostMapping public ResponseEntity<ScoreVO> createScore(@RequestBody @Valid ScoreDTO scoreDTO) { return ResponseEntity.ok(scoreService.createScore(scoreDTO)); } @PutMapping("/{id}/audit") public ResponseEntity<ScoreVO> auditScore(@PathVariable Long id, @RequestBody @Valid ScoreAuditDTO auditDTO) { return ResponseEntity.ok(scoreService.auditScore(id, auditDTO)); } @GetMapping("/records") public ResponseEntity<List<RecordBreakingVO>> getRecordBreakings() { return ResponseEntity.ok(scoreService.getRecordBreakings()); } } // ScoreServiceImpl.java @Service @Slf4j public class ScoreServiceImpl implements ScoreService { @Autowired private ScoreMapper scoreMapper; @Autowired private EventMapper eventMapper; @Autowired private WebSocketService webSocketService; @Override @Transactional public ScoreVO createScore(ScoreDTO scoreDTO) { // 验证成绩格式 validateScore(scoreDTO.getScore(), scoreDTO.getEventId()); // 检查是否破记录 boolean isBrokenRecord = checkRecordBreaking(scoreDTO); // 保存成绩 Score score = convertToEntity(scoreDTO); score.setIsBrokenRecord(isBrokenRecord); scoreMapper.insert(score); // 如果破记录,发送通知 if (isBrokenRecord) { sendRecordBreakingNotification(score); } // 实时推送成绩更新 webSocketService.broadcastScore(convertToVO(score)); return convertToVO(score); } @Override @Transactional public ScoreVO auditScore(Long id, ScoreAuditDTO auditDTO) { Score score = scoreMapper.selectById(id); if (score == null) { throw new NotFoundException("成绩记录不存在"); } // 更新审核状态 score.setScoreStatus(auditDTO.getStatus()); score.setAuditorId(auditDTO.getAuditorId()); score.setAuditTime(LocalDateTime.now()); score.setAuditRemark(auditDTO.getRemark()); scoreMapper.updateById(score); // 如果审核通过,更新排名和团体分 if (auditDTO.getStatus() == 1) { updateRankingAndTeamScore(score); } return convertToVO(score); } } 2.2 数据统计服务 // StatisticsController.java @RestController @RequestMapping("/api/statistics") @Slf4j public class StatisticsController { @Autowired private StatisticsService statisticsService; @GetMapping("/team-scores") public ResponseEntity<List<TeamScoreVO>> getTeamScores() { return ResponseEntity.ok(statisticsService.getTeamScores()); } @GetMapping("/event-rankings/{eventId}") public ResponseEntity<List<EventRankingVO>> getEventRankings(@PathVariable Long eventId) { return ResponseEntity.ok(statisticsService.getEventRankings(eventId)); } @GetMapping("/medal-statistics") public ResponseEntity<List<MedalStatisticsVO>> getMedalStatistics() { return ResponseEntity.ok(statisticsService.getMedalStatistics()); } } // StatisticsServiceImpl.java @Service @Slf4j public class StatisticsServiceImpl implements StatisticsService { @Autowired private TeamScoreMapper teamScoreMapper; @Autowired private EventRankingMapper eventRankingMapper; @Override @Transactional(readOnly = true) public List<TeamScoreVO> getTeamScores() { List<TeamScore> teamScores = teamScoreMapper.selectList(null); return teamScores.stream() .map(this::convertToVO) .sorted(Comparator paring(TeamScoreVO::getTotalScore).reversed()) .collect(Collectors.toList()); } @Override @Transactional(readOnly = true) public List<EventRankingVO> getEventRankings(Long eventId) { return eventRankingMapper.selectRankingsByEventId(eventId); } } 3. 前端实现 3.1 成绩录入组件 <!-- views/score/ScoreInput.vue --> <template> <div class="score-input"> <div class="input-header"> <h2>{{ event.name }} - 成绩录入</h2> <div class="round-selector"> <el-radio-group v-model="roundType"> <el-radio :label="1">预赛</el-radio> <el-radio :label="2">决赛</el-radio> </el-radio-group> </div> </div> <el-table :data="athletes" border> <el-table-column prop="name" label="运动员"></el-table-column> <el-table-column prop="teamName" label="代表队"></el-table-column> <el-table-column prop="trackNumber" label="道次"></el-table-column> <el-table-column label="成绩"> <template #default="scope"> <el-input v-model="scope.row.score" :placeholder="getScorePlaceholder(event.type)" @blur="validateScore(scope.row)" ></el-input> </template> </el-table-column> <el-table-column label="状态" width="100"> <template #default="scope"> <el-tag :type="getStatusType(scope.row.status)"> {{ getStatusText(scope.row.status) }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="200"> <template #default="scope"> <el-button v-if="scope.row.status === 0" type="success" size="small" @click="handleAudit(scope.row, 1)" > 通过 </el-button> <el-button v-if="scope.row.status === 0" type="danger" size="small" @click="handleAudit(scope.row, -1)" > 驳回 </el-button> <el-button v-if="scope.row.status === 1" type="primary" size="small" @click="handlePublish(scope.row)" > 公示 </el-button> </template> </el-table-column> </el-table> <!-- 审核对话框 --> <el-dialog :title="auditType === 1 ? '通过审核' : '驳回审核'" v-model="auditDialogVisible" width="500px" > <el-form ref="auditForm" :model="auditForm" :rules="auditRules"> <el-form-item label="审核意见" prop="remark"> <el-input type="textarea" v-model="auditForm.remark" :rows="4" placeholder="请输入审核意见" ></el-input> </el-form-item> </el-form> <template #footer> <el-button @click="auditDialogVisible = false">取消</el-button> <el-button type="primary" @click="submitAudit">确定</el-button> </template> </el-dialog> </div> </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { getScores, auditScore, publishScore } from '@/api/score' import { getEvents } from '@/api/event' import { ElMessage, ElMessageBox } from 'element-plus' // 数据定义 const scores = ref([]) const events = ref([]) const auditDialogVisible = ref(false) const auditType = ref(1) const currentScore = ref(null) const queryForm = reactive({ eventId: '', status: 0 }) const auditForm = reactive({ remark: '' }) const auditRules = { remark: [ { required: true, message: '请输入审核意见', trigger: 'blur' }, { min: 5, message: '审核意见至少5个字符', trigger: 'blur' } ] } // 初始化数据 onMounted(async () => { await Promise.all([ loadEvents(), loadScores() ]) }) // 加载项目列表 const loadEvents = async () => { try { const res = await getEvents() events.value = res.data } catch (error) { ElMessage.error('加载项目列表失败') } } // 加载成绩列表 const loadScores = async () => { try { const res = await getScores(queryForm) scores.value = res.data } catch (error) { ElMessage.error('加载成绩列表失败') } } // 处理审核 const handleAudit = (score, type) => { currentScore.value = score auditType.value = type auditForm.remark = '' auditDialogVisible.value = true } // 提交审核 const submitAudit = async () => { try { const auditData = { status: auditType.value === 1 ? 1 : 0, remark: auditForm.remark } await auditScore(currentScore.value.id, auditData) ElMessage.success('审核操作成功') auditDialogVisible.value = false loadScores() } catch (error) { ElMessage.error('审核操作失败') } } // 处理公示 const handlePublish = async (score) => { try { await ElMessageBox.confirm('确认公示该成绩?', '提示', { type: 'warning' }) await publishScore(score.id) ElMessage.success('成绩公示成功') loadScores() } catch (error) { if (error !== 'cancel') { ElMessage.error('成绩公示失败') } } } // 状态转换 const getStatusType = (status) => { const statusMap = { 0: 'warning', 1: 'success', 2: 'info' } return statusMap[status] } const getStatusText = (status) => { const statusMap = { 0: '待审核', 1: '已审核', 2: '已公示' } return statusMap[status] } </script> <style scoped> .score-audit { padding: 20px; } .filter-section { margin-bottom: 20px; } </style> 3.3 数据统计组件 <!-- views/statistics/Dashboard.vue --> <template> <div class="dashboard"> <el-row :gutter="20"> <!-- 团体总分排名 --> <el-col :span="12"> <el-card class="rank-card"> <template #header> <div class="card-header"> <span>团体总分排名</span> <el-button type="primary" size="small" @click="exportTeamScores"> 导出 </el-button> </div> </template> <div class="chart-container"> <bar-chart :data="teamScores" /> </div> </el-card> </el-col> <!-- 奖牌榜 --> <el-col :span="12"> <el-card class="rank-card"> <template #header> <div class="card-header"> <span>奖牌榜</span> </div> </template> <div class="chart-container"> <stacked-bar-chart :data="medalStatistics" /> </div> </el-card> </el-col> </el-row> <!-- 单项成绩排名 --> <el-card class="rank-card mt-20"> <template #header> <div class="card-header"> <span>单项成绩排名</span> <el-select v-model="selectedEvent" placeholder="请选择比赛项目"> <el-option v-for="event in events" :key="event.id" :label="event.name" :value="event.id" ></el-option> </el-select> </div> </template> <el-table :data="eventRankings" border stripe> <el-table-column type="index" label="名次" width="80"></el-table-column> <el-table-column prop="athleteName" label="运动员"></el-table-column> <el-table-column prop="teamName" label="代表队"></el-table-column> <el-table-column prop="score" label="成绩"></el-table-column> <el-table-column prop="isBrokenRecord" label="破记录" width="100"> <template #default="scope"> <el-tag v-if="scope.row.isBrokenRecord" type="success">破记录</el-tag> </template> </el-table-column> </el-table> </el-card> <!-- 破记录提醒 --> <el-card class="rank-card mt-20"> <template #header> <div class="card-header"> <span>破记录记录</span> </div> </template> <el-timeline> <el-timeline-item v-for="record in recordBreakings" :key="record.id" :timestamp="record.breakingTime" type="success" > <h4>{{ record.eventName }}</h4> <p> 运动员 {{ record.athleteName }} ({{ record.teamName }}) 以 {{ record.newRecord }} 的成绩打破原记录 {{ record.oldRecord }} </p> </el-timeline-item> </el-timeline> </el-card> </div> </template> <script setup> import { ref, onMounted, watch } from 'vue' import { getTeamScores, getMedalStatistics, getEventRankings, getRecordBreakings } from '@/api/statistics' import { getEvents } from '@/api/event' import BarChart from '@/components/charts/BarChart.vue' import StackedBarChart from '@/components/charts/StackedBarChart.vue' import { useWebSocket } from '@/hooks/useWebSocket' // 数据定义 const teamScores = ref([]) const medalStatistics = ref([]) const eventRankings = ref([]) const recordBreakings = ref([]) const events = ref([]) const selectedEvent = ref('') // WebSocket连接 const { messages } = useWebSocket('/ws/statistics') // 监听实时数据更新 watch(messages, (newMessage) => { if (newMessage) { switch (newMessage.type) { case 'TEAM_SCORE_UPDATE': updateTeamScores(newMessage.data) break case 'RECORD_BREAKING': handleRecordBreaking(newMessage.data) break } } }) // 加载数据 onMounted(async () => { await Promise.all([ loadEvents(), loadTeamScores(), loadMedalStatistics(), loadRecordBreakings() ]) }) // 监听项目选择 watch(selectedEvent, (newValue) => { if (newValue) { loadEventRankings(newValue) } }) // 加载团体总分 const loadTeamScores = async () => { try { const res = await getTeamScores() teamScores.value = res.data } catch (error) { ElMessage.error('加载团体总分失败') } } // 加载奖牌统计 const loadMedalStatistics = async () => { try { const res = await getMedalStatistics() medalStatistics.value = res.data } catch (error) { ElMessage.error('加载奖牌统计失败') } } // 加载项目列表 const loadEvents = async () => { try { const res = await getEvents() events.value = res.data } catch (error) { ElMessage.error('加载项目列表失败') } } // 加载单项排名 const loadEventRankings = async (eventId) => { try { const res = await getEventRankings(eventId) eventRankings.value = res.data } catch (error) { ElMessage.error('加载单项排名失败') } } // 加载破记录记录 const loadRecordBreakings = async () => { try { const res = await getRecordBreakings() recordBreakings.value = res.data } catch (error) { ElMessage.error('加载破记录记录失败') } } // 导出团体总分 const exportTeamScores = () => { // 实现导出逻辑 } // 处理实时破记录通知 const handleRecordBreaking = (record) => { ElNotification({ title: '破记录提醒', message: `${record.athleteName} 在 ${record.eventName} 中破记录!`, type: 'success', duration: 0 }) recordBreakings.value.unshift(record) } </script> <style scoped> .dashboard { padding: 20px; } .rank-card { margin-bottom: 20px; } .card-header { display: flex; justify-content: space-between; align-items: center; } .chart-container { height: 400px; } .mt-20 { margin-top: 20px; } </style> 3.4 图表组件实现 <!-- components/charts/BarChart.vue --> <template> <div ref="chartRef" class="chart"></div> </template> <script setup> import { ref, onMounted, watch } from 'vue' import * as echarts from 'echarts' const props = defineProps({ data: { type: Array, required: true } }) const chartRef = ref(null) let chart = null onMounted(() => { initChart() }) watch(() => props.data, (newVal) => { if (chart) { updateChart(newVal) } }) const initChart = () => { chart = echarts.init(chartRef.value) updateChart(props.data) } const updateChart = (data) => { const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: data.map(item => item.teamName) }, yAxis: { type: 'value' }, series: [ { name: '总分', type: 'bar', data: data.map(item => item.totalScore), itemStyle: { color: '#409EFF' } } ] } chart.setOption(option) } </script> <style scoped> .chart { width: 100%; height: 100%; } </style>

这样,我们完成了成绩管理和数据统计模块的完整实现。

标签:

Java全栈项目-田径运动会管理系统由讯客互联人工智能栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Java全栈项目-田径运动会管理系统