Files
kitchen/scripts/test_body_analysis.dart
Developer 5e979d7115 refactor: 将项目名称从mom_kitchen改为cute_kitchen
更新项目名称及相关引用,包括README、iOS/macOS/Linux配置、文档和代码中的包引用。同时更新版本号至1.3.5并清理无用的HarmonyOS配置文件。

- 修改所有代码中的包引用路径
- 更新各平台配置文件和安装脚本
- 清理HarmonyOS相关无用文件
- 更新应用版本号至1.3.5
- 修正文档中的项目名称引用
2026-04-24 05:05:10 +08:00

536 lines
17 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<String> _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岁男性175cm75kg
final male30 = BodyAnalysisCalculator(
gender: Gender.male,
age: 30,
height: 175,
weight: 75,
activityLevel: ActivityLevel.moderate,
);
// 标准测试数据25岁女性165cm55kg
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);
}
}