feat: 新增公告功能及外卖备注工具

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

View File

@@ -0,0 +1,156 @@
// 2026-04-20 | test_filter_steps.dart | 动态筛选接口测试 | 验证filter_steps动态筛选+api_filter分类
// 运行: dart run scripts/test_filter_steps.dart
import 'dart:convert';
import 'dart:io';
const String baseUrl = 'https://eat.wktyl.com/api';
Future<void> main() async {
print('=== 动态筛选接口测试 ===\n');
print('━━━ 1. api_filter: 获取菜谱大类 ━━━');
final mainCats = await getJson('api_filter.php', {'act': 'recipe_main_categories'});
if (mainCats != null) {
final list = mainCats['data']?['list'] as List? ?? [];
print(' 大类数量: ${list.length}');
for (final c in list) {
final m = c as Map;
print(' 📂 ${m['name']} (ID:${m['id']}): ${m['recipe_count']}');
}
}
print('\n━━━ 2. api_filter: 获取中国菜子类 ━━━');
final subCats = await getJson('api_filter.php', {'act': 'recipe_sub_categories', 'parent_id': '12'});
if (subCats != null) {
final list = subCats['data']?['list'] as List? ?? [];
print(' 子类数量: ${list.length}');
for (final c in list.take(5)) {
final m = c as Map;
print(' 📂 ${m['name']} (ID:${m['id']}): ${m['recipe_count']}');
}
}
print('\n━━━ 3. filter_steps: 无筛选条件 ━━━');
final fs1 = await fetchFilterSteps();
if (fs1 != null) {
print(' 匹配菜谱数: ${fs1['matched_count']}');
final opts = fs1['available_options'] as List? ?? [];
print(' 可用分类数: ${opts.length}');
for (final o in opts.take(3)) {
final m = o as Map;
print(' 📂 ${m['name']}: ${m['count']}道 (${(m['children'] as List?)?.length ?? 0}子类)');
}
final tags1 = fs1['available_tags'] as List? ?? [];
print(' 可用标签数: ${tags1.length}');
for (final t in tags1.take(5)) {
final tm = t as Map;
print(' 🏷️ ${tm['name']}: ${tm['count']}');
}
}
print('\n━━━ 4. filter_steps: 选分类[12] ━━━');
final fs2 = await fetchFilterSteps(categories: [12]);
if (fs2 != null) {
print(' 匹配菜谱数: ${fs2['matched_count']}');
final tags2 = fs2['available_tags'] as List? ?? [];
print(' 可用标签数: ${tags2.length}');
for (final t in tags2.take(5)) {
final tm = t as Map;
print(' 🏷️ ${tm['name']}: ${tm['count']}');
}
}
print('\n━━━ 5. filter_steps: 分类[12]+标签[1] ━━━');
final fs3 = await fetchFilterSteps(categories: [12], tags: [1]);
if (fs3 != null) {
print(' 匹配菜谱数: ${fs3['matched_count']}');
final tags3 = fs3['available_tags'] as List? ?? [];
print(' 可用标签数: ${tags3.length}');
for (final t in tags3.take(5)) {
final tm = t as Map;
print(' 🏷️ ${tm['name']}: ${tm['count']}');
}
}
print('\n━━━ 6. 验证动态筛选效果 ━━━');
final c1 = fs1?['matched_count'] ?? 0;
final c2 = fs2?['matched_count'] ?? 0;
final c3 = fs3?['matched_count'] ?? 0;
print(' 无筛选: $c1');
print(' 选分类: $c2');
print(' 分类+标签: $c3');
if (c2 < c1 && c3 < c2) {
print(' ✅ 动态筛选正常:选项越多,匹配越少');
} else if (c2 <= c1 && c3 <= c2) {
print(' ⚠️ 动态筛选部分正常');
} else {
print(' ❌ 动态筛选异常');
}
print('\n━━━ 7. filter_apply: 获取推荐菜谱 ━━━');
final applyResult = await fetchFilterApply(categories: [12], tags: [1], count: 3);
if (applyResult != null) {
final recipes = applyResult['recipes'] as List? ?? [];
print(' 返回菜谱数: ${recipes.length}');
print(' 总匹配数: ${applyResult['total_matched']}');
for (final r in recipes) {
final rm = r as Map;
print(' 🍳 ${rm['title']} (ID: ${rm['id']})');
}
}
print('\n=== 测试完成 ===');
}
Future<Map<String, dynamic>?> getJson(String endpoint, Map<String, String> params) async {
try {
final uri = Uri.parse('$baseUrl/$endpoint').replace(queryParameters: params);
final response = await HttpClient()
.getUrl(uri)
.then((r) => r.close())
.timeout(const Duration(seconds: 15));
final body = await response.transform(utf8.decoder).join();
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>?;
}
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>?;
}
return null;
}

View File

@@ -0,0 +1,394 @@
// 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;
}