接口更新

This commit is contained in:
Developer
2026-04-11 07:07:13 +08:00
parent 346fc795f7
commit 2d7484fd29
40 changed files with 2680 additions and 2254 deletions

View File

@@ -1,406 +0,0 @@
# 营养中心性能优化报告
## 📊 接口验证结果
### 测试时间
**2026-04-10** - 使用实际 API 接口测试
### 测试项目
1.**营养报告接口连通性测试** - 通过
2.**热门排行数据验证** - 通过
3.**性能基准测试** - 平均 1393ms
### 测试脚本
```bash
dart scripts/verify_nutrition_api.dart
```
### 测试结果摘要
| 测试项 | 状态 | 响应时间 | 说明 |
|--------|------|----------|------|
| 总排行接口 | ✅ 通过 | 1363ms | 获取 10 条数据 |
| 月排行接口 | ✅ 通过 | 1391ms | 获取 5 条数据 |
| 今日排行 | ⚠️ 部分通过 | 1373ms | 数据结构不匹配 |
| 性能评级 | 🟡 一般 | 平均 1393ms | 100% 成功率 |
---
## 🔍 API 接口文档
**基础地址**: `http://eat.wktyl.com/api/`
### 核心接口
| 接口文件 | 功能 | 使用场景 |
|---------|------|---------|
| `api.php` | 主接口 | 菜谱列表、详情、搜索 |
| `stats_full.php` | 全面统计 | 热门排行、在线统计 |
| `api_what_to_eat.php` | 智能选择 | 今天吃什么、随机推荐 |
| `api_feed.php` | 信息流 | 推荐、热门、个性化 |
| `api_action.php` | 动态交互 | 点赞、推荐、浏览量 |
### 热门排行接口详解
```
GET stats_full.php?act=hot&period=total&limit=10
```
**返回数据结构**:
```json
{
"code": 200,
"message": "success",
"data": {
"today": { ... },
"month": { ... },
"total": {
"recipe_view": [...],
"recipe_like": [...],
"ingredient_view": [...]
}
}
}
```
---
## 📈 实际性能测试结果
### 基准测试5 次请求)
| 指标 | 数值 | 评级 |
|------|------|------|
| 平均响应时间 | 1393ms | 🟡 一般 |
| 最快响应 | 1353ms | 良好 |
| 最慢响应 | 1522ms | 一般 |
| 成功率 | 100% | ✅ 优秀 |
### 性能分析
**优势**:
- ✅ 接口稳定性高100% 成功率)
- ✅ 响应时间波动小(标准差 < 100ms
- ✅ 数据格式规范
**待优化**:
- 🟡 响应时间 > 1000ms建议优化到 500ms 以内)
- 🟡 无缓存机制(重复请求相同数据)
- 🟡 无压缩传输(数据量较大)
### 优化建议
#### 1. 实施缓存策略(优先级 P0
```dart
// 建议缓存时间
- 热门排行5 分钟
- 营养数据1 小时
- 菜谱详情30 分钟
```
#### 2. 启用 Gzip 压缩(优先级 P1
```
添加参数_format=gzip
预计节省75%+ 流量
```
#### 3. 预加载策略(优先级 P2
```dart
// 在应用启动时预加载
- 热门排行数据
- 分类列表
- 标签数据
```
---
## 🔍 发现的问题
### 1. 控制器初始化问题
**问题描述:**
- `MealRecordController` 未正确初始化导致页面卡死
- 缺少错误处理机制
**解决方案:**
```dart
// ❌ 错误写法
late final MealRecordController _ctrl;
@override
void initState() {
super.initState();
_ctrl = Get.find<MealRecordController>(); // 可能抛出异常
}
// ✅ 正确写法
MealRecordController? _ctrl;
String? _error;
@override
void initState() {
super.initState();
try {
_ctrl = Get.find<MealRecordController>();
} catch (e) {
debugPrint('MealRecordController not found: $e');
_error = '控制器初始化失败';
_ctrl = null;
}
}
```
### 2. 空指针保护缺失
**问题描述:**
- 访问控制器数据时未检查 null
- 导航时未捕获异常
**解决方案:**
```dart
// 添加 null 检查
if (_error != null || _ctrl == null) {
return CupertinoPageScaffold(
// 错误提示页面
);
}
// 导航时添加错误处理
onTap: () {
try {
Get.toNamed(AppRoutes.nutritionReport);
} catch (e) {
debugPrint('Navigate error: $e');
ToastService.show(message: '打开报告失败:$e 🔄');
}
}
```
### 3. API 响应时间优化
**当前问题:**
- 无超时保护
- 无缓存机制
- 重复请求
**优化建议:**
#### a) 添加超时保护
```dart
final results = await _repository.fetchData()
.timeout(
const Duration(seconds: 12),
onTimeout: () {
debugPrint('API timeout');
return [];
},
);
```
#### b) 实现缓存策略
```dart
// 使用 Hive 缓存营养数据
class MealRecordRepository {
Future<List<MealRecordModel>> fetchRecords(String date) async {
// 1. 检查缓存
final cached = await _cacheService.get('nutrition_$date');
if (cached != null) {
return cached;
}
// 2. 从 API 获取
final data = await _api.get('/nutrition/records?date=$date');
// 3. 保存缓存
await _cacheService.set('nutrition_$date', data);
return data;
}
}
```
#### c) 防抖处理
```dart
// 防止频繁请求
Timer? _debounceTimer;
void onDateChanged(String date) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
_ctrl.selectDate(date);
});
}
```
---
## 📈 性能指标
### 目标性能标准
| 指标 | 优秀 | 良好 | 一般 | 较差 |
|------|------|------|------|------|
| 冷启动时间 | < 2s | 2-3s | 3-5s | > 5s |
| 页面切换 | < 200ms | 200-400ms | 400-800ms | > 800ms |
| API 响应 | < 500ms | 500-1000ms | 1000-2000ms | > 2000ms |
| 列表滚动 FPS | 60fps | 50-60fps | 30-50fps | < 30fps |
### 优化建议
#### 1. 减少 initState 中的同步操作
```dart
// ❌ 避免在 initState 中执行耗时操作
@override
void initState() {
super.initState();
_loadData(); // 同步加载大量数据
}
// ✅ 使用异步加载
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadDataAsync();
});
}
```
#### 2. 优化 Obx 使用
```dart
// ❌ 避免在大范围 rebuild
Obx(() => ListView(
children: controller.items.map((item) => ComplexWidget(item)).toList(),
))
// ✅ 使用独立 Observer
items.map((item) => Obx(() => ComplexWidget(item))).toList()
```
#### 3. 图片懒加载
```dart
// 使用 CachedNetworkImage
CachedNetworkImage(
imageUrl: recipe.imageUrl,
placeholder: (context, url) => SkeletonLoader(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
```
---
## 🛠️ 已实施的修复
### 文件修改清单
1. **nutrition_report_page.dart**
- ✅ 添加控制器初始化错误处理
- ✅ 添加 null 检查
- ✅ 添加错误提示页面
2. **nutrition_center_page.dart**
- ✅ 添加控制器初始化错误处理
- ✅ 导航时添加 try-catch
- ✅ 添加空状态处理
3. **hot_repository.dart**
- ✅ 添加调试日志
- ✅ 添加详细的错误信息
- ✅ 优化数据结构兼容性
---
## 📝 测试清单
### 功能测试
- [ ] 打开营养中心页面
- [ ] 点击报告按钮
- [ ] 切换周/月视图
- [ ] 添加饮食记录
- [ ] 删除饮食记录
- [ ] 日期选择器
- [ ] 今天按钮跳转
### 性能测试
```bash
# 运行接口验证脚本
dart scripts/verify_nutrition_api.dart
# 检查响应时间
# - 平均 < 1000ms ✓
# - 成功率 > 95% ✓
```
### 边界测试
- [ ] 无网络状态
- [ ] 控制器未初始化
- [ ] 空数据状态
- [ ] 异常数据处理
---
## 🎯 下一步优化计划
### 短期P0
1. ~~修复控制器初始化问题~~ ✅
2. ~~添加错误处理~~ ✅
3. 添加加载状态指示器
4. 优化内存使用
### 中期P1
1. 实现数据缓存
2. 添加离线模式
3. 优化图表渲染性能
4. 减少不必要的 rebuild
### 长期P2
1. 实现预加载策略
2. 添加数据预取
3. 优化动画性能
4. 实现增量更新
---
## 📞 调试工具
### 日志查看
```dart
// 在控制器中添加调试日志
debugPrint('MealRecordController: loading data for $date');
debugPrint('MealRecordController: got ${records.length} records');
```
### 性能监控
```dart
// 使用 PerformanceOverlay
import 'package:flutter/scheduler.dart';
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
debugPrint('Frame time: ${duration.inMilliseconds}ms');
});
```
### 内存分析
```bash
# Flutter 性能工具
flutter pub global activate devtools
flutter pub global run devtools
```
---
## ✅ 验收标准
- [x] 营养中心页面正常打开
- [x] 报告按钮正常响应
- [x] 无卡死闪退现象
- [x] 错误提示友好
- [x] 接口响应时间 < 2s
- [x] 数据展示正确
- [ ] 缓存机制实现(待开发)
- [ ] 离线模式支持(待开发)
---
*最后更新2026-04-10*
*测试环境Dart 3.0+*

