|
|
|
@ -1,21 +1,26 @@ |
|
|
|
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'; |
|
|
|
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<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs; |
|
|
|
final Rx<RealtimeUsageStats> realtimeTranslationStats = |
|
|
|
RealtimeUsageStats().obs; |
|
|
|
|
|
|
|
// 当日统计数据 |
|
|
|
final Rx<TranslationUsageStats> todayStats = |
|
|
|
final Rx<TranslationUsageStats> todayTranslationStats = |
|
|
|
TranslationUsageStats.empty('').obs; |
|
|
|
|
|
|
|
@override |
|
|
|
@ -26,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 = |
|
|
|
@ -39,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<String, dynamic>.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()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -63,6 +70,69 @@ class UsageStatsService extends GetxService { |
|
|
|
return 'anonymous'; |
|
|
|
} |
|
|
|
|
|
|
|
MeetingAsrDailyStats _loadMeetingDaily(String dateKey) { |
|
|
|
final data = _storage.read('${_meetingDailyKey}_$dateKey'); |
|
|
|
if (data != null) { |
|
|
|
return MeetingAsrDailyStats.fromJson(Map<String, dynamic>.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, |
|
|
|
@ -73,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<String, LanguagePairStats>.from( |
|
|
|
todayStats.value.languagePairStats); |
|
|
|
todayTranslationStats.value.languagePairStats); |
|
|
|
if (newLanguagePairStats.containsKey(languagePair)) { |
|
|
|
final existing = newLanguagePairStats[languagePair]!; |
|
|
|
newLanguagePairStats[languagePair] = LanguagePairStats( |
|
|
|
@ -103,8 +173,8 @@ class UsageStatsService extends GetxService { |
|
|
|
} |
|
|
|
|
|
|
|
// 更新模式统计 |
|
|
|
final newModeStats = |
|
|
|
Map<String, TranslationModeStats>.from(todayStats.value.modeStats); |
|
|
|
final newModeStats = Map<String, TranslationModeStats>.from( |
|
|
|
todayTranslationStats.value.modeStats); |
|
|
|
if (newModeStats.containsKey(mode)) { |
|
|
|
final existing = newModeStats[mode]!; |
|
|
|
newModeStats[mode] = existing.copyWith( |
|
|
|
@ -120,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, |
|
|
|
); |
|
|
|
@ -150,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<String, TranslationModeStats>.from(todayStats.value.modeStats); |
|
|
|
final newModeStats = Map<String, TranslationModeStats>.from( |
|
|
|
todayTranslationStats.value.modeStats); |
|
|
|
if (newModeStats.containsKey(mode)) { |
|
|
|
final existing = newModeStats[mode]!; |
|
|
|
newModeStats[mode] = existing.copyWith( |
|
|
|
@ -170,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, |
|
|
|
); |
|
|
|
|
|
|
|
@ -186,6 +260,89 @@ class UsageStatsService extends GetxService { |
|
|
|
} catch (e) { |
|
|
|
Logger.error('记录ASR API调用失败: $e'); |
|
|
|
} |
|
|
|
|
|
|
|
_reportAsrUsageToServer( |
|
|
|
effectiveDurationSeconds: effectiveDurationSeconds, |
|
|
|
//totalDurationSeconds: totalDurationSeconds, |
|
|
|
language: language, |
|
|
|
mode: mode, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// 上报ASR用量到服务器 |
|
|
|
Future<void> _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': |
|
|
|
realtimeTranslationStats.value.sourceTextCharacters, |
|
|
|
'TRANSLATION_TARGET_CHARACTERS': |
|
|
|
realtimeTranslationStats.value.ttsTextCharacters, |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
Logger.info('lxm---ASR用量上报参数: $params'); |
|
|
|
|
|
|
|
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<String, dynamic>.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<String, dynamic> 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调用 |
|
|
|
@ -196,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(); |
|
|
|
@ -219,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(); |
|
|
|
@ -233,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(); |
|
|
|
@ -250,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'); |
|
|
|
} |
|
|
|
@ -384,4 +543,47 @@ class UsageStatsService extends GetxService { |
|
|
|
return TranslationUsageStats.empty(getCurrentUserId()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Future<List<MeetingAsrDailyStats>> getMeetingHistoryStats({ |
|
|
|
required DateTime startDate, |
|
|
|
required DateTime endDate, |
|
|
|
}) async { |
|
|
|
try { |
|
|
|
final List<MeetingAsrDailyStats> 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<MeetingAsrDailyStats> 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, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|