公司动态

基于Spring Boot与Vue 3的田径训练管理全栈系统开发实战

📅 2026/8/20 13:10:20
基于Spring Boot与Vue 3的田径训练管理全栈系统开发实战
最近在开发一个面向田径教练和运动员的训练管理工具时深刻体会到从需求梳理到技术落地的复杂性。市面上通用的项目管理软件难以满足田径训练中诸如周期计划、技术动作分析、实时数据反馈等专业需求而定制开发又常因技术选型不当或架构设计缺陷导致项目延期。本文将完整复盘这款“田径训练工具”从零到一的开发全过程涵盖技术选型、核心模块实现、数据可视化以及部署上线的完整闭环。无论你是想学习全栈开发流程还是希望为特定领域构建垂直应用本文提供的思路和代码都能直接复用。1. 项目背景与核心需求分析田径训练是一个高度专业化、数据驱动的过程。传统的训练管理多依赖于纸质记录、Excel表格或零散的即时通讯工具存在数据分散、难以回溯分析、计划调整不灵活、教练与运动员沟通效率低等问题。因此我们决定开发一款集训练计划制定、执行跟踪、数据记录与分析、即时通讯于一体的数字化工具。核心需求拆解如下用户与权限管理系统需区分超级管理员、教练、运动员三种角色实现精细化的权限控制。周期化训练计划支持教练为运动员创建年、月、周、日级别的训练计划计划内容需结构化如热身、主项、放松、负荷量、强度等。训练日志与反馈运动员可查看每日计划并提交完成情况、主观感受RPE、甚至上传图片/视频。教练可批注反馈。数据看板与分析对运动员的训练负荷、出勤率、关键指标如跑速、心率进行可视化分析支持趋势查看。消息与通知系统实现教练与运动员之间的即时通讯以及系统关键事件如计划更新的推送。移动端适配考虑到训练场地的特性运动员端必须对手机操作友好。技术价值本项目涉及前后端分离架构、复杂业务状态流转、实时通信、数据可视化等典型企业级应用技术点是一个非常好的全栈实践项目。2. 技术栈选型与环境准备在技术选型上我们遵循“生态成熟、开发高效、易于维护”的原则最终技术栈如下前端Vue 3 TypeScript Vite Element Plus EChartsVue 3的Composition API更适合复杂业务逻辑组织TypeScript能显著提升代码健壮性。Element Plus提供丰富的UI组件ECharts用于数据可视化。后端Spring Boot 2.7.x MyBatis-Plus Spring Security JWTSpring Boot快速构建RESTful API。MyBatis-Plus极大简化数据库操作。Spring Security JWT处理认证与授权。数据库MySQL 8.0关系型数据库用于存储用户、计划、日志等结构化数据。实时通信WebSocket (采用Spring Boot原生支持spring-boot-starter-websocket)用于实现训练日志新反馈、新消息的实时推送避免轮询。部署与运维Docker Docker Compose Nginx容器化部署保证环境一致性Nginx作为反向代理和静态资源服务器。本地开发环境准备基础环境操作系统Windows 10/11, macOS 或 Linux 均可。JDK版本 11 或 17 (本文示例使用 JDK 17)。Node.js版本 18.x 或更高并安装 npm 或 yarn。MySQL版本 8.0并启动服务。IDEIntelliJ IDEA (后端) Visual Studio Code (前端)。Docker Desktop(可选用于容器化部署)。初始化项目结构 创建项目根目录track-field-training-tool其下包含两个子项目track-field-training-tool/ ├── backend/ # Spring Boot 后端项目 └── frontend/ # Vue 3 前端项目3. 后端核心模块设计与实现后端采用经典的分层架构Controller - Service - Mapper - Model。3.1 数据库设计与实体类根据需求设计核心数据表如下仅展示部分关键字段-- 用户表 CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, avatar varchar(255) DEFAULT NULL COMMENT 头像, phone varchar(20) DEFAULT NULL COMMENT 手机号, role varchar(20) NOT NULL COMMENT 角色ADMIN/COACH/ATHLETE, coach_id bigint DEFAULT NULL COMMENT 所属教练ID (仅运动员有), status tinyint DEFAULT 1 COMMENT 状态 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 训练计划表 CREATE TABLE training_plan ( id bigint NOT NULL AUTO_INCREMENT, athlete_id bigint NOT NULL COMMENT 运动员ID, coach_id bigint NOT NULL COMMENT 制定计划的教练ID, title varchar(200) NOT NULL COMMENT 计划标题, plan_type varchar(50) NOT NULL COMMENT 计划类型YEARLY/MONTHLY/WEEKLY/DAILY, start_date date NOT NULL COMMENT 开始日期, end_date date NOT NULL COMMENT 结束日期, content text COMMENT 计划详细内容 (JSON或富文本), status varchar(20) DEFAULT DRAFT COMMENT 状态DRAFT/ACTIVE/COMPLETED/CANCELLED, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_athlete_date (athlete_id,start_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT训练计划表; -- 训练日志表 CREATE TABLE training_log ( id bigint NOT NULL AUTO_INCREMENT, plan_id bigint NOT NULL COMMENT 关联的计划ID, athlete_id bigint NOT NULL COMMENT 运动员ID, log_date date NOT NULL COMMENT 日志日期, completed_content text COMMENT 实际完成情况, subjective_feeling text COMMENT 主观感受RPE疲劳感等, attachment_urls json DEFAULT NULL COMMENT 附件URL列表图片/视频, coach_feedback text COMMENT 教练反馈, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_plan_log_date (plan_id,log_date), KEY idx_athlete_date (athlete_id,log_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT训练日志表;使用MyBatis-Plus对应的实体类TrainingPlan如下// 文件路径backend/src/main/java/com/trackfield/entity/TrainingPlan.java package com.trackfield.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; Data TableName(training_plan) public class TrainingPlan { TableId(type IdType.AUTO) private Long id; private Long athleteId; private Long coachId; private String title; private String planType; // YEARLY, MONTHLY, WEEKLY, DAILY private LocalDate startDate; private LocalDate endDate; TableField(typeHandler com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class) private String content; // 存储为JSON字符串 private String status; // DRAFT, ACTIVE, COMPLETED, CANCELLED TableField(fill FieldFill.INSERT) private LocalDateTime createTime; }3.2 安全认证与授权 (Spring Security JWT)配置Spring Security实现基于JWT的无状态认证。// 文件路径backend/src/main/java/com/trackfield/config/SecurityConfig.java package com.trackfield.config; import com.trackfield.filter.JwtAuthenticationFilter; import com.trackfield.handler.AccessDeniedHandlerImpl; import com.trackfield.handler.AuthenticationEntryPointImpl; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; Configuration EnableGlobalMethodSecurity(prePostEnabled true) RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final AuthenticationEntryPointImpl authenticationEntryPoint; private final AccessDeniedHandlerImpl accessDeniedHandler; Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 关闭CSRF和Session .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 配置请求授权 .authorizeRequests() .antMatchers(/api/auth/login, /api/auth/register).permitAll() // 登录注册公开 .antMatchers(/websocket/**).permitAll() // WebSocket端点公开 .anyRequest().authenticated() // 其他所有请求需要认证 .and() // 添加JWT过滤器 .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 异常处理 .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) // 认证失败处理 .accessDeniedHandler(accessDeniedHandler); // 权限不足处理 return http.build(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }JwtAuthenticationFilter负责从HTTP头部解析Token并设置认证信息。3.3 训练计划与日志业务实现以创建训练计划为例展示Service层逻辑// 文件路径backend/src/main/java/com/trackfield/service/impl/TrainingPlanServiceImpl.java package com.trackfield.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.trackfield.entity.TrainingPlan; import com.trackfield.mapper.TrainingPlanMapper; import com.trackfield.service.TrainingPlanService; import com.trackfield.vo.PlanCreateVO; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; Service RequiredArgsConstructor public class TrainingPlanServiceImpl extends ServiceImplTrainingPlanMapper, TrainingPlan implements TrainingPlanService { Override Transactional(rollbackFor Exception.class) public Long createPlan(PlanCreateVO createVO, Long coachId) { // 1. 基础校验 if (createVO.getEndDate().isBefore(createVO.getStartDate())) { throw new RuntimeException(结束日期不能早于开始日期); } // 2. 构建实体 TrainingPlan plan new TrainingPlan(); BeanUtils.copyProperties(createVO, plan); plan.setCoachId(coachId); plan.setStatus(DRAFT); // 初始状态为草稿 // 3. 保存到数据库 this.save(plan); // 4. 可在此处添加异步操作如发送系统通知给对应运动员 // asyncNotificationService.notifyAthleteNewPlan(plan.getAthleteId(), plan.getId()); return plan.getId(); } // 其他方法更新计划、查询运动员计划列表、更改计划状态等... }3.4 实时消息推送 (WebSocket)配置WebSocket实现当教练对日志进行反馈后实时推送给运动员。// 文件路径backend/src/main/java/com/trackfield/config/WebSocketConfig.java package com.trackfield.config; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; Configuration EnableWebSocketMessageBroker // 启用WebSocket消息代理 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册一个STOMP端点客户端使用SockJS连接 registry.addEndpoint(/ws-training) .setAllowedOriginPatterns(*) // 生产环境应指定具体前端地址 .withSockJS(); } Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 配置消息代理以/app开头的消息会被路由到MessageMapping注解的方法 registry.setApplicationDestinationPrefixes(/app); // 配置一个简单的内存消息代理以/topic和/user开头的消息会广播给客户端 registry.enableSimpleBroker(/topic, /user); // 点对点消息前缀 registry.setUserDestinationPrefix(/user); } }消息发送Service示例// 文件路径backend/src/main/java/com/trackfield/service/WebSocketMessageService.java package com.trackfield.service; import lombok.RequiredArgsConstructor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; Service RequiredArgsConstructor public class WebSocketMessageService { private final SimpMessagingTemplate messagingTemplate; /** * 向特定运动员发送训练日志反馈通知 * param athleteId 运动员用户ID * param logId 训练日志ID * param feedback 反馈内容 */ public void sendFeedbackToAthlete(Long athleteId, Long logId, String feedback) { String destination String.format(/user/%d/queue/feedback, athleteId); String message String.format(您的日志 #%d 收到教练新反馈%s, logId, feedback); messagingTemplate.convertAndSend(destination, message); } }4. 前端核心功能实现前端使用Vue 3 Pinia进行状态管理。4.1 用户登录与状态管理使用Pinia存储用户信息和Token。// 文件路径frontend/src/stores/user.ts import { defineStore } from pinia import { ref } from vue import { loginApi, getUserInfoApi } from /api/auth import type { LoginForm, UserInfo } from /types/user export const useUserStore defineStore(user, () { const token refstring | null(localStorage.getItem(token)) const userInfo refUserInfo | null(null) const login async (loginForm: LoginForm) { const res await loginApi(loginForm) token.value res.data.token localStorage.setItem(token, token.value) // 登录成功后获取用户信息 await getUserInfo() } const getUserInfo async () { const res await getUserInfoApi() userInfo.value res.data } const logout () { token.value null userInfo.value null localStorage.removeItem(token) // 跳转到登录页 window.location.href /login } return { token, userInfo, login, getUserInfo, logout } })4.2 训练计划日历视图使用FullCalendar库展示运动员的周/日训练计划。!-- 文件路径frontend/src/views/athlete/PlanCalendar.vue -- template div classplan-calendar FullCalendar :optionscalendarOptions / /div /template script setup langts import { ref, onMounted } from vue import FullCalendar from fullcalendar/vue3 import dayGridPlugin from fullcalendar/daygrid import timeGridPlugin from fullcalendar/timegrid import interactionPlugin from fullcalendar/interaction import { fetchTrainingPlans } from /api/training import type { TrainingPlan } from /types/training const calendarOptions ref({ plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin], initialView: timeGridWeek, headerToolbar: { left: prev,next today, center: title, right: dayGridMonth,timeGridWeek,timeGridDay }, events: [] as any[], // 将从API加载的事件 eventClick: (info: any) { // 点击事件查看计划详情 console.log(点击计划:, info.event) } }) const loadPlans async () { const res await fetchTrainingPlans({ /* 查询参数 */ }) const plans: TrainingPlan[] res.data // 将后端数据转换为FullCalendar事件格式 calendarOptions.value.events plans.map(plan ({ id: plan.id, title: plan.title, start: plan.startDate, end: plan.endDate, extendedProps: { type: plan.planType, content: plan.content } })) } onMounted(() { loadPlans() }) /script4.3 训练日志提交与反馈组件运动员提交日志教练查看并反馈。!-- 文件路径frontend/src/components/TrainingLogForm.vue -- template el-form :modellogForm :rulesrules refformRef label-width100px el-form-item label完成情况 propcompletedContent el-input typetextarea v-modellogForm.completedContent :rows4 placeholder请详细描述训练完成情况... / /el-form-item el-form-item label主观感受(RPE) propsubjectiveFeeling el-rate v-modellogForm.rpe :max10 show-score / el-input typetextarea v-modellogForm.subjectiveFeeling :rows2 placeholder其他感受如疲劳、疼痛部位... classmt-2 / /el-form-item el-form-item label上传附件 el-upload action/api/upload :on-successhandleUploadSuccess :before-uploadbeforeUpload list-typepicture-card :file-listfileList el-iconPlus //el-icon /el-upload /el-form-item el-form-item el-button typeprimary clicksubmitLog提交日志/el-button /el-form-item /el-form /template script setup langts import { ref, reactive } from vue import { ElMessage, type UploadFile } from element-plus import { Plus } from element-plus/icons-vue import { submitTrainingLogApi } from /api/training import type { TrainingLogForm } from /types/training const props defineProps{ planId: number }() const logForm reactiveTrainingLogForm({ planId: props.planId, completedContent: , subjectiveFeeling: , rpe: 5, attachmentUrls: [] }) const fileList refUploadFile[]([]) const handleUploadSuccess (response: any) { logForm.attachmentUrls.push(response.data.url) ElMessage.success(上传成功) } const beforeUpload (file: File) { const isImage file.type.startsWith(image/) const isVideo file.type.startsWith(video/) const isLt10M file.size / 1024 / 1024 10 if (!isImage !isVideo) { ElMessage.error(只能上传图片或视频文件) return false } if (!isLt10M) { ElMessage.error(文件大小不能超过10MB) return false } return true } const submitLog async () { try { await submitTrainingLogApi(logForm) ElMessage.success(日志提交成功) // 清空表单 Object.assign(logForm, { completedContent: , subjectiveFeeling: , rpe: 5, attachmentUrls: [] }) fileList.value [] } catch (error) { ElMessage.error(提交失败) } } /script4.4 数据可视化看板使用ECharts展示运动员的训练负荷趋势。!-- 文件路径frontend/src/views/coach/AthleteDashboard.vue -- template div classdashboard el-row :gutter20 el-col :span16 el-card template #header span近期训练负荷趋势/span /template div refloadChartRef styleheight: 400px;/div /el-card /el-col el-col :span8 !-- 其他统计卡片 -- /el-col /el-row /div /template script setup langts import { ref, onMounted, onUnmounted } from vue import * as echarts from echarts import { getTrainingLoadData } from /api/analysis const loadChartRef refHTMLElement() let loadChart: echarts.ECharts | null null const initChart async () { if (!loadChartRef.value) return loadChart echarts.init(loadChartRef.value) const res await getTrainingLoadData({ athleteId: 1 /* 动态传入 */ }) const { dates, loads } res.data const option { tooltip: { trigger: axis }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: 负荷指数 }, series: [{ data: loads, type: line, smooth: true, areaStyle: { color: rgba(64, 158, 255, 0.2) }, lineStyle: { color: #409EFF }, itemStyle: { color: #409EFF } }], grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true } } loadChart.setOption(option) } onMounted(() { initChart() window.addEventListener(resize, () loadChart?.resize()) }) onUnmounted(() { window.removeEventListener(resize, () loadChart?.resize()) loadChart?.dispose() }) /script5. 项目部署与上线采用Docker Compose进行容器化部署保证环境一致性。5.1 Dockerfile 配置后端Dockerfile:# backend/Dockerfile FROM openjdk:17-jdk-slim AS builder WORKDIR /app COPY . . RUN chmod x ./gradlew ./gradlew bootJar FROM openjdk:17-jdk-slim WORKDIR /app COPY --frombuilder /app/build/libs/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]前端Dockerfile:# frontend/Dockerfile FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 805.2 Docker Compose 编排# docker-compose.yml version: 3.8 services: mysql: image: mysql:8.0 container_name: trackfield-mysql environment: MYSQL_ROOT_PASSWORD: your_strong_password MYSQL_DATABASE: trackfield_db volumes: - mysql_data:/var/lib/mysql ports: - 3306:3306 networks: - trackfield-network backend: build: ./backend container_name: trackfield-backend depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/trackfield_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: your_strong_password ports: - 8080:8080 networks: - trackfield-network frontend: build: ./frontend container_name: trackfield-frontend depends_on: - backend ports: - 80:80 networks: - trackfield-network volumes: mysql_data: networks: trackfield-network: driver: bridge5.3 Nginx 配置 (用于前端)# frontend/nginx.conf server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; # 前端路由支持 location / { try_files $uri $uri/ /index.html; } # 反向代理API请求到后端 location /api/ { proxy_pass http://backend:8080; 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; } # 代理WebSocket连接 location /ws-training/ { proxy_pass http://backend:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; } }启动命令# 在项目根目录执行 docker-compose up -d6. 常见问题与排查思路在开发和部署过程中你可能会遇到以下典型问题问题现象可能原因排查步骤与解决方案前端访问后端API出现CORS错误后端未配置跨域或配置不正确1. 检查后端WebMvcConfig或CrossOrigin注解。2. 生产环境建议通过Nginx反向代理解决而非直接开放CORS。数据库连接失败1. MySQL服务未启动。2. 连接URL、用户名或密码错误。3. 时区设置不一致。1.docker ps检查MySQL容器状态。2. 检查application.yml中的spring.datasource配置。3. 在JDBC URL中添加serverTimezoneAsia/Shanghai。JWT Token无效或过期1. Token未在请求头中正确携带。2. Token已过期。3. 生成Token的密钥与验证密钥不一致。1. 检查前端请求头Authorization: Bearer token。2. 检查Token过期时间配置。3. 确保后端重启后密钥未改变生产环境应使用固定密钥。WebSocket连接失败1. 后端WebSocket端点路径错误。2. 前端SockJS或STOMP客户端版本不兼容。3. Nginx未配置WebSocket代理。1. 检查前端连接的ws://或wss://URL是否正确。2. 查看浏览器控制台WebSocket连接错误信息。3. 确保Nginx配置了proxy_set_header Upgrade和Connection upgrade。文件上传失败或大小限制1. 超过Spring Boot默认文件大小限制1MB。2. 上传目录权限不足。1. 在application.yml中配置spring.servlet.multipart.max-file-size和max-request-size。2. 检查服务器上存储路径的读写权限。前端构建后访问空白页1. 静态资源路径错误。2. Vue Router使用了history模式但Nginx未配置try_files。1. 检查vite.config.ts中的base配置。2. 确保Nginx配置中包含try_files $uri $uri/ /index.html;。7. 最佳实践与工程建议API设计规范遵循RESTful风格使用合适的HTTP方法GET/POST/PUT/DELETE。返回统一的数据封装格式如{ code: 200, data: {}, message: success }。对列表查询、分页参数进行标准化。安全性增强密码存储必须使用BCrypt等强哈希算法切勿明文存储。SQL注入坚持使用MyBatis-Plus等ORM框架的参数化查询避免手动拼接SQL。XSS防护对用户输入的内容进行转义或过滤尤其在富文本和反馈内容中。权限校验在Service层或方法上使用PreAuthorize进行细粒度权限控制不要仅依赖前端路由隐藏。数据管理与性能索引优化为高频查询条件如athlete_id,log_date建立数据库索引。数据归档训练日志等数据会随时间快速增长需设计历史数据归档或分表策略。缓存应用对不常变动的数据如运动员基本信息、基础字典使用Redis等缓存减轻数据库压力。前端工程化组件化将可复用的UI和逻辑封装成组件如LoadingButton、ImageUploader。状态管理复杂跨组件状态使用Pinia管理避免Props深层传递。错误边界使用Vue的onErrorCaptured或第三方库捕获组件渲染错误避免整个页面崩溃。按需加载使用Vue Router的懒加载和组件异步导入优化首屏加载速度。可维护性代码注释对复杂业务逻辑、算法、特殊处理添加清晰注释。日志记录使用SLF4JLogback在关键业务节点如创建计划、提交日志记录INFO日志在异常处记录ERROR日志。配置外部化将数据库连接、文件存储路径、第三方密钥等配置项放在application.yml或环境变量中与代码分离。生产环境部署使用HTTPS通过Nginx配置SSL证书强制使用HTTPS。进程管理使用Docker Compose或Kubernetes管理容器生命周期配合健康检查。备份策略定期备份MySQL数据库并测试恢复流程。监控与告警集成Prometheus和Grafana监控应用性能指标JVM、API响应时间并设置关键错误告警。开发此类垂直领域工具技术实现只是基础更重要的是深入理解业务场景。与教练、运动员持续沟通收集反馈迭代功能才能使工具真正贴合训练实际提升效率。从技术角度看本项目串联了现代Web开发的完整链路是巩固全栈技能的绝佳实践。你可以在此基础上继续拓展如移动端小程序、训练视频AI分析、可穿戴设备数据同步等更高级的功能。