From 9ea0fbea5ff0bce00625d8f8acde9f7b349655a1 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 17 Nov 2025 16:13:22 +0800 Subject: [PATCH 1/6] =?UTF-8?q?refactor(goods):=20=E5=B0=86Trade=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=B1=BB=E5=9E=8B=E9=87=8D=E5=91=BD=E5=90=8D=E4=B8=BA?= =?UTF-8?q?Translation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新商品相关枚举和控制器,将Trade使用类型统一更名为Translation以更准确描述其用途 --- lib/data/models/goods.dart | 14 ++++---- lib/data/services/network/api.dart | 32 ++++++++++++++++++- .../controllers/goodstrade_controller.dart | 4 +-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/data/models/goods.dart b/lib/data/models/goods.dart index 99485eafe..bda3a6c37 100644 --- a/lib/data/models/goods.dart +++ b/lib/data/models/goods.dart @@ -6,7 +6,7 @@ enum UsageType { /// 未知使用量类型 UsageNull, // 0 /// 翻译 - Trade, // 1 + Translation, // 1 /// 会议 Meet, // 2 /// 智能体 @@ -19,7 +19,7 @@ enum UsageType { case 0: return UsageType.UsageNull; case 1: - return UsageType.Trade; + return UsageType.Translation; case 2: return UsageType.Meet; case 3: @@ -36,8 +36,8 @@ enum UsageType { case 'null': case 'unknown': return UsageType.UsageNull; - case 'trade': - return UsageType.Trade; + case 'translation': + return UsageType.Translation; case 'meet': case 'meeting': return UsageType.Meet; @@ -54,7 +54,7 @@ enum UsageType { switch (this) { case UsageType.UsageNull: return 0; - case UsageType.Trade: + case UsageType.Translation: return 1; case UsageType.Meet: return 2; @@ -67,8 +67,8 @@ enum UsageType { switch (this) { case UsageType.UsageNull: return 'UsageNull'; - case UsageType.Trade: - return 'Trade'; + case UsageType.Translation: + return 'Translation'; case UsageType.Meet: return 'Meet'; case UsageType.AI: diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index e405bd87f..ca56fc8a5 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -244,7 +244,37 @@ class Api { ); } - //资源使用请求 + /// 资源使用上报(`/api/home/user_usages`) + /// + /// 用途:在 AI 聊天等场景中,上报本次会话的消耗与功能调用次数,服务端据此统计并更新积分。 + /// + /// 请求方式:`POST /api/home/user_usages` + /// + /// 认证:已通过全局拦截器自动携带 `Authorization`(见 `AuthInterceptor`)。 + /// + /// 参数(`params`)结构: + /// - `usagetype`:`int` 类型,使用场景类型;Android 端 Agent 使用 `3` 表示 AICHAT。 + /// - `usages`:`Map`,各项指标的键值对,例如: + /// - `AICHAT_PROMPT_TOKENS`:本次对话的提示词 Token 数 + /// - `AICHAT_COMPLETION_TOKENS`:本次对话的生成 Token 数 + /// - `AICHAT_FUNCTION_{函数名}`:被调用的函数次数(按函数名累加) + /// + /// 返回:标准封装后 `code == 0` 时返回 `data`。其中 `data.usages` 可能包含键 `"3"`,表示最新的 AI 积分值。 + /// + /// 使用示例: + /// ```dart + /// final params = { + /// 'usagetype': 3, // AICHAT + /// 'usages': { + /// 'AICHAT_PROMPT_TOKENS': 120, + /// 'AICHAT_COMPLETION_TOKENS': 256, + /// 'AICHAT_FUNCTION_toolX': 1, + /// } + /// }; + /// final data = await Api.usagesResPoints(params); + /// final usages = (data['usages'] as Map?)?.cast(); + /// final aiIntegral = usages?['3']; // 服务端返回的最新 AI 积分(如有) + /// ``` static usagesResPoints([params]) { return DioManager().request( NWMethod.post, diff --git a/lib/modules/goods/controllers/goodstrade_controller.dart b/lib/modules/goods/controllers/goodstrade_controller.dart index 871241f9c..d0a218c5b 100644 --- a/lib/modules/goods/controllers/goodstrade_controller.dart +++ b/lib/modules/goods/controllers/goodstrade_controller.dart @@ -7,7 +7,7 @@ import 'goods_base_controller.dart'; class GoodstradeController extends GoodsBaseController { @override Future> fetchProducts() async { - final resp = await Api.getGoods({'usagetype': UsageType.Trade.toInt}); + final resp = await Api.getGoods({'usagetype': UsageType.Translation.toInt}); if (resp == null || resp is! Map) return []; if (resp.containsKey('error') || resp.containsKey('msg')) { final msg = resp['error'] ?? resp['msg'] ?? '服务器错误'; @@ -21,7 +21,7 @@ class GoodstradeController extends GoodsBaseController { if (data is List) { final list = data.whereType>().map(DBGoods.fromJson).toList(); - return list.where((g) => g.usagetype == UsageType.Trade).toList(); + return list.where((g) => g.usagetype == UsageType.Translation).toList(); } return []; } From f57258069f2b95ff7fb1a9607663cb3b69f4d4a1 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 19 Nov 2025 15:18:46 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(ASR):=20=E6=B7=BB=E5=8A=A0ASR=E7=94=A8?= =?UTF-8?q?=E9=87=8F=E4=B8=8A=E6=8A=A5=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现ASR使用情况上报到服务器的功能,包括会话统计和用量参数处理。同时在翻译控制器中添加相关调用逻辑,确保在会话结束时上报数据。 --- lib/data/services/usage_stats_service.dart | 41 +++++++++++++++++++ .../controllers/translation_controller.dart | 16 +++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index 5a361e341..ddd3677dd 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -3,6 +3,8 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../models/translation_usage_stats.dart'; import '../../core/utils/logger.dart'; +import 'network/api.dart'; +import '../models/goods.dart'; /// 使用量统计服务 class UsageStatsService extends GetxService { @@ -186,6 +188,45 @@ class UsageStatsService extends GetxService { } catch (e) { Logger.error('记录ASR API调用失败: $e'); } + + _reportAsrUsageToServer( + effectiveDurationSeconds: effectiveDurationSeconds, + //totalDurationSeconds: totalDurationSeconds, + language: language, + mode: mode, + ); + } + + /// 上报ASR用量到服务器 + Future _reportAsrUsageToServer({ + required int effectiveDurationSeconds, + //required int totalDurationSeconds, + required String language, + required String mode, + }) async { + try { + final params = { + 'usagetype': UsageType.Translation.toInt, + 'usages': { + // 'TRANSLATION_ASR_EFFECTIVE_SECONDS': effectiveDurationSeconds, + //'TRANSLATION_ASR_TOTAL_SECONDS': totalDurationSeconds, + 'TRANSLATION_MODE_$mode': effectiveDurationSeconds, + 'TRANSLATION_ASR_LANGUAGE_$language': 1, + 'TRANSLATION_SOURCE_CHARACTERS': + realtimeStats.value.sourceTextCharacters, + 'TRANSLATION_TARGET_CHARACTERS': + realtimeStats.value.ttsTextCharacters, + }, + }; + + Logger.info('lxm---ASR用量上报参数: $params'); + + final response = await Api.usagesResPoints(params); + + Logger.info('lxm---ASR用量上报响应: $response'); + } catch (e) { + Logger.error('记录ASR API调用失败: $e'); + } } /// 记录TTS API调用 diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index b89d5a6e7..bf53d8f36 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -318,6 +318,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// [speakerId] 说话者ID Future _startFaceToFaceRecognition(int speakerId) async { try { + if (_asrSessionStartTime == null) { + await _initializeSession(); + } await _asrService.startContinuousRecognition(_audioSourceType); //开启识别 _startAsrActiveTracking(); //开启识别活动跟踪 if (isRecording.value) { @@ -347,6 +350,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } await _asrService.stopContinuousRecognition(); _stopAsrActiveTracking(); //停止识别活动跟踪 + _recordAsrUsageStats(sourceLanguage.value); isRecognizing.value = false; currentSessionId = null; @@ -902,6 +906,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver { await _astService.stopContinuousTranslation(); } _stopAsrActiveTracking(); + final langName = _languageManager + .getChineseNameByAsrCode( + (detectedLanguageCode.isNotEmpty + ? detectedLanguageCode + : sourceLanguageCode.value)) ?? + '未知'; + _recordAsrUsageStats(langName); isRecognizing.value = false; currentSessionId = null; } catch (e) { @@ -1197,7 +1208,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.info( '翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName'); - _recordAsrUsageStats(sourceLanguageName); } catch (e) { Logger.error('记录翻译统计失败: $e'); } @@ -1579,6 +1589,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.info( 'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language'); + _asrSessionStartTime = null; + _asrActiveDuration = Duration.zero; + _isAsrActive = false; + _asrActiveStartTime = null; } catch (e) { Logger.error('记录ASR统计失败: $e'); } From 522937c6005e20788e6c645167c12bc6f772ceea Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 20 Nov 2025 12:24:08 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(usage=5Fstats):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=A7=AF=E5=88=86=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BC=9A=E8=AE=AEASR=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加用户积分统计显示界面,实现根据服务器响应更新积分逻辑 新增会议音频转文字相关统计模型类 移除视图中的导出数据功能 --- lib/data/models/meeting_usage_stats.dart | 113 +++++++++++ lib/data/services/usage_stats_service.dart | 46 +++++ .../usage/views/translation_stats_view.dart | 179 +++++++++++++++--- 3 files changed, 310 insertions(+), 28 deletions(-) create mode 100644 lib/data/models/meeting_usage_stats.dart diff --git a/lib/data/models/meeting_usage_stats.dart b/lib/data/models/meeting_usage_stats.dart new file mode 100644 index 000000000..2b486fe1f --- /dev/null +++ b/lib/data/models/meeting_usage_stats.dart @@ -0,0 +1,113 @@ +const int asrStatusProcessing = 0; // 处理中 +const int asrStatusFailed = 1; // 处理失败 +const int asrStatusCompleted = 2; // 已完成 + +// 会议音频文件转文字单次数据 +class MeetingAsrRecord { + String id; // 本次ASR唯一标识 + int meetingId; // 会议记录ID + String taskId; // 云端任务ID + int audioSeconds; // 音频总时长(秒) + int fileSizeBytes; // 音频文件大小(字节) + String language; // ASR语言 + int submitTimestamp; // 提交时间戳(毫秒) + + int status; // 状态: 0-处理中, 1-处理失败, 2-已完成 + int transcriptBytes; // 转写后的字节数(utf8) + int completeTimestamp; // 完成时间戳(毫秒) + + MeetingAsrRecord({ + required this.id, + required this.meetingId, + required this.taskId, + required this.audioSeconds, + required this.fileSizeBytes, + required this.language, + required this.submitTimestamp, + required this.status, + required this.transcriptBytes, + required this.completeTimestamp, + }); + + Map toJson() => { + 'id': id, + 'meetingId': meetingId, + 'taskId': taskId, + 'audioSeconds': audioSeconds, + 'fileSizeBytes': fileSizeBytes, + 'language': language, + 'submitTimestamp': submitTimestamp, + 'status': status, + 'transcriptBytes': transcriptBytes, + 'completeTimestamp': completeTimestamp, + }; + + factory MeetingAsrRecord.fromJson(Map json) { + return MeetingAsrRecord( + id: json['id'] ?? '', + meetingId: json['meetingId'] ?? 0, + taskId: json['taskId'] ?? '', + audioSeconds: json['audioSeconds'] ?? 0, + fileSizeBytes: json['fileSizeBytes'] ?? 0, + language: json['language'] ?? '', + submitTimestamp: json['submitTimestamp'] ?? 0, + status: json['status'] ?? 0, + transcriptBytes: json['transcriptBytes'] ?? 0, + completeTimestamp: json['completeTimestamp'] ?? 0, + ); + } +} + +// 会议音频文件转文字单日统计 +class MeetingAsrDailyStats { + String date; // 日期字符串: YYYY-MM-DD + String userId; // 用户ID + List items; // 当天所有单次ASR + + MeetingAsrDailyStats({ + required this.date, + required this.userId, + required this.items, + }); + + // 计算型聚合,不冗余存储 + 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); + + Map toJson() => { + 'date': date, + 'userId': userId, + 'items': items.map((e) => e.toJson()).toList(), + }; + + factory MeetingAsrDailyStats.fromJson(Map json) { + final list = (json['items'] as List? ?? []) + .map((e) => MeetingAsrRecord.fromJson(Map.from(e))) + .toList(); + return MeetingAsrDailyStats( + date: json['date'] ?? '', + userId: json['userId'] ?? 'anonymous', + items: list, + ); + } +} + +// 会议音频文件转文字历史统计 +class MeetingAsrHistoryStats { + String userId; // 用户ID + int allAsrCalls; // 所有ASR调用次数 + int allAudioSeconds; // 所有音频总时长(秒) + int allAudioFileBytes; // 所有音频文件大小(字节) + int allTranscriptBytes; // 所有转写后的字节数(utf8) + + MeetingAsrHistoryStats({ + required this.userId, + required this.allAsrCalls, + required this.allAudioSeconds, + required this.allAudioFileBytes, + required this.allTranscriptBytes, + }); +} diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index ddd3677dd..0f930fab5 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -5,8 +5,10 @@ import '../models/translation_usage_stats.dart'; import '../../core/utils/logger.dart'; import 'network/api.dart'; import '../models/goods.dart'; +import '../models/user_Info.dart'; /// 使用量统计服务 +/// class UsageStatsService extends GetxService { static const String _storageKey = 'usage_stats'; static const String _dailyStatsKey = 'daily_translation_stats'; @@ -224,11 +226,55 @@ class UsageStatsService extends GetxService { final response = await Api.usagesResPoints(params); Logger.info('lxm---ASR用量上报响应: $response'); + + _applyUsageResponse(response); } catch (e) { Logger.error('记录ASR API调用失败: $e'); } } + /// 根据服务器用量上报回应,更新用户积分(非总积分) + /// 用量类型:0 AI聊天、1 翻译、2 会议 + void _applyUsageResponse(dynamic response) { + try { + if (response is! Map) return; + final usagesObj = response['usages']; + if (usagesObj is! Map) return; + final usages = Map.from(usagesObj as Map); + 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; + } + + final Map update = {}; + usages.forEach((k, v) { + final key = int.tryParse(k.toString()); + final val = _asInt(v); + if (key == null) return; + switch (key) { + case 0: + update['aichatintegral'] = val; + break; + case 1: + update['tradeintegral'] = val; + break; + case 2: + update['meetintegral'] = val; + break; + } + }); + if (update.isNotEmpty) { + User.instance.updateUserInfo(update); + Logger.info('积分更新: $update'); + } + } catch (e) { + Logger.error('解析用量回应并更新积分失败: $e'); + } + } + /// 记录TTS API调用 void recordTtsApiCall({ required String text, diff --git a/lib/modules/usage/views/translation_stats_view.dart b/lib/modules/usage/views/translation_stats_view.dart index ee1751234..19ceef070 100644 --- a/lib/modules/usage/views/translation_stats_view.dart +++ b/lib/modules/usage/views/translation_stats_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/translation_stats_controller.dart'; import '../../../core/theme/app_colors.dart'; +import '../../../data/models/user_Info.dart'; class TranslationStatsView extends GetView { const TranslationStatsView({Key? key}) : super(key: key); @@ -49,32 +50,32 @@ class TranslationStatsView extends GetView { ), onPressed: controller.refreshData, ), - PopupMenuButton( - icon: Icon( - Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black, - ), - onSelected: (value) { - switch (value) { - case 'cleanup': - controller.cleanupOldData(); - break; - case 'export': - controller.exportStats(); - break; - } - }, - itemBuilder: (context) => const [ - PopupMenuItem( - value: 'cleanup', - child: Text('清理过期数据'), - ), - PopupMenuItem( - value: 'export', - child: Text('导出数据'), - ), - ], - ), + // PopupMenuButton( + // icon: Icon( + // Icons.more_vert, + // color: isDarkMode ? Colors.white : Colors.black, + // ), + // onSelected: (value) { + // switch (value) { + // case 'cleanup': + // controller.cleanupOldData(); + // break; + // // case 'export': + // // controller.exportStats(); + // // break; + // } + // }, + // itemBuilder: (context) => const [ + // PopupMenuItem( + // value: 'cleanup', + // child: Text('清理过期数据'), + // ), + // // PopupMenuItem( + // // value: 'export', + // // child: Text('导出数据'), + // // ), + // ], + // ), ], ); } @@ -99,6 +100,8 @@ class TranslationStatsView extends GetView { SizedBox(height: 16.h), _buildOverviewCard(isDarkMode), SizedBox(height: 16.h), + _buildIntegralCard(isDarkMode), + SizedBox(height: 16.h), _buildApiCallsCard(isDarkMode), SizedBox(height: 16.h), _buildDataVolumeCard(isDarkMode), @@ -300,7 +303,6 @@ class TranslationStatsView extends GetView { isDarkMode, ), ), - ], )), SizedBox(height: 12.h), @@ -422,6 +424,127 @@ class TranslationStatsView extends GetView { ); } + Widget _buildIntegralCard(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.stacked_line_chart, + color: AppColors.primary, + size: 20.sp, + ), + SizedBox(width: 8.w), + Text( + '积分统计', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + SizedBox(height: 16.h), + Obx(() { + final _ = User.refreshTick.value; + final u = User.instance; + return Column( + children: [ + _buildIntegralRow( + 'AI聊天积分', + controller.formatNumber(u.aichatintegral), + Icons.chat_bubble_outline, + isDarkMode, + ), + SizedBox(height: 12.h), + _buildIntegralRow( + 'AI聊天总积分', + controller.formatNumber(u.aichattotalintegral), + Icons.chat_outlined, + isDarkMode, + ), + SizedBox(height: 12.h), + _buildIntegralRow( + '翻译积分', + controller.formatNumber(u.tradeintegral), + Icons.translate, + isDarkMode, + ), + SizedBox(height: 12.h), + _buildIntegralRow( + '翻译总积分', + controller.formatNumber(u.tradetotalintegral), + Icons.g_translate, + isDarkMode, + ), + SizedBox(height: 12.h), + _buildIntegralRow( + '会议积分', + controller.formatNumber(u.meetintegral), + Icons.groups, + isDarkMode, + ), + SizedBox(height: 12.h), + _buildIntegralRow( + '会议总积分', + controller.formatNumber(u.meettotalintegral), + Icons.meeting_room, + isDarkMode, + ), + ], + ); + }), + ], + ), + ); + } + + Widget _buildIntegralRow( + String label, String value, IconData icon, bool isDarkMode) { + return Row( + children: [ + Icon( + icon, + color: AppColors.primary, + size: 18.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ), + Text( + value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ); + } + Widget _buildApiCallRow( String label, int count, IconData icon, bool isDarkMode) { return Row( @@ -747,4 +870,4 @@ class TranslationStatsView extends GetView { ], ); } -} \ No newline at end of file +} From 134537fba4e05d15536381698009dadfcf0590aa Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 20 Nov 2025 12:24:52 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(meeting):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BC=9A=E8=AE=AEASR=E4=BB=BB=E5=8A=A1=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在MeetingTaskService中添加会议ASR任务的使用统计记录功能,包括任务提交和完成时的数据收集。同时在UsageStatsService中实现相关统计数据的存储和更新逻辑。 --- .../meeting/meeting_task_service.dart | 37 +++++++++++ lib/data/services/usage_stats_service.dart | 65 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/lib/data/services/meeting/meeting_task_service.dart b/lib/data/services/meeting/meeting_task_service.dart index 7ec049fc7..fc6f6b8fc 100644 --- a/lib/data/services/meeting/meeting_task_service.dart +++ b/lib/data/services/meeting/meeting_task_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -9,6 +10,8 @@ import '../../../core/utils/synchrodata.dart'; import '../db/sqflite_api.dart'; import '../deapsound_ai_service.dart'; import 'meeting_upload_service.dart'; +import '../usage_stats_service.dart'; +import '../../models/meeting_usage_stats.dart'; ///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。 class MeetingTaskService extends GetxService { @@ -117,6 +120,30 @@ class MeetingTaskService extends GetxService { taskId = await _submitTask(audiourl, isSpeaker, language); } await SqfliteApi.editMeeting(id, {'tasktype': 1, 'taskid': taskId}); + try { + final detail = await SqfliteApi.getMeetingDetails(id); + final seconds = (detail['seconds'] ?? 0) as int; + final filepath = (detail['filepath'] ?? '') as String; + int fileSizeBytes = 0; + if (filepath.isNotEmpty) { + try { + fileSizeBytes = await File(filepath).length(); + } catch (_) {} + } + final record = MeetingAsrRecord( + id: taskId, + meetingId: id, + taskId: taskId, + audioSeconds: seconds, + fileSizeBytes: fileSizeBytes, + language: language, + submitTimestamp: DateTime.now().millisecondsSinceEpoch, + status: asrStatusProcessing, + transcriptBytes: 0, + completeTimestamp: 0, + ); + Get.find().addMeetingAsrRecord(record); + } catch (_) {} _taskList.add({ 'id': id, 'taskid': taskId, @@ -373,6 +400,16 @@ class MeetingTaskService extends GetxService { _storage.write("meeting_task_list", _taskList); _storage.write("meeting_task_finish_list", taskFinishList); } + try { + final details = await SqfliteApi.getMeetingDetails(id); + final tid = (details['taskid'] ?? '') as String; + final transcriptBytes = utf8.encode(text).length; + Get.find().completeMeetingAsrRecord( + id: tid, + transcriptBytes: transcriptBytes, + completeTimestamp: DateTime.now().millisecondsSinceEpoch, + ); + } catch (_) {} // 触发AI分析,分别请求总结和概览 aiServiceMessage(id, text, prompt, outline); diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index 0f930fab5..f8a2d1be7 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../models/translation_usage_stats.dart'; +import '../models/meeting_usage_stats.dart'; import '../../core/utils/logger.dart'; import 'network/api.dart'; import '../models/goods.dart'; @@ -12,6 +13,7 @@ import '../models/user_Info.dart'; class UsageStatsService extends GetxService { static const String _storageKey = 'usage_stats'; static const String _dailyStatsKey = 'daily_translation_stats'; + static const String _meetingDailyKey = 'daily_meeting_asr_stats'; final GetStorage _storage = GetStorage(); @@ -67,6 +69,69 @@ class UsageStatsService extends GetxService { return 'anonymous'; } + MeetingAsrDailyStats _loadMeetingDaily(String dateKey) { + final data = _storage.read('${_meetingDailyKey}_$dateKey'); + if (data != null) { + return MeetingAsrDailyStats.fromJson(Map.from(data)); + } + return MeetingAsrDailyStats( + date: dateKey, userId: getCurrentUserId(), items: []); + } + + void _saveMeetingDaily(MeetingAsrDailyStats stats) { + _storage.write('${_meetingDailyKey}_${stats.date}', stats.toJson()); + } + + String _formatDate(DateTime d) { + final y = d.year; + final m = d.month.toString().padLeft(2, '0'); + final day = d.day.toString().padLeft(2, '0'); + return '$y-$m-$day'; + } + + void addMeetingAsrRecord(MeetingAsrRecord record) { + final dateKey = _formatDate( + DateTime.fromMillisecondsSinceEpoch(record.submitTimestamp)); + final daily = _loadMeetingDaily(dateKey); + final idx = daily.items.indexWhere((e) => e.id == record.id); + if (idx >= 0) { + daily.items[idx] = record; + } else { + daily.items.add(record); + } + _saveMeetingDaily(daily); + } + + void completeMeetingAsrRecord({ + required String id, + required int transcriptBytes, + required int completeTimestamp, + }) { + final now = DateTime.now(); + for (int i = 0; i < 3; i++) { + final dateKey = _formatDate(now.subtract(Duration(days: i))); + final daily = _loadMeetingDaily(dateKey); + final idx = daily.items.indexWhere((e) => e.id == id); + if (idx >= 0) { + final old = daily.items[idx]; + daily.items[idx] = MeetingAsrRecord( + id: old.id, + meetingId: old.meetingId, + taskId: old.taskId, + audioSeconds: old.audioSeconds, + fileSizeBytes: old.fileSizeBytes, + language: old.language, + submitTimestamp: old.submitTimestamp, + status: asrStatusCompleted, + transcriptBytes: transcriptBytes, + completeTimestamp: completeTimestamp, + ); + _saveMeetingDaily(daily); + return; + } + } + } + /// 记录翻译API调用 void recordTranslationApiCall({ required String sourceText, From 39b09d9e2470b671f777a87e210c157e7ebc6a37 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 24 Nov 2025 16:32:25 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor(usage):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=9C=8D=E5=8A=A1=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构统计服务中的变量命名,将realtimeStats和todayStats改为更明确的realtimeTranslationStats和todayTranslationStats 简化用户模型中的初始化逻辑,添加日志记录 合并积分显示行,优化UI展示 --- lib/data/models/meeting_usage_stats.dart | 4 +- lib/data/models/user_Info.dart | 37 +++--- lib/data/services/usage_stats_service.dart | 105 ++++++++++-------- .../translation_stats_controller.dart | 15 +-- .../usage/views/translation_stats_view.dart | 33 +----- 5 files changed, 89 insertions(+), 105 deletions(-) diff --git a/lib/data/models/meeting_usage_stats.dart b/lib/data/models/meeting_usage_stats.dart index 2b486fe1f..88ef7e69c 100644 --- a/lib/data/models/meeting_usage_stats.dart +++ b/lib/data/models/meeting_usage_stats.dart @@ -1,3 +1,5 @@ +///会议模块用量统计的模型类 + const int asrStatusProcessing = 0; // 处理中 const int asrStatusFailed = 1; // 处理失败 const int asrStatusCompleted = 2; // 已完成 @@ -95,7 +97,7 @@ class MeetingAsrDailyStats { } } -// 会议音频文件转文字历史统计 +// 会议音频文件转文字历史统计(所有的用量) class MeetingAsrHistoryStats { String userId; // 用户ID int allAsrCalls; // 所有ASR调用次数 diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index 861d1604a..7620e93b6 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -83,11 +83,9 @@ class User { final devicesData = json['devices'] as List?; // 允许空值 //final token = json['data']['token'] as String? ?? ''; //final token = ''; - // 更新单例实例 - return _instance = User._( + final u = User._( token: '', - //token: token as String? ?? '', - name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 + name: (userData?['name'] as String?) ?? '默认名称', uid: (userData?['uid'] as String?) ?? '', avatar: (userData?['avatar'] as String?) ?? '', language: (userData?['language'] as String?) ?? 'en', @@ -95,42 +93,43 @@ class User { phone: (userData?['phone'] as String?) ?? '', phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', - viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 - viptime: (userData?['viptime'] as int?) ?? 0, // 提供默认值 - + viplv: (userData?['viplv'] as int?) ?? 0, + viptime: (userData?['viptime'] as int?) ?? 0, aichatintegral: (userData?['aichatintegral'] as int?) ?? 0, aichattotalintegral: (userData?['aichattotalintegral'] as int?) ?? 0, tradeintegral: (userData?['tradeintegral'] as int?) ?? 0, tradetotalintegral: (userData?['tradetotalintegral'] as int?) ?? 0, meetintegral: (userData?['meetintegral'] as int?) ?? 0, meettotalintegral: (userData?['meettotalintegral'] as int?) ?? 0, - devices: devicesData ?.map((e) => UserDevice.fromJson(e as Map)) .toList() ?? - [], // 处理设备列表 + [], ); + _instance = u; + Logger.i("User", + "初始化用户: uid=${u.uid}, name=${u.name}, viplv=${u.viplv}, viptime=${u.viptime}, ai聊天=${u.aichatintegral}/${u.aichattotalintegral}, 翻译=${u.tradeintegral}/${u.tradetotalintegral}, 会议=${u.meetintegral}/${u.meettotalintegral}"); + return u; } factory User.fronTokenJson(Map json) { // 修复1:安全获取嵌套的 user 字段 final userData = json['user'] as Map?; // 允许空值 final devicesData = json['devices'] as List?; // 允许空值 - print("数据获取fronTokenJson$userData $devicesData"); - // 更新单例实例 - return _instance = User._( + Logger.d("User", + "fronTokenJson: user=$userData, devices=${devicesData?.length ?? 0}"); + final u = User._( token: '', - name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 + name: (userData?['name'] as String?) ?? '默认名称', uid: (userData?['uid'] as String?) ?? '', avatar: (userData?['avatar'] as String?) ?? '', language: (userData?['language'] as String?) ?? 'en', - mail: (userData?['mail'] as String?) ?? '', phone: (userData?['phone'] as String?) ?? '', phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', - viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 - viptime: (userData?['viptime'] as int?) ?? 0, // 提供默认值 + viplv: (userData?['viplv'] as int?) ?? 0, + viptime: (userData?['viptime'] as int?) ?? 0, aichatintegral: (userData?['aichatintegral'] as int?) ?? 0, aichattotalintegral: (userData?['aichattotalintegral'] as int?) ?? 0, tradeintegral: (userData?['tradeintegral'] as int?) ?? 0, @@ -141,8 +140,12 @@ class User { ? (devicesData) .map((e) => UserDevice.fromJson(e as Map)) .toList() - : [], // 处理设备列表 + : [], ); + _instance = u; + Logger.i("User", + "初始化用户(token): uid=${u.uid}, name=${u.name}, viplv=${u.viplv}, viptime=${u.viptime}, ai聊天=${u.aichatintegral}/${u.aichattotalintegral}, 翻译=${u.tradeintegral}/${u.tradetotalintegral}, 会议=${u.meetintegral}/${u.meettotalintegral}"); + return u; } updateUserInfo(Map user) { diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index f8a2d1be7..9a9d0003f 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../models/translation_usage_stats.dart'; @@ -11,17 +10,17 @@ import '../models/user_Info.dart'; /// 使用量统计服务 /// class UsageStatsService extends GetxService { - static const String _storageKey = 'usage_stats'; static const String _dailyStatsKey = 'daily_translation_stats'; static const String _meetingDailyKey = 'daily_meeting_asr_stats'; final GetStorage _storage = GetStorage(); // 实时统计数据 - final Rx realtimeStats = RealtimeUsageStats().obs; + final Rx realtimeTranslationStats = + RealtimeUsageStats().obs; // 当日统计数据 - final Rx todayStats = + final Rx todayTranslationStats = TranslationUsageStats.empty('').obs; @override @@ -32,12 +31,12 @@ class UsageStatsService extends GetxService { /// 初始化服务 void _initializeService() { - _loadTodayStats(); + _loadTodayTranslationStats(); Logger.info('lxm--使用量统计服务初始化完成'); } - /// 加载当日统计数据 - void _loadTodayStats() { + /// 加载当日翻译统计数据 + void _loadTodayTranslationStats() { try { final today = DateTime.now(); final todayKey = @@ -45,14 +44,16 @@ class UsageStatsService extends GetxService { final statsData = _storage.read('${_dailyStatsKey}_$todayKey'); if (statsData != null) { - todayStats.value = TranslationUsageStats.fromJson( + todayTranslationStats.value = TranslationUsageStats.fromJson( Map.from(statsData)); } else { - todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); + todayTranslationStats.value = + TranslationUsageStats.empty(getCurrentUserId()); } } catch (e) { - Logger.error('加载当日统计数据失败: $e'); - todayStats.value = TranslationUsageStats.empty(getCurrentUserId()); + Logger.error('加载当日翻译统计数据失败: $e'); + todayTranslationStats.value = + TranslationUsageStats.empty(getCurrentUserId()); } } @@ -142,20 +143,20 @@ class UsageStatsService extends GetxService { }) { try { // 更新实时统计 - realtimeStats.value.translationCount++; - realtimeStats.value.sourceTextCharacters += sourceText.length; - realtimeStats.value.ttsTextCharacters += targetText.length; - realtimeStats.value.currentLanguagePair = + realtimeTranslationStats.value.translationCount++; + realtimeTranslationStats.value.sourceTextCharacters += sourceText.length; + realtimeTranslationStats.value.ttsTextCharacters += targetText.length; + realtimeTranslationStats.value.currentLanguagePair = '$sourceLanguage-$targetLanguage'; - realtimeStats.value.currentMode = mode; - realtimeStats.refresh(); + realtimeTranslationStats.value.currentMode = mode; + realtimeTranslationStats.refresh(); // 更新当日统计 - 使用多维度统计 final languagePair = '$sourceLanguage→$targetLanguage'; // 更新语言对统计 final newLanguagePairStats = Map.from( - todayStats.value.languagePairStats); + todayTranslationStats.value.languagePairStats); if (newLanguagePairStats.containsKey(languagePair)) { final existing = newLanguagePairStats[languagePair]!; newLanguagePairStats[languagePair] = LanguagePairStats( @@ -172,8 +173,8 @@ class UsageStatsService extends GetxService { } // 更新模式统计 - final newModeStats = - Map.from(todayStats.value.modeStats); + final newModeStats = Map.from( + todayTranslationStats.value.modeStats); if (newModeStats.containsKey(mode)) { final existing = newModeStats[mode]!; newModeStats[mode] = existing.copyWith( @@ -189,12 +190,13 @@ class UsageStatsService extends GetxService { ); } - todayStats.value = todayStats.value.copyWith( - translationApiCalls: todayStats.value.translationApiCalls + 1, - sourceTextCharacters: - todayStats.value.sourceTextCharacters + sourceText.length, + todayTranslationStats.value = todayTranslationStats.value.copyWith( + translationApiCalls: + todayTranslationStats.value.translationApiCalls + 1, + sourceTextCharacters: todayTranslationStats.value.sourceTextCharacters + + sourceText.length, ttsTextCharacters: - todayStats.value.ttsTextCharacters + targetText.length, + todayTranslationStats.value.ttsTextCharacters + targetText.length, languagePairStats: newLanguagePairStats, modeStats: newModeStats, ); @@ -219,13 +221,15 @@ class UsageStatsService extends GetxService { }) { try { // 更新实时统计(使用有效语音时长作为主要计费依据) - realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds; - realtimeStats.value.totalSessionDuration += totalDurationSeconds; - realtimeStats.refresh(); + realtimeTranslationStats.value.effectiveAudioDuration += + effectiveDurationSeconds; + realtimeTranslationStats.value.totalSessionDuration += + totalDurationSeconds; + realtimeTranslationStats.refresh(); // 更新模式统计 - final newModeStats = - Map.from(todayStats.value.modeStats); + final newModeStats = Map.from( + todayTranslationStats.value.modeStats); if (newModeStats.containsKey(mode)) { final existing = newModeStats[mode]!; newModeStats[mode] = existing.copyWith( @@ -239,12 +243,13 @@ class UsageStatsService extends GetxService { } // 更新当日统计 - todayStats.value = todayStats.value.copyWith( - asrApiCalls: todayStats.value.asrApiCalls + 1, + todayTranslationStats.value = todayTranslationStats.value.copyWith( + asrApiCalls: todayTranslationStats.value.asrApiCalls + 1, effectiveAudioDuration: - todayStats.value.effectiveAudioDuration + effectiveDurationSeconds, - totalSessionDuration: - todayStats.value.totalSessionDuration + totalDurationSeconds, + todayTranslationStats.value.effectiveAudioDuration + + effectiveDurationSeconds, + totalSessionDuration: todayTranslationStats.value.totalSessionDuration + + totalDurationSeconds, modeStats: newModeStats, ); @@ -280,9 +285,9 @@ class UsageStatsService extends GetxService { 'TRANSLATION_MODE_$mode': effectiveDurationSeconds, 'TRANSLATION_ASR_LANGUAGE_$language': 1, 'TRANSLATION_SOURCE_CHARACTERS': - realtimeStats.value.sourceTextCharacters, + realtimeTranslationStats.value.sourceTextCharacters, 'TRANSLATION_TARGET_CHARACTERS': - realtimeStats.value.ttsTextCharacters, + realtimeTranslationStats.value.ttsTextCharacters, }, }; @@ -348,15 +353,16 @@ class UsageStatsService extends GetxService { }) { try { // 更新实时统计 - realtimeStats.value.totalSessionDuration += durationSeconds; - realtimeStats.refresh(); + realtimeTranslationStats.value.totalSessionDuration += durationSeconds; + realtimeTranslationStats.refresh(); // 更新当日统计(TTS按字符数计费,不是按时长) - todayStats.value = todayStats.value.copyWith( - ttsApiCalls: todayStats.value.ttsApiCalls + 1, - ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length, + todayTranslationStats.value = todayTranslationStats.value.copyWith( + ttsApiCalls: todayTranslationStats.value.ttsApiCalls + 1, + ttsTextCharacters: + todayTranslationStats.value.ttsTextCharacters + text.length, totalSessionDuration: - todayStats.value.totalSessionDuration + durationSeconds, + todayTranslationStats.value.totalSessionDuration + durationSeconds, ); _saveTodayStats(); @@ -371,8 +377,8 @@ class UsageStatsService extends GetxService { void recordAiSummaryApiCall() { try { // 更新当日统计 - todayStats.value = todayStats.value.copyWith( - aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1, + todayTranslationStats.value = todayTranslationStats.value.copyWith( + aiSummaryApiCalls: todayTranslationStats.value.aiSummaryApiCalls + 1, ); _saveTodayStats(); @@ -385,11 +391,11 @@ class UsageStatsService extends GetxService { /// 开始新会话 void startNewSession() { - realtimeStats.value.reset(); + realtimeTranslationStats.value.reset(); // 更新当日会话数 - todayStats.value = todayStats.value.copyWith( - sessionCount: todayStats.value.sessionCount + 1, + todayTranslationStats.value = todayTranslationStats.value.copyWith( + sessionCount: todayTranslationStats.value.sessionCount + 1, ); _saveTodayStats(); @@ -402,7 +408,8 @@ class UsageStatsService extends GetxService { final today = DateTime.now(); final todayKey = '${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}'; - _storage.write('${_dailyStatsKey}_$todayKey', todayStats.value.toJson()); + _storage.write( + '${_dailyStatsKey}_$todayKey', todayTranslationStats.value.toJson()); } catch (e) { Logger.error('保存当日统计数据失败: $e'); } diff --git a/lib/modules/usage/controllers/translation_stats_controller.dart b/lib/modules/usage/controllers/translation_stats_controller.dart index 81694dad7..7015537aa 100644 --- a/lib/modules/usage/controllers/translation_stats_controller.dart +++ b/lib/modules/usage/controllers/translation_stats_controller.dart @@ -19,7 +19,8 @@ class TranslationStatsController extends GetxController { final RxList historyStats = [].obs; - RealtimeUsageStats get realtimeStats => _usageService.realtimeStats.value; + RealtimeUsageStats get realtimeStats => + _usageService.realtimeTranslationStats.value; final RxMap costConfig = {}.obs; final RxDouble dailyCostThreshold = 10.0.obs; @@ -34,17 +35,11 @@ class TranslationStatsController extends GetxController { } void _initializeData() { - ever(_usageService.todayStats, (stats) { + ever(_usageService.todayTranslationStats, (stats) { todayStats.value = stats; _updateChartData(); }); - - - - - - loadAllStats(); } @@ -52,7 +47,7 @@ class TranslationStatsController extends GetxController { isLoading.value = true; try { - todayStats.value = _usageService.todayStats.value; + todayStats.value = _usageService.todayTranslationStats.value; await _loadWeekStats(); @@ -136,7 +131,6 @@ class TranslationStatsController extends GetxController { int totalTtsPlayDuration = 0; int totalSessions = 0; - final Map combinedLanguagePairUsage = {}; final Map combinedModeUsage = {}; @@ -151,7 +145,6 @@ class TranslationStatsController extends GetxController { totalTtsPlayDuration += stats.totalSessionDuration; totalSessions += stats.sessionCount; - stats.languagePairUsage.forEach((key, value) { combinedLanguagePairUsage[key] = (combinedLanguagePairUsage[key] ?? 0) + value; diff --git a/lib/modules/usage/views/translation_stats_view.dart b/lib/modules/usage/views/translation_stats_view.dart index 19ceef070..30d0501a6 100644 --- a/lib/modules/usage/views/translation_stats_view.dart +++ b/lib/modules/usage/views/translation_stats_view.dart @@ -466,46 +466,25 @@ class TranslationStatsView extends GetView { return Column( children: [ _buildIntegralRow( - 'AI聊天积分', - controller.formatNumber(u.aichatintegral), + 'AI聊天', + '${controller.formatNumber(u.aichatintegral)}/${controller.formatNumber(u.aichattotalintegral)}', Icons.chat_bubble_outline, isDarkMode, ), SizedBox(height: 12.h), _buildIntegralRow( - 'AI聊天总积分', - controller.formatNumber(u.aichattotalintegral), - Icons.chat_outlined, - isDarkMode, - ), - SizedBox(height: 12.h), - _buildIntegralRow( - '翻译积分', - controller.formatNumber(u.tradeintegral), + '翻译', + '${controller.formatNumber(u.tradeintegral)}/${controller.formatNumber(u.tradetotalintegral)}', Icons.translate, isDarkMode, ), SizedBox(height: 12.h), _buildIntegralRow( - '翻译总积分', - controller.formatNumber(u.tradetotalintegral), - Icons.g_translate, - isDarkMode, - ), - SizedBox(height: 12.h), - _buildIntegralRow( - '会议积分', - controller.formatNumber(u.meetintegral), + '会议', + '${controller.formatNumber(u.meetintegral)}/${controller.formatNumber(u.meettotalintegral)}', Icons.groups, isDarkMode, ), - SizedBox(height: 12.h), - _buildIntegralRow( - '会议总积分', - controller.formatNumber(u.meettotalintegral), - Icons.meeting_room, - isDarkMode, - ), ], ); }), From 4ad36a1bc749700aa6c6befe0605dd86a72abba3 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 24 Nov 2025 20:20:40 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat(usage):=20=E9=87=8D=E6=9E=84=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E7=BB=9F=E8=AE=A1=E4=B8=BA=E7=94=A8=E9=87=8F=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=B9=B6=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AE=AE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将翻译统计页面重构为用量统计,支持切换翻译和会议统计 - 新增会议统计相关功能,包括数据获取、聚合和展示 - 更新路由和绑定配置以支持新功能 - 添加会议统计的数据模型和服务方法 --- lib/data/services/usage_stats_service.dart | 43 +++++ .../controllers/explore_controller.dart | 6 +- lib/modules/explore/views/explore_view.dart | 2 +- .../bindings/translation_stats_binding.dart | 13 -- lib/modules/usage/bindings/usage_binding.dart | 20 ++ .../controllers/meeting_stats_controller.dart | 152 ++++++++++++++++ .../usage/controllers/usage_controller.dart | 20 ++ .../usage/views/meeting_stats_view.dart | 171 ++++++++++++++++++ lib/modules/usage/views/usage_view.dart | 50 +++++ lib/routes/app_pages.dart | 10 +- lib/routes/app_routes.dart | 2 +- 11 files changed, 466 insertions(+), 23 deletions(-) delete mode 100644 lib/modules/usage/bindings/translation_stats_binding.dart create mode 100644 lib/modules/usage/bindings/usage_binding.dart create mode 100644 lib/modules/usage/controllers/meeting_stats_controller.dart create mode 100644 lib/modules/usage/controllers/usage_controller.dart create mode 100644 lib/modules/usage/views/meeting_stats_view.dart create mode 100644 lib/modules/usage/views/usage_view.dart diff --git a/lib/data/services/usage_stats_service.dart b/lib/data/services/usage_stats_service.dart index 9a9d0003f..fc99643cb 100644 --- a/lib/data/services/usage_stats_service.dart +++ b/lib/data/services/usage_stats_service.dart @@ -543,4 +543,47 @@ class UsageStatsService extends GetxService { return TranslationUsageStats.empty(getCurrentUserId()); } } + + Future> getMeetingHistoryStats({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final List list = []; + for (DateTime date = startDate; + date.isBefore(endDate) || date.isAtSameMomentAs(endDate); + date = date.add(const Duration(days: 1))) { + final key = _formatDate(date); + final daily = _loadMeetingDaily(key); + if (daily.items.isNotEmpty) { + list.add(daily); + } + } + return list; + } catch (e) { + Logger.error('获取会议历史统计失败: $e'); + return []; + } + } + + MeetingAsrHistoryStats aggregateMeetingStats( + List daily) { + int calls = 0; + int audioSeconds = 0; + int audioBytes = 0; + int transcriptBytes = 0; + for (final d in daily) { + calls += d.asrCalls; + audioSeconds += d.audioSecondsTotal; + audioBytes += d.audioBytesTotal; + transcriptBytes += d.transcriptBytesTotal; + } + return MeetingAsrHistoryStats( + userId: getCurrentUserId(), + allAsrCalls: calls, + allAudioSeconds: audioSeconds, + allAudioFileBytes: audioBytes, + allTranscriptBytes: transcriptBytes, + ); + } } diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index d5c3b811b..6376e3fa4 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/lib/modules/explore/controllers/explore_controller.dart @@ -263,9 +263,9 @@ class ExploreController extends GetxController { // 实际应用中,这里应该跳转到新功能页面 } - // 跳转到翻译统计页面 - void goToTranslationStats() { - Get.toNamed(Routes.translationStats); + // 跳转到用量统计页面 + void goToUsageStats() { + Get.toNamed(Routes.usageStats); } @override diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 2c9c076ee..c501ab04b 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -120,7 +120,7 @@ class ExploreView extends GetView { color: isDarkMode ? Colors.white : Colors.grey[700], ), padding: EdgeInsets.zero, - onPressed: controller.goToTranslationStats, + onPressed: controller.goToUsageStats, ), ), ], diff --git a/lib/modules/usage/bindings/translation_stats_binding.dart b/lib/modules/usage/bindings/translation_stats_binding.dart deleted file mode 100644 index da6c55d3a..000000000 --- a/lib/modules/usage/bindings/translation_stats_binding.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/translation_stats_controller.dart'; -import '../../../data/services/usage_stats_service.dart'; - -class TranslationStatsBinding implements Bindings { - @override - void dependencies() { - if (!Get.isRegistered()) { - Get.put(UsageStatsService(), permanent: true); - } - Get.lazyPut(() => TranslationStatsController()); - } -} diff --git a/lib/modules/usage/bindings/usage_binding.dart b/lib/modules/usage/bindings/usage_binding.dart new file mode 100644 index 000000000..1d3145df9 --- /dev/null +++ b/lib/modules/usage/bindings/usage_binding.dart @@ -0,0 +1,20 @@ +import 'package:get/get.dart'; +import '../controllers/usage_controller.dart'; +import '../controllers/meeting_stats_controller.dart'; +import '../controllers/translation_stats_controller.dart'; +import '../../../data/services/usage_stats_service.dart'; + +class UsageBinding implements Bindings { + @override + void dependencies() { + if (!Get.isRegistered()) { + Get.put(UsageStatsService(), permanent: true); + } + if (!Get.isRegistered()) { + Get.lazyPut(() => TranslationStatsController()); + } + Get.lazyPut(() => MeetingStatsController()); + Get.lazyPut(() => UsageController()); + } +} + diff --git a/lib/modules/usage/controllers/meeting_stats_controller.dart b/lib/modules/usage/controllers/meeting_stats_controller.dart new file mode 100644 index 000000000..bbcb7f9f6 --- /dev/null +++ b/lib/modules/usage/controllers/meeting_stats_controller.dart @@ -0,0 +1,152 @@ +import 'package:get/get.dart'; +import '../../../data/services/usage_stats_service.dart'; +import '../../../data/models/meeting_usage_stats.dart'; +import '../../../core/utils/logger.dart'; + +class MeetingStatsController extends GetxController { + final UsageStatsService _usageService = Get.find(); + + final isLoading = false.obs; + final selectedTabIndex = 0.obs; + final selectedTimeRange = '今日'.obs; + + final Rx todayStats = + MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + final Rx weekStats = + MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + final Rx monthStats = + MeetingAsrHistoryStats( + userId: '', + allAsrCalls: 0, + allAudioSeconds: 0, + allAudioFileBytes: 0, + allTranscriptBytes: 0, + ).obs; + + final RxList historyDaily = [].obs; + + @override + void onInit() { + super.onInit(); + loadAllStats(); + } + + Future loadAllStats() async { + isLoading.value = true; + try { + final now = DateTime.now(); + + final todayList = await _usageService.getMeetingHistoryStats( + startDate: DateTime(now.year, now.month, now.day), + endDate: DateTime(now.year, now.month, now.day), + ); + todayStats.value = _usageService.aggregateMeetingStats(todayList); + + final weekStart = now.subtract(Duration(days: now.weekday - 1)); + final weekEnd = weekStart.add(const Duration(days: 6)); + final weekList = await _usageService.getMeetingHistoryStats( + startDate: DateTime(weekStart.year, weekStart.month, weekStart.day), + endDate: DateTime(weekEnd.year, weekEnd.month, weekEnd.day), + ); + weekStats.value = _usageService.aggregateMeetingStats(weekList); + + final monthStart = DateTime(now.year, now.month, 1); + final monthEnd = DateTime(now.year, now.month + 1, 0); + final monthList = await _usageService.getMeetingHistoryStats( + startDate: monthStart, + endDate: monthEnd, + ); + monthStats.value = _usageService.aggregateMeetingStats(monthList); + + final historyStart = now.subtract(const Duration(days: 7)); + historyDaily.value = await _usageService.getMeetingHistoryStats( + startDate: DateTime(historyStart.year, historyStart.month, historyStart.day), + endDate: DateTime(now.year, now.month, now.day), + ); + + Logger.info('会议统计数据加载完成'); + } catch (e) { + Logger.error('加载会议统计数据失败: $e'); + } finally { + isLoading.value = false; + } + } + + void switchTimeRange(int index) { + selectedTabIndex.value = index; + switch (index) { + case 0: + selectedTimeRange.value = '今日'; + break; + case 1: + selectedTimeRange.value = '本周'; + break; + case 2: + selectedTimeRange.value = '本月'; + break; + case 3: + selectedTimeRange.value = '历史'; + break; + } + } + + MeetingAsrHistoryStats get currentStats { + switch (selectedTabIndex.value) { + case 0: + return todayStats.value; + case 1: + return weekStats.value; + case 2: + return monthStats.value; + default: + return todayStats.value; + } + } + + String formatDuration(int seconds) { + if (seconds < 60) { + return '$seconds秒'; + } else if (seconds < 3600) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + return '$minutes分$remainingSeconds秒'; + } else { + final hours = seconds ~/ 3600; + final minutes = (seconds % 3600) ~/ 60; + return '$hours小时$minutes分'; + } + } + + String formatBytes(int bytes) { + if (bytes < 1024) return '$bytes B'; + final kb = bytes / 1024; + if (kb < 1024) return '${kb.toStringAsFixed(1)} KB'; + final mb = kb / 1024; + if (mb < 1024) return '${mb.toStringAsFixed(1)} MB'; + final gb = mb / 1024; + return '${gb.toStringAsFixed(1)} GB'; + } + + String formatNumber(int number) { + if (number < 1000) return number.toString(); + if (number < 1000000) return '${(number / 1000).toStringAsFixed(1)}K'; + return '${(number / 1000000).toStringAsFixed(1)}M'; + } + + Future refreshData() async { + await loadAllStats(); + } +} + diff --git a/lib/modules/usage/controllers/usage_controller.dart b/lib/modules/usage/controllers/usage_controller.dart new file mode 100644 index 000000000..bf2c5a4be --- /dev/null +++ b/lib/modules/usage/controllers/usage_controller.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../views/translation_stats_view.dart'; +import '../views/meeting_stats_view.dart'; + +class UsageController extends GetxController { + late PageController pageController; + final RxInt pageIndex = 0.obs; + + final List pageList = const [ + TranslationStatsView(), + MeetingStatsView(), + ]; + + @override + void onInit() { + super.onInit(); + pageController = PageController(initialPage: 0); + } +} diff --git a/lib/modules/usage/views/meeting_stats_view.dart b/lib/modules/usage/views/meeting_stats_view.dart new file mode 100644 index 000000000..e648dc14f --- /dev/null +++ b/lib/modules/usage/views/meeting_stats_view.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../../core/theme/app_colors.dart'; +import '../controllers/meeting_stats_controller.dart'; + +class MeetingStatsView extends GetView { + const MeetingStatsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: Obx(() => controller.isLoading.value + ? const Center(child: CircularProgressIndicator()) + : _buildBody(isDarkMode)), + ); + } + + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, color: isDarkMode ? Colors.white : Colors.black, size: 20.sp), + onPressed: () => Get.back(), + ), + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.white, + elevation: 0.5, + title: Text( + '会议统计', + style: TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black), + ), + centerTitle: true, + actions: [ + IconButton( + icon: Icon(Icons.refresh, color: isDarkMode ? Colors.white : Colors.black), + onPressed: controller.refreshData, + ), + ], + ); + } + + Widget _buildBody(bool isDarkMode) { + return Column( + children: [ + _buildTimeRangeSelector(isDarkMode), + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + _buildOverviewCard(isDarkMode), + ], + ), + ), + ), + ], + ); + } + + Widget _buildTimeRangeSelector(bool isDarkMode) { + return Container( + margin: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2)), + ], + ), + child: Obx(() => Row( + children: [ + _buildTimeRangeTab('今日', 0, isDarkMode), + _buildTimeRangeTab('本周', 1, isDarkMode), + _buildTimeRangeTab('本月', 2, isDarkMode), + _buildTimeRangeTab('历史', 3, isDarkMode), + ], + )), + ); + } + + Widget _buildTimeRangeTab(String title, int index, bool isDarkMode) { + final isSelected = controller.selectedTabIndex.value == index; + + return Expanded( + child: GestureDetector( + onTap: () => controller.switchTimeRange(index), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12.h), + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + color: isSelected ? Colors.white : (isDarkMode ? Colors.white70 : Colors.black54), + ), + ), + ), + ), + ); + } + + Widget _buildOverviewCard(bool isDarkMode) { + final stats = controller.currentStats; + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 2)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.dashboard, color: AppColors.primary, size: 20.sp), + SizedBox(width: 8.w), + Obx(() => Text( + '${controller.selectedTimeRange.value}概览', + style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black), + )), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded(child: _buildStatItem('ASR调用次数', controller.formatNumber(stats.allAsrCalls), Icons.api, isDarkMode)), + Expanded(child: _buildStatItem('音频总时长', controller.formatDuration(stats.allAudioSeconds), Icons.mic, isDarkMode)), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded(child: _buildStatItem('音频文件大小', controller.formatBytes(stats.allAudioFileBytes), Icons.folder, isDarkMode)), + Expanded(child: _buildStatItem('转写字节', controller.formatNumber(stats.allTranscriptBytes), Icons.text_snippet, isDarkMode)), + ], + ), + ], + ), + ); + } + + Widget _buildStatItem(String label, String value, IconData icon, bool isDarkMode) { + return Column( + children: [ + Icon(icon, color: AppColors.primary, size: 24.sp), + SizedBox(height: 8.h), + Text( + value, + style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black), + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle(fontSize: 12.sp, color: isDarkMode ? Colors.white70 : Colors.black54), + ), + ], + ); + } +} + diff --git a/lib/modules/usage/views/usage_view.dart b/lib/modules/usage/views/usage_view.dart new file mode 100644 index 000000000..efa1b02e8 --- /dev/null +++ b/lib/modules/usage/views/usage_view.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../../core/theme/app_colors.dart'; +import '../controllers/usage_controller.dart'; + +class UsageView extends GetView { + const UsageView({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], + body: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: controller.pageController, + children: controller.pageList, + ), + bottomNavigationBar: Obx( + () => BottomNavigationBar( + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.translate), + label: 'interpretation'.tr, + ), + BottomNavigationBarItem( + icon: const Icon(Icons.groups), + label: 'meeting'.tr, + ), + ], + backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFB), + currentIndex: controller.pageIndex.value, + unselectedItemColor: isDarkMode ? Colors.grey[500] : const Color(0xFF6B7280), + selectedItemColor: const Color(0xFF3B82F6), + unselectedFontSize: 10.sp, + selectedFontSize: 10.sp, + onTap: (int index) { + if (index == controller.pageIndex.value) return; + controller.pageIndex.value = index; + controller.pageController.jumpToPage(index); + }, + type: BottomNavigationBarType.fixed, + ), + ), + ); + } +} + diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 4f9b22eb5..a45b799f9 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -34,8 +34,8 @@ import '../modules/timbre/views/timbre_view.dart'; import '../modules/translation/views/translation_view.dart'; import '../modules/translation/bindings/translation_binding.dart'; import '../modules/translation/views/translation_history_view.dart'; -import '../modules/usage/views/translation_stats_view.dart'; -import '../modules/usage/bindings/translation_stats_binding.dart'; +import '../modules/usage/views/usage_view.dart'; +import '../modules/usage/bindings/usage_binding.dart'; import '../modules/translation/bindings/translation_list_binding.dart'; import '../modules/translation/views/translation_list_view.dart'; import '../modules/translation/bindings/translation_history_binding.dart'; @@ -197,9 +197,9 @@ abstract class AppPages { transitionDuration: Duration(milliseconds: 250), ), GetPage( - name: Routes.translationStats, - page: () => const TranslationStatsView(), - binding: TranslationStatsBinding(), + name: Routes.usageStats, + page: () => const UsageView(), + binding: UsageBinding(), transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index c964ffb57..f76c442cb 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -31,7 +31,7 @@ abstract class Routes { static const pictureTranslation = '/picture_translation'; // 图片翻译 static const textTranslation = '/text_translation'; // 文本翻译 static const translationHistory = '/translation/history'; // 翻译历史 - static const translationStats = '/translation/stats'; // 翻译统计 + static const usageStats = '/usage/stats'; // 用量统计 static const speechDemo = '/speech_demo'; // 语音演示 static const translationList = '/translation/List'; // 翻译历史 static const ftftranslation = '/ftftranslation'; // 面对面翻译