From d0c445ca3b629c972cdba8e1b322070bf31f4b2f Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 28 Nov 2025 11:47:10 +0800 Subject: [PATCH] =?UTF-8?q?refactor(usage=5Fstats):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E4=BC=9A=E8=AE=AE=E7=94=A8=E9=87=8F=E7=BB=9F=E8=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E4=B8=BA=E7=8B=AC=E7=AB=8B=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的混合统计模型拆分为独立的ASR统计和对象存储统计类 新增组合类MeetingUsageDailySnapshot用于同时展示两种统计 --- lib/data/models/meeting_usage_stats.dart | 148 ++++++++++++++++----- lib/data/services/usage_stats_service.dart | 51 +++++++ 2 files changed, 163 insertions(+), 36 deletions(-) diff --git a/lib/data/models/meeting_usage_stats.dart b/lib/data/models/meeting_usage_stats.dart index 97b65439a..4568b488a 100644 --- a/lib/data/models/meeting_usage_stats.dart +++ b/lib/data/models/meeting_usage_stats.dart @@ -106,80 +106,95 @@ class MeetingObjectStorageRecord { } } -// ============== 单日统计 ============== -// 会议模块用量单日统计 -class MeetingUsageDailyStats { +// 会议音频文件转文字单日统计 +class MeetingAsrDailyStats { String date; // 日期字符串: YYYY-MM-DD String userId; // 用户ID List items; // 当天所有单次ASR - List storageItems; // 当天所有单次对象存储上传记录 - MeetingUsageDailyStats({ + MeetingAsrDailyStats({ required this.date, required this.userId, required this.items, - required this.storageItems, }); - // asr聚合计算 + // 计算型聚合,不冗余存储 int get asrCalls => items.length; int get audioSecondsTotal => items.fold(0, (s, r) => s + r.audioSeconds); int get audioBytesTotal => items.fold(0, (s, r) => s + r.fileSizeBytes); int get transcriptBytesTotal => items.fold(0, (s, r) => s + r.transcriptBytes); - // 对象存储聚合计算 - int get storageUploads => storageItems.length; - int get storageBytesTotal => - storageItems.fold(0, (s, r) => s + r.storageBytes); - int get transferBytesTotal => - storageItems.fold(0, (s, r) => s + r.transferBytes); - Map toJson() => { 'date': date, 'userId': userId, 'items': items.map((e) => e.toJson()).toList(), - 'storageItems': storageItems.map((e) => e.toJson()).toList(), }; - factory MeetingUsageDailyStats.fromJson(Map json) { - final asrList = (json['items'] as List? ?? []) + factory MeetingAsrDailyStats.fromJson(Map json) { + final list = (json['items'] as List? ?? []) .map((e) => MeetingAsrRecord.fromJson(Map.from(e))) .toList(); - final storageList = (json['storageItems'] as List? ?? []) + return MeetingAsrDailyStats( + date: json['date'] ?? '', + userId: json['userId'] ?? 'anonymous', + items: list, + ); + } +} + +// ============== 对象存储单日统计 ============== +// 👇 职责单一:仅统计对象存储相关数据 +class MeetingStorageDailyStats { + String date; // 日期字符串: YYYY-MM-DD + String userId; // 用户ID + List items; // 当天所有单次对象存储记录 + + MeetingStorageDailyStats({ + required this.date, + required this.userId, + required this.items, + }); + + // 对象存储 聚合计算 + int get storageUploads => items.length; + int get storageBytesTotal => items.fold(0, (s, r) => s + r.storageBytes); + int get transferBytesTotal => items.fold(0, (s, r) => s + r.transferBytes); + + Map toJson() => { + 'date': date, + 'userId': userId, + 'items': items.map((e) => e.toJson()).toList(), + }; + + factory MeetingStorageDailyStats.fromJson(Map json) { + final storageList = (json['items'] as List? ?? []) .map((e) => MeetingObjectStorageRecord.fromJson(Map.from(e))) .toList(); - return MeetingUsageDailyStats( + + return MeetingStorageDailyStats( date: json['date'] ?? '', userId: json['userId'] ?? 'anonymous', - items: asrList, - storageItems: storageList, + items: storageList, ); } } -// ============== 历史统计 ============== -// 会议模块用量历史统计(所有的用量) -class MeetingUsageHistoryStats { +// 会议音频文件转文字历史统计(所有的用量) +class MeetingAsrHistoryStats { String userId; // 用户ID int allAsrCalls; // 所有ASR调用次数 int allAudioSeconds; // 所有音频总时长(秒) int allAudioFileBytes; // 所有音频文件大小(字节) int allTranscriptBytes; // 所有转写后的字节数(utf8) - int allStorageUploads; // 所有对象存储上传次数 - int allStorageBytes; // 所有存储文件总大小(字节) - int allTransferBytes; // 所有传输总字节数 - MeetingUsageHistoryStats({ + MeetingAsrHistoryStats({ required this.userId, required this.allAsrCalls, required this.allAudioSeconds, required this.allAudioFileBytes, required this.allTranscriptBytes, - required this.allStorageUploads, - required this.allStorageBytes, - required this.allTransferBytes, }); Map toJson() => { @@ -188,12 +203,9 @@ class MeetingUsageHistoryStats { 'allAudioSeconds': allAudioSeconds, 'allAudioFileBytes': allAudioFileBytes, 'allTranscriptBytes': allTranscriptBytes, - 'allStorageUploads': allStorageUploads, - 'allStorageBytes': allStorageBytes, - 'allTransferBytes': allTransferBytes, }; - factory MeetingUsageHistoryStats.fromJson(Map json) { + factory MeetingAsrHistoryStats.fromJson(Map json) { int _asInt(dynamic v) { if (v == null) return 0; if (v is int) return v; @@ -202,15 +214,79 @@ class MeetingUsageHistoryStats { return 0; } - return MeetingUsageHistoryStats( + return MeetingAsrHistoryStats( userId: json['userId'] ?? 'anonymous', allAsrCalls: _asInt(json['allAsrCalls']), allAudioSeconds: _asInt(json['allAudioSeconds']), allAudioFileBytes: _asInt(json['allAudioFileBytes']), allTranscriptBytes: _asInt(json['allTranscriptBytes']), + ); + } +} + +// ============== 对象存储历史统计 ============== +// 👇 职责单一:仅统计对象存储累计数据 +class MeetingStorageHistoryStats { + String userId; // 用户ID + int allStorageUploads; // 所有对象存储上传次数 + int allStorageBytes; // 所有存储文件总大小(字节) + int allTransferBytes; // 所有传输总字节数 + + MeetingStorageHistoryStats({ + required this.userId, + required this.allStorageUploads, + required this.allStorageBytes, + required this.allTransferBytes, + }); + + Map toJson() => { + 'userId': userId, + 'allStorageUploads': allStorageUploads, + 'allStorageBytes': allStorageBytes, + 'allTransferBytes': allTransferBytes, + }; + + factory MeetingStorageHistoryStats.fromJson(Map json) { + int _asInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is num) return v.toInt(); + if (v is String) return int.tryParse(v) ?? 0; + return 0; + } + + return MeetingStorageHistoryStats( + userId: json['userId'] ?? 'anonymous', allStorageUploads: _asInt(json['allStorageUploads']), allStorageBytes: _asInt(json['allStorageBytes']), allTransferBytes: _asInt(json['allTransferBytes']), ); } } + +// 👇 当需要同时展示两种统计时使用 +class MeetingUsageDailySnapshot { + String date; + String userId; + MeetingAsrDailyStats? asrStats; + MeetingStorageDailyStats? storageStats; + + MeetingUsageDailySnapshot({ + required this.date, + required this.userId, + this.asrStats, + this.storageStats, + }); +} + +class MeetingUsageHistorySnapshot { + String userId; + MeetingAsrHistoryStats? asrStats; + MeetingStorageHistoryStats? storageStats; + + MeetingUsageHistorySnapshot({ + required this.userId, + this.asrStats, + this.storageStats, + }); +} diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index a4d573ab2..e0f76a799 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -12,6 +12,7 @@ import '../models/user_Info.dart'; class UsageStatsService extends GetxService { static const String _dailyStatsKey = 'daily_translation_stats'; static const String _meetingDailyKey = 'daily_meeting_asr_stats'; + static const String _meetingOsDailykey = 'daily_meeting_os_stats'; final GetStorage _storage = GetStorage(); @@ -92,6 +93,56 @@ class UsageStatsService extends GetxService { return '$y-$m-$day'; } + /// 记录单次对象存储上传 + /// [meetingId] 会议ID + /// [storageUrl] 云端存储URL + /// [storageBytes] 文件大小(字节) + /// [transferBytes] 数据传输字节数 + /// [uploadTimestamp] 上传完成时间戳(毫秒) + void recordMeetingObjectStorage({ + required int meetingId, + required String storageUrl, + required int storageBytes, + required int transferBytes, + required int uploadTimestamp, + }) { + final dateKey = + _formatDate(DateTime.fromMillisecondsSinceEpoch(uploadTimestamp)); + final storageKey = '${_meetingOsDailykey}_$dateKey'; + + final raw = _storage.read(storageKey); + List uploads = []; + if (raw is List) { + uploads = raw + .map((e) => + MeetingObjectStorageRecord.fromJson(Map.from(e))) + .toList(); + } + + // final id = 'os_${uploadTimestamp}_${meetingId}'; + final record = MeetingObjectStorageRecord( + meetingId: meetingId, + storageUrl: storageUrl, + storageBytes: storageBytes, + transferBytes: transferBytes, + uploadTimestamp: uploadTimestamp, + ); + + // 使用 meetingId + storageUrl 作为唯一标识来判断重复 + final exists = uploads.indexWhere( + (e) => e.meetingId == meetingId && e.storageUrl == storageUrl) >= + 0; + if (!exists) { + uploads.add(record); + _storage.write(storageKey, uploads.map((e) => e.toJson()).toList()); + Logger.d('MeetingOSrecord', + '新增对象存储上传: meetingId=$meetingId, size=$storageBytes, transfer=$transferBytes'); + } else { + Logger.d('MeetingOSrecord', + '重复对象存储上传忽略: meetingId=$meetingId, url=$storageUrl'); + } + } + /// 添加或更新会议ASR记录 /// [record] 会议ASR记录 /// 该方法用于记录一次会议语音识别(ASR)的提交记录。它会根据记录的时间戳确定日期,