Files
xianyan/lib/features/file_transfer/services/transfer_stats_service.dart
Developer 228095f80a chore: 完成v6.7.0版本迭代更新
本次更新涵盖多个功能模块的优化与新增:
1. 新增Wi-Fi直连配对方式与协作画布模块
2. 完成设备管理重命名API与前端适配
3. 优化日签卡片空数据保护与UI细节
4. 新增剪贴板工具与每日运势会话
5. 修复应用锁恢复、语音消息录制等已知问题
6. 完善文件传输统计与配对逻辑
7. 更新安卓权限配置与iOS隐私描述
8. 新增自动化测试脚本与文档
9. 清理旧版审计报告与测试文件
2026-05-14 05:35:18 +08:00

481 lines
13 KiB
Dart

// ============================================================
// 闲言APP — 传输统计服务
// 创建时间: 2026-05-12
// 更新时间: 2026-05-12
// 作用: 传输数据聚合查询+每日快照写入 — 日/周/月维度统计
// 上次更新: v11.4.0 初始版本
// ============================================================
import 'package:drift/drift.dart' hide Column;
import 'package:intl/intl.dart';
import 'package:xianyan/core/storage/database/app_database.dart';
import 'package:xianyan/core/utils/logger.dart';
import 'package:xianyan/features/file_transfer/database/transfer_database.dart';
class DailyStats {
const DailyStats({
required this.date,
this.sentBytes = 0,
this.receivedBytes = 0,
this.fileCount = 0,
this.avgSpeed = 0.0,
this.successCount = 0,
this.failCount = 0,
});
final String date;
final int sentBytes;
final int receivedBytes;
int get totalBytes => sentBytes + receivedBytes;
final int fileCount;
final double avgSpeed;
final int successCount;
final int failCount;
int get totalCount => successCount + failCount;
double get successRate => totalCount > 0 ? successCount / totalCount : 0.0;
}
class DeviceRanking {
const DeviceRanking({
required this.peerId,
required this.peerAlias,
this.fileCount = 0,
this.totalBytes = 0,
this.avgSpeed = 0.0,
});
final String peerId;
final String peerAlias;
final int fileCount;
final int totalBytes;
final double avgSpeed;
}
class FileTypeDistribution {
const FileTypeDistribution({
required this.category,
this.fileCount = 0,
this.totalBytes = 0,
});
final String category;
final int fileCount;
final int totalBytes;
}
class TransferQuality {
const TransferQuality({
this.totalTasks = 0,
this.successCount = 0,
this.failCount = 0,
this.avgSpeed = 0.0,
this.maxSpeed = 0.0,
this.retryCount = 0,
});
final int totalTasks;
final int successCount;
final int failCount;
final double avgSpeed;
final double maxSpeed;
final int retryCount;
double get successRate => totalTasks > 0 ? successCount / totalTasks : 0.0;
double get failRate => totalTasks > 0 ? failCount / totalTasks : 0.0;
}
class TransferStatsOverview {
const TransferStatsOverview({
this.totalSentBytes = 0,
this.totalReceivedBytes = 0,
this.totalFileCount = 0,
this.avgSpeed = 0.0,
this.topTransport = 'localsend_http',
this.totalSuccessCount = 0,
this.totalFailCount = 0,
});
final int totalSentBytes;
final int totalReceivedBytes;
final int totalFileCount;
final double avgSpeed;
final String topTransport;
final int totalSuccessCount;
final int totalFailCount;
int get totalBytes => totalSentBytes + totalReceivedBytes;
}
class TransferStatsService {
TransferStatsService._();
static final TransferStatsService _instance = TransferStatsService._();
static TransferStatsService get instance => _instance;
final _db = TransferDatabase.instance;
final _dateFormat = DateFormat('yyyy-MM-dd');
// ============================================================
// 总览统计
// ============================================================
Future<TransferStatsOverview> getOverview() async {
try {
final records = await _db.getAllRecords();
if (records.isEmpty) {
return const TransferStatsOverview();
}
int totalSent = 0;
int totalReceived = 0;
int fileCount = 0;
double totalSpeed = 0.0;
int speedCount = 0;
int successCount = 0;
int failCount = 0;
final transportCounts = <String, int>{};
for (final r in records) {
if (r.direction == 'send') {
totalSent += r.fileSize;
} else {
totalReceived += r.fileSize;
}
fileCount++;
if (r.speed > 0) {
totalSpeed += r.speed;
speedCount++;
}
if (r.status == 'completed') {
successCount++;
} else if (r.status == 'failed') {
failCount++;
}
transportCounts[r.transport] = (transportCounts[r.transport] ?? 0) + 1;
}
final topTransport = transportCounts.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key;
return TransferStatsOverview(
totalSentBytes: totalSent,
totalReceivedBytes: totalReceived,
totalFileCount: fileCount,
avgSpeed: speedCount > 0 ? totalSpeed / speedCount : 0.0,
topTransport: topTransport,
totalSuccessCount: successCount,
totalFailCount: failCount,
);
} catch (e) {
Log.e('TransferStatsService: getOverview failed: $e');
return const TransferStatsOverview();
}
}
// ============================================================
// 每日趋势
// ============================================================
Future<List<DailyStats>> getDailyTrend({int days = 7}) async {
try {
final records = await _db.getAllRecords();
final now = DateTime.now();
final dailyMap = <String, _DailyAccumulator>{};
for (int i = 0; i < days; i++) {
final date = now.subtract(Duration(days: i));
final key = _dateFormat.format(date);
dailyMap[key] = _DailyAccumulator(date: key);
}
for (final r in records) {
final key = _dateFormat.format(r.startTime);
if (!dailyMap.containsKey(key)) continue;
final acc = dailyMap[key]!;
if (r.direction == 'send') {
acc.sentBytes += r.fileSize;
} else {
acc.receivedBytes += r.fileSize;
}
acc.fileCount++;
if (r.speed > 0) {
acc.totalSpeed += r.speed;
acc.speedCount++;
}
if (r.status == 'completed') {
acc.successCount++;
} else if (r.status == 'failed') {
acc.failCount++;
}
}
final result = dailyMap.values
.map(
(acc) => DailyStats(
date: acc.date,
sentBytes: acc.sentBytes,
receivedBytes: acc.receivedBytes,
fileCount: acc.fileCount,
avgSpeed: acc.speedCount > 0
? acc.totalSpeed / acc.speedCount
: 0.0,
successCount: acc.successCount,
failCount: acc.failCount,
),
)
.toList();
result.sort((a, b) => a.date.compareTo(b.date));
return result;
} catch (e) {
Log.e('TransferStatsService: getDailyTrend failed: $e');
return [];
}
}
// ============================================================
// 设备排行
// ============================================================
Future<List<DeviceRanking>> getDeviceRanking({int limit = 10}) async {
try {
final records = await _db.getAllRecords();
final deviceMap = <String, _DeviceAccumulator>{};
for (final r in records) {
if (!deviceMap.containsKey(r.peerId)) {
deviceMap[r.peerId] = _DeviceAccumulator(
peerId: r.peerId,
peerAlias: r.peerAlias,
);
}
final acc = deviceMap[r.peerId]!;
acc.fileCount++;
acc.totalBytes += r.fileSize;
if (r.speed > 0) {
acc.totalSpeed += r.speed;
acc.speedCount++;
}
}
final result = deviceMap.values
.map(
(acc) => DeviceRanking(
peerId: acc.peerId,
peerAlias: acc.peerAlias,
fileCount: acc.fileCount,
totalBytes: acc.totalBytes,
avgSpeed: acc.speedCount > 0
? acc.totalSpeed / acc.speedCount
: 0.0,
),
)
.toList();
result.sort((a, b) => b.fileCount.compareTo(a.fileCount));
return result.take(limit).toList();
} catch (e) {
Log.e('TransferStatsService: getDeviceRanking failed: $e');
return [];
}
}
// ============================================================
// 文件类型分布
// ============================================================
Future<List<FileTypeDistribution>> getFileTypeDistribution() async {
try {
final records = await _db.getAllRecords();
final typeMap = <String, _TypeAccumulator>{};
for (final r in records) {
final category = _categorizeMimeType(r.mimeType);
if (!typeMap.containsKey(category)) {
typeMap[category] = _TypeAccumulator(category: category);
}
final acc = typeMap[category]!;
acc.fileCount++;
acc.totalBytes += r.fileSize;
}
final result = typeMap.values
.map(
(acc) => FileTypeDistribution(
category: acc.category,
fileCount: acc.fileCount,
totalBytes: acc.totalBytes,
),
)
.toList();
result.sort((a, b) => b.fileCount.compareTo(a.fileCount));
return result;
} catch (e) {
Log.e('TransferStatsService: getFileTypeDistribution failed: $e');
return [];
}
}
// ============================================================
// 传输质量
// ============================================================
Future<TransferQuality> getTransferQuality() async {
try {
final records = await _db.getAllRecords();
if (records.isEmpty) {
return const TransferQuality();
}
int successCount = 0;
int failCount = 0;
double totalSpeed = 0.0;
double maxSpeed = 0.0;
int speedCount = 0;
for (final r in records) {
if (r.status == 'completed') {
successCount++;
} else if (r.status == 'failed') {
failCount++;
}
if (r.speed > 0) {
totalSpeed += r.speed;
speedCount++;
if (r.speed > maxSpeed) maxSpeed = r.speed;
}
}
return TransferQuality(
totalTasks: records.length,
successCount: successCount,
failCount: failCount,
avgSpeed: speedCount > 0 ? totalSpeed / speedCount : 0.0,
maxSpeed: maxSpeed,
);
} catch (e) {
Log.e('TransferStatsService: getTransferQuality failed: $e');
return const TransferQuality();
}
}
// ============================================================
// 每日快照写入
// ============================================================
Future<void> writeDailySnapshot() async {
try {
final today = _dateFormat.format(DateTime.now());
final existing = await _db.getStatsRecordByDate(today);
final records = await _db.getAllRecords();
final todayRecords = records.where(
(r) => _dateFormat.format(r.startTime) == today,
);
int sentBytes = 0;
int receivedBytes = 0;
int fileCount = 0;
double totalSpeed = 0.0;
int speedCount = 0;
for (final r in todayRecords) {
if (r.direction == 'send') {
sentBytes += r.fileSize;
} else {
receivedBytes += r.fileSize;
}
fileCount++;
if (r.speed > 0) {
totalSpeed += r.speed;
speedCount++;
}
}
final avgSpeed = speedCount > 0 ? totalSpeed / speedCount : 0.0;
if (existing != null) {
await _db.updateStatsRecord(
existing.copyWith(
totalSentBytes: sentBytes,
totalReceivedBytes: receivedBytes,
fileCount: fileCount,
avgSpeed: avgSpeed,
),
);
} else {
await _db.insertStatsRecord(
TransferStatsRecordsCompanion(
date: Value(today),
totalSentBytes: Value(sentBytes),
totalReceivedBytes: Value(receivedBytes),
fileCount: Value(fileCount),
avgSpeed: Value(avgSpeed),
transportType: const Value('all'),
createdAt: Value(DateTime.now()),
),
);
}
Log.i('TransferStatsService: Daily snapshot written for $today');
} catch (e) {
Log.e('TransferStatsService: writeDailySnapshot failed: $e');
}
}
// ============================================================
// 工具方法
// ============================================================
String _categorizeMimeType(String? mimeType) {
if (mimeType == null) return '其他';
if (mimeType.startsWith('image/')) return '图片';
if (mimeType.startsWith('video/')) return '视频';
if (mimeType.startsWith('audio/')) return '音频';
if (mimeType.contains('pdf') ||
mimeType.contains('document') ||
mimeType.contains('spreadsheet') ||
mimeType.contains('presentation') ||
mimeType.contains('text/'))
return '文档';
if (mimeType.contains('zip') ||
mimeType.contains('rar') ||
mimeType.contains('tar') ||
mimeType.contains('7z'))
return '压缩包';
return '其他';
}
}
class _DailyAccumulator {
_DailyAccumulator({required this.date});
final String date;
int sentBytes = 0;
int receivedBytes = 0;
int fileCount = 0;
double totalSpeed = 0.0;
int speedCount = 0;
int successCount = 0;
int failCount = 0;
}
class _DeviceAccumulator {
_DeviceAccumulator({required this.peerId, required this.peerAlias});
final String peerId;
final String peerAlias;
int fileCount = 0;
int totalBytes = 0;
double totalSpeed = 0.0;
int speedCount = 0;
}
class _TypeAccumulator {
_TypeAccumulator({required this.category});
final String category;
int fileCount = 0;
int totalBytes = 0;
}