refactor: 优化网络请求和错误处理 fix: 修复颜色引用和UI细节问题 docs: 更新API文档和设计规范 chore: 清理无用文件和脚本 perf: 优化图片导出和压缩逻辑 build: 更新依赖和构建配置 style: 调整代码格式和注释 test: 添加接口验证脚本 ci: 更新CI配置和脚本
516 lines
18 KiB
Dart
516 lines
18 KiB
Dart
// ============================================================
|
||
// 闲言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);
|
||
}
|