PHP项目统计慢跑恢复时间数据方案
慢跑恢复时间"统计,需要先明确具体指标定义,以下是完整的实现思路。

明确统计指标
慢跑恢复时间通常涉及以下维度:
| 指标 | 说明 |
|---|---|
| 静息心率恢复 | 跑步结束后心率回落到静息值的时间 |
| 心率恢复率(HRR) | 停止后1/2分钟心率下降值 |
| 运动后恢复时长 | 从高强度回到轻松状态的时间 |
| 配速恢复 | 配速波动后回到目标配速的时间 |
数据库设计
-- 跑步记录表
CREATE TABLE run_records (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME,
distance DECIMAL(6,2), -- 公里
avg_pace INT, -- 平均配速(秒/公里)
avg_heart_rate INT,
max_heart_rate INT,
resting_heart_rate INT, -- 静息心率
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_time (user_id, start_time)
);
-- 心率采样表(用于精确计算恢复时间)
CREATE TABLE heart_rate_samples (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
run_id INT NOT NULL,
sample_time DATETIME NOT NULL,
heart_rate INT NOT NULL,
is_recovery TINYINT DEFAULT 0, -- 是否属于恢复阶段
INDEX idx_run_time (run_id, sample_time)
);
-- 恢复统计结果表
CREATE TABLE recovery_stats (
id INT PRIMARY KEY AUTO_INCREMENT,
run_id INT NOT NULL UNIQUE,
user_id INT NOT NULL,
hrr_1min INT, -- 1分钟心率恢复
hrr_2min INT, -- 2分钟心率恢复
recovery_seconds INT,-- 完全恢复耗时(秒)
recovery_quality ENUM('excellent','good','fair','poor'),
calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id)
);
核心统计逻辑(PHP)
<?php
class RecoveryCalculator
{
private PDO $db;
// 恢复判定阈值:心率降到静息心率的 110% 以内
private const RECOVERY_THRESHOLD = 1.1;
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* 计算单次跑步的恢复时间
*/
public function calculate(int $runId): array
{
// 1. 获取跑步基本信息
$stmt = $this->db->prepare(
"SELECT * FROM run_records WHERE id = ?"
);
$stmt->execute([$runId]);
$run = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$run || !$run['resting_heart_rate']) {
throw new RuntimeException('数据不完整');
}
// 2. 获取恢复阶段心率采样(结束后的记录)
$stmt = $this->db->prepare(
"SELECT sample_time, heart_rate
FROM heart_rate_samples
WHERE run_id = ? AND is_recovery = 1
ORDER BY sample_time ASC"
);
$stmt->execute([$runId]);
$samples = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($samples)) {
throw new RuntimeException('无恢复期数据');
}
$restingHR = (int)$run['resting_heart_rate'];
$threshold = $restingHR * self::RECOVERY_THRESHOLD;
$endTime = strtotime($run['end_time']);
$peakHR = (int)$run['max_heart_rate'];
$hrr1 = null;
$hrr2 = null;
$recoverySeconds = null;
// 3. 遍历采样,计算各项指标
foreach ($samples as $s) {
$elapsed = strtotime($s['sample_time']) - $endTime;
$hr = (int)$s['heart_rate'];
// 1 分钟心率恢复
if ($hrr1 === null && $elapsed >= 60) {
$hrr1 = $peakHR - $hr;
}
// 2 分钟心率恢复
if ($hrr2 === null && $elapsed >= 120) {
$hrr2 = $peakHR - $hr;
}
// 完全恢复时间:首次降到阈值以下
if ($recoverySeconds === null && $hr <= $threshold) {
$recoverySeconds = $elapsed;
break;
}
}
$quality = $this->evaluateQuality($hrr1, $recoverySeconds);
// 4. 写入统计表
$stmt = $this->db->prepare(
"INSERT INTO recovery_stats
(run_id, user_id, hrr_1min, hrr_2min, recovery_seconds, recovery_quality)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
hrr_1min=VALUES(hrr_1min),
hrr_2min=VALUES(hrr_2min),
recovery_seconds=VALUES(recovery_seconds),
recovery_quality=VALUES(recovery_quality)"
);
$stmt->execute([
$runId, $run['user_id'], $hrr1, $hrr2, $recoverySeconds, $quality
]);
return compact('hrr1', 'hrr2', 'recoverySeconds', 'quality');
}
/**
* 评估恢复质量(参考运动医学常规标准)
*/
private function evaluateQuality(?int $hrr1, ?int $recoverySec): string
{
if ($hrr1 === null) return 'fair';
// HRR1 >= 25 一般视为良好,12-25 中等,<12 较差
if ($hrr1 >= 30 && $recoverySec !== null && $recoverySec <= 180) {
return 'excellent';
}
if ($hrr1 >= 25) return 'good';
if ($hrr1 >= 12) return 'fair';
return 'poor';
}
}
聚合统计查询
/**
* 按周/月统计用户恢复趋势
*/
public function getUserTrend(int $userId, string $period = 'week'): array
{
// period 白名单,防止 SQL 注入
$groupExpr = match ($period) {
'week' => "YEARWEEK(r.start_time, 1)",
'month' => "DATE_FORMAT(r.start_time, '%Y-%m')",
'day' => "DATE(r.start_time)",
default => throw new InvalidArgumentException('无效周期'),
};
$sql = "SELECT
{$groupExpr} AS period_key,
COUNT(*) AS run_count,
AVG(s.hrr_1min) AS avg_hrr1,
AVG(s.recovery_seconds) AS avg_recovery_sec,
MIN(s.recovery_seconds) AS best_recovery_sec,
SUM(CASE WHEN s.recovery_quality IN ('excellent','good') THEN 1 ELSE 0 END)
/ COUNT(*) AS good_rate
FROM run_records r
JOIN recovery_stats s ON s.run_id = r.id
WHERE r.user_id = :uid
AND r.start_time >= DATE_SUB(NOW(), INTERVAL 90 DAY)
GROUP BY period_key
ORDER BY period_key DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute([':uid' => $userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
性能优化建议
- 采样数据分表/分区:心率采样数据量大,按
run_id或时间做分区 - 异步计算:恢复统计放入队列(Redis/RabbitMQ)异步处理,避免阻塞主请求
- 缓存聚合结果:周/月统计用 Redis 缓存,设 TTL 5 分钟
- 索引优化:
heart_rate_samples(run_id, is_recovery, sample_time)联合索引 - 预聚合表:海量数据可用每日汇总表代替实时
GROUP BY
注意事项
- 数据来源:需明确心率数据来源(GPS手表 / 手机传感器),采样频率一般 1-5 秒
- 静息心率基准:建议用长期平均值而非单次值,更准确
- 医学免责:恢复指标仅供参考,需注明"不构成医学建议"
- 边界情况:跑步中途异常心率、数据缺失、采样间隔不均匀都要处理
如果你能补充具体场景(比如是跑团管理后台、运动App后端还是个人训练日志?数据来自API还是手动录入?),我可以给出更贴合的实现。