接口更新
This commit is contained in:
@@ -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+*
|
||||
@@ -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']}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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('');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user