1. 项目概述:健康健身追踪系统技术架构
这个基于SpringBoot+Vue的健康健身追踪系统,是我在2023年主导开发的一个全栈项目。系统采用经典的前后端分离架构,后端使用SpringBoot 2.7提供RESTful API服务,前端则通过Vue 3组合式API实现响应式界面。项目从零开始搭建到最终上线耗时约3个月,目前已稳定运行半年多,日均活跃用户超过2000人。
技术选型时特别考虑了健身行业的特性:需要实时数据更新但不需要极高并发,因此放弃了React而选择更轻量的Vue;后端则因为需要复杂的数据统计功能,选择了Spring Data JPA + QueryDSL的组合方案。
系统主要分为两大模块:
- 用户端:包含运动数据记录、健康指标分析、训练计划推荐等核心功能
- 管理端:提供用户管理、内容审核、数据统计分析等后台功能
2. 技术架构详解
2.1 后端SpringBoot实现
后端采用经典的三层架构设计:
code复制com.fitness.tracker
├── config # 配置类
├── controller # 暴露的API接口
├── service # 业务逻辑层
├── repository # 数据持久层
├── model # 实体类
└── dto # 数据传输对象
数据库使用MySQL 8.0,主要表结构设计考虑到了健身数据的时序特性:
sql复制CREATE TABLE `user_activity` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`activity_type` enum('RUNNING','CYCLING','SWIMMING') NOT NULL,
`duration` int NOT NULL COMMENT '秒',
`distance` decimal(10,2) DEFAULT NULL COMMENT '公里',
`calories` int DEFAULT NULL,
`start_time` datetime NOT NULL,
`end_time` datetime NOT NULL,
`heart_rate_avg` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_time` (`user_id`,`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2.2 前端Vue.js实现
前端项目采用Vue 3 + Vite构建,主要技术栈包括:
- Vue Router 4:处理前端路由
- Pinia:状态管理
- Axios:HTTP请求
- ECharts:数据可视化
- Vant 4:移动端UI组件
项目结构如下:
code复制src/
├── api/ # 接口定义
├── assets/ # 静态资源
├── components/ # 公共组件
├── composables/ # 组合式函数
├── router/ # 路由配置
├── stores/ # Pinia状态
├── styles/ # 全局样式
└── views/ # 页面组件
3. 核心功能实现
3.1 用户认证模块
采用JWT认证方案,关键实现代码如下:
java复制// JWT工具类
public class JwtUtil {
private static final String SECRET = "your-256-bit-secret";
private static final long EXPIRATION = 86400000L; // 24小时
public static String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
public static String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
前端封装了axios拦截器处理token自动刷新:
javascript复制// axios拦截器配置
api.interceptors.response.use(response => {
return response
}, async error => {
const originalRequest = error.config
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const newToken = await refreshToken()
localStorage.setItem('token', newToken)
originalRequest.headers['Authorization'] = `Bearer ${newToken}`
return api(originalRequest)
}
return Promise.reject(error)
})
3.2 运动数据采集
支持多种数据采集方式:
- 手动录入:用户自行填写运动数据
- 设备同步:通过蓝牙连接智能手环/手表
- 第三方API:接入Strava等运动平台数据
数据校验逻辑示例:
java复制@PostMapping("/activities")
public ResponseEntity<?> recordActivity(@Valid @RequestBody ActivityDTO activityDTO) {
// 校验运动时间不超过24小时
if (activityDTO.getDuration() > 86400) {
throw new ValidationException("运动时长不能超过24小时");
}
// 校验心率数据在合理范围
if (activityDTO.getHeartRateAvg() != null
&& (activityDTO.getHeartRateAvg() < 40 || activityDTO.getHeartRateAvg() > 220)) {
throw new ValidationException("心率数据异常");
}
Activity activity = activityService.recordActivity(activityDTO);
return ResponseEntity.ok(activity);
}
4. 数据可视化实现
4.1 运动数据统计
使用ECharts实现多维度的数据可视化展示:
vue复制<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref(null)
onMounted(async () => {
const res = await getActivityStats()
const chart = echarts.init(chartRef.value)
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['跑步', '骑行', '游泳'] },
xAxis: { type: 'category', data: res.dates },
yAxis: { type: 'value' },
series: [
{ name: '跑步', type: 'line', data: res.running },
{ name: '骑行', type: 'line', data: res.cycling },
{ name: '游泳', type: 'line', data: res.swimming }
]
})
})
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 400px"></div>
</template>
4.2 健康指标分析
基于用户的基础数据计算BMI、基础代谢率等指标:
java复制public class HealthCalculator {
// 计算BMI
public static BigDecimal calculateBMI(BigDecimal height, BigDecimal weight) {
return weight.divide(height.pow(2), 2, RoundingMode.HALF_UP);
}
// 计算基础代谢率(Mifflin-St Jeor公式)
public static int calculateBMR(Gender gender, BigDecimal weight,
BigDecimal height, int age) {
return (int) (gender == Gender.MALE
? (10 * weight.doubleValue() + 6.25 * height.doubleValue() - 5 * age + 5)
: (10 * weight.doubleValue() + 6.25 * height.doubleValue() - 5 * age - 161));
}
}
5. 管理后台功能实现
5.1 用户管理
实现分页查询和批量操作:
java复制@GetMapping("/users")
public Page<UserVO> getUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {
Specification<User> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.hasText(keyword)) {
predicates.add(cb.or(
cb.like(root.get("username"), "%" + keyword + "%"),
cb.like(root.get("email"), "%" + keyword + "%")
));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
return userRepository.findAll(spec, PageRequest.of(page - 1, size))
.map(user -> modelMapper.map(user, UserVO.class));
}
5.2 数据统计报表
使用Spring Data JPA的聚合查询:
java复制public interface ActivityRepository extends JpaRepository<Activity, Long> {
@Query("SELECT new com.fitness.tracker.dto.ActivityStatsDTO("
+ "a.activityType, COUNT(a), SUM(a.duration), AVG(a.heartRateAvg)) "
+ "FROM Activity a "
+ "WHERE a.startTime BETWEEN :start AND :end "
+ "GROUP BY a.activityType")
List<ActivityStatsDTO> getActivityStats(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}
6. 部署与性能优化
6.1 生产环境部署
采用Docker Compose部署方案:
yaml复制version: '3.8'
services:
backend:
image: fitness-tracker-backend:1.0.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://mysql:3306/fitness
depends_on:
- mysql
frontend:
image: fitness-tracker-frontend:1.0.0
ports:
- "80:80"
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=fitness
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
6.2 性能优化措施
- 缓存策略:
- 使用Redis缓存热点数据
- 实现Spring Cache抽象层
java复制@Cacheable(value = "userActivities", key = "#userId + '-' + #date.format(@dateFormatter)")
public List<Activity> getUserActivitiesByDate(Long userId, LocalDate date) {
return activityRepository.findByUserIdAndDate(userId, date);
}
-
数据库优化:
- 为常用查询字段添加索引
- 对大表进行分区(按时间范围)
-
前端性能:
- 实现路由懒加载
- 使用Tree-shaking优化打包体积
7. 开发经验与踩坑记录
7.1 跨域问题解决方案
开发初期遇到的跨域问题最终通过以下配置解决:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8081")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
7.2 文件上传大小限制
SpringBoot默认的文件上传大小限制(1MB)导致用户上传运动图片失败,通过调整配置解决:
properties复制# application.properties
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
7.3 时区问题处理
数据库服务器与应用服务器时区不一致导致时间显示错误,最终统一使用UTC时间存储:
java复制@Configuration
public class JpaConfig {
@Bean
public HibernatePropertiesCustomizer hibernatePropertiesCustomizer() {
return props -> props.put("hibernate.jdbc.time_zone", "UTC");
}
}
8. 项目扩展方向
- 社交功能:添加好友系统、运动排行榜
- AI训练建议:基于用户历史数据生成个性化训练计划
- 健康预警:异常生理指标提醒
- 多端同步:开发移动端App(React Native方案)
这个项目从技术选型到最终上线,让我对现代Web全栈开发有了更深入的理解。特别是在处理实时数据同步和移动端适配方面积累了不少经验。对于想学习SpringBoot+Vue技术栈的开发者,我建议先从这个小而完整的项目开始实践。