news 2026/8/20 13:10:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot与Vue 3的田径训练管理全栈系统开发实战

最近在开发一个面向田径教练和运动员的训练管理工具时,深刻体会到从需求梳理到技术落地的复杂性。市面上通用的项目管理软件难以满足田径训练中诸如周期计划、技术动作分析、实时数据反馈等专业需求,而定制开发又常因技术选型不当或架构设计缺陷导致项目延期。本文将完整复盘这款“田径训练工具”从零到一的开发全过程,涵盖技术选型、核心模块实现、数据可视化以及部署上线的完整闭环。无论你是想学习全栈开发流程,还是希望为特定领域构建垂直应用,本文提供的思路和代码都能直接复用。

1. 项目背景与核心需求分析

田径训练是一个高度专业化、数据驱动的过程。传统的训练管理多依赖于纸质记录、Excel表格或零散的即时通讯工具,存在数据分散、难以回溯分析、计划调整不灵活、教练与运动员沟通效率低等问题。因此,我们决定开发一款集训练计划制定、执行跟踪、数据记录与分析、即时通讯于一体的数字化工具。

核心需求拆解如下:

  1. 用户与权限管理:系统需区分超级管理员、教练、运动员三种角色,实现精细化的权限控制。
  2. 周期化训练计划:支持教练为运动员创建年、月、周、日级别的训练计划,计划内容需结构化(如热身、主项、放松、负荷量、强度等)。
  3. 训练日志与反馈:运动员可查看每日计划并提交完成情况、主观感受(RPE)、甚至上传图片/视频。教练可批注反馈。
  4. 数据看板与分析:对运动员的训练负荷、出勤率、关键指标(如跑速、心率)进行可视化分析,支持趋势查看。
  5. 消息与通知系统:实现教练与运动员之间的即时通讯,以及系统关键事件(如计划更新)的推送。
  6. 移动端适配:考虑到训练场地的特性,运动员端必须对手机操作友好。

技术价值:本项目涉及前后端分离架构、复杂业务状态流转、实时通信、数据可视化等典型企业级应用技术点,是一个非常好的全栈实践项目。

2. 技术栈选型与环境准备

在技术选型上,我们遵循“生态成熟、开发高效、易于维护”的原则,最终技术栈如下:

  • 前端:Vue 3 + TypeScript + Vite + Element Plus + ECharts
    • Vue 3的Composition API更适合复杂业务逻辑组织,TypeScript能显著提升代码健壮性。Element Plus提供丰富的UI组件,ECharts用于数据可视化。
  • 后端:Spring Boot 2.7.x + MyBatis-Plus + Spring Security + JWT
    • Spring Boot快速构建RESTful API。MyBatis-Plus极大简化数据库操作。Spring Security + JWT处理认证与授权。
  • 数据库:MySQL 8.0
    • 关系型数据库,用于存储用户、计划、日志等结构化数据。
  • 实时通信:WebSocket (采用Spring Boot原生支持spring-boot-starter-websocket)
    • 用于实现训练日志新反馈、新消息的实时推送,避免轮询。
  • 部署与运维:Docker + Docker Compose + Nginx
    • 容器化部署保证环境一致性,Nginx作为反向代理和静态资源服务器。

本地开发环境准备:

  1. 基础环境

    • 操作系统:Windows 10/11, macOS 或 Linux 均可。
    • JDK:版本 11 或 17 (本文示例使用 JDK 17)。
    • Node.js:版本 18.x 或更高,并安装 npm 或 yarn。
    • MySQL:版本 8.0,并启动服务。
    • IDE:IntelliJ IDEA (后端), Visual Studio Code (前端)。
    • Docker Desktop(可选,用于容器化部署)。
  2. 初始化项目结构: 创建项目根目录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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 ServiceImpl<TrainingPlanMapper, 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 = ref<string | null>(localStorage.getItem('token')) const userInfo = ref<UserInfo | 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 class="plan-calendar"> <FullCalendar :options="calendarOptions" /> </div> </template> <script setup lang="ts"> 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() }) </script>

4.3 训练日志提交与反馈组件

运动员提交日志,教练查看并反馈。

<!-- 文件路径:frontend/src/components/TrainingLogForm.vue --> <template> <el-form :model="logForm" :rules="rules" ref="formRef" label-width="100px"> <el-form-item label="完成情况" prop="completedContent"> <el-input type="textarea" v-model="logForm.completedContent" :rows="4" placeholder="请详细描述训练完成情况..." /> </el-form-item> <el-form-item label="主观感受(RPE)" prop="subjectiveFeeling"> <el-rate v-model="logForm.rpe" :max="10" show-score /> <el-input type="textarea" v-model="logForm.subjectiveFeeling" :rows="2" placeholder="其他感受(如疲劳、疼痛部位)..." class="mt-2" /> </el-form-item> <el-form-item label="上传附件"> <el-upload action="/api/upload" :on-success="handleUploadSuccess" :before-upload="beforeUpload" list-type="picture-card" :file-list="fileList" > <el-icon><Plus /></el-icon> </el-upload> </el-form-item> <el-form-item> <el-button type="primary" @click="submitLog">提交日志</el-button> </el-form-item> </el-form> </template> <script setup lang="ts"> 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 = reactive<TrainingLogForm>({ planId: props.planId, completedContent: '', subjectiveFeeling: '', rpe: 5, attachmentUrls: [] }) const fileList = ref<UploadFile[]>([]) 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('提交失败') } } </script>

4.4 数据可视化看板

使用ECharts展示运动员的训练负荷趋势。

