当前位置: 首页 > article >正文

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:路由管理

核心功能

运动员管理

  • 运动员信息录入与维护
  • 参赛项目分配
  • 号码簿生成
  • 运动员成绩查询

比赛项目管理

  • 项目设置与编排
  • 赛程安排
  • 场地分配
  • 裁判员分配

成绩管理

  • 实时成绩录入
  • 成绩审核
  • 成绩公示
  • 破纪录提醒

数据统计

  • 团体总分排名
  • 单项成绩排名
  • 奖牌榜统计
  • 数据可视化展示

项目特点

  1. 实时数据更新

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

    • 灵活的项目设置
    • 可自定义计分规则
    • 支持多种赛制
  3. 安全性

    • 基于RBAC的权限管理
    • 操作日志记录
    • 数据备份恢复
  4. 用户体验

    • 响应式设计
    • 多终端适配
    • 直观的数据展示

项目部署

环境要求

  • JDK 1.8+
  • Maven 3.6+
  • MySQL 8.0+
  • Redis 6.0+
  • Node.js 14+

部署步骤

  1. 后端部署
# 克隆项目
git clone https://github.com/username/sports-meet-manager.git

# 进入项目目录
cd sports-meet-manager

# 编译打包
mvn clean package

# 运行项目
java -jar target/sports-meet-manager.jar
  1. 前端部署
# 进入前端项目目录
cd web

# 安装依赖
npm install

# 编译打包
npm run build

# 部署到nginx
cp -r dist/* /usr/share/nginx/html/

项目展望

  1. 功能扩展

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

    • 引入分布式架构
    • 优化数据处理算法
    • 提升系统并发能力
  3. 用户体验提升

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

总结

本项目采用主流的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.comparing(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>

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


http://www.kler.cn/a/553688.html

相关文章:

  • docker中pull hello-world的时候出现报错
  • DeepSeek与ChatGPT的对比分析
  • Es的text和keyword类型以及如何修改类型
  • 安卓基础(Firebase Cloud Messaging)
  • 图解循环神经网络(RNN)
  • 15-贪心算法
  • stream流常用方法
  • mac os设置jdk版本
  • DeepSeek-V3模型底层架构的核心技术一(多Token预测(MTP)技术)
  • 动态规划之背包问题
  • 力扣-二叉树-235 二叉搜索树的最近公共祖先
  • 位运算,双指针,二分,排序算法
  • 一台服务器将docker image打包去另一天服务器安装这个镜像
  • 2025年02月18日Github流行趋势
  • 【基础架构篇九】《DeepSeek模型版本管理:Git+MLflow集成实践》
  • MySQL面试考点汇总
  • 基于SpringBoot+Vue的老年人体检管理系统的设计与实现(源码+SQL脚本+LW+部署讲解等)
  • 变相提高大模型上下文长度-RAG文档压缩-3.优化map-reduce(reranker过滤+社区聚类)
  • 零基础学QT、C++(三)魔改QT组件库(付源码)
  • 闲鱼IP属地为何频繁变化:深入解析与应对策略