前阵子一个做私教工作室的朋友找我聊说会员训练全靠教练肉眼盯一个教练同时带三四个会员动作做没做标准、组间歇多久、一周训练量够不够基本靠感觉他们想要一套能自动记录、能帮忙“盯”动作的系统。聊到最后需求逐渐清晰前端用Vue做交互后端用SpringBoot出接口再塞一个智能AI姿态识别模块把健身动作变成可量化的数据。这个项目前后做了两个多月中间踩了不少坑也把很多“看着能跑、实际没法用”的细节重新捋了一遍。整体做完之后这套“Vue基于SpringBoot的智能AI技术的健身跟踪系统”其实已经不只是个打卡工具更像一个不会累的助教实时计数、动作标准度评分、训练量统计、个性化计划推荐都能跑通。这篇文章就把整个落地过程拆开讲重点放在AI识别怎么接、数据怎么存、上线之后有哪些坑适合正在做类似运动健康类系统、或者想把姿态识别集成到Web项目里的朋友参考。1. 需求澄清阶段客户要的其实不是“智能”而是“少操点心”很多项目一开口就说“要做个智能系统”但如果你直接奔着AI去设计八成会跑偏。这个项目初期只聊了半小时大致摸到一个关键事实客户对“智能”的理解非常朴素——会员做得对不对、做了多少、今天练得累不累教练能随时看到。换句话说AI只是手段自动化的记录与预警才是目的。1.1 最初的需求只是一张Excel表第一版需求文档基本是Excel搬家会员姓名、日期、训练动作、重量、次数、组数。说实话这些表带信息用任何一个在线表单工具都能做。但客户真正头疼的不是记录本身而是补录和编造。会员练完不会自己去填表教练课后补填又容易漏于是数据永远不完整。所以我们把第一个功能定位成“自动采集”会员对着摄像头做动作系统自动识别动作名和次数训练结束自动生成记录。AI在这里的第一个价值就是把数据产生的环节前置到运动过程本身。1.2 拆解后的功能清单和优先级聊完第二轮我们整理出四层需求实时层姿态关键点检测、动作计数、动作标准度提示记录层训练项目、组数、次数、每组耗时、训练总量分析层周训练量趋势、动作习惯偏差、疲劳积累预警计划层根据会员的身体数据和历史表现生成下一周计划优先级上第一层实时反馈最刚需因为教练最怕会员做错动作还猛做练伤比不练更麻烦。分析层和计划层可以后置但数据表设计一开始就要留好扩展空间否则后面补字段会非常痛苦。1.3 为什么技术栈选Vue加SpringBoot而不是别的前端选Vue是基于整个团队最熟的生态Vue 3的组合式API写这种多页面逻辑训练页、历史记录页、计划页、个人中心非常顺手状态管理用Pinia比Vuex清爽很多。后端选SpringBoot主要是稳定和生态成熟权限、数据库事务、定时任务、文件上传都有现成方案招人也好找。也有人问为什么不把后端换成Node或者直接用Python FastAPI甚至把AI推理直接塞进后端。我的回答是AI推理和业务接口的负载特征完全不同。业务接口是短平快AI姿态检测可能一秒钟要处理十几帧图像CPU密集、耗时长。硬塞在一起一个推理请求就能把整个业务线程池堵住。所以最后拆成了三个部分Vue前端、SpringBoot业务后端、独立的AI推理服务。后面有一整段专门讲这个拆分。2. 整体架构与数据流三个服务之间如何配合系统虽然不算大但涉及前端、业务后端、AI推理服务三个进程数据流理不顺就会出连锁问题。我们最终确定的链路是这样的浏览器里的Vue页面调用摄像头把视频帧通过WebRTC在本地预览的同时抽帧交给后端SpringBoot接收到帧数据后把帧转投给AI推理服务处理推理结果再原路返回前端做展示。训练结束后前端把清洗好的结构化数据一次性提交SpringBoot负责写入数据库。2.1 前端怎么抽帧并提交给后端浏览器里直接上传整段视频不现实太占带宽也做不到实时反馈。实际方案是用canvas把video元素当前帧截成图片压缩后以Base64或Blob形式发给后端。前端核心逻辑大致是这样// 在Vue 3组件中定时抽取视频帧 let intervalId null const fps 10 // 实测10帧每秒足够跑太快前端和接口都吃力 function startFrameLoop(videoElement) { intervalId setInterval(() { const canvas document.createElement(canvas) canvas.width 640 canvas.height 480 canvas.getContext(2d).drawImage(videoElement, 0, 0, 640, 480) canvas.toBlob((blob) { const formData new FormData() formData.append(frame, blob, frame_${Date.now()}.jpg) // 交给后端处理返回关键点和动作计数 fetch(/api/pose/detect, { method: POST, body: formData }) }, image/jpeg, 0.7) }, 1000 / fps) }这里有个关键点抽帧大小统一成640×480并且用JPEG 0.7压缩比。一开始我们直接发1080P原图结果一张就几百KB10帧每秒等于每秒几MB流量后端还要花大量时间在解码上延迟高得离谱。后来统一压缩到640宽单帧在30KB到50KB之间识别精度肉眼看不出来明显差异但吞吐量翻了好几倍。2.2 SpringBoot如何设计接收接口后端接收帧的接口要用异步思路不能直接同步等待推理完再返回否则在前端那个setInterval里会越积越多。我们第一个版本就是同步返回结果前端每次请求还没回来下一帧又到了最后浏览器直接卡死。改后的接口设计分两步先接收帧立即返回一个requestIdAI推理服务处理完通过内部消息队列把结果推回SpringBoot或者在SpringBoot里使用CompletableFuture异步编排前端轮询或WebSocket接收结果。PostMapping(/api/pose/detect) public ResponseEntityDetectTaskVO detect(RequestParam(frame) MultipartFile frame) { String taskId UUID.randomUUID().toString(); // 将帧交给推理服务线程池处理 poseDetectService.asyncDetect(taskId, frame); return ResponseEntity.ok(new DetectTaskVO(taskId, PENDING)); }客户端拿到taskId之后继续抽帧同时通过WebSocket订阅结果推送这样既不会阻塞抽帧循环又能保证识别结果及时到前端。2.3 为什么要单独部署AI推理服务一开始我也图省事把姿态识别的模型加载在SpringBoot进程里跑。开发环境一切正常一到实测就露馅加载模型占了大几百MB内存不说推理时CPU飙到90%以上其他业务接口延迟从几十毫秒变成几百毫秒。后来忍痛把推理服务拆出去用Python单独起一个进程只负责“收图片返回关键点坐标”其他一概不管。这样拆分的好处很明显推理服务崩了不影响业务接口重启也只需要针对推理服务推理服务需要GPU时只需要扩那一台机器SpringBoot内存和CPU保持干净业务请求响应稳定坏处是增加了一次网络开销但局域网内一次HTTP调用也就1到2毫秒完全可接受。3. AI姿态识别模块落地从“能识别”到“能当助教”的关键细节这部分是整个系统里最难啃的骨头。开源姿态估计模型能直接输出人体关键点坐标但“能识别”到“能当教练”之间还隔着好几个坎坐标怎么统一、计数怎么防抖、动作标准度怎么打分、误识别怎么处理。3.1 关键点坐标系统一与归一化推理服务返回的关键点坐标默认是像素值直接拿像素判断角度会出现问题同一个人站得近、站得远同样的动作关节角度没变但像素坐标差很多。所以第一步要把关键点归一化。我们用的办法是取左右肩膀中点和左右髋部中点把躯干长度当作参考单位。所有关键点都减去肩髋中点坐标再除以躯干像素长度。这样处理之后无论摄像头远近同一个动作算出来的相对坐标基本一致。处理逻辑如下def normalize_keypoints(keypoints, shoulder_center, hip_center): trunk_length distance(shoulder_center, hip_center) normalized [] for point in keypoints: x (point[x] - shoulder_center[x]) / trunk_length y (point[y] - shoulder_center[y]) / trunk_length normalized.append({x: x, y: y, score: point[score]}) return normalized归一化之后再用向量夹角公式计算关节角度。比如俯卧撑看肘关节角度深蹲看髋关节和膝关节角度。角度计算用余弦定理就好不需要引入复杂库。3.2 动作计数的核心阈值加滞回区间直接拿角度阈值判断“做了一次”会出现一个经典问题会员把动作停在半程系统会来回计数或者数一下停一下。解决思路是加滞回区间类似空调温控的上下限。比如俯卧撑肘关节角度在160度以上算“高位”小于90度算“低位”。系统进入“高位”状态后只有角度低于90度才切换到“低位”再重新回到160度以上计数加1。这样半程抖动不会触发多次计数一个完整下压-上推过程只计一次。COUNTS_HIGH_ANGLE 160 COUNTS_LOW_ANGLE 90 def count_rep(current_angle, state): # state: HIGH 或 LOW if state HIGH and current_angle COUNTS_LOW_ANGLE: state LOW elif state LOW and current_angle COUNTS_HIGH_ANGLE: state HIGH return 1, state return 0, state滞回区间写死之后要能调整不同动作的关节角度阈值不一样甚至同一个人不同阶段的柔韧性也不同。所以阈值全部放配置表里数据库一张表存动作名称、关键点组合、上下阈值方便运营微调。3.3 动作标准度评分如何设计计数只是第一步客户更看重的“动作标不标准”。标准度评分我们设计了两个维度角度偏差和节奏偏差。角度偏差是拿当前关节角度和标准动作的理想角度区间比。比如杠铃深蹲大腿与地面平行时髋关节屈曲角度约在75到80度区间如果会员只蹲到100度就起来说明幅度不足该组直接扣分。节奏偏差是检测动作完成时间下放过快容易受伤上推过快容易借力我们设定单次下放不少于1秒上推不少于0.5秒。综合分数用加权方式score 0.7 * angle_score 0.3 * tempo_score这个分数不是给会员看的唯一指标但用来排序“谁的动作需要教练重点纠正”非常好用。后台可以拉出一周内平均分低于60分的会员名单教练优先盯这部分人。3.4 平滑滤波与误识别处理原始关键点坐标每一帧都有抖动直接算角度会毛刺满满。我们试了简单移动平均和指数平滑最后选了指数平滑因为它在平滑程度和响应延迟之间更均衡。公式很简单smooth_value alpha * current_value (1 - alpha) * previous_smoothalpha取0.4左右太大平滑不足太小动作会被“滞后”显得迟钝。处理后角度曲线干净很多计数也更稳定。误识别方面遇到过会员背景里有人路过模型把路人当成主训练人的情况。解决办法是锁定人物训练开始时让会员站到摄像头前特定区域框出主体人物之后只跟踪离摄像头最近且置信度最高的人体框。这个逻辑加完之后误识别率直线下降。4. 训练数据存储与统计要能查历史更要能算趋势AI识别出来的原始数据如果只是丢进一张大表后面查询和统计都会很痛苦。设计存储时我坚持把数据分了三层原始关键点序列、结构化训练记录、汇总统计表。4.1 数据表结构设计上的取舍原始关键点序列只保留最近三天的主要用于回放和拆解动作细节数据量大且基本不需要长期存。结构化训练记录是核心包含训练日期、动作名称、每组次数、每组平均角度、平均速度、标准度评分。汇总统计表则是按天、按周预聚合的训练量、时长、动作次数。核心表结构大致如下CREATE TABLE workout_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, exercise_name VARCHAR(50) NOT NULL, workout_date DATE NOT NULL, set_number INT NOT NULL, reps INT NOT NULL, avg_score DECIMAL(5,2), avg_speed DECIMAL(5,2), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_date (user_id, workout_date) ) ENGINEInnoDB; CREATE TABLE workout_daily_summary ( user_id BIGINT NOT NULL, summary_date DATE NOT NULL, total_reps INT NOT NULL, total_sets INT NOT NULL, total_score DECIMAL(7,2), exercise_count INT NOT NULL, PRIMARY KEY (user_id, summary_date) ) ENGINEInnoDB;有个容易忽略的细节workout_record里一定要存set_number否则同一天同一动作的多组数据只能靠时间拼接后期对不上组序号。4.2 卡路里预估与训练量计算卡路里预估我们没用通用公式硬套因为健身动作的消耗和重量、次数、体重强相关。最后用的是修正公式calories MET_value * weight(kg) * duration(hours) * intensity_factorMET值按动作类型查表比如杠铃深蹲取5.0卧推取4.5强度因子根据本次平均速度调整速度偏快取1.1正常取1.0偏慢取0.9。算出来之后再和会员手环的心率数据交叉验证误差基本控制在15%以内。训练量统计采用“吨位法”也就是总重量乘以总次数。这个指标用来做渐进超负荷判断非常直观如果连续一周单次训练总量比上周下降超过20%结合标准度评分偏低我们就会触发疲劳预警建议教练安排减载周。4.3 离线补录与数据一致性训练过程中网络波动很常见前端不可能每次都保证实时上报成功。我们设计了一个本地批次队列每完成一组训练数据先写浏览器的localStorage标记为“待同步”网络恢复后按时间顺序批量提交服务端用idempotent_key字段去重防止重复写入。这里踩过一个坑如果队列里的记录时间戳以本机为准会员把手机时间改了自己都不知道数据排序会乱七八糟。后来后端统一以服务端收到的时间为准同时保留前端上报的设备时间作为参考字段训练分析的日期都取服务器日期。5. 智能推荐模块从历史数据到下一周训练计划推荐模块是客户眼里最“智能”的部分但底层逻辑其实不复杂先建立用户画像再基于训练容量做渐进超负荷推荐最后用规则引擎兜底。AI在这个模块里更多是优化权重而不是神奇地“猜”出计划。5.1 用户画像的构成画像不只看体型数据还包括训练行为数据基础信息体重、身高、体脂率、静息心率运动能力最大力量、1RM估算、核心稳定性评分训练习惯每周训练次数、常用动作、平均组数恢复信号训练后心率回归时间、连续训练日后标准度评分下降幅度这些数据主要从训练记录自动算出来加上会员首次录入的基础信息。不需要太多魔法参数数据积累两周之后画像就会稳定。5.2 训练容量与渐进超负荷逻辑推荐计划的核心是对“下周该练多重、多少次”给出建议。我们用经典原则训练容量每周小幅上升通常控制在3%到5%。比如上周卧推总容量是6000公斤这周就建议6000乘以1.04约为6240公斤。同时把冲击次数设定在8到12次范围如果会员连续三次训练都能完成12次则下次增加重量而不是继续增加次数。public WorkoutPlan generateNextPlan(UserProfile profile) { double currentVolume profile.getLastWeekVolume(); double targetVolume currentVolume * 1.04; int targetReps (int) Math.ceil(targetVolume / profile.getWorkingWeight()); // 限制次数范围过多则建议增加重量 if (targetReps 12) { double newWeight profile.getWorkingWeight() * 1.025; targetReps (int) Math.ceil(targetVolume / newWeight); return WorkoutPlan.weightUp(newWeight, targetReps); } return WorkoutPlan.keepWeight(profile.getWorkingWeight(), targetReps); }这个逻辑很朴素但胜在可解释。会员和教练都能看懂“为什么这周加了这个重量”比黑盒神经网络输出一个“建议”更让人信服。5.3 规则优先AI做调节我们没有让模型直接输出整份计划而是先走规则引擎根据会员历史动作偏好、可用器械、伤痛限制生成基础模板再用AI模型调整顺序和组间休息。比如模型发现会员上周高强度训练后第二天标准度评分骤降就会自动把原本排在第2天的腿部训练推迟到第3天并把组间休息从60秒调到90秒。这类调节逻辑用决策树就能覆盖八成场景不需要上大模型。真遇到复杂情况再让推理服务的模型给一个“疲劳度”评分后台人工确认。推荐模块的核心不是算法炫技而是让教练和会员都觉得建议靠谱、理由清晰。6. 上线后的性能与稳定性踩坑记录能跑通和能稳定跑是两码事。我们上线前做了一轮内部压力测试结果发现的问题比开发期多得多专门列出来希望后来的人少踩。6.1 摄像头帧率上不去的瓶颈排查第一次实机测试浏览器端预览画面一直卡顿。排查链路是先看前端再看网络最后定位到问题在canvas导出图片环节。开发时一直在电脑上跑没注意手机端的性能差异低端手机一次canvas绘图加编码要120毫秒10帧每秒根本跑不满。后面把抽帧频率降到5帧每秒并把帧数据大小降到30KB左右预览卡顿消失识别精度也够用。如果是重要动作比如硬拉这种需要精确捕捉杠铃轨迹的动作我们保留前端的“手动校准模式”会员进入动作前可以自己按一下补拍一帧不至于为了流畅牺牲关键判断。6.2 高并发上报时的数据库写入压力训练高峰期是晚上7点到10点几十个会员同时结束训练系统出现批量上报数据库瞬时写入量很大。刚开始用的单次insert每条记录刷一次事务结果日志里大面积“锁等待超时”。改动方式很粗暴但有效批量插入每次累积20条记录或者2秒内攒到的所有记录一次INSERT INTO ... VALUES (...), (...)写入。同时把最频繁的汇总表更新挪到异步任务里不直接阻塞主链路。实测写入量翻三倍也没再出锁问题。6.3 HTTPS与摄像头权限必须一起解决WebRTC获取摄像头有一个绕不开的前提页面必须跑在HTTPS环境下否则浏览器直接拒绝授权摄像头权限。本地开发还能用localhost豁免一旦部署到公网服务器不是HTTPS就全部白搭。这个问题我们上线当天才发现临时申请证书又拖了一天。经验教训是这类涉及浏览器硬件API的项目环境准备清单里把“域名加HTTPS证书”放在最前面不要等页面写完才想起来。另外AI推理服务的CPU消耗也要提前评估。我们的推理服务在单机8核配置下单帧推理耗时约60到80毫秒5帧每秒已经是上限。如果以后要支撑更多并发用户优先方案是给推理服务加GPU或者横向扩容而不是去优化业务后端。这套系统做完我最大的感受是AI技术本身没有多玄乎姿态识别模型选型合适就能用真正决定项目成败的往往是数据链路设计、异常兜底和那些看起来不重要的字段。比如关键点归一化、计数滞回、批量插入、HTTPS证书任何一环松了整个系统都会给人一种“能用但别扭”的感觉。如果你也在做类似方向建议先把训练记录的数据模型设计扎实再考虑上多花哨的AI能力。数据干净了后续分析、推荐、预警都是水到渠成的事。