// 2026-04-24 | test_body_analysis.dart | 身体分析算法验证脚本 | 验证所有计算公式正确性 // 运行: dart run scripts/test_body_analysis.dart import 'dart:math' as math; import 'dart:io'; // ===== 复制核心计算逻辑(独立验证,不依赖Flutter) ===== enum Gender { male, female } enum ActivityLevel { sedentary, light, moderate, active, veryActive } class BodyAnalysisCalculator { final Gender gender; final double age; final double height; final double weight; final double? waist; final double? hip; final double? neck; final double? wrist; final ActivityLevel activityLevel; final double? bodyFatInput; BodyAnalysisCalculator({ required this.gender, required this.age, required this.height, required this.weight, this.waist, this.hip, this.neck, this.wrist, this.activityLevel = ActivityLevel.moderate, this.bodyFatInput, }); bool get isValid => age > 0 && height > 0 && weight > 0; double get bmi { if (!isValid) return 0; final h = height / 100; return weight / (h * h); } String get bmiCategory { if (bmi < 18.5) return '偏瘦'; if (bmi < 24) return '正常'; if (bmi < 28) return '偏胖'; return '肥胖'; } double get bmr { if (!isValid) return 0; if (gender == Gender.male) { return 10 * weight + 6.25 * height - 5 * age + 5; } return 10 * weight + 6.25 * height - 5 * age - 161; } double get tdee { final multipliers = { ActivityLevel.sedentary: 1.2, ActivityLevel.light: 1.375, ActivityLevel.moderate: 1.55, ActivityLevel.active: 1.725, ActivityLevel.veryActive: 1.9, }; return bmr * (multipliers[activityLevel] ?? 1.55); } double get bodyFatRate { if (bodyFatInput != null && bodyFatInput! > 0 && bodyFatInput! < 80) { return bodyFatInput!; } if (neck != null && neck! > 0 && waist != null && waist! > 0) { return navyBodyFat; } return bmiBodyFat; } double get bmiBodyFat { if (bmi <= 0) return 0; if (gender == Gender.male) { return (1.20 * bmi) + (0.23 * age) - 16.2; } return (1.20 * bmi) + (0.23 * age) - 5.4; } double get navyBodyFat { if (waist == null || waist! <= 0 || neck == null || neck! <= 0) return 0; if (gender == Gender.male) { final logWaistNeck = _log10(waist! - neck!); final logHeight = _log10(height); return 495 / (1.0324 - 0.19077 * logWaistNeck + 0.15456 * logHeight) - 450; } if (hip == null || hip! <= 0) return 0; final logWaistHipNeck = _log10(waist! + hip! - neck!); final logHeight = _log10(height); return 495 / (1.29579 - 0.35004 * logWaistHipNeck + 0.22100 * logHeight) - 450; } double get dailyWater => isValid ? weight * 35 : 0; double get leanBodyMass => isValid ? weight * (1 - bodyFatRate / 100) : 0; double get boneWeight => isValid ? weight * 0.15 : 0; double get bloodVolume { if (!isValid) return 0; return gender == Gender.male ? weight * 75 : weight * 65; } double get bodySurfaceArea { if (!isValid) return 0; return 0.007184 * math.pow(height, 0.725) * math.pow(weight, 0.425); } double get idealWeightDevine { if (!isValid) return 0; final base = gender == Gender.male ? 50.0 : 45.5; return base + 2.3 * ((height / 2.54) - 60); } double get idealWeightRobinson { if (!isValid) return 0; final base = gender == Gender.male ? 52.0 : 49.0; final factor = gender == Gender.male ? 1.9 : 1.7; return base + factor * ((height / 2.54) - 60); } double get idealWeightMiller { if (!isValid) return 0; final base = gender == Gender.male ? 56.2 : 53.1; final factor = gender == Gender.male ? 1.41 : 1.36; return base + factor * ((height / 2.54) - 60); } double get idealWeightHamwi { if (!isValid) return 0; final base = gender == Gender.male ? 48.0 : 45.5; final factor = gender == Gender.male ? 2.7 : 2.2; return base + factor * ((height / 2.54) - 60); } int get metabolicAge { if (!isValid) return 0; final avgBmr = gender == Gender.male ? 10 * 70 + 6.25 * 170 - 5 * age + 5 : 10 * 60 + 6.25 * 160 - 5 * age - 161; final ratio = bmr / avgBmr; return (age * ratio).round().clamp(1, 120); } double get waistToHeightRatio { if (waist == null || waist! <= 0) return -1; return waist! / height; } double get waistToHipRatio { if (waist == null || waist! <= 0 || hip == null || hip! <= 0) return -1; return waist! / hip!; } double get ffmi { if (!isValid) return 0; final heightM = height / 100; return leanBodyMass / (heightM * heightM); } double get normalizedFfmi { if (!isValid) return 0; final heightM = height / 100; return ffmi + 6.1 * (1.8 - heightM); } double get ponderalIndex { if (!isValid) return 0; final heightM = height / 100; return weight / (heightM * heightM * heightM); } double get bmrPerKg => isValid ? bmr / weight : 0; double get conicityIndex { if (!isValid || waist == null || waist! <= 0) return -1; final waistM = waist! / 100; final heightM = height / 100; return waistM / (0.109 * math.sqrt(weight / heightM)); } int get maxHeartRate => (220 - age).round(); int get dailyHeartbeats { if (age < 1) return 130 * 60 * 24; if (age < 10) return 90 * 60 * 24; if (age < 20) return 75 * 60 * 24; if (age < 40) return 72 * 60 * 24; return 70 * 60 * 24; } int get dailyBreaths => 16 * 60 * 24; int get dailyBlinks => 14400; double get fatLossCalories => tdee - 500; double get muscleGainCalories => tdee + 300; double get maintenanceCalories => tdee; double get proteinForFatLoss => weight * 2.0; double get proteinForMuscleGain => weight * 2.2; double get proteinForMaintenance => weight * 1.2; double _log10(double x) { if (x <= 0) return 0; return math.log(x) / math.ln10; } } // ===== 测试框架 ===== int _passCount = 0; int _failCount = 0; final List _failMessages = []; void expect( String name, double actual, double expected, { double tolerance = 0.5, }) { final diff = (actual - expected).abs(); if (diff <= tolerance) { _passCount++; print(' ✅ $name: $actual (期望 $expected, 误差 ${diff.toStringAsFixed(2)})'); } else { _failCount++; final msg = ' ❌ $name: $actual (期望 $expected, 误差 ${diff.toStringAsFixed(2)})'; _failMessages.add(msg); print(msg); } } void expectRange(String name, double actual, double min, double max) { if (actual >= min && actual <= max) { _passCount++; print(' ✅ $name: $actual (范围 $min-$max)'); } else { _failCount++; final msg = ' ❌ $name: $actual (超出范围 $min-$max)'; _failMessages.add(msg); print(msg); } } void expectPositive(String name, double actual) { if (actual > 0) { _passCount++; print(' ✅ $name: $actual (> 0)'); } else { _failCount++; final msg = ' ❌ $name: $actual (应 > 0)'; _failMessages.add(msg); print(msg); } } void section(String title) { print('\n📋 $title'); print('${'─' * 50}'); } // ===== 测试用例 ===== void main() { print('🧬 身体分析算法验证脚本'); print('📅 2026-04-24'); print('${'═' * 50}'); // 标准测试数据:30岁男性,175cm,75kg final male30 = BodyAnalysisCalculator( gender: Gender.male, age: 30, height: 175, weight: 75, activityLevel: ActivityLevel.moderate, ); // 标准测试数据:25岁女性,165cm,55kg final female25 = BodyAnalysisCalculator( gender: Gender.female, age: 25, height: 165, weight: 55, activityLevel: ActivityLevel.moderate, ); // 带选填参数的男性 final maleWithOptional = BodyAnalysisCalculator( gender: Gender.male, age: 30, height: 175, weight: 75, waist: 85, hip: 95, neck: 38, wrist: 17, activityLevel: ActivityLevel.moderate, ); // 带选填参数的女性 final femaleWithOptional = BodyAnalysisCalculator( gender: Gender.female, age: 25, height: 165, weight: 55, waist: 70, hip: 95, neck: 34, wrist: 15, activityLevel: ActivityLevel.light, ); // ===== 1. BMI 测试 ===== section('1. BMI 身体质量指数'); expect('男性BMI', male30.bmi, 24.5, tolerance: 0.1); expect('女性BMI', female25.bmi, 20.2, tolerance: 0.1); expectRange('男性BMI范围', male30.bmi, 18, 30); expectRange('女性BMI范围', female25.bmi, 18, 25); // ===== 2. BMR 测试 ===== section('2. BMR 基础代谢率 (Mifflin-St Jeor)'); // 男性: 10*75 + 6.25*175 - 5*30 + 5 = 750 + 1093.75 - 150 + 5 = 1698.75 expect('男性BMR', male30.bmr, 1698.75, tolerance: 0.5); // 女性: 10*55 + 6.25*165 - 5*25 - 161 = 550 + 1031.25 - 125 - 161 = 1295.25 expect('女性BMR', female25.bmr, 1295.25, tolerance: 0.5); // ===== 3. TDEE 测试 ===== section('3. TDEE 每日总能量消耗'); // 男性中度: 1698.75 * 1.55 = 2633.06 expect('男性TDEE(中度)', male30.tdee, 2633.06, tolerance: 1.0); // 女性轻度: 1295.25 * 1.375 = 1780.97 expect('女性TDEE(轻度)', femaleWithOptional.tdee, 1780.97, tolerance: 1.0); // ===== 4. 体脂率测试 ===== section('4. 体脂率 (BMI估算法)'); // 男性: 1.20*24.5 + 0.23*30 - 16.2 = 29.4 + 6.9 - 16.2 = 20.1 expect('男性体脂率(BMI)', male30.bodyFatRate, 20.1, tolerance: 0.5); // 女性: 1.20*20.2 + 0.23*25 - 5.4 = 24.24 + 5.75 - 5.4 = 24.59 expect('女性体脂率(BMI)', female25.bodyFatRate, 24.59, tolerance: 0.5); // ===== 5. Navy体脂率测试 ===== section('5. 体脂率 (Navy法)'); // 男性Navy: 495/(1.0324 - 0.19077*log10(85-38) + 0.15456*log10(175)) - 450 expectRange('男性Navy体脂率', maleWithOptional.bodyFatRate, 10, 30); expectRange('女性Navy体脂率', femaleWithOptional.bodyFatRate, 15, 35); // ===== 6. 每日喝水量 ===== section('6. 每日喝水量'); expect('男性喝水量', male30.dailyWater, 75 * 35, tolerance: 1); expect('女性喝水量', female25.dailyWater, 55 * 35, tolerance: 1); // ===== 7. 人体表面积 (Mosteller) ===== section('7. 人体表面积 (Mosteller)'); // BSA = sqrt(175 * 75 / 3600) = sqrt(3.6458) = 1.909 expect('男性BSA', male30.bodySurfaceArea, 1.909, tolerance: 0.05); // BSA = sqrt(165 * 55 / 3600) = sqrt(2.5208) = 1.588 expect('女性BSA', female25.bodySurfaceArea, 1.588, tolerance: 0.05); // ===== 8. 血液总量 ===== section('8. 血液总量'); expect('男性血液', male30.bloodVolume, 75 * 75, tolerance: 1); expect('女性血液', female25.bloodVolume, 55 * 65, tolerance: 1); // ===== 9. 骨骼重量 ===== section('9. 骨骼重量'); expect('男性骨骼', male30.boneWeight, 75 * 0.15, tolerance: 0.5); expect('女性骨骼', female25.boneWeight, 55 * 0.15, tolerance: 0.5); // ===== 10. 去脂体重 ===== section('10. 去脂体重 (LBM)'); expectRange('男性LBM', male30.leanBodyMass, 50, 70); expectRange('女性LBM', female25.leanBodyMass, 35, 50); // ===== 11. 理想体重 ===== section('11. 理想体重 (4种公式)'); // Devine: 50 + 2.3*((175/2.54)-60) = 50 + 2.3*8.8976 = 50 + 20.46 = 70.46 expect('男性Devine', male30.idealWeightDevine, 70.46, tolerance: 0.5); expectRange('男性Robinson', male30.idealWeightRobinson, 60, 80); expectRange('男性Miller', male30.idealWeightMiller, 60, 80); expectRange('男性Hamwi', male30.idealWeightHamwi, 60, 80); expectRange('女性Devine', female25.idealWeightDevine, 45, 65); expectRange('女性Robinson', female25.idealWeightRobinson, 45, 65); // ===== 12. 代谢年龄 ===== section('12. 代谢年龄'); expectRange('男性代谢年龄', male30.metabolicAge.toDouble(), 20, 40); expectRange('女性代谢年龄', female25.metabolicAge.toDouble(), 18, 35); // ===== 13. 腰围身高比 ===== section('13. 腰围身高比 (WHtR)'); expect( '男性WHtR', maleWithOptional.waistToHeightRatio, 85 / 175, tolerance: 0.01, ); expect( '女性WHtR', femaleWithOptional.waistToHeightRatio, 70 / 165, tolerance: 0.01, ); // ===== 14. 腰臀比 ===== section('14. 腰臀比 (WHR)'); expect('男性WHR', maleWithOptional.waistToHipRatio, 85 / 95, tolerance: 0.01); expect('女性WHR', femaleWithOptional.waistToHipRatio, 70 / 95, tolerance: 0.01); // ===== 15. FFMI ===== section('15. 无脂肪质量指数 (FFMI)'); expectRange('男性FFMI', male30.ffmi, 15, 25); expectRange('女性FFMI', female25.ffmi, 12, 22); expectRange('男性标准化FFMI', male30.normalizedFfmi, 15, 25); // ===== 16. Ponderal Index ===== section('16. Ponderal 体重指数'); // PI = 75 / (1.75^3) = 75 / 5.359 = 13.99 expect('男性PI', male30.ponderalIndex, 14.0, tolerance: 0.5); // PI = 55 / (1.65^3) = 55 / 4.492 = 12.24 expect('女性PI', female25.ponderalIndex, 12.24, tolerance: 0.5); // ===== 17. BMR效率 ===== section('17. 基础代谢效率 (BMR/kg)'); expect('男性BMR/kg', male30.bmrPerKg, 1698.75 / 75, tolerance: 0.5); expect('女性BMR/kg', female25.bmrPerKg, 1295.25 / 55, tolerance: 0.5); // ===== 18. 锥度指数 ===== section('18. 锥度指数 (Conicity Index)'); // CI = 85 / (0.109 * sqrt(75/175)) = 85 / (0.109 * 0.6547) = 85 / 0.07136 = 1191.2 // Wait, that seems wrong. Let me recalculate: // CI = waist / (0.109 * sqrt(weight/height)) where waist is in meters? No, cm. // Actually the formula uses waist in meters: 0.85 / (0.109 * sqrt(75/1.75)) = 0.85 / (0.109 * 6.547) = 0.85 / 0.7136 = 1.191 // But our code uses waist in cm: 85 / (0.109 * sqrt(75/175)) = 85 / (0.109 * 0.6547) = 85 / 0.07136 = 1191.2 // This looks like a bug! The formula should use waist in meters. expectRange('男性CI', maleWithOptional.conicityIndex, 1.0, 1.5); // ===== 19. 最大心率 ===== section('19. 最大心率'); expect('男性最大心率', male30.maxHeartRate.toDouble(), 190, tolerance: 1); expect('女性最大心率', female25.maxHeartRate.toDouble(), 195, tolerance: 1); // ===== 20. 每日心跳/呼吸/眨眼 ===== section('20. 每日心跳/呼吸/眨眼'); expectPositive('男性每日心跳', male30.dailyHeartbeats.toDouble()); expectPositive('男性每日呼吸', male30.dailyBreaths.toDouble()); expectPositive('男性每日眨眼', male30.dailyBlinks.toDouble()); // ===== 21. 摄入需求 ===== section('21. 减脂/增肌/维持摄入需求'); expect('男性减脂', male30.fatLossCalories, male30.tdee - 500, tolerance: 1); expect('男性增肌', male30.muscleGainCalories, male30.tdee + 300, tolerance: 1); expect('男性维持', male30.maintenanceCalories, male30.tdee, tolerance: 1); // ===== 22. 蛋白质需求 ===== section('22. 蛋白质需求量'); expect('男性减脂蛋白', male30.proteinForFatLoss, 75 * 2.0, tolerance: 1); expect('男性增肌蛋白', male30.proteinForMuscleGain, 75 * 2.2, tolerance: 1); expect('男性维持蛋白', male30.proteinForMaintenance, 75 * 1.2, tolerance: 1); // ===== 23. 边界值测试 ===== section('23. 边界值测试'); final invalid = BodyAnalysisCalculator( gender: Gender.male, age: 0, height: 0, weight: 0, ); expect('无效输入BMI', invalid.bmi, 0, tolerance: 0.01); expect('无效输入BMR', invalid.bmr, 0, tolerance: 0.01); expect('无效输入TDEE', invalid.tdee, 0, tolerance: 0.01); final extreme = BodyAnalysisCalculator( gender: Gender.male, age: 80, height: 200, weight: 120, ); expectRange('极端BMI', extreme.bmi, 20, 40); expectPositive('极端BMR', extreme.bmr); expectPositive('极端TDEE', extreme.tdee); // ===== 结果汇总 ===== print('\n${'═' * 50}'); print('📊 测试结果汇总'); print('${'═' * 50}'); print('✅ 通过: $_passCount'); print('❌ 失败: $_failCount'); print('📝 总计: ${_passCount + _failCount}'); if (_failMessages.isNotEmpty) { print('\n❌ 失败详情:'); for (final msg in _failMessages) { print(msg); } } // ===== 检查Conicity Index公式是否正确 ===== print('\n${'═' * 50}'); print('🔍 Conicity Index 公式验证'); print('${'═' * 50}'); // 标准公式: CI = waist(m) / (0.109 * sqrt(weight(kg) / height(m))) // 我们代码中: waist(cm) / (0.109 * sqrt(weight / height)) // 这里有单位问题!waist应该用米,但代码用的是厘米 final ciCalc = maleWithOptional.conicityIndex; print('当前计算结果: $ciCalc'); print('期望范围: 1.0-1.5 (正常人体范围)'); if (ciCalc > 10) { print('⚠️ 发现问题: Conicity Index值异常大,可能waist单位错误'); print(' 修正: 应将waist从cm转为m再计算'); print(' 公式应为: waist_m / (0.109 * sqrt(weight / height_m))'); } print('\n🏁 验证完成'); if (_failCount > 0) { exit(1); } }