feat: 新增文件传输助手功能及相关组件

新增文件传输助手功能,包含设备发现、配对、传输等核心模块。主要变更包括:
1. 新增局域网、蓝牙、NFC等多种设备发现方式
2. 实现基于WebRTC、TCP、USB等多种传输协议
3. 添加相关权限管理及状态监控
4. 完善UI界面及交互流程
5. 更新依赖库及版本号至4.19.0

同时优化部分现有功能:
1. 聊天会话增加隐藏功能
2. 完善本地通知权限处理
3. 修复部分已知问题
This commit is contained in:
Developer
2026-05-10 02:48:52 +08:00
parent 41a60b0288
commit 72f64f9ca9
458 changed files with 56803 additions and 72785 deletions

504
scripts/chat_flow_test.dart Normal file
View File

@@ -0,0 +1,504 @@
/// ============================================================
/// 闲言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);
}

View File

@@ -0,0 +1,82 @@
🚀 闲言APP — 聊天会话流综合测试报告
📅 2026-05-08T08:51:14.700607
==================================================
━━━ 🌐 IP查询API测试 ━━━
✅ PASS: IP API POST请求返回200
✅ PASS: IP API 响应code=1 (成功)
✅ PASS: IP API 响应包含data字段
✅ PASS: data包含ip字段: 114.114.114.114
✅ PASS: data包含city字段: 江苏省南京市 南京信风网络科技有限公司GreatbitDNS服务器
✅ PASS: GET请求正确返回错误 (应使用POST)
❌ FAIL: 无效IP意外返回成功
━━━ 📊 数据结构验证 ━━━
✅ PASS: ChatMessage模型定义字段: 13个
✅ PASS: ChatConversation模型定义字段: 15个
✅ PASS: ChatAttachment模型定义字段: 14个
✅ PASS: IpLocationCache模型定义字段: 5个
✅ PASS: ChatMessageType枚举: text/image/file/audio/video/push
✅ PASS: ChatMessageRole枚举: user/assistant/system
✅ PASS: SyncMode枚举: local/cloud/both
━━━ 📦 导入导出逻辑验证 ━━━
✅ PASS: 导出JSON序列化/反序列化成功
✅ PASS: 导出JSON包含version字段
✅ PASS: 导出JSON包含conversation字段
✅ PASS: 导出JSON包含messages字段
✅ PASS: 导出JSON包含attachments字段
✅ PASS: 导出JSON messages非空 (1条)
✅ PASS: 导出JSON attachments非空 (1个)
✅ PASS: 导入数据版本校验通过
✅ PASS: 空数据导入JSON格式正确
━━━ 🧹 缓存管理逻辑验证 ━━━
✅ PASS: CacheStats字段: 11个 (含6个聊天字段)
✅ PASS: 字节格式化: 0 → 0 B
✅ PASS: 字节格式化: 512 → 512 B
✅ PASS: 字节格式化: 1024 → 1.0 KB
✅ PASS: 字节格式化: 1536 → 1.5 KB
✅ PASS: 字节格式化: 1048576 → 1.0 MB
✅ PASS: 字节格式化: 5242880 → 5.0 MB
✅ PASS: CacheService清理方法: cleanExpired/clearAllCache/cleanChatTrash/cleanChatThumbnails/clearAllChatData
✅ PASS: 31天前消息应被清理
✅ PASS: 29天前消息不应被清理
━━━ 📎 文件选择与附件逻辑验证 ━━━
✅ PASS: 支持图片类型: jpg/jpeg/png/gif/webp/heic
✅ PASS: 支持文件类型: pdf/doc/docx/xls/xlsx/ppt/pptx/txt/zip/rar
✅ PASS: 单文件大小限制: 100.0 MB
✅ PASS: 文件图标映射: 8种MIME类型
✅ PASS: image/jpeg → 🖼️
✅ PASS: application/pdf → 📕
✅ PASS: application/msword → 📘
✅ PASS: application/vnd.ms-excel → 📗
✅ PASS: application/zip → 📦
✅ PASS: audio/mpeg → 🎵
✅ PASS: video/mp4 → 🎬
✅ PASS: text/plain → 📄
✅ PASS: 图片缩略图: 最大边300px, 质量70%
✅ PASS: 文件缩略图: 无, 显示文件类型图标
✅ PASS: 视频缩略图: 首帧截图 + ▶播放图标
━━━ 🗄️ 数据库Schema验证 ━━━
✅ PASS: 新增Drift表: 4张
✅ PASS: 📊 chat_conversations
✅ PASS: 📊 chat_msg_records
✅ PASS: 📊 chat_attachments
✅ PASS: 📊 ip_location_caches
✅ PASS: 新增索引: 4个
✅ PASS: 🔗 idx_msg_records_conv_time (conversation_id, created_at)
✅ PASS: 🔗 idx_msg_records_deleted (is_deleted)
✅ PASS: 🔗 idx_attachments_message (message_id)
✅ PASS: 🔗 idx_attachments_conversation (conversation_id)
✅ PASS: Schema版本: 8 → 9
✅ PASS: 迁移策略: from<9 创建4张新表+4个索引
==================================================
📊 测试结果汇总
✅ 通过: 59
❌ 失败: 1
📋 总计: 60
📈 通过率: 98.3%