Files
xianyan/Script/feed_enhance_scenario_test.dart
Developer a4b7105999 feat: 新增API响应模型、缓存配置和状态管理
refactor: 优化网络请求和错误处理

fix: 修复颜色引用和UI细节问题

docs: 更新API文档和设计规范

chore: 清理无用文件和脚本

perf: 优化图片导出和压缩逻辑

build: 更新依赖和构建配置

style: 调整代码格式和注释

test: 添加接口验证脚本

ci: 更新CI配置和脚本
2026-04-29 01:39:48 +08:00

516 lines
18 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.
// ============================================================
// 闲言APP — Feed增强功能场景验证 + 性能分析
// 创建时间: 2026-04-28
// 更新时间: 2026-04-28
// 作用: 模拟用户使用场景验证Feed增强功能分析性能可优化点
// 上次更新: 初始创建,覆盖骨架屏/主题色/缓存/排序/智能刷新/推荐/详情
// ============================================================
import 'dart:convert';
import 'dart:io';
const baseUrl = 'https://tools.wktyl.com';
String? authToken;
int? userId;
int passCount = 0;
int failCount = 0;
int skipCount = 0;
int serverIssueCount = 0;
final List<String> failures = [];
final List<String> serverIssues = [];
final List<String> perfNotes = [];
void logTest(String name, bool pass, String msg) {
final icon = pass ? '' : '';
final tag = pass ? 'PASS' : 'FAIL';
print(' $icon $tag | $name$msg');
if (pass) {
passCount++;
} else {
failCount++;
failures.add('$name: $msg');
}
}
void logServerIssue(String name, String msg) {
print(' 🖥️ SERVER | $name — 服务器端问题: $msg');
serverIssueCount++;
serverIssues.add('$name: $msg');
}
void logSkip(String name, String msg) {
print(' ⏭️ SKIP | $name$msg');
skipCount++;
}
void logPerf(String note) {
print(' ⚡ PERF | $note');
perfNotes.add(note);
}
void printSection(String title) {
print('\n ── $title ──');
}
bool isSuccess(Map<String, dynamic> resp) {
return resp['code'] == 1;
}
String getMsg(Map<String, dynamic> resp) {
return resp['msg']?.toString() ?? '';
}
Future<Map<String, dynamic>> apiGet(
String path, {
Map<String, dynamic>? queryParameters,
}) async {
try {
final sw = Stopwatch()..start();
final uri = Uri.parse(baseUrl + path);
final params = <String, String>{};
if (queryParameters != null) {
queryParameters.forEach((k, v) => params[k] = v.toString());
}
final finalUri = uri.replace(queryParameters: params);
final request = await HttpClient().getUrl(finalUri);
if (authToken != null) {
request.headers.set('token', authToken!);
}
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
sw.stop();
if (sw.elapsedMilliseconds > 2000) {
logPerf('$path 响应慢: ${sw.elapsedMilliseconds}ms');
}
return jsonDecode(body) as Map<String, dynamic>;
} catch (e) {
return {'code': -1, 'msg': '请求异常: $e'};
}
}
Future<Map<String, dynamic>> apiPost(
String path, {
Map<String, dynamic>? data,
}) async {
try {
final sw = Stopwatch()..start();
final uri = Uri.parse(baseUrl + path);
final request = await HttpClient().postUrl(uri);
if (authToken != null) {
request.headers.set('token', authToken!);
}
request.headers.contentType = ContentType(
'application',
'x-www-form-urlencoded',
);
if (data != null && data.isNotEmpty) {
final encoded = data.entries
.map((e) =>
'${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}')
.join('&');
request.write(encoded);
}
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
sw.stop();
if (sw.elapsedMilliseconds > 2000) {
logPerf('$path 响应慢: ${sw.elapsedMilliseconds}ms');
}
return jsonDecode(body) as Map<String, dynamic>;
} catch (e) {
return {'code': -1, 'msg': '请求异常: $e'};
}
}
Future<void> main() async {
final totalSw = Stopwatch()..start();
print('╔══════════════════════════════════════════════════════════╗');
print('║ 闲言APP — Feed增强功能场景验证 + 性能分析 ║');
print('╚══════════════════════════════════════════════════════════╝');
print(' 🕐 开始时间: ${DateTime.now()}');
print(' 🌐 服务器: $baseUrl');
// ─── 0. 认证 ───
printSection('🔐 0. 认证(获取Token)');
final regUsername =
'enhance_${DateTime.now().millisecondsSinceEpoch % 100000}';
final regPassword = 'Test123456';
final registerResp = await apiPost(
'/api/user/register',
data: {'username': regUsername, 'password': regPassword},
);
if (isSuccess(registerResp)) {
final regData = registerResp['data'] as Map<String, dynamic>?;
final regUserinfo = regData?['userinfo'] as Map<String, dynamic>?;
final regToken = regUserinfo?['token'] as String?;
if (regToken != null && regToken.isNotEmpty) {
authToken = regToken;
userId = regUserinfo?['id'] as int?;
logTest('注册+获取Token', true, 'token=${authToken!.substring(0, 20)}...');
} else {
final autoLogin = await apiPost(
'/api/user/login',
data: {'account': regUsername, 'password': regPassword},
);
if (isSuccess(autoLogin)) {
final loginData = autoLogin['data'] as Map<String, dynamic>?;
final loginUserInfo = loginData?['userinfo'] as Map<String, dynamic>?;
authToken = loginUserInfo?['token'] as String?;
userId = loginUserInfo?['id'] as int?;
logTest('注册后自动登录', true, 'token=${authToken?.substring(0, 20)}...');
} else {
logTest('注册后自动登录', false, getMsg(autoLogin));
}
}
} else {
logTest('注册', false, getMsg(registerResp));
}
// ─── 场景1: 首次打开APP — 频道+列表+推荐并行加载 ───
printSection('📱 场景1: 首次打开APP(频道+列表+推荐并行)');
final sw1 = Stopwatch()..start();
final channelsFuture = apiGet('/api/feed/channels');
final listFuture = apiGet('/api/feed/list', queryParameters: {
'channel': 'all',
'sort': 'newest',
'page': '1',
'lite': 'true',
});
final recommendFuture = apiGet('/api/feed/recommend', queryParameters: {
'limit': '1',
});
final channelsResp = await channelsFuture;
final listResp = await listFuture;
final recommendResp = await recommendFuture;
sw1.stop();
logTest('频道列表加载', isSuccess(channelsResp), '耗时: ${sw1.elapsedMilliseconds}ms');
logTest('信息流列表加载', isSuccess(listResp), '耗时: ${sw1.elapsedMilliseconds}ms');
logTest('每日推荐加载', isSuccess(recommendResp), '耗时: ${sw1.elapsedMilliseconds}ms');
if (sw1.elapsedMilliseconds > 3000) {
logPerf('首次加载总耗时 ${sw1.elapsedMilliseconds}ms > 3s建议并行请求+骨架屏');
}
// ─── 场景2: 频道缓存验证 ───
printSection('💾 场景2: 频道缓存(二次请求应命中缓存)');
final sw2a = Stopwatch()..start();
final channelsResp2 = await apiGet('/api/feed/channels');
sw2a.stop();
logTest('频道二次请求', isSuccess(channelsResp2),
'耗时: ${sw2a.elapsedMilliseconds}ms');
if (sw2a.elapsedMilliseconds > 1000) {
logPerf('频道二次请求 ${sw2a.elapsedMilliseconds}ms应使用Hive缓存避免重复请求');
}
// ─── 场景3: 排序切换 ───
printSection('🔄 场景3: 排序切换(最新/热门/经典)');
final sortModes = ['newest', 'hot', 'classic'];
for (final sort in sortModes) {
final sw = Stopwatch()..start();
final sortResp = await apiGet('/api/feed/list', queryParameters: {
'channel': 'all',
'sort': sort,
'page': '1',
'lite': 'true',
});
sw.stop();
logTest('排序=$sort', isSuccess(sortResp),
'耗时: ${sw.elapsedMilliseconds}ms, 条目: ${(sortResp['data'] as Map?)?['list'] != null ? (sortResp['data']['list'] as List).length : 0}');
if (sw.elapsedMilliseconds > 1500) {
logPerf('排序$sort 响应 ${sw.elapsedMilliseconds}ms切换时可复用已有数据');
}
}
// ─── 场景4: 智能刷新 ───
printSection('🔄 场景4: 智能刷新(检查新内容)');
final listData = listResp['data'] as Map<String, dynamic>?;
final listItems = listData?['list'] as List<dynamic>? ?? [];
final firstId = listItems.isNotEmpty ? listItems.first['id'] : 0;
final sw4 = Stopwatch()..start();
final refreshResp = await apiGet('/api/feed/refresh', queryParameters: {
'channel': 'all',
'since_id': firstId.toString(),
});
sw4.stop();
logTest('智能刷新检查', isSuccess(refreshResp), '耗时: ${sw4.elapsedMilliseconds}ms');
if (isSuccess(refreshResp)) {
final refreshData = refreshResp['data'] as Map<String, dynamic>?;
final hasNew = refreshData?['has_new'] as bool? ?? false;
final newCount = refreshData?['new_count'] as int? ?? 0;
logTest('刷新结果', true, 'hasNew=$hasNew, newCount=$newCount');
if (sw4.elapsedMilliseconds < 500) {
logPerf('智能刷新仅 ${sw4.elapsedMilliseconds}ms适合做预检查');
}
}
// ─── 场景5: 推荐频道 ───
printSection('✨ 场景5: 推荐频道');
final sw5 = Stopwatch()..start();
final recommendFullResp = await apiGet('/api/feed/recommend', queryParameters: {
'limit': '20',
});
sw5.stop();
logTest('推荐列表', isSuccess(recommendFullResp), '耗时: ${sw5.elapsedMilliseconds}ms');
if (isSuccess(recommendFullResp)) {
final recData = recommendFullResp['data'] as Map<String, dynamic>?;
final recList = recData?['list'] as List<dynamic>? ?? [];
logTest('推荐条目数', recList.isNotEmpty, '${recList.length}');
if (sw5.elapsedMilliseconds > 2000) {
logPerf('推荐列表 ${sw5.elapsedMilliseconds}ms可考虑分页加载');
}
}
// ─── 场景6: 内容详情 ───
printSection('📄 场景6: 内容详情(异步加载)');
if (listItems.isNotEmpty) {
final firstItem = listItems.first as Map<String, dynamic>;
final feedType = firstItem['feed_type'] ?? firstItem['type'] ?? 'poetry';
final feedId = firstItem['id'];
final sw6 = Stopwatch()..start();
final detailResp = await apiGet('/api/feed/detail', queryParameters: {
'type': feedType.toString(),
'id': feedId.toString(),
});
sw6.stop();
logTest('详情加载', isSuccess(detailResp), '耗时: ${sw6.elapsedMilliseconds}ms');
if (isSuccess(detailResp)) {
final detailData = detailResp['data'] as Map<String, dynamic>?;
final hasContent = detailData?['content'] != null;
final hasSummary = detailData?['summary'] != null;
final views = detailData?['views'] ?? 0;
logTest('详情字段完整性', hasContent || hasSummary,
'content=$hasContent, summary=$hasSummary, views=$views');
}
if (sw6.elapsedMilliseconds > 1500) {
logPerf('详情加载 ${sw6.elapsedMilliseconds}ms建议先展示lite数据再异步加载详情');
}
} else {
logSkip('内容详情', '列表为空,无法测试');
}
// ─── 场景7: 互动操作(点赞/收藏/稍后读) ───
printSection('👍 场景7: 互动操作(点赞→收藏→稍后读→取消)');
if (listItems.isNotEmpty && authToken != null) {
final targetItem = listItems.first as Map<String, dynamic>;
final feedType = targetItem['feed_type'] ?? targetItem['type'] ?? 'poetry';
final feedId = targetItem['id'];
final sw7a = Stopwatch()..start();
final likeResp = await apiPost('/api/feed/action', data: {
'action': 'like',
'feed_type': feedType.toString(),
'feed_id': feedId.toString(),
});
sw7a.stop();
logTest('点赞', isSuccess(likeResp), '耗时: ${sw7a.elapsedMilliseconds}ms');
final sw7b = Stopwatch()..start();
final favResp = await apiPost('/api/feed/action', data: {
'action': 'favorite',
'feed_type': feedType.toString(),
'feed_id': feedId.toString(),
});
sw7b.stop();
logTest('收藏', isSuccess(favResp), '耗时: ${sw7b.elapsedMilliseconds}ms');
final sw7c = Stopwatch()..start();
final readLaterResp = await apiPost('/api/feed/action', data: {
'action': 'readlater',
'feed_type': feedType.toString(),
'feed_id': feedId.toString(),
});
sw7c.stop();
logTest('稍后读', isSuccess(readLaterResp), '耗时: ${sw7c.elapsedMilliseconds}ms');
final sw7d = Stopwatch()..start();
final unlikeResp = await apiPost('/api/feed/action', data: {
'action': 'unlike',
'feed_type': feedType.toString(),
'feed_id': feedId.toString(),
});
sw7d.stop();
logTest('取消点赞', isSuccess(unlikeResp), '耗时: ${sw7d.elapsedMilliseconds}ms');
final totalInteraction = sw7a.elapsedMilliseconds +
sw7b.elapsedMilliseconds +
sw7c.elapsedMilliseconds +
sw7d.elapsedMilliseconds;
if (totalInteraction > 4000) {
logPerf('4次互动操作总耗时 ${totalInteraction}ms建议乐观更新UI+异步同步');
}
} else {
logSkip('互动操作', '无数据或未登录');
}
// ─── 场景8: 频道切换 ───
printSection('📺 场景8: 频道切换(各频道列表)');
if (isSuccess(channelsResp)) {
final chData = channelsResp['data'] as Map<String, dynamic>?;
final chList = chData?['channels'] as List<dynamic>? ?? [];
for (int i = 0; i < chList.length && i < 5; i++) {
final ch = chList[i] as Map<String, dynamic>;
final chKey = ch['key'] ?? '';
final sw = Stopwatch()..start();
final chResp = await apiGet('/api/feed/list', queryParameters: {
'channel': chKey.toString(),
'sort': 'newest',
'page': '1',
'lite': 'true',
});
sw.stop();
final itemCount = (chResp['data'] as Map?)?['list'] != null
? (chResp['data']['list'] as List).length
: 0;
logTest('频道=${ch['name']}', isSuccess(chResp),
'耗时: ${sw.elapsedMilliseconds}ms, ${itemCount}');
if (sw.elapsedMilliseconds > 2000) {
logPerf('频道${ch['name']}加载 ${sw.elapsedMilliseconds}ms切换时可用缓存');
}
}
} else {
logSkip('频道切换', '频道列表加载失败');
}
// ─── 场景9: 分页加载 ───
printSection('📄 场景9: 分页加载(模拟滚动)');
for (int page = 1; page <= 3; page++) {
final sw = Stopwatch()..start();
final pageResp = await apiGet('/api/feed/list', queryParameters: {
'channel': 'all',
'sort': 'newest',
'page': page.toString(),
'lite': 'true',
});
sw.stop();
final itemCount = (pageResp['data'] as Map?)?['list'] != null
? (pageResp['data']['list'] as List).length
: 0;
logTest('${page}', isSuccess(pageResp),
'耗时: ${sw.elapsedMilliseconds}ms, ${itemCount}');
if (sw.elapsedMilliseconds > 2000) {
logPerf('${page}页加载 ${sw.elapsedMilliseconds}ms可预加载下一页');
}
}
// ─── 场景10: Feed类型主题色映射验证 ───
printSection('🎨 场景10: Feed类型主题色映射');
final feedTypeColors = <String, String>{
'poetry': '#4A90D9',
'wisdom': '#D4A843',
'story': '#4CAF50',
'essay': '#9C27B0',
'lyric': '#E91E63',
'movie': '#FF5722',
'book': '#795548',
'anime': '#00BCD4',
'game': '#607D8B',
'travel': '#009688',
'food': '#FF9800',
'music': '#673AB7',
'photo': '#CDDC39',
'tech': '#2196F3',
'health': '#4DB6AC',
'history': '#8D6E63',
'philosophy': '#5C6BC0',
'humor': '#FFCA28',
};
logTest('主题色映射数量', feedTypeColors.length == 18,
'${feedTypeColors.length}种类型');
if (listItems.isNotEmpty) {
final types = <String>{};
for (final item in listItems) {
final ft = (item as Map<String, dynamic>)['feed_type'] ??
item['type'] ??
'unknown';
types.add(ft.toString());
}
final mappedCount =
types.where((t) => feedTypeColors.containsKey(t)).length;
logTest('列表中类型映射覆盖率', mappedCount > 0,
'${mappedCount}/${types.length} 已映射');
}
// ─── 性能分析总结 ───
printSection('⚡ 性能分析总结');
totalSw.stop();
print(' 📊 总测试耗时: ${totalSw.elapsedMilliseconds}ms');
print(' 📊 性能注意事项: ${perfNotes.length}');
for (final note in perfNotes) {
print('$note');
}
print('\n 💡 优化建议:');
print(' 1. 首次加载: 频道+列表+推荐并行请求 → 骨架屏占位');
print(' 2. 频道缓存: Hive缓存channels避免每次启动重复请求');
print(' 3. 排序切换: 切换时先展示loading可复用已加载的数据结构');
print(' 4. 智能刷新: /api/feed/refresh 轻量检查,无新内容不重新拉取');
print(' 5. 互动操作: 乐观更新UI + 异步同步服务端,避免等待');
print(' 6. 详情加载: 先展示lite数据异步加载完整content+summary');
print(' 7. 分页预加载: 滚动到80%时预加载下一页');
print(' 8. 列表去重: existingIds Set 去重,避免重复展示');
// ─── 结果汇总 ───
print('\n ══════════════════════════════════════════');
print(' 📊 测试结果汇总');
print(' ══════════════════════════════════════════');
print(' ✅ 通过: $passCount');
print(' ❌ 失败: $failCount');
print(' 🖥️ 服务器问题: $serverIssueCount');
print(' ⏭️ 跳过: $skipCount');
print(' ⚡ 性能注意事项: ${perfNotes.length}');
print(' 🕐 总耗时: ${totalSw.elapsedMilliseconds}ms');
if (failures.isNotEmpty) {
print('\n ❌ 失败详情:');
for (final f in failures) {
print('$f');
}
}
if (serverIssues.isNotEmpty) {
print('\n 🖥️ 服务器端问题:');
for (final s in serverIssues) {
print('$s');
}
}
print('\n 🏁 测试完成: ${DateTime.now()}');
exit(0);
}