你搜遍了网上常见的健身记录App会发现一个尴尬的事实绝大多数产品只能做到“手动录入”——练完一组自己记下重量、次数再填一个自我感觉完事。真正能做到“打开摄像头AI自动识别你正在做什么动作、计数、评价标准度、估算消耗”的少之又少。或者产品有但闭源、贵、定制困难。所以我决定自己动手做了这么一套“Vue Spring Boot 智能AI技术”的健身跟踪系统。核心思路很简单用前端Vue负责交互和视频流展示用Spring Boot承担业务逻辑和数据处理再夹一层AI推理引擎做姿态关键点检测让系统真正“看懂”人在练什么。这篇文章会把项目的整体设计、后端接口、前端交互、AI识别细节、性能调优和踩坑记录全部拆开讲。适合正在做全栈项目的开发者、想给系统加“视觉能力”的同学以及准备拿这类题目做毕业设计或工程实践的读者直接参考。整套东西做下来最有价值的不是写了一堆CRUD接口而是把“前端拿视频帧”、“后端算角度”、“AI下结论”这三件事顺畅地串到了一起。下面直接进入正题。1. 项目概述一个“会看”的健身跟踪系统1.1 核心需求拆解这个项目表面上是“健身记录”但拆开需求后内部有三层完全不同的活业务层用户的注册登录、个人资料、训练计划、运动历史、数据统计报表这些是典型的管理系统功能。交互层前端页面、实时视频预览、训练过程动画、历史曲线、仪表盘。这部分对用户体验要求很高摄像头画面要流畅骨架关键点要实时叠加在人体上。AI层从视频帧中检测人体姿态关键点计算关节角度判断当前动作类别统计动作次数评估动作标准度。这是整套系统的核心亮点也是难度最高的部分。如果只是按普通管理系统去做数据库建几张表、写几个CRUD接口一个月就能做完。但加入AI识别后系统的复杂度立刻上了一个台阶——你得处理视频流、管理推理状态、协调前端与AI服务的通信还要考虑性能问题。我做了一个关键决定把AI推理独立成一个服务层而不是塞进Spring Boot的业务代码里。原因后面细讲这里先记住结论。1.2 技术选型为什么是这套组合技术栈的选择是在动手前就要定死的中途换框架是灾难。我最终定下来的是模块技术选型理由前端框架Vue 3 Element Plus组件化开发效率高生态成熟视频/Canvas操作方便前端构建Vite开发冷启动快热更新及时调试AI交互时体感极好后端框架Spring Boot 2.7稳定、生态全、JPA集成快文档量大遇到问题好搜数据库MySQL 8.0存用户和训练记录足够事务成熟缓存Redis存AI推理过程的临时状态、验证码、Token黑名单AI推理框架MediaPipe Pose轻量、自带关键点模型实时性够用不用自己训练模型通信方式RESTful API WebSocket业务接口走REST实时帧处理结果走Socket减少握手开销选择Vue而不选React纯粹是从团队熟悉度出发Vue的模板语法配合Element Plus做后台管理系统非常顺手而且Vue 3的Composition API在处理摄像头这种“多状态”场景时逻辑清晰度明显比Options API高。Spring Boot不多说Java后端在中小型项目里还是最稳妥的选择。AI部分用MediaPipe Pose而不是自己训练Pose模型原因很实际单人姿态估计这种基础任务开源模型精度已经足够自己训练数据量不够反而容易翻车。MediaPipe Pose能给出33个关键点坐标覆盖全身配合前端Canvas可以直接把骨架画到视频画面上开发效率极高。2. 数据库与后端整体设计2.1 数据模型设计思路健身跟踪系统有几类核心实体必须一开始就理清关系用户表存账号、密码、昵称、身高、体重、年龄段。体重会参与卡路里计算所以要单独存并且支持用户后续修改。动作类别表预设动作库比如深蹲、俯卧撑、开合跳、卷腹、平板支撑等。每条记录包含动作名称、类别、默认消耗MET值、初始标准角度参数。训练记录表一次训练的汇总信息包括开始时间、结束时间、总时长、总次数、总消耗卡路里。训练明细表训练过程中每一组的详情关联到具体动作记录单组次数、持续时长、平均标准度得分。身体指标记录表用户各阶段的体重、BMI变动用于趋势分析。这些表之间的关系训练记录和训练明细是典型的一对多用户和训练记录也是一对多。建表时直接用JPA实体映射省去写大量SQL的时间。MySQL选InnoDB引擎要注意给外键字段都加上索引否则数据量上来以后联表查询明显变慢。2.2 后端模块划分与接口设计Spring Boot项目我按照功能做了分包每个包职责单一避免出现一个几百行的God类controller接收请求、参数校验、返回统一响应体。service业务逻辑实现事务开在这一层。repository数据访问层继承JpaRepository。entity数据库映射实体。dto前端数据传输对象避免直接把实体暴露给客户端。aiAI推理调度封装调用Python推理服务。config配置类包括跨域、WebSocket、拦截器配置。接口设计按模块分用户模块注册、登录、更新资料、刷新Token。注册时后端做了密码加密用BCrypt这个比MD5安全不止一个量级。动作库模块管理员维护动作前端在开始训练时拉取列表展示。训练模块开始训练、结束训练、上报单组明细、获取历史记录。训练进行中前端每完成一组动作就提交一次明细后端汇总更新总次数和总热量。统计模块按时间段聚合训练频次、动作分布、卡路里趋势给前端ECharts渲染。统一响应体我封装成了ResultT结构包含code、message、data三个字段。这个看似基础实际用处很大前端统一处理错误提示不用每个接口单独判断。登录鉴权用的是JWTAccess Token有效期两小时Refresh Token有效期七天。前端在Axios拦截器里统一附带Token后端在Spring Security配置里放行登录、注册等公开接口其他请求全部校验。Redis存Token黑名单用户退出登录时把Token标记失效。3. 前端Vue设计与视频交互实现3.1 页面结构与组件规划前端页面围绕用户使用路径来组织登录注册页纯表单验证码联动后端Redis。首页仪表盘展示今日训练情况、本周运动天数、累计卡路里、快速开始按钮。训练页核心页面占整个系统开发量的一半。左侧视频预览区域右侧实时数据显示当前动作、次数、标准度、卡路里预估。历史记录页展示训练日历Heatmap点击某天查看当日详情。个人中心页修改资料、身高体重变化曲线。训练页用Vue 3的script setup语法逻辑拆分成了几个组合式函数useCamera负责摄像头启动和关闭usePoseDetect负责帧处理与关键点计算useTrainingSession负责训练会话状态维护。这样每个文件的行数控制在一定范围内出了问题也好定位。3.2 摄像头视频流与AI画布叠加这是整个前端最核心的实现。视频不能直接拿video元素就完事因为要在画面上叠加人体骨架。我的做法是// 启动摄像头 async function startCamera() { const stream await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 }, audio: false }); videoRef.value.srcObject stream; await videoRef.value.play(); // 开始帧循环 requestAnimationFrame(processFrame); }拿到视频流后用一个隐藏的Canvas做AI推理的输入源每帧把视频画面绘制到Canvas上再将video帧转成ImageData传给后端的AI推理服务。推理返回的关键点坐标归一化的x、y乘上视频实际宽高再画到展示用的Canvas上// 关键点在原始视频坐标系中绘制骨架和关节点 function drawSkeleton(ctx, landmarks, width, height) { ctx.clearRect(0, 0, width, height); // 连接线定义MediaPipe Pose 33个关键点按骨骼顺序连接 const connections [ [11, 12], [11, 13], [13, 15], [12, 14], [14, 16], [11, 23], [12, 24], [23, 24], [23, 25], [25, 27], [24, 26], [26, 28] ]; connections.forEach(([i, j]) { const a landmarks[i]; const b landmarks[j]; if (a b) { ctx.beginPath(); ctx.moveTo(a.x * width, a.y * height); ctx.lineTo(b.x * width, b.y * height); ctx.strokeStyle #00e676; ctx.lineWidth 3; ctx.stroke(); } }); }这里有个容易踩的坑MediaPipe返回的关键点是归一化坐标范围在0到1之间绘制前必须乘以对应的视频帧宽度和高度。如果直接把归一化坐标当像素位置用骨架会画得奇形怪状我最初调试时差点以为是模型问题。3.3 实时状态展示与组数上报训练过程中右下角要实时显示当前动作次数和标准度评分。这块逻辑放在前端做判断还是一个动作做完了就立即向后端发起一次请求我最终采用了“前端汇总、组完成上报”的策略。前端的useTrainingSession中维护一个会话对象记录当前动作、已做次数、组内标准度平均值。当用户点击“完成本组”或在连续完成N次动作后前端把这一组的汇总数据动作ID、次数、平均标准度、开始时间、结束时间一次性POST到后端。这样后端压力小也避免网络抖动导致数据重复。标准度评分在前端实时算基于AI推理返回的角度值与标准角度的偏差。偏差越小评分越高每帧的评分经过移动平均后展示出来避免数值忽高忽低让人困惑。4. AI姿态识别与动作分析的核心实现4.1 姿态关键点检测的原理这里必须把AI层的原理讲透因为这是整套系统的灵魂也是最容易出问题的部分。姿势估计本质上是回归问题输入一张图像输出人体若干个关键点landmarks的像素坐标。MediaPipe Pose的模型基于BlazePose架构能在CPU级设备上跑出每秒几十帧的效果。模型输出33个关键点覆盖脸部、躯干、手臂和腿部。每个关键点有三个属性x、y坐标归一化和可见性visibility0到1之间表示该点被遮挡的程度。对于手机或笔记本摄像头场景视野里有一个人笔直站立关键点通常能全出。但做俯卧撑时身体垂直于摄像头平面关键点会因遮挡变得不可靠。这时候就要依赖可见性来判断如果某个点的visibility低于阈值计算角度时就跳过该关节改用相邻帧的数据做平滑避免出现角度值突变导致计数错误。4.2 关节角度计算与动作判定有了关键点坐标下一步就是计算关节角度。我用的是三维向量的夹角计算假设要计算左肘关节的角度取左肩11、左肘13、左腕15三个点先算出从肘到肩的向量A再从肘到腕的向量B然后用向量点积公式cos(θ) (A·B) / (|A| * |B|)双向量夹角公式用代码实现就是public double calculateAngle(Point a, Point b, Point c) { double v1x a.x - b.x; double v1y a.y - b.y; double v2x c.x - b.x; double v2y c.y - b.y; double dot v1x * v2x v1y * v2y; double norm1 Math.sqrt(v1x * v1x v1y * v1y); double norm2 Math.sqrt(v2x * v2x v2y * v2y); double angle Math.acos(dot / (norm1 * norm2)); return Math.toDegrees(angle); }以深蹲为例动作判定的核心是髋关节角度23、25、27三个点在屈膝时超过一定阈值时认为进入下蹲状态。膝关节角度25、27、29在伸直时回到阈值以上认为完成一次。我在代码里定义了一个状态机用state变量标记当前是在“上升”还是“下降”。只有从“上升”切换回“下降”才计数一次这就避免了一个动作被重复计数。4.3 AI服务独立拆分的架构选择最初我尝试过在Spring Boot进程内直接调用MediaPipe的Java版本但发现两个问题一是Java版的模型推理效率不如Python版CPU占用高二是如果模型崩溃整个后端服务就得重启影响不可控。后来我把AI推理独立成微服务Spring Boot通过HTTP请求调用一个单独的Python FastAPI服务该服务加载MediaPipe模型接收前端传来的视频帧Base64编码返回关键点列表。两者之间走LAN内网地址延迟大约20到30毫秒完全在可接受范围内。流程示意如下前端摄像头每帧画面经Canvas转成30KB左右的JPEG Base64。Vue通过WebSocket把Base64帧发送给Spring Boot服务端。Spring Boot把帧数据转发给Python AI服务。Python服务用MediaPipe推理返回33个关键点的归一化坐标。Spring Boot把关键点坐标推回前端前端绘制骨架并计算角度。前端基于角度判定动作次数完成后上报训练明细。这个链路看起来长但因为WebSocket长连接保持复用实际每帧处理耗时在80毫秒以内摄像头采集帧率设置为15帧每秒时CPU依旧平稳。5. 性能优化与部署注意事项5.1 帧率与延迟的平衡训练运动中人物动作是快速的如果推理延迟过高骨架会“拖影”体验极差。我调整了几个关键参数控制WebSocket发送帧率不能每帧都发会占用大量带宽。实测15帧每秒是性价比最高的区间视觉流畅度足够AI服务也能稳定处理。压缩帧体积Canvas导出的图像先用JPEG格式且质量设到0.7单帧大小从100KB降到30KB以内WebSocket传帧延迟明显下降。前端使用requestAnimationFrame但通过时间戳控制只有间隔达到约66毫秒才真正传输一帧防止因浏览器渲染机制导致队列积压。5.2 后端并发与数据库压力控制训练高并发期集中在晚上大量用户同时提交训练明细。每个用户提交一组明细是一秒内低频操作数据库压力不大。真正的瓶颈在AI服务的CPU。如果一台服务器上同时来了20个用户做姿态识别Python推理服务可能会把CPU打满。我的处理办法是给Python服务加了一个简单的令牌桶限流当并发推理请求超过某个阈值时直接拒绝排队中的请求并通知前端“AI服务繁忙”。前端收到这个状态后会降级为“手动计数模式”不再依赖AI识别。这个降级方案保证了系统在极端情况下不崩溃。5.3 部署架构与资源规划部署时我把前端静态资源打包后放到NginxNginx代理/api请求到Spring BootWebSocket走/ws路径单独代理。MySQL和Redis各自部署Python AI服务单独开一个Python环境。服务器配置方面建议至少4核8G因为AI推理是CPU密集型如果服务器只有2核识别帧率只能压到10帧左右。Nginx关键配置示例server { listen 80; server_name your-domain; location / { root /opt/fit/frontend/dist; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /ws { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }部署过程中特别注意跨域问题。虽然开发时用了前端代理解决跨域但生产环境的Nginx反向代理让前后端处于同一域名下实际不存在跨域。如果非要前后端分离部署成两个域名Spring Boot这边必须配置CORS并放开对应Origin地址。5.4 数据安全与权限控制健身数据属于个人敏感数据。除了登录鉴权我为训练记录接口做了“只能访问自己数据”的校验每个查询都从JWT解析出用户IDSQL查询强制带上这个条件防止越权访问。这层逻辑虽然简单但必须写我看到过太多后端只做登录校验不校验属主用户改了遍历参数就能看到别人数据的案例。网络传输全站启用HTTPS这属于必选项。摄像头画面属于实时数据虽然WebSocket内部走加密通道但前端页面必须确保在HTTPS下才能使用getUserMedia——如果是HTTP环境浏览器直接拒绝打开摄像头本地调试时可以用localhost绕过但部署到服务器上就必须配置证书。6. 实测效果与体验优化6.1 动作识别准确率与误判处理项目完成后我找了几位朋友分别测试深蹲、俯卧撑、开合跳、卷腹四个动作。整体准确率如下基于完整完成动作的次数动作识别成功率常见误判原因深蹲93%半蹲时髋关节角度未达阈值未计次俯卧撑87%身体过低导致关键点遮挡严重开合跳91%快速动作时帧率不够跳帧导致漏计卷腹78%摄像头视角过低躯干关键点重叠降低误判的方法是给每个动作增加了“置信度”概念一组动作连续两次满足状态机条件才判定有效。这确实减少了一部分真次数漏计但避免了更多次误加在体验上误加更让人抓狂。6.2 界面交互细节的打磨训练页右侧实时数据卡的数值更新频率做了限制每200毫秒刷新一次UI。如果每帧都更新数字用户的注意力会被快速跳动的数字分散反而不利于投入训练。标准度评分采用EMA指数移动平均// 指数移动平均平滑标准度分数 let emaScore null; function smoothScore(rawScore) { emaScore emaScore null ? rawScore : emaScore * 0.7 rawScore * 0.3; return Math.round(emaScore); }训练结束时页面会展示本次训练的总时间、总次数、总消耗卡路里和动作分布饼图。卡路里计算用的是MET公式卡路里 MET值 × 体重(kg) × 持续时间(h)每个动作的MET值预设如下深蹲5.0、开合跳8.0、俯卧撑3.8、卷腹2.8。同一动作不同强度下MET值差异很大这套系统暂时用固定值后面可以按用户心率数据动态修正。6.3 用户反馈驱动的迭代优化第一轮测试中超过半数用户反馈“不知道当前姿势是否正确”。于是我在视频画布上用不同颜色标出关键角度当前角度与标准角度偏差在10度以内显示绿色10到20度显示橙色超过20度显示红色并在旁边提示该调整哪个关节。这个功能改动不大但显著提升了用户对系统的信任感。另外一轮迭代中我把训练历史从列表改成日历热力图每天一个色块颜色深浅代表训练量。用户打开历史页就能直观看到自己一周练了几天不用像看表格一样去数记录。这种小型交互优化对留存率很有帮助。7. 踩过的坑和排查方法总结这部分全是实战中换来的教训写出来给后来人排雷。7.1 摄像头在Safari浏览器上无法打开排查过程同一套代码在Chrome上完全正常但iPhone上的Safari毫无反应。查了浏览器报错发现是NotAllowedError提示权限被拒。原因有两层一是通过HTTP访问时Safari对权限要求更严格必须HTTPS二是getUserMedia要在用户手势触发的事件回调里调用如果放在页面加载的onMounted里自动触发Safari会直接拒绝。解决方法是把开启摄像头的操作绑定在“开始训练”按钮的点击事件中并且提前检查协议是否为HTTPS或localhost做友好提示。国内访问Safari用户也不在少数这坑必须提前处理。7.2 WebSocket连接频繁断开表现训练进行中前端图标一会在线一会离线训练数据中断。排查大量断线发生在摄像头启动和停止的瞬间。原因是我在WebSocket连接建立后又在组件里反复执行了initCamera逻辑每次操作都会触发一次前端重新连接。解决WebSocket连接只在训练会话开始时建立一次摄像头停止时先断开Socket再释放视频流。同时给Socket加心跳机制每30秒发送一次ping超过60秒未收到pong就主动重连。7.3 坐标绘制偏移问题表现骨架在人身体左侧抖动且随着人物移动偏移越来越大。排查前端requestAnimationFrame绘制的图像宽高和Canvas自身坐标系不一致。具体来说video的实际视频宽高设为640×480但Canvas的样式宽度被CSS设置成了100%容器宽度。在drawImage绘制时未按实际像素取值骨架自然就歪了。解决给绘制Canvas固定像素宽高并取消CSS缩放。必要时用ctx.scale做适配但绝对不要让Canvas的width属性和CSS宽度不一致。7.4 深蹲计数时单次动作被记成两次表现用户只做了一个深蹲系统计了两次。原因状态机判断逻辑有缺陷我在“膝盖角度超过阈值”这个条件里没有区分是屈膝还是伸膝导致同一动作在屈膝和伸膝过程中各触发了一次计数。解决状态机必须显式定义状态转换方向。只有从“下蹲中”状态切换到“站立中”状态才计数而从“站立中”进入“下蹲中”不计数。代码增加了动态的状态迁移表后续增加新动作时只需配置迁移条件不用改核心逻辑。7.5 AI服务内存泄漏导致响应越来越慢表现系统运行几天后AI服务响应从20毫秒退化到500毫秒。排查内存占用持续上涨重启后恢复。查了代码发现MediaPipe实例被重复创建每次请求都new一个Pose对象旧对象没有被释放导致GPU/CPU资源被耗尽。解决AI服务使用单例模式启动时创建一个全局Pose实例所有推理请求复用该实例同时增加请求处理前的显式gc()和关键点缓冲区清理。8. 后期扩展方向与个人体会这套系统做完后我最大的感受是AI技术本身没有神奇魔力能不能落地完全取决于工程化能力。MediaPipe的认识模型是现成的但把它无缝嵌入到业务系统里、保证实时性、处理各种边界情况这部分工作才是项目真正的难点。后续扩展我规划了几个方向一是接入心率监测设备把生理数据纳入训练负荷分析二是做多动作连续识别自动判断用户在同一个计划中从深蹲切换到俯卧撑三是训练报告加入动作标准度排名指出最容易出错的关节部位。这些功能在现有架构下都能平滑扩展因为AI层、业务层、展示层已经彻底解耦了。如果你也打算做类似项目我的建议是先跑通一条最简链路再填充业务功能。先把摄像头画面、AI关键点、骨架绘制、角度计算这四个环节串起来你会对整套系统有一种“通了”的掌控感。之后再往里加用户、课程、统计这些模块就不会被复杂度淹没。另外代码中凡是涉及角度阈值、平滑系数、帧率限制这些参数建议都提取到配置文件里前端和后端各维护一份运行时配置。我在开发过程中反复调整这些参数如果没有配置化改一次参数得重新部署一次太浪费时间了。最后务必把摄像头权限的HTTPS要求、浏览器兼容性这些环境问题排在开发计划前面——它们不解决功能做得再漂亮也跑不起来。