View File

@@ -1,32 +0,0 @@
// 2026-04-11 | verify_categories_detail.dart | 分类数据详细验证 | 检查子分类parent_id字段
import 'dart:convert';
import 'dart:io';
const String baseUrl = 'http://eat.wktyl.com/api';
void main() async {
final uri = Uri.parse('$baseUrl/api.php').replace(queryParameters: {'act': 'categories'});
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 12);
final request = await client.getUrl(uri);
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(body) as Map<String, dynamic>;
final data = json['data'] as List;
for (final topCat in data) {
final m = topCat as Map<String, dynamic>;
print('=== Top: id=${m['id']}, name=${m['name']} ===');
final children = m['children'] as List?;
if (children != null && children.isNotEmpty) {
print(' children count: ${children.length}');
for (final child in children.take(5)) {
final cm = child as Map<String, dynamic>;
print(' child keys: ${cm.keys.join(', ')}');
print(' child: id=${cm['id'] ?? cm['cate_id']}, name=${cm['name'] ?? cm['cate_name']}, parent_id=${cm['parent_id']}');
}
}
}
}

View File

@@ -1,27 +0,0 @@
// 2026-04-11 | verify_eating_times.dart | 用餐时段数据验证 | 获取eating_times.json数据结构
import 'dart:convert';
import 'dart:io';
void main() async {
final uri = Uri.parse('http://eat.wktyl.com/api/assets/eating_times.json');
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 12);
final request = await client.getUrl(uri);
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(body);
if (json is List) {
print('Total items: ${json.length}');
for (final item in json.take(5)) {
final m = item as Map<String, dynamic>;
print('keys: ${m.keys.join(', ')}');
print('item: ${jsonEncode(m)}');
print('');
}
} else if (json is Map) {
print('Top-level keys: ${json.keys.join(', ')}');
print(jsonEncode(json).substring(0, 500));
}
}

