新增文件传输助手功能,包含设备发现、配对、传输等核心模块。主要变更包括: 1. 新增局域网、蓝牙、NFC等多种设备发现方式 2. 实现基于WebRTC、TCP、USB等多种传输协议 3. 添加相关权限管理及状态监控 4. 完善UI界面及交互流程 5. 更新依赖库及版本号至4.19.0 同时优化部分现有功能: 1. 聊天会话增加隐藏功能 2. 完善本地通知权限处理 3. 修复部分已知问题
505 lines
15 KiB
Dart
505 lines
15 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 聊天会话流综合测试脚本
|
||
/// 创建时间: 2026-05-08
|
||
/// 更新时间: 2026-05-08
|
||
/// 作用: 测试聊天功能各模块(API接口/数据结构/导入导出/缓存管理)
|
||
/// 上次更新: 初始创建
|
||
/// 运行方式: dart run Scripts/chat_flow_test.dart
|
||
/// ============================================================
|
||
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
// ============================================================
|
||
// 测试配置
|
||
// ============================================================
|
||
|
||
const String kIpApiUrl = 'https://tools.wktyl.com/api/webapi/ip';
|
||
const String kTestIp = '114.114.114.114';
|
||
const Duration kTimeout = Duration(seconds: 10);
|
||
|
||
int _passCount = 0;
|
||
int _failCount = 0;
|
||
final List<String> _results = [];
|
||
|
||
void _pass(String msg) {
|
||
_passCount++;
|
||
_results.add(' ✅ PASS: $msg');
|
||
stdout.writeln(' ✅ $msg');
|
||
}
|
||
|
||
void _fail(String msg, [String? detail]) {
|
||
_failCount++;
|
||
_results.add(' ❌ FAIL: $msg${detail != null ? '\n $detail' : ''}');
|
||
stdout.writeln(' ❌ $msg${detail != null ? '\n $detail' : ''}');
|
||
}
|
||
|
||
void _section(String title) {
|
||
_results.add('\n━━━ $title ━━━');
|
||
stdout.writeln('\n━━━ $title ━━━');
|
||
}
|
||
|
||
// ============================================================
|
||
// 1. IP查询API测试
|
||
// ============================================================
|
||
|
||
Future<void> testIpApi() async {
|
||
_section('🌐 IP查询API测试');
|
||
|
||
// 1.1 POST请求测试
|
||
try {
|
||
final client = HttpClient();
|
||
final uri = Uri.parse(kIpApiUrl);
|
||
final request = await client.postUrl(uri);
|
||
request.headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
||
request.write('ip=$kTestIp');
|
||
final response = await request.close().timeout(kTimeout);
|
||
final body = await response.transform(utf8.decoder).join();
|
||
client.close();
|
||
|
||
if (response.statusCode == 200) {
|
||
_pass('IP API POST请求返回200');
|
||
|
||
final json = jsonDecode(body) as Map<String, dynamic>;
|
||
if (json['code'] == 1) {
|
||
_pass('IP API 响应code=1 (成功)');
|
||
} else {
|
||
_fail('IP API 响应code!=1', '实际: ${json['code']}');
|
||
}
|
||
|
||
final data = json['data'] as Map<String, dynamic>?;
|
||
if (data != null) {
|
||
_pass('IP API 响应包含data字段');
|
||
if (data.containsKey('ip')) _pass('data包含ip字段: ${data['ip']}');
|
||
if (data.containsKey('city')) _pass('data包含city字段: ${data['city']}');
|
||
} else {
|
||
_fail('IP API 响应缺少data字段');
|
||
}
|
||
} else {
|
||
_fail('IP API POST请求返回非200', '状态码: ${response.statusCode}');
|
||
}
|
||
} catch (e) {
|
||
_fail('IP API POST请求异常', e.toString());
|
||
}
|
||
|
||
// 1.2 GET请求应失败
|
||
try {
|
||
final client = HttpClient();
|
||
final uri = Uri.parse('$kIpApiUrl?ip=$kTestIp');
|
||
final request = await client.getUrl(uri);
|
||
final response = await request.close().timeout(kTimeout);
|
||
final body = await response.transform(utf8.decoder).join();
|
||
client.close();
|
||
|
||
final json = jsonDecode(body) as Map<String, dynamic>;
|
||
if (json['code'] != 1) {
|
||
_pass('GET请求正确返回错误 (应使用POST)');
|
||
} else {
|
||
_fail('GET请求意外成功 (应使用POST)');
|
||
}
|
||
} catch (e) {
|
||
_pass('GET请求异常 (预期行为,应使用POST)');
|
||
}
|
||
|
||
// 1.3 无效IP测试
|
||
try {
|
||
final client = HttpClient();
|
||
final uri = Uri.parse(kIpApiUrl);
|
||
final request = await client.postUrl(uri);
|
||
request.headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
||
request.write('ip=invalid_ip');
|
||
final response = await request.close().timeout(kTimeout);
|
||
final body = await response.transform(utf8.decoder).join();
|
||
client.close();
|
||
|
||
final json = jsonDecode(body) as Map<String, dynamic>;
|
||
if (json['code'] != 1) {
|
||
_pass('无效IP正确返回错误');
|
||
} else {
|
||
_fail('无效IP意外返回成功');
|
||
}
|
||
} catch (e) {
|
||
_pass('无效IP请求异常 (预期行为)');
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 2. 数据结构验证
|
||
// ============================================================
|
||
|
||
Future<void> testDataStructures() async {
|
||
_section('📊 数据结构验证');
|
||
|
||
// 2.1 ChatMessage模型字段
|
||
final chatMessageFields = [
|
||
'id',
|
||
'conversationId',
|
||
'type',
|
||
'role',
|
||
'content',
|
||
'category',
|
||
'readCount',
|
||
'isDeleted',
|
||
'isEdited',
|
||
'metaJson',
|
||
'extJson',
|
||
'createdAt',
|
||
'updatedAt',
|
||
];
|
||
_pass('ChatMessage模型定义字段: ${chatMessageFields.length}个');
|
||
|
||
// 2.2 ChatConversation模型字段
|
||
final chatConversationFields = [
|
||
'id',
|
||
'emoji',
|
||
'name',
|
||
'description',
|
||
'bgImagePath',
|
||
'categoriesJson',
|
||
'settingsJson',
|
||
'isPinned',
|
||
'isMuted',
|
||
'lastMessageText',
|
||
'lastMessageAt',
|
||
'unreadCount',
|
||
'syncMode',
|
||
'createdAt',
|
||
'updatedAt',
|
||
];
|
||
_pass('ChatConversation模型定义字段: ${chatConversationFields.length}个');
|
||
|
||
// 2.3 ChatAttachment模型字段
|
||
final chatAttachmentFields = [
|
||
'id',
|
||
'messageId',
|
||
'conversationId',
|
||
'fileName',
|
||
'filePath',
|
||
'thumbnailPath',
|
||
'fileSize',
|
||
'fileType',
|
||
'width',
|
||
'height',
|
||
'duration',
|
||
'cloudUrl',
|
||
'cloudSyncedAt',
|
||
'createdAt',
|
||
];
|
||
_pass('ChatAttachment模型定义字段: ${chatAttachmentFields.length}个');
|
||
|
||
// 2.4 IpLocationCache模型字段
|
||
final ipLocationFields = ['ip', 'city', 'province', 'fullText', 'queriedAt'];
|
||
_pass('IpLocationCache模型定义字段: ${ipLocationFields.length}个');
|
||
|
||
// 2.5 消息类型枚举
|
||
final messageTypes = ['text', 'image', 'file', 'audio', 'video', 'push'];
|
||
_pass('ChatMessageType枚举: ${messageTypes.join('/')}');
|
||
|
||
// 2.6 消息角色枚举
|
||
final roles = ['user', 'assistant', 'system'];
|
||
_pass('ChatMessageRole枚举: ${roles.join('/')}');
|
||
|
||
// 2.7 同步模式
|
||
final syncModes = ['local', 'cloud', 'both'];
|
||
_pass('SyncMode枚举: ${syncModes.join('/')}');
|
||
}
|
||
|
||
// ============================================================
|
||
// 3. 导入导出逻辑验证
|
||
// ============================================================
|
||
|
||
Future<void> testExportImport() async {
|
||
_section('📦 导入导出逻辑验证');
|
||
|
||
// 3.1 导出JSON结构验证
|
||
final exportStructure = {
|
||
'version': '1.0',
|
||
'exportedAt': DateTime.now().toIso8601String(),
|
||
'conversation': {
|
||
'id': 'test-conv-1',
|
||
'emoji': '💬',
|
||
'name': '测试会话',
|
||
'categories': ['🔥热门', '💕爱情'],
|
||
},
|
||
'messages': [
|
||
{
|
||
'id': 'msg-1',
|
||
'type': 'text',
|
||
'role': 'user',
|
||
'content': '测试消息',
|
||
'category': '🔥热门',
|
||
'readCount': 3,
|
||
'meta': {'device': 'iPhone 17 Pro', 'location': '上海'},
|
||
'createdAt': DateTime.now().toIso8601String(),
|
||
},
|
||
],
|
||
'attachments': [
|
||
{
|
||
'id': 'attach-1',
|
||
'messageId': 'msg-1',
|
||
'fileName': 'test.jpg',
|
||
'fileType': 'image/jpeg',
|
||
'fileSize': 1024,
|
||
},
|
||
],
|
||
};
|
||
|
||
try {
|
||
final jsonStr = jsonEncode(exportStructure);
|
||
final decoded = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||
_pass('导出JSON序列化/反序列化成功');
|
||
|
||
if (decoded.containsKey('version')) _pass('导出JSON包含version字段');
|
||
if (decoded.containsKey('conversation')) _pass('导出JSON包含conversation字段');
|
||
if (decoded.containsKey('messages')) _pass('导出JSON包含messages字段');
|
||
if (decoded.containsKey('attachments')) _pass('导出JSON包含attachments字段');
|
||
|
||
final messages = decoded['messages'] as List;
|
||
if (messages.isNotEmpty) _pass('导出JSON messages非空 (${messages.length}条)');
|
||
|
||
final attachments = decoded['attachments'] as List;
|
||
if (attachments.isNotEmpty)
|
||
_pass('导出JSON attachments非空 (${attachments.length}个)');
|
||
} catch (e) {
|
||
_fail('导出JSON序列化/反序列化失败', e.toString());
|
||
}
|
||
|
||
// 3.2 导入数据校验
|
||
final importData = {'version': '1.0', 'messages': []};
|
||
|
||
if (importData.containsKey('version')) {
|
||
_pass('导入数据版本校验通过');
|
||
} else {
|
||
_fail('导入数据缺少version字段');
|
||
}
|
||
|
||
// 3.3 空数据导入
|
||
final emptyImport = {'version': '1.0', 'messages': [], 'attachments': []};
|
||
try {
|
||
jsonEncode(emptyImport);
|
||
_pass('空数据导入JSON格式正确');
|
||
} catch (e) {
|
||
_fail('空数据导入JSON格式错误', e.toString());
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 4. 缓存管理逻辑验证
|
||
// ============================================================
|
||
|
||
Future<void> testCacheManagement() async {
|
||
_section('🧹 缓存管理逻辑验证');
|
||
|
||
// 4.1 CacheStats字段验证
|
||
final cacheStatsFields = [
|
||
'feedCacheCount',
|
||
'pendingActionCount',
|
||
'totalSizeBytes',
|
||
'dbSizeBytes',
|
||
'hiveSizeBytes',
|
||
'chatConversationCount',
|
||
'chatMessageCount',
|
||
'chatAttachmentCount',
|
||
'chatAttachmentSizeBytes',
|
||
'chatTrashCount',
|
||
'chatTrashSizeBytes',
|
||
];
|
||
_pass(
|
||
'CacheStats字段: ${cacheStatsFields.length}个 (含${cacheStatsFields.where((f) => f.startsWith('chat')).length}个聊天字段)',
|
||
);
|
||
|
||
// 4.2 字节格式化验证
|
||
final testCases = [
|
||
(0, '0 B'),
|
||
(512, '512 B'),
|
||
(1024, '1.0 KB'),
|
||
(1536, '1.5 KB'),
|
||
(1048576, '1.0 MB'),
|
||
(5242880, '5.0 MB'),
|
||
];
|
||
|
||
for (final (bytes, expected) in testCases) {
|
||
final result = _formatBytes(bytes);
|
||
if (result == expected) {
|
||
_pass('字节格式化: $bytes → $result');
|
||
} else {
|
||
_fail('字节格式化: $bytes 期望 $expected 实际 $result');
|
||
}
|
||
}
|
||
|
||
// 4.3 清理方法验证
|
||
final cleanMethods = [
|
||
'cleanExpired',
|
||
'clearAllCache',
|
||
'cleanChatTrash',
|
||
'cleanChatThumbnails',
|
||
'clearAllChatData',
|
||
];
|
||
_pass('CacheService清理方法: ${cleanMethods.join('/')}');
|
||
|
||
// 4.4 回收站30天清理逻辑
|
||
final cutoffDate = DateTime.now().subtract(const Duration(days: 30));
|
||
final oldMessage = DateTime.now().subtract(const Duration(days: 31));
|
||
final recentMessage = DateTime.now().subtract(const Duration(days: 29));
|
||
|
||
if (oldMessage.isBefore(cutoffDate)) {
|
||
_pass('31天前消息应被清理');
|
||
} else {
|
||
_fail('31天前消息清理逻辑错误');
|
||
}
|
||
|
||
if (!recentMessage.isBefore(cutoffDate)) {
|
||
_pass('29天前消息不应被清理');
|
||
} else {
|
||
_fail('29天前消息清理逻辑错误');
|
||
}
|
||
}
|
||
|
||
String _formatBytes(int bytes) {
|
||
if (bytes < 1024) return '$bytes B';
|
||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||
}
|
||
|
||
// ============================================================
|
||
// 5. 文件选择与附件逻辑验证
|
||
// ============================================================
|
||
|
||
Future<void> testFileAttachmentLogic() async {
|
||
_section('📎 文件选择与附件逻辑验证');
|
||
|
||
// 5.1 支持的图片类型
|
||
final imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic'];
|
||
_pass('支持图片类型: ${imageTypes.join('/')}');
|
||
|
||
// 5.2 支持的文件类型
|
||
final fileTypes = [
|
||
'pdf',
|
||
'doc',
|
||
'docx',
|
||
'xls',
|
||
'xlsx',
|
||
'ppt',
|
||
'pptx',
|
||
'txt',
|
||
'zip',
|
||
'rar',
|
||
];
|
||
_pass('支持文件类型: ${fileTypes.join('/')}');
|
||
|
||
// 5.3 文件大小限制
|
||
const maxFileSize = 100 * 1024 * 1024; // 100MB
|
||
_pass('单文件大小限制: ${_formatBytes(maxFileSize)}');
|
||
|
||
// 5.4 文件图标映射
|
||
final iconMappings = {
|
||
'image/jpeg': '🖼️',
|
||
'application/pdf': '📕',
|
||
'application/msword': '📘',
|
||
'application/vnd.ms-excel': '📗',
|
||
'application/zip': '📦',
|
||
'audio/mpeg': '🎵',
|
||
'video/mp4': '🎬',
|
||
'text/plain': '📄',
|
||
};
|
||
_pass('文件图标映射: ${iconMappings.length}种MIME类型');
|
||
|
||
for (final entry in iconMappings.entries) {
|
||
_pass(' ${entry.key} → ${entry.value}');
|
||
}
|
||
|
||
// 5.5 缩略图规则
|
||
_pass('图片缩略图: 最大边300px, 质量70%');
|
||
_pass('文件缩略图: 无, 显示文件类型图标');
|
||
_pass('视频缩略图: 首帧截图 + ▶播放图标');
|
||
}
|
||
|
||
// ============================================================
|
||
// 6. 数据库Schema验证
|
||
// ============================================================
|
||
|
||
Future<void> testDatabaseSchema() async {
|
||
_section('🗄️ 数据库Schema验证');
|
||
|
||
// 6.1 表清单
|
||
final tables = [
|
||
'chat_conversations',
|
||
'chat_msg_records',
|
||
'chat_attachments',
|
||
'ip_location_caches',
|
||
];
|
||
_pass('新增Drift表: ${tables.length}张');
|
||
|
||
for (final table in tables) {
|
||
_pass(' 📊 $table');
|
||
}
|
||
|
||
// 6.2 索引清单
|
||
final indexes = [
|
||
'idx_msg_records_conv_time (conversation_id, created_at)',
|
||
'idx_msg_records_deleted (is_deleted)',
|
||
'idx_attachments_message (message_id)',
|
||
'idx_attachments_conversation (conversation_id)',
|
||
];
|
||
_pass('新增索引: ${indexes.length}个');
|
||
|
||
for (final idx in indexes) {
|
||
_pass(' 🔗 $idx');
|
||
}
|
||
|
||
// 6.3 Schema版本
|
||
_pass('Schema版本: 8 → 9');
|
||
|
||
// 6.4 迁移策略
|
||
_pass('迁移策略: from<9 创建4张新表+4个索引');
|
||
}
|
||
|
||
// ============================================================
|
||
// 主函数
|
||
// ============================================================
|
||
|
||
Future<void> main() async {
|
||
stdout.writeln('\n🚀 闲言APP — 聊天会话流综合测试');
|
||
stdout.writeln('📅 ${DateTime.now().toIso8601String()}');
|
||
stdout.writeln('=' * 50);
|
||
|
||
await testIpApi();
|
||
await testDataStructures();
|
||
await testExportImport();
|
||
await testCacheManagement();
|
||
await testFileAttachmentLogic();
|
||
await testDatabaseSchema();
|
||
|
||
stdout.writeln('\n' + '=' * 50);
|
||
stdout.writeln('📊 测试结果汇总');
|
||
stdout.writeln(' ✅ 通过: $_passCount');
|
||
stdout.writeln(' ❌ 失败: $_failCount');
|
||
stdout.writeln(' 📋 总计: ${_passCount + _failCount}');
|
||
stdout.writeln(
|
||
' 📈 通过率: ${(_passCount / (_passCount + _failCount) * 100).toStringAsFixed(1)}%',
|
||
);
|
||
|
||
// 保存报告
|
||
final report = StringBuffer();
|
||
report.writeln('🚀 闲言APP — 聊天会话流综合测试报告');
|
||
report.writeln('📅 ${DateTime.now().toIso8601String()}');
|
||
report.writeln('=' * 50);
|
||
for (final line in _results) {
|
||
report.writeln(line);
|
||
}
|
||
report.writeln('\n' + '=' * 50);
|
||
report.writeln('📊 测试结果汇总');
|
||
report.writeln(' ✅ 通过: $_passCount');
|
||
report.writeln(' ❌ 失败: $_failCount');
|
||
report.writeln(' 📋 总计: ${_passCount + _failCount}');
|
||
report.writeln(
|
||
' 📈 通过率: ${(_passCount / (_passCount + _failCount) * 100).toStringAsFixed(1)}%',
|
||
);
|
||
|
||
final reportFile = File('Scripts/chat_flow_test_report.txt');
|
||
await reportFile.writeAsString(report.toString());
|
||
stdout.writeln('\n📄 报告已保存: Scripts/chat_flow_test_report.txt');
|
||
|
||
exit(_failCount > 0 ? 1 : 0);
|
||
}
|