From 8305c89fc43390bde90e7c01d76175b5defb7889 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 27 Nov 2025 21:07:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=94=A8=E9=87=8F=E7=BB=9F=E8=AE=A1):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E8=B1=A1=E5=AD=98=E5=82=A8=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E7=94=A8=E9=87=8F=E7=BB=9F=E8=AE=A1=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在会议上传服务中添加对象存储用量记录功能 - 重构原有ASR统计模型为综合用量统计模型 - 新增对象存储记录相关数据结构 - 完善统计数据的聚合计算和持久化 --- lib/data/models/meeting_usage_stats.dart | 123 ++++++++++++++++-- .../meeting/meeting_upload_service.dart | 35 ++++- 2 files changed, 145 insertions(+), 13 deletions(-) diff --git a/lib/data/models/meeting_usage_stats.dart b/lib/data/models/meeting_usage_stats.dart index 88ef7e69c..97b65439a 100644 --- a/lib/data/models/meeting_usage_stats.dart +++ b/lib/data/models/meeting_usage_stats.dart @@ -1,9 +1,11 @@ ///会议模块用量统计的模型类 +// ============== ASR 状态常量 ============== const int asrStatusProcessing = 0; // 处理中 const int asrStatusFailed = 1; // 处理失败 const int asrStatusCompleted = 2; // 已完成 +// ============== ASR 单次记录 ============== // 会议音频文件转文字单次数据 class MeetingAsrRecord { String id; // 本次ASR唯一标识 @@ -60,56 +62,155 @@ class MeetingAsrRecord { } } -// 会议音频文件转文字单日统计 -class MeetingAsrDailyStats { +// ============== 对象存储单次记录 ============== +// 对象存储单次上传记录 +class MeetingObjectStorageRecord { + int meetingId; // 会议记录ID + String storageUrl; // 云端存储URL + int storageBytes; // 文件大小(字节) + int transferBytes; // 实际传输字节数 + int uploadTimestamp; // 上传完成时间戳(毫秒) + + MeetingObjectStorageRecord({ + required this.meetingId, + required this.storageUrl, + required this.storageBytes, + required this.transferBytes, + required this.uploadTimestamp, + }); + + Map toJson() => { + 'meetingId': meetingId, + 'storageUrl': storageUrl, + 'storageBytes': storageBytes, + 'transferBytes': transferBytes, + 'uploadTimestamp': uploadTimestamp, + }; + + factory MeetingObjectStorageRecord.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 MeetingObjectStorageRecord( + meetingId: _asInt(json['meetingId']), + storageUrl: (json['storageUrl'] ?? '').toString(), + storageBytes: _asInt(json['storageBytes']), + transferBytes: _asInt(json['transferBytes']), + uploadTimestamp: _asInt(json['uploadTimestamp']), + ); + } +} + +// ============== 单日统计 ============== +// 会议模块用量单日统计 +class MeetingUsageDailyStats { String date; // 日期字符串: YYYY-MM-DD String userId; // 用户ID List items; // 当天所有单次ASR + List storageItems; // 当天所有单次对象存储上传记录 - MeetingAsrDailyStats({ + MeetingUsageDailyStats({ 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 MeetingAsrDailyStats.fromJson(Map json) { - final list = (json['items'] as List? ?? []) + factory MeetingUsageDailyStats.fromJson(Map json) { + final asrList = (json['items'] as List? ?? []) .map((e) => MeetingAsrRecord.fromJson(Map.from(e))) .toList(); - return MeetingAsrDailyStats( + final storageList = (json['storageItems'] as List? ?? []) + .map((e) => + MeetingObjectStorageRecord.fromJson(Map.from(e))) + .toList(); + return MeetingUsageDailyStats( date: json['date'] ?? '', userId: json['userId'] ?? 'anonymous', - items: list, + items: asrList, + storageItems: storageList, ); } } -// 会议音频文件转文字历史统计(所有的用量) -class MeetingAsrHistoryStats { +// ============== 历史统计 ============== +// 会议模块用量历史统计(所有的用量) +class MeetingUsageHistoryStats { String userId; // 用户ID int allAsrCalls; // 所有ASR调用次数 int allAudioSeconds; // 所有音频总时长(秒) int allAudioFileBytes; // 所有音频文件大小(字节) int allTranscriptBytes; // 所有转写后的字节数(utf8) + int allStorageUploads; // 所有对象存储上传次数 + int allStorageBytes; // 所有存储文件总大小(字节) + int allTransferBytes; // 所有传输总字节数 - MeetingAsrHistoryStats({ + MeetingUsageHistoryStats({ 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() => { + 'userId': userId, + 'allAsrCalls': allAsrCalls, + 'allAudioSeconds': allAudioSeconds, + 'allAudioFileBytes': allAudioFileBytes, + 'allTranscriptBytes': allTranscriptBytes, + 'allStorageUploads': allStorageUploads, + 'allStorageBytes': allStorageBytes, + 'allTransferBytes': allTransferBytes, + }; + + factory MeetingUsageHistoryStats.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 MeetingUsageHistoryStats( + userId: json['userId'] ?? 'anonymous', + allAsrCalls: _asInt(json['allAsrCalls']), + allAudioSeconds: _asInt(json['allAudioSeconds']), + allAudioFileBytes: _asInt(json['allAudioFileBytes']), + allTranscriptBytes: _asInt(json['allTranscriptBytes']), + allStorageUploads: _asInt(json['allStorageUploads']), + allStorageBytes: _asInt(json['allStorageBytes']), + allTransferBytes: _asInt(json['allTransferBytes']), + ); + } } diff --git a/lib/data/services/meeting/meeting_upload_service.dart b/lib/data/services/meeting/meeting_upload_service.dart index 53a5f57d3..4472cc409 100644 --- a/lib/data/services/meeting/meeting_upload_service.dart +++ b/lib/data/services/meeting/meeting_upload_service.dart @@ -1,12 +1,15 @@ +import 'dart:io'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/upload_oss.dart'; +import '../usage_stats_service.dart'; import '../db/sqflite_api.dart'; import 'meeting_task_service.dart'; import 'meeting_task_service2.dart'; +/// 管理会议音频文件上传服务 class MeetingUploadService extends GetxService { final _taskService = Get.find(); @@ -17,7 +20,7 @@ class MeetingUploadService extends GetxService { RxList uploadList = [].obs; bool _isUpload = false; - int _handled = 0; + int _handled = 0; //队列处理指针,表示当前循环语句处理的任务数量 @override void onInit() { @@ -45,7 +48,9 @@ class MeetingUploadService extends GetxService { bool isUpload = true, }) async { bool flash = false; + int fileSize = 0; try { + fileSize = await File(filePath).length(); //获取文件大小 final details = await SqfliteApi.getMeetingDetails(id); final int seconds = (details['seconds'] ?? 0) as int; flash = seconds > 0 && seconds <= 7200; @@ -62,6 +67,7 @@ class MeetingUploadService extends GetxService { 'prompt': '', 'outline': '', 'useFlashASR': flash, + 'fileSizeBytes': fileSize, }); _storage.write("meeting_upload_list", uploadList); if (isUpload) { @@ -79,17 +85,42 @@ class MeetingUploadService extends GetxService { if (element['audiourl'].isEmpty) { meetingId.value = element['id']; try { + int _sent = 0; + int _total = 0; final audiourl = await UploadOss.upload( filepath: element['filepath'], rootDir: element['rootdir'], onSendProgress: (int count, int total) { - uploadProgress.value = count / total; + // uploadProgress.value = count / total; + _sent = count; + _total = total; + uploadProgress.value = total > 0 ? count / total : 0.0; }, ); await SqfliteApi.editMeetingTitle( element['id'], {'audiourl': audiourl}, ); + try { + // 记录对象存储用量 + final svc = Get.isRegistered() + ? Get.find() + : Get.put(UsageStatsService(), permanent: true); + int fileSizeBytes = 0; + try { + fileSizeBytes = element['fileSizeBytes'] ?? 0; + } catch (_) {} + final transferred = _total > 0 ? _total : fileSizeBytes; + svc.recordMeetingObjectStorage( + meetingId: element['id'], + storageUrl: audiourl, + storageBytes: fileSizeBytes, + transferBytes: transferred, + uploadTimestamp: DateTime.now().millisecondsSinceEpoch, + ); + } catch (e) { + Logger.w('MeetingOSrecord', '对象存储用量记录失败: ' + e.toString()); + } if (element['isTask']) { if (element['useFlashASR'] == true) { final service2 = Get.find();