View File

@@ -1,54 +0,0 @@
// 2026-04-11 | verify_filter_apply.dart | filter_apply接口验证 | 测试不同分类ID的筛选
import 'dart:convert';
import 'dart:io';
const String baseUrl = 'http://eat.wktyl.com/api';
void main() async {
await testFilterApply(category: '12', label: '中国菜(id=12)');
await testFilterApply(category: '13', label: '粤菜(id=13)');
await testFilterApply(category: '11', label: '菜谱(id=11)');
await testFilterApply(tag: '74', label: '粉蒸(tag=74)');
await testFilterApply(category: '12', tag: '74', label: '中国菜+粉蒸');
}
Future<void> testFilterApply({String? category, String? tag, required String label}) async {
print('$label');
final params = <String, String>{'act': 'filter_apply', 'count': '3'};
if (category != null) params['category'] = category;
if (tag != null) params['tag'] = tag;
final uri = Uri.parse('$baseUrl/api_what_to_eat.php').replace(queryParameters: params);
try {
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 12);
final request = await client.getUrl(uri);
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
client.close();
final json = jsonDecode(body) as Map<String, dynamic>;
final code = json['code'];
final data = json['data'];
if (code == 200 && data != null) {
if (data is Map) {
final recipes = data['recipes'] as List?;
print(' ✅ code=$code, recipes=${recipes?.length ?? 0}');
if (recipes != null && recipes.isNotEmpty) {
for (final r in recipes.take(2)) {
final m = r as Map<String, dynamic>;
print(' - id=${m['id']}, title=${m['title']}');
}
}
} else if (data is List) {
print(' ✅ code=$code, data is List, count=${data.length}');
}
} else {
print(' ❌ code=$code, message=${json['message']}');
}
} catch (e) {
print(' ❌ error: $e');
}
print('');
}

