Files
kitchen/scripts/test_what_to_eat.dart
Developer b1acdbdf05 feat: 新增公告功能及外卖备注工具
新增公告功能接口及页面,支持查看最新公告信息
添加外卖备注工具,可管理常用备注并一键生成
优化动态筛选接口,支持多分类和标签组合筛选
移除flutter_dotenv依赖,不再使用.env文件
修复布局溢出错误处理逻辑,避免生产环境报错
新增文件选择器插件,替换receive_sharing_intent实现文件导入
2026-04-20 08:21:40 +08:00

395 lines
15 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-20 | test_what_to_eat.dart | 今天吃什么完整接口测试 | 验证动态筛选全流程
// 运行: dart run scripts/test_what_to_eat.dart
import 'dart:convert';
import 'dart:math';
import 'dart:io';
const String baseUrl = 'https://eat.wktyl.com/api';
int passCount = 0;
int failCount = 0;
int warnCount = 0;
void pass(String msg) {
passCount++;
print('$msg');
}
void fail(String msg) {
failCount++;
print('$msg');
}
void warn(String msg) {
warnCount++;
print(' ⚠️ $msg');
}
Future<void> main() async {
print('╔══════════════════════════════════════════╗');
print('║ 🍽️ 今天吃什么 - 动态筛选接口测试 ║');
print('╚══════════════════════════════════════════╝\n');
// ─── 1. api_filter: 菜谱大类 ───
print('━━━ 1. api_filter: 获取菜谱大类 ━━━');
final mainCatsData = await getJson('api_filter.php', {'act': 'recipe_main_categories'});
List<Map> mainCats = [];
if (mainCatsData != null && mainCatsData['code'] == 200) {
mainCats = (mainCatsData['data']?['list'] as List? ?? []).cast<Map>();
print(' 大类数量: ${mainCats.length}');
for (final c in mainCats) {
print(' 📂 ${c['name']} (ID:${c['id']}): ${c['recipe_count']}');
}
if (mainCats.isNotEmpty) {
pass('菜谱大类加载成功 (${mainCats.length}个)');
} else {
fail('菜谱大类为空');
}
} else {
fail('菜谱大类请求失败');
}
// ─── 2. api_filter: 子类 ───
print('\n━━━ 2. api_filter: 获取子分类 ━━━');
int? testParentId;
List<Map> subCats = [];
if (mainCats.isNotEmpty) {
testParentId = mainCats.first['id'] as int;
final subData = await getJson('api_filter.php', {
'act': 'recipe_sub_categories',
'parent_id': '$testParentId',
});
if (subData != null && subData['code'] == 200) {
subCats = (subData['data']?['list'] as List? ?? []).cast<Map>();
print(' 父类ID=$testParentId 的子类数量: ${subCats.length}');
for (final c in subCats.take(5)) {
print(' 📂 ${c['name']} (ID:${c['id']}): ${c['recipe_count']}');
}
if (subCats.isNotEmpty) {
pass('子分类加载成功 (${subCats.length}个)');
} else {
warn('子分类为空,可能该大类无子分类');
}
} else {
fail('子分类请求失败');
}
} else {
warn('无大类数据,跳过子分类测试');
}
// ─── 3. filter_steps: 无筛选条件 ───
print('\n━━━ 3. filter_steps: 无筛选条件(基线) ━━━');
final fs0 = await fetchFilterSteps();
int baseMatched = 0;
int baseTagCount = 0;
if (fs0 != null) {
baseMatched = fs0['matched_count'] ?? 0;
final tags = (fs0['available_tags'] as List? ?? []);
baseTagCount = tags.length;
print(' 匹配菜谱数: $baseMatched');
print(' 可用标签数: $baseTagCount');
for (final t in tags.take(5)) {
final tm = t as Map;
print(' 🏷️ ${tm['name']}: ${tm['count']}');
}
if (baseMatched > 0) {
pass('无筛选基线: $baseMatched 道菜谱');
} else {
fail('无筛选基线: 匹配数为0');
}
if (baseTagCount > 0) {
pass('无筛选基线: $baseTagCount 个标签');
} else {
warn('无筛选基线: 标签为空');
}
} else {
fail('filter_steps 无筛选请求失败');
}
// ─── 4. filter_steps: 选1个分类 ───
print('\n━━━ 4. filter_steps: 选1个分类 ━━━');
int cat1Matched = 0;
int cat1TagCount = 0;
List<Map> cat1Tags = [];
if (testParentId != null) {
final fs1 = await fetchFilterSteps(categories: [testParentId]);
if (fs1 != null) {
cat1Matched = fs1['matched_count'] ?? 0;
cat1Tags = ((fs1['available_tags'] as List? ?? [])).cast<Map>();
cat1TagCount = cat1Tags.length;
print(' 选分类[$testParentId]: 匹配 $cat1Matched');
print(' 可用标签数: $cat1TagCount');
for (final t in cat1Tags.take(5)) {
print(' 🏷️ ${t['name']}: ${t['count']}');
}
if (cat1Matched <= baseMatched) {
pass('选1分类后匹配数($cat1Matched) <= 基线($baseMatched)');
} else {
fail('选1分类后匹配数($cat1Matched) > 基线($baseMatched),逻辑异常');
}
if (cat1TagCount <= baseTagCount) {
pass('选1分类后标签数($cat1TagCount) <= 基线($baseTagCount)');
} else {
warn('选1分类后标签数($cat1TagCount) > 基线($baseTagCount)');
}
} else {
fail('filter_steps 选1分类请求失败');
}
} else {
warn('无分类ID跳过');
}
// ─── 5. filter_steps: 选分类+标签 ───
print('\n━━━ 5. filter_steps: 选分类+标签 ━━━');
int catTagMatched = 0;
if (testParentId != null && cat1Tags.isNotEmpty) {
final firstTagId = cat1Tags.first['id'];
final firstTagName = cat1Tags.first['name'];
final fs2 = await fetchFilterSteps(categories: [testParentId], tags: [firstTagId]);
if (fs2 != null) {
catTagMatched = fs2['matched_count'] ?? 0;
final tags2 = (fs2['available_tags'] as List? ?? []);
print(' 选分类[$testParentId]+标签[$firstTagName($firstTagId)]: 匹配 $catTagMatched');
print(' 可用标签数: ${tags2.length}');
for (final t in tags2.take(5)) {
final tm = t as Map;
print(' 🏷️ ${tm['name']}: ${tm['count']}');
}
if (catTagMatched <= cat1Matched) {
pass('分类+标签匹配数($catTagMatched) <= 仅分类($cat1Matched)');
} else {
fail('分类+标签匹配数($catTagMatched) > 仅分类($cat1Matched),逻辑异常');
}
} else {
fail('filter_steps 分类+标签请求失败');
}
} else {
warn('无标签数据,跳过');
}
// ─── 6. filter_steps: 多分类组合 ───
print('\n━━━ 6. filter_steps: 多分类组合 ━━━');
if (mainCats.length >= 2 && testParentId != null) {
final secondCatId = mainCats[1]['id'] as int;
final secondCatName = mainCats[1]['name'];
final fs3 = await fetchFilterSteps(categories: [testParentId, secondCatId]);
if (fs3 != null) {
final multiMatched = fs3['matched_count'] ?? 0;
print(' 选分类[$testParentId,$secondCatId($secondCatName)]: 匹配 $multiMatched');
print(' 对比: 仅1分类=$cat1Matched, 2分类=$multiMatched');
if (multiMatched >= cat1Matched) {
pass('多分类匹配数($multiMatched) >= 单分类($cat1Matched) [OR逻辑正确]');
} else {
warn('多分类匹配数($multiMatched) < 单分类($cat1Matched),可能子分类重叠');
}
} else {
fail('filter_steps 多分类请求失败');
}
} else {
warn('大类不足2个跳过多分类测试');
}
// ─── 7. filter_steps: 子分类筛选 ───
print('\n━━━ 7. filter_steps: 子分类筛选 ━━━');
if (subCats.isNotEmpty) {
final subCatId = subCats.first['id'] as int;
final subCatName = subCats.first['name'];
final fs4 = await fetchFilterSteps(categories: [subCatId]);
if (fs4 != null) {
final subMatched = fs4['matched_count'] ?? 0;
print(' 选子分类[$subCatName($subCatId)]: 匹配 $subMatched');
if (subMatched <= cat1Matched && subMatched > 0) {
pass('子分类匹配数($subMatched) <= 父分类($cat1Matched) 且 > 0');
} else if (subMatched == 0) {
warn('子分类匹配数为0可能无直接关联菜谱');
} else {
warn('子分类匹配数($subMatched) > 父分类($cat1Matched)');
}
} else {
fail('filter_steps 子分类请求失败');
}
} else {
warn('无子分类数据,跳过');
}
// ─── 8. filter_apply: 获取推荐菜谱 ───
print('\n━━━ 8. filter_apply: 获取推荐菜谱 ━━━');
if (testParentId != null) {
final applyResult = await fetchFilterApply(categories: [testParentId], count: 3);
if (applyResult != null) {
final recipes = (applyResult['recipes'] as List? ?? []);
final total = applyResult['total_matched'] ?? 0;
print(' 返回菜谱数: ${recipes.length}');
print(' 总匹配数: $total');
for (final r in recipes) {
final rm = r as Map;
print(' 🍳 ${rm['title']} (ID: ${rm['id']})');
}
if (recipes.isNotEmpty) {
pass('filter_apply 返回 ${recipes.length} 道菜谱');
} else {
warn('filter_apply 返回空列表');
}
if (total == cat1Matched) {
pass('filter_apply 总匹配数($total) = filter_steps 匹配数($cat1Matched)');
} else {
warn('filter_apply 总匹配数($total) != filter_steps 匹配数($cat1Matched)');
}
} else {
fail('filter_apply 请求失败');
}
} else {
warn('无分类ID跳过');
}
// ─── 9. filter_apply: 分类+标签组合 ───
print('\n━━━ 9. filter_apply: 分类+标签组合 ━━━');
if (testParentId != null && cat1Tags.isNotEmpty) {
final tagId = cat1Tags.first['id'];
final tagName = cat1Tags.first['name'];
final applyResult2 = await fetchFilterApply(
categories: [testParentId],
tags: [tagId],
count: 3,
);
if (applyResult2 != null) {
final recipes = (applyResult2['recipes'] as List? ?? []);
final total = applyResult2['total_matched'] ?? 0;
print(' 分类[$testParentId]+标签[$tagName]: 返回 ${recipes.length} 道菜谱');
print(' 总匹配数: $total');
for (final r in recipes) {
final rm = r as Map;
print(' 🍳 ${rm['title']} (ID: ${rm['id']})');
}
if (total <= cat1Matched) {
pass('分类+标签总匹配($total) <= 仅分类($cat1Matched)');
} else {
fail('分类+标签总匹配($total) > 仅分类($cat1Matched),逻辑异常');
}
} else {
fail('filter_apply 分类+标签请求失败');
}
} else {
warn('无标签数据,跳过');
}
// ─── 10. 动态筛选核心验证 ───
print('\n━━━ 10. 动态筛选核心验证 ━━━');
print(' 基线(无筛选): $baseMatched');
print(' 选1分类: $cat1Matched');
print(' 分类+标签: $catTagMatched');
if (cat1Matched > 0 && cat1Matched < baseMatched && catTagMatched > 0 && catTagMatched < cat1Matched) {
pass('🎯 动态筛选核心逻辑正确:选项越多,匹配越少');
} else if (cat1Matched > 0 && cat1Matched <= baseMatched && catTagMatched >= 0 && catTagMatched <= cat1Matched) {
warn('动态筛选部分正常(可能标签筛选后匹配数不变)');
} else {
fail('动态筛选逻辑异常');
}
// ─── 11. 标签count准确性抽检 ───
print('\n━━━ 11. 标签count准确性抽检 ━━━');
if (cat1Tags.isNotEmpty && testParentId != null) {
final pid = testParentId;
final sampleTag = cat1Tags[Random().nextInt(cat1Tags.length.clamp(0, cat1Tags.length - 1))];
final sampleTagId = sampleTag['id'] as int;
final sampleTagName = sampleTag['name'];
final sampleTagCount = sampleTag['count'] ?? 0;
print(' 抽检标签: $sampleTagName (ID:$sampleTagId, 声称:$sampleTagCount道)');
final verifyResult = await fetchFilterSteps(categories: [pid], tags: [sampleTagId]);
if (verifyResult != null) {
final verifyMatched = verifyResult['matched_count'] ?? 0;
print(' 实际匹配: $verifyMatched');
if (verifyMatched == sampleTagCount) {
pass('标签count准确: $sampleTagCount == $verifyMatched');
} else if ((verifyMatched - sampleTagCount).abs() <= 2) {
warn('标签count近似: 声称$sampleTagCount vs 实际$verifyMatched (差${(verifyMatched - sampleTagCount).abs()})');
} else {
fail('标签count偏差大: 声称$sampleTagCount vs 实际$verifyMatched (差${(verifyMatched - sampleTagCount).abs()})');
}
} else {
fail('抽检验证请求失败');
}
} else {
warn('无标签数据,跳过抽检');
}
// ─── 汇总 ───
print('\n╔══════════════════════════════════════════╗');
print('║ 📊 测试结果汇总 ║');
print('╠══════════════════════════════════════════╣');
print('║ ✅ 通过: $passCount');
print('║ ⚠️ 警告: $warnCount');
print('║ ❌ 失败: $failCount');
print('╚══════════════════════════════════════════╝');
if (failCount == 0) {
print('\n🎉 所有核心测试通过!动态筛选功能正常。');
} else {
print('\n⚠️ 存在 $failCount 个失败项,请检查接口逻辑。');
}
exit(failCount > 0 ? 1 : 0);
}
Future<Map<String, dynamic>?> getJson(String endpoint, Map<String, String> params) async {
try {
final uri = Uri.parse('$baseUrl/$endpoint').replace(queryParameters: params);
final client = HttpClient();
client.connectionTimeout = const Duration(seconds: 15);
final request = await client.getUrl(uri);
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
client.close();
return json.decode(body) as Map<String, dynamic>;
} catch (e) {
print(' ❌ 请求失败: $e');
return null;
}
}
Future<Map<String, dynamic>?> fetchFilterSteps({
List<int>? categories,
List<int>? tags,
}) async {
final params = <String, String>{'act': 'filter_steps'};
if (categories != null && categories.isNotEmpty) {
params['category'] = categories.join(',');
}
if (tags != null && tags.isNotEmpty) {
params['tag'] = tags.join(',');
}
final data = await getJson('api_what_to_eat.php', params);
if (data != null && data['code'] == 200) {
return data['data'] as Map<String, dynamic>?;
}
if (data != null && data['code'] != 200) {
print(' ❌ 接口返回错误: code=${data['code']}, msg=${data['message']}');
}
return null;
}
Future<Map<String, dynamic>?> fetchFilterApply({
List<int>? categories,
List<int>? tags,
int count = 5,
}) async {
final params = <String, String>{'act': 'filter_apply', 'count': '$count'};
if (categories != null && categories.isNotEmpty) {
params['category'] = categories.join(',');
}
if (tags != null && tags.isNotEmpty) {
params['tag'] = tags.join(',');
}
final data = await getJson('api_what_to_eat.php', params);
if (data != null && data['code'] == 200) {
return data['data'] as Map<String, dynamic>?;
}
if (data != null && data['code'] != 200) {
print(' ❌ 接口返回错误: code=${data['code']}, msg=${data['message']}');
}
return null;
}