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 +}