refactor: 将项目名称从mom_kitchen改为cute_kitchen
更新项目名称及相关引用,包括README、iOS/macOS/Linux配置、文档和代码中的包引用。同时更新版本号至1.3.5并清理无用的HarmonyOS配置文件。 - 修改所有代码中的包引用路径 - 更新各平台配置文件和安装脚本 - 清理HarmonyOS相关无用文件 - 更新应用版本号至1.3.5 - 修正文档中的项目名称引用
This commit is contained in:
535
scripts/test_body_analysis.dart
Normal file
535
scripts/test_body_analysis.dart
Normal file
@@ -0,0 +1,535 @@
|
||||
// 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岁男性,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);
|
||||
}
|
||||
}
|
||||
163
scripts/test_export_import.dart
Normal file
163
scripts/test_export_import.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
// 2026-04-22 | test_export_import.dart | 数据导出/导入逻辑验证 | 验证JSON格式正确性和导入识别
|
||||
// 运行: dart run scripts/test_export_import.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
// ─── 模拟核心逻辑 ───
|
||||
|
||||
enum DataSource {
|
||||
favorites('❤️ 收藏', 'favorites'),
|
||||
shoppingList('🛒 购物清单', 'shopping_list'),
|
||||
mealRecords('🍽️ 饮食记录', 'meal_records'),
|
||||
cookingNotes('📝 烹饪笔记', 'cooking_notes'),
|
||||
weeklyMenu('📅 每周菜单', 'weekly_menu'),
|
||||
browseHistory('👀 浏览记录', 'browse_history');
|
||||
|
||||
final String label;
|
||||
final String fileName;
|
||||
const DataSource(this.label, this.fileName);
|
||||
}
|
||||
|
||||
String _exportSingleSourceJson(DataSource source) {
|
||||
switch (source) {
|
||||
case DataSource.favorites:
|
||||
return const JsonEncoder.withIndent(' ').convert([
|
||||
{'id': 1, 'title': '红烧肉', 'favoriteType': 'recipe', 'favoriteAt': '2026-04-22'},
|
||||
]);
|
||||
case DataSource.shoppingList:
|
||||
return const JsonEncoder.withIndent(' ').convert([
|
||||
{'name': '鸡蛋', 'amount': '3', 'unit': '个', 'isChecked': false},
|
||||
]);
|
||||
case DataSource.mealRecords:
|
||||
return const JsonEncoder.withIndent(' ').convert([
|
||||
{'date': '2026-04-22', 'mealType': 'lunch', 'recipeId': 1},
|
||||
]);
|
||||
case DataSource.cookingNotes:
|
||||
return const JsonEncoder.withIndent(' ').convert([
|
||||
{'recipeId': 1, 'content': '少放盐', 'tags': ['tips']},
|
||||
]);
|
||||
case DataSource.weeklyMenu:
|
||||
return const JsonEncoder.withIndent(' ').convert({
|
||||
'weekStart': '2026-04-21',
|
||||
'dailyMenus': {'Mon': {'breakfast': 1}},
|
||||
});
|
||||
case DataSource.browseHistory:
|
||||
return const JsonEncoder.withIndent(' ').convert([
|
||||
{'recipeId': 1, 'title': '红烧肉', 'viewCount': 3, 'viewedAt': '2026-04-22'},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/// 新版 exportAll:先构建Map再编码
|
||||
String exportAllNew() {
|
||||
final Map<String, dynamic> allData = {};
|
||||
for (final source in DataSource.values) {
|
||||
final content = _exportSingleSourceJson(source);
|
||||
try {
|
||||
allData[source.fileName] = jsonDecode(content);
|
||||
} catch (e) {
|
||||
print(' ⚠️ 解码 ${source.fileName} 失败: $e');
|
||||
}
|
||||
}
|
||||
return const JsonEncoder.withIndent(' ').convert(allData);
|
||||
}
|
||||
|
||||
/// previewImport:标准格式识别 + List格式自动推断
|
||||
({Map<DataSource, int> sourceCounts, String? error}) previewImport(String jsonContent) {
|
||||
final result = <DataSource, int>{};
|
||||
try {
|
||||
final decoded = jsonDecode(jsonContent);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
for (final source in DataSource.values) {
|
||||
if (decoded.containsKey(source.fileName)) {
|
||||
final data = decoded[source.fileName];
|
||||
if (data is List) {
|
||||
result[source] = data.length;
|
||||
} else if (data is Map && source == DataSource.weeklyMenu) {
|
||||
result[source] = data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (decoded is List) {
|
||||
if (decoded.isNotEmpty && decoded.first is Map<String, dynamic>) {
|
||||
final inferred = _inferDataSource(decoded.first as Map<String, dynamic>);
|
||||
result[inferred ?? DataSource.favorites] = decoded.length;
|
||||
} else {
|
||||
result[DataSource.favorites] = decoded.length;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return (sourceCounts: result, error: e.toString());
|
||||
}
|
||||
return (sourceCounts: result, error: null);
|
||||
}
|
||||
|
||||
DataSource? _inferDataSource(Map<String, dynamic> item) {
|
||||
if (item.containsKey('favoriteType') || item.containsKey('favoriteAt')) return DataSource.favorites;
|
||||
if (item.containsKey('isChecked') && item.containsKey('amount')) return DataSource.shoppingList;
|
||||
if (item.containsKey('mealType') && item.containsKey('date')) return DataSource.mealRecords;
|
||||
if (item.containsKey('recipeId') && item.containsKey('content')) return DataSource.cookingNotes;
|
||||
if (item.containsKey('dailyMenus') || item.containsKey('weekStart')) return DataSource.weeklyMenu;
|
||||
if (item.containsKey('viewCount') && item.containsKey('viewedAt')) return DataSource.browseHistory;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── 测试 ───
|
||||
|
||||
void main() {
|
||||
print('╔══════════════════════════════════════════════════╗');
|
||||
print('║ 📦 数据导出/导入 逻辑验证 ║');
|
||||
print('╚══════════════════════════════════════════════════╝\n');
|
||||
|
||||
testFullExport();
|
||||
testSingleSourceImport();
|
||||
|
||||
print('\n╔══════════════════════════════════════════════════╗');
|
||||
print('║ ✅ 全部测试完成 ║');
|
||||
print('╚══════════════════════════════════════════════════╝');
|
||||
}
|
||||
|
||||
void testFullExport() {
|
||||
print('━━━ 1. 全量导出JSON格式验证 ━━━');
|
||||
final json = exportAllNew();
|
||||
try {
|
||||
final decoded = jsonDecode(json) as Map<String, dynamic>;
|
||||
print(' ✅ JSON可解析,${decoded.length} 个数据源');
|
||||
for (final key in decoded.keys) {
|
||||
final val = decoded[key];
|
||||
final count = val is List ? val.length : (val is Map ? val.length : 0);
|
||||
print(' - "$key": ${val.runtimeType} ($count项)');
|
||||
}
|
||||
} catch (e) {
|
||||
print(' ❌ JSON解析失败: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
final preview = previewImport(json);
|
||||
if (preview.error != null) {
|
||||
print(' ❌ previewImport失败: ${preview.error}');
|
||||
} else {
|
||||
print(' ✅ 识别 ${preview.sourceCounts.length}/${DataSource.values.length} 个数据源');
|
||||
for (final entry in preview.sourceCounts.entries) {
|
||||
print(' - ${entry.key.label}: ${entry.value} 条');
|
||||
}
|
||||
}
|
||||
print('');
|
||||
}
|
||||
|
||||
void testSingleSourceImport() {
|
||||
print('━━━ 2. 单数据源导入自动推断 ━━━');
|
||||
for (final source in DataSource.values) {
|
||||
final json = _exportSingleSourceJson(source);
|
||||
final preview = previewImport(json);
|
||||
if (preview.sourceCounts.isNotEmpty) {
|
||||
final e = preview.sourceCounts.entries.first;
|
||||
final ok = e.key == source ? '✅' : '⚠️ 偏移';
|
||||
print(' $ok ${source.label} → 推断为 ${e.key.label} (${e.value}条)');
|
||||
} else {
|
||||
print(' ❌ ${source.label} 无法识别');
|
||||
}
|
||||
}
|
||||
print('');
|
||||
}
|
||||
Reference in New Issue
Block a user