<!-- 文件路径:frontend/src/views/coach/AthleteDashboard.vue --> <template> <div class="dashboard"> <el-row :gutter="20"> <el-col :span="16"> <el-card> <template #header> <span>近期训练负荷趋势</span> </template> <div ref="loadChartRef" style="height: 400px;"></div> </el-card> </el-col> <el-col :span="8"> <!-- 其他统计卡片 --> </el-col> </el-row> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' import * as echarts from 'echarts' import { getTrainingLoadData } from '@/api/analysis' const loadChartRef = ref<HTMLElement>() 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() }) </script>

5. 项目部署与上线

采用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 --from=builder /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 --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80

5.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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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: bridge

5.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 -d

6. 常见问题与排查思路

在开发和部署过程中,你可能会遇到以下典型问题:

问题现象可能原因排查步骤与解决方案
前端访问后端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中添加&serverTimezone=Asia/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 UpgradeConnection "upgrade"
文件上传失败或大小限制1. 超过Spring Boot默认文件大小限制(1MB)。
2. 上传目录权限不足。
1. 在application.yml中配置:spring.servlet.multipart.max-file-sizemax-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. 最佳实践与工程建议

  1. API设计规范

    • 遵循RESTful风格,使用合适的HTTP方法(GET/POST/PUT/DELETE)。
    • 返回统一的数据封装格式,如{ code: 200, data: {}, message: 'success' }
    • 对列表查询、分页参数进行标准化。
  2. 安全性增强

    • 密码存储:必须使用BCrypt等强哈希算法,切勿明文存储。
    • SQL注入:坚持使用MyBatis-Plus等ORM框架的参数化查询,避免手动拼接SQL。
    • XSS防护:对用户输入的内容进行转义或过滤,尤其在富文本和反馈内容中。
    • 权限校验:在Service层或方法上使用@PreAuthorize进行细粒度权限控制,不要仅依赖前端路由隐藏。
  3. 数据管理与性能

    • 索引优化:为高频查询条件(如athlete_id,log_date)建立数据库索引。
    • 数据归档:训练日志等数据会随时间快速增长,需设计历史数据归档或分表策略。
    • 缓存应用:对不常变动的数据(如运动员基本信息、基础字典)使用Redis等缓存,减轻数据库压力。
  4. 前端工程化

    • 组件化:将可复用的UI和逻辑封装成组件,如LoadingButtonImageUploader
    • 状态管理:复杂跨组件状态使用Pinia管理,避免Props深层传递。
    • 错误边界:使用Vue的onErrorCaptured或第三方库捕获组件渲染错误,避免整个页面崩溃。
    • 按需加载:使用Vue Router的懒加载和组件异步导入,优化首屏加载速度。
  5. 可维护性

    • 代码注释:对复杂业务逻辑、算法、特殊处理添加清晰注释。
    • 日志记录:使用SLF4J+Logback,在关键业务节点(如创建计划、提交日志)记录INFO日志,在异常处记录ERROR日志。
    • 配置外部化:将数据库连接、文件存储路径、第三方密钥等配置项放在application.yml或环境变量中,与代码分离。
  6. 生产环境部署

    • 使用HTTPS:通过Nginx配置SSL证书,强制使用HTTPS。
    • 进程管理:使用Docker Compose或Kubernetes管理容器生命周期,配合健康检查。
    • 备份策略:定期备份MySQL数据库,并测试恢复流程。
    • 监控与告警:集成Prometheus和Grafana监控应用性能指标(JVM、API响应时间),并设置关键错误告警。

开发此类垂直领域工具,技术实现只是基础,更重要的是深入理解业务场景。与教练、运动员持续沟通,收集反馈,迭代功能,才能使工具真正贴合训练实际,提升效率。从技术角度看,本项目串联了现代Web开发的完整链路,是巩固全栈技能的绝佳实践。你可以在此基础上,继续拓展如移动端小程序、训练视频AI分析、可穿戴设备数据同步等更高级的功能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/20 13:01:40

订阅过期横幅又弹出来?ohook 帮 Microsoft 365 全功能满血复活

订阅过期横幅又弹出来&#xff1f;ohook 帮 Microsoft 365 全功能满血复活 【免费下载链接】ohook An universal Office "activation" hook with main focus of enabling full functionality of subscription editions 项目地址: https://gitcode.com/gh_mirrors/o…

作者头像 李华
网站建设 2026/8/20 12:59:23

构建信心驱动的移动端智能体:从不确定性量化到鲁棒操作

1. 项目缘起&#xff1a;当大模型助手遇上移动端&#xff0c;为何“自信”成了关键&#xff1f; 最近在捣鼓各种基于大语言模型&#xff08;LLM&#xff09;的移动端智能体&#xff0c;也就是常说的 MLLM-based Mobile-Using Agents。说白了&#xff0c;就是让 AI 助手能像真人…

作者头像 李华
网站建设 2026/8/20 12:57:52

std1.97.1——fmt模块总览

目录0. 准备0.1 查看当前toolchain的std文档0.2 语法和词法结构1. std::fmt2. 格式化字符串2.1 位置参数2.2 命名参数2.3 格式化参数2.3.1 宽度2.3.2 填充/对齐2.3.3 标志2.3.4 精度2.3.5 本地化2.3.6 转义3. 语法4. Trait4.1 实现Display trait4.2 Display与Debug的区别5. 宏5…

作者头像 李华
网站建设 2026/8/20 12:57:30

ARM开发入门指南:从架构认知到实战环境搭建

1. 为什么说“了解完这些再学ARM也不迟”&#xff1f; 每次看到有朋友兴致勃勃地打开一本《ARM体系结构权威指南》&#xff0c;或者准备一头扎进某个嵌入式开发板的教程时&#xff0c;我总想先拉住他聊上几句。不是要泼冷水&#xff0c;而是因为我自己在ARM这条路上踩过的坑&am…

作者头像 李华