View File

@@ -1,298 +0,0 @@
/**
* 文件verify_nutrition_api.dart
* 名称:营养中心接口验证脚本
* 作用:验证营养中心相关接口的连通性和性能
* 使用dart scripts/verify_nutrition_api.dart
* 更新2026-04-10 创建,用于测试 API 响应和数据格式
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
// 配置
const String baseUrl = 'http://eat.wktyl.com/api';
const String statsFullEndpoint = '/stats_full.php';
const int timeoutSeconds = 12;
// 颜色常量
const String reset = '\x1B[0m';
const String green = '\x1B[32m';
const String red = '\x1B[31m';
const String yellow = '\x1B[33m';
const String blue = '\x1B[34m';
const String cyan = '\x1B[36m';
void main() async {
printHeader();
// 测试 1验证营养报告接口总览
await testNutritionOverview();
// 测试 2验证热门排行接口
await testHotRanking();
// 测试 3性能基准测试
await performanceBenchmark();
printFooter();
}
void printHeader() {
print(
'\n${cyan}╔════════════════════════════════════════════════════════╗${reset}',
);
print(
'${cyan}${reset} ${green}🔬 营养中心接口验证脚本${reset} ${cyan}${reset}',
);
print(
'${cyan}${reset} ${yellow}验证 API 连通性、数据格式、响应时间${reset} ${cyan}${reset}',
);
print(
'${cyan}╚════════════════════════════════════════════════════════╝${reset}\n',
);
}
void printFooter() {
print(
'\n${cyan}═══════════════════════════════════════════════════════${reset}',
);
print('${green}✅ 验证完成${reset}');
print(
'${cyan}═══════════════════════════════════════════════════════${reset}\n',
);
}
Future<void> testNutritionOverview() async {
printSection('测试 1营养报告接口 (stats_full.php)');
final url = Uri.parse(
'$baseUrl$statsFullEndpoint?act=hot&period=total&limit=10',
);
print('${blue}请求 URL:${reset} $url');
try {
final stopwatch = Stopwatch()..start();
final response = await HttpClient()
.getUrl(url)
.timeout(Duration(seconds: timeoutSeconds));
final data = await response.close();
final body = await utf8.decoder.bind(data).join();
stopwatch.stop();
print('${green}✓ 响应状态码:${reset} ${data.statusCode}');
print('${green}✓ 响应时间:${reset} ${stopwatch.elapsedMilliseconds}ms');
// 解析 JSON
final jsonData = jsonDecode(body) as Map<String, dynamic>;
print('${blue}JSON 结构分析:${reset}');
print(' - code: ${jsonData['code']}');
print(' - message: ${jsonData['message']}');
if (jsonData['data'] != null) {
final dataMap = jsonData['data'] as Map<String, dynamic>;
print(' - data keys: ${dataMap.keys.toList()}');
// 检查热门排行数据结构
if (dataMap.containsKey('total')) {
final total = dataMap['total'] as Map<String, dynamic>;
print(' - total 字段存在 ✓');
if (total.containsKey('recipe_view')) {
final recipes = total['recipe_view'] as List;
print(' - recipe_view: ${recipes.length} 条记录');
if (recipes.isNotEmpty) {
final first = recipes.first as Map<String, dynamic>;
print(' 示例数据:');
print(' id: ${first['id']}');
print(' name: ${first['name']}');
print(' value: ${first['value']}');
}
}
if (total.containsKey('recipe_like')) {
final likes = total['recipe_like'] as List;
print(' - recipe_like: ${likes.length} 条记录');
}
if (total.containsKey('ingredient_view')) {
final ingredients = total['ingredient_view'] as List;
print(' - ingredient_view: ${ingredients.length} 条记录');
}
} else {
print('${yellow}⚠ 警告total 字段不存在${reset}');
}
} else {
print('${red}✗ data 字段为空${reset}');
}
print('${green}✓ 接口连通性测试通过${reset}\n');
} on TimeoutException catch (e) {
print('${red}✗ 请求超时:${e.message}${reset}');
print('${yellow}建议:检查网络连接或 API 服务器状态${reset}\n');
} catch (e) {
print('${red}✗ 请求失败:$e${reset}');
print('${yellow}建议:检查 URL 是否正确${reset}\n');
}
}
Future<void> testHotRanking() async {
printSection('测试 2热门排行数据验证');
final testCases = [
{'period': 'total', 'name': '总排行'},
{'period': 'month', 'name': '月排行'},
{'period': 'today', 'name': '今日排行'},
];
for (final testCase in testCases) {
final period = testCase['period']!;
final name = testCase['name']!;
print('${blue}测试 $name ($period):${reset}');
final url = Uri.parse(
'$baseUrl$statsFullEndpoint?act=hot&period=$period&limit=5',
);
try {
final stopwatch = Stopwatch()..start();
final response = await HttpClient()
.getUrl(url)
.timeout(Duration(seconds: timeoutSeconds));
final data = await response.close();
final body = await utf8.decoder.bind(data).join();
stopwatch.stop();
final jsonData = jsonDecode(body) as Map<String, dynamic>;
if (jsonData['code'] == 200) {
print(
' ${green}✓ 状态码 200${reset} - ${stopwatch.elapsedMilliseconds}ms',
);
final dataMap = jsonData['data'] as Map<String, dynamic>?;
if (dataMap != null) {
// 检查不同可能的数据结构
int recipeCount = 0;
if (dataMap.containsKey(period) && dataMap[period] is Map) {
final periodData = dataMap[period] as Map<String, dynamic>;
if (periodData.containsKey('recipe_view')) {
recipeCount = (periodData['recipe_view'] as List).length;
}
} else if (dataMap.containsKey('recipe_view')) {
recipeCount = (dataMap['recipe_view'] as List).length;
} else {
// 检查嵌套结构
for (final key in ['total', 'month', 'today']) {
if (dataMap.containsKey(key) && dataMap[key] is Map) {
final sub = dataMap[key] as Map<String, dynamic>;
if (sub.containsKey('recipe_view')) {
recipeCount = (sub['recipe_view'] as List).length;
break;
}
}
}
}
if (recipeCount > 0) {
print(' ${green}✓ 获取到 $recipeCount 条菜谱数据${reset}');
} else {
print(' ${yellow}⚠ 未获取到有效数据${reset}');
}
}
} else {
print(
' ${red}✗ 状态码 ${jsonData['code']}: ${jsonData['message']}${reset}',
);
}
} catch (e) {
print(' ${red}✗ 请求失败:$e${reset}');
}
}
print('');
}
Future<void> performanceBenchmark() async {
printSection('测试 3性能基准测试');
final iterations = 5;
final results = <int>[];
print('${blue}执行 $iterations 次连续请求测试...${reset}\n');
for (int i = 0; i < iterations; i++) {
final url = Uri.parse(
'$baseUrl$statsFullEndpoint?act=hot&period=total&limit=5',
);
try {
final stopwatch = Stopwatch()..start();
final response = await HttpClient()
.getUrl(url)
.timeout(Duration(seconds: timeoutSeconds));
await response.close();
stopwatch.stop();
results.add(stopwatch.elapsedMilliseconds);
print(' 请求 #${i + 1}: ${stopwatch.elapsedMilliseconds}ms');
} catch (e) {
print(' 请求 #${i + 1}: ${red}失败 ($e)${reset}');
results.add(-1);
}
}
// 计算统计信息
final validResults = results.where((r) => r > 0).toList();
if (validResults.isNotEmpty) {
final avg = validResults.reduce((a, b) => a + b) / validResults.length;
final min = validResults.reduce((a, b) => a < b ? a : b);
final max = validResults.reduce((a, b) => a > b ? a : b);
print('\n${blue}性能统计:${reset}');
print(' - 平均响应时间:${avg.toStringAsFixed(0)}ms');
print(' - 最快响应时间:${min}ms');
print(' - 最慢响应时间:${max}ms');
print(
' - 成功率:${validResults.length}/${iterations} (${(validResults.length / iterations * 100).toStringAsFixed(0)}%)',
);
// 性能评级
String rating;
if (avg < 500) {
rating = '${green}优秀 🌟${reset}';
} else if (avg < 1000) {
rating = '${green}良好 ✓${reset}';
} else if (avg < 2000) {
rating = '${yellow}一般 ⚠${reset}';
} else {
rating = '${red}较差 ✗${reset}';
}
print(' - 性能评级:$rating');
} else {
print('\n${red}所有请求均失败,无法计算性能统计${reset}');
}
print('');
}
void printSection(String title) {
print('\n${cyan}───────────────────────────────────────────────────${reset}');
print('${green}$title${reset}');
print('${cyan}───────────────────────────────────────────────────${reset}\n');
}

View File

@@ -1,40 +0,0 @@
// 2026-04-11 | verify_recipe_images.dart | 菜谱图片URL验证 | 测试fallback链
import 'dart:io';
void main() async {
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 8);
final testIds = [1, 150, 1585];
final base = 'http://eat.wktyl.com/api/assets';
for (final id in testIds) {
print('\n--- Testing id=$id ---');
final urls = [
'$base/pic/${id}a.jpg',
'$base/pic/${id}b.jpg',
'$base/pic/$id.jpg',
];
for (final url in urls) {
try {
final req = await client.headUrl(Uri.parse(url));
final resp = await req.close();
print(' ${resp.statusCode == 200 ? "" : ""} $url${resp.statusCode}');
} catch (e) {
print('$url → error: $e');
}
}
}
// Test back.png
try {
final req = await client.headUrl(Uri.parse('$base/back.png'));
final resp = await req.close();
print('\n${resp.statusCode == 200 ? "" : ""} $base/back.png → ${resp.statusCode}');
} catch (e) {
print('\n❌ back.png → error: $e');
}
client.close();
}

View File

@@ -1,185 +0,0 @@
// 2026-04-11 | verify_what_to_eat_api.dart | 今天吃什么接口验证脚本 | 验证filter_apply/categories/tags接口连通性和数据格式
import 'dart:async';
import 'dart:convert';
import 'dart:io';
const String baseUrl = 'http://eat.wktyl.com/api';
const int timeoutSeconds = 12;
const String reset = '\x1B[0m';
const String green = '\x1B[32m';
const String red = '\x1B[31m';
const String yellow = '\x1B[33m';
const String blue = '\x1B[34m';
const String cyan = '\x1B[36m';
void main() async {
printHeader();
await testFilterApply();
await testCategories();
await testTags();
await testFilterSteps();
await testFilterApplyWithCategory();
printSummary();
}
void printHeader() {
print('$cyan═══════════════════════════════════════════════════$reset');
print('$cyan 🎲 今天吃什么 API 接口验证$reset');
print('$cyan═══════════════════════════════════════════════════$reset');
print('');
}
Future<Map<String, dynamic>?> apiGet(String endpoint, Map<String, String> params) async {
final uri = Uri.parse('$baseUrl$endpoint').replace(queryParameters: params);
final stopwatch = Stopwatch()..start();
try {
final client = HttpClient();
client.connectionTimeout = Duration(seconds: timeoutSeconds);
final request = await client.getUrl(uri);
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
stopwatch.stop();
client.close();
if (response.statusCode != 200) {
print('$red ❌ HTTP ${response.statusCode} (${stopwatch.elapsedMilliseconds}ms)$reset');
return null;
}
final json = jsonDecode(body) as Map<String, dynamic>;
print('$green${stopwatch.elapsedMilliseconds}ms | code=${json['code']}$reset');
return json;
} on TimeoutException {
stopwatch.stop();
print('$red ❌ 超时 (${stopwatch.elapsedMilliseconds}ms)$reset');
return null;
} catch (e) {
stopwatch.stop();
print('$red ❌ 错误: $e (${stopwatch.elapsedMilliseconds}ms)$reset');
return null;
}
}
Future<void> testFilterApply() async {
print('$yellow▶ 测试 1: filter_apply (无筛选随机推荐)$reset');
final result = await apiGet('/api_what_to_eat.php', {'act': 'filter_apply', 'count': '5'});
if (result != null) {
final data = result['data'];
if (data is Map) {
final recipes = data['recipes'] as List?;
print(' recipes count: ${recipes?.length ?? 0}');
if (recipes != null && recipes.isNotEmpty) {
final first = recipes.first as Map<String, dynamic>;
print(' first recipe: id=${first['id']}, title=${first['title']}');
print(' fields: ${first.keys.take(15).join(', ')}...');
}
print(' total_matched: ${data['total_matched']}');
print(' filters_applied: ${data['filters_applied']}');
} else if (data is List) {
print(' data is List, count: ${data.length}');
if (data.isNotEmpty) {
final first = data.first as Map<String, dynamic>;
print(' first: id=${first['id']}, title=${first['title']}');
}
}
}
print('');
}
Future<void> testCategories() async {
print('$yellow▶ 测试 2: categories (分类列表)$reset');
final result = await apiGet('/api.php', {'act': 'categories'});
if (result != null) {
final data = result['data'];
if (data is List) {
print(' categories count: ${data.length}');
for (final cat in data.take(5)) {
final m = cat as Map<String, dynamic>;
print(' - id=${m['id'] ?? m['cate_id']}, name=${m['name'] ?? m['cate_name']}, parent_id=${m['parent_id']}');
final children = m['children'] as List?;
if (children != null && children.isNotEmpty) {
print(' children: ${children.length}');
for (final child in children.take(3)) {
final cm = child as Map<String, dynamic>;
print(' - id=${cm['id'] ?? cm['cate_id']}, name=${cm['name'] ?? cm['cate_name']}');
}
}
}
} else {
print(' data type: ${data.runtimeType}');
}
}
print('');
}
Future<void> testTags() async {
print('$yellow▶ 测试 3: tags (标签列表)$reset');
final result = await apiGet('/api.php', {'act': 'tags'});
if (result != null) {
final data = result['data'];
if (data is List) {
print(' tags count: ${data.length}');
for (final tag in data.take(5)) {
final m = tag as Map<String, dynamic>;
print(' - id=${m['id'] ?? m['tag_id']}, name=${m['name'] ?? m['tag_name']}');
}
} else {
print(' data type: ${data.runtimeType}');
}
}
print('');
}
Future<void> testFilterSteps() async {
print('$yellow▶ 测试 4: filter_steps (筛选步骤)$reset');
final result = await apiGet('/api_what_to_eat.php', {'act': 'filter_steps'});
if (result != null) {
final data = result['data'];
if (data is Map) {
print(' keys: ${data.keys.join(', ')}');
final steps = data['steps'] as List?;
if (steps != null) {
print(' steps count: ${steps.length}');
for (final step in steps.take(3)) {
final m = step as Map<String, dynamic>;
print(' - step: ${m['step']}, title: ${m['title']}, type: ${m['type']}');
final options = m['options'] as List? ?? m['available_options'] as List? ?? [];
print(' options: ${options.length}');
}
}
final available = data['available_options'] as List?;
if (available != null) {
print(' available_options count: ${available.length}');
}
}
}
print('');
}
Future<void> testFilterApplyWithCategory() async {
print('$yellow▶ 测试 5: filter_apply (带分类筛选)$reset');
final result = await apiGet('/api_what_to_eat.php', {'act': 'filter_apply', 'category': '1', 'count': '3'});
if (result != null) {
final data = result['data'];
if (data is Map) {
final recipes = data['recipes'] as List?;
print(' recipes count: ${recipes?.length ?? 0}');
if (recipes != null && recipes.isNotEmpty) {
for (final r in recipes.take(3)) {
final m = r as Map<String, dynamic>;
print(' - id=${m['id']}, title=${m['title']}');
}
}
} else if (data is List) {
print(' data is List, count: ${data.length}');
}
}
print('');
}
void printSummary() {
print('$cyan═══════════════════════════════════════════════════$reset');
print('$cyan 验证完成$reset');
print('$cyan═══════════════════════════════════════════════════$reset');
}