新增公告功能接口及页面,支持查看最新公告信息 添加外卖备注工具,可管理常用备注并一键生成 优化动态筛选接口,支持多分类和标签组合筛选 移除flutter_dotenv依赖,不再使用.env文件 修复布局溢出错误处理逻辑,避免生产环境报错 新增文件选择器插件,替换receive_sharing_intent实现文件导入
395 lines
15 KiB
Dart
395 lines
15 KiB
Dart
// 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;
|
||
}
|