You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
474 lines
15 KiB
474 lines
15 KiB
import 'dart:convert';
|
|
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';
|
|
import '../models/user_Info.dart';
|
|
|
|
/// 使用量统计服务
|
|
///
|
|
class UsageStatsService extends GetxService {
|
|
static const String _storageKey = 'usage_stats';
|
|
static const String _dailyStatsKey = 'daily_translation_stats';
|
|
|
|
final GetStorage _storage = GetStorage();
|
|
|
|
// 实时统计数据
|
|
final Rx<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs;
|
|
|
|
// 当日统计数据
|
|
final Rx<TranslationUsageStats> todayStats =
|
|
TranslationUsageStats.empty('').obs;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initializeService();
|
|
}
|
|
|
|
/// 初始化服务
|
|
void _initializeService() {
|
|
_loadTodayStats();
|
|
Logger.info('lxm--使用量统计服务初始化完成');
|
|
}
|
|
|
|
/// 加载当日统计数据
|
|
void _loadTodayStats() {
|
|
try {
|
|
final today = DateTime.now();
|
|
final todayKey =
|
|
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
|
|
|
|
final statsData = _storage.read('${_dailyStatsKey}_$todayKey');
|
|
if (statsData != null) {
|
|
todayStats.value = TranslationUsageStats.fromJson(
|
|
Map<String, dynamic>.from(statsData));
|
|
} else {
|
|
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
|
|
}
|
|
} catch (e) {
|
|
Logger.error('加载当日统计数据失败: $e');
|
|
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
|
|
}
|
|
}
|
|
|
|
/// 获取当前用户ID
|
|
String getCurrentUserId() {
|
|
try {
|
|
final userInfo = _storage.read('user_info');
|
|
if (userInfo != null && userInfo['user'] != null) {
|
|
return userInfo['user']['uid'] ?? 'anonymous';
|
|
}
|
|
} catch (e) {
|
|
Logger.warning('获取用户ID失败: $e');
|
|
}
|
|
return 'anonymous';
|
|
}
|
|
|
|
/// 记录翻译API调用
|
|
void recordTranslationApiCall({
|
|
required String sourceText,
|
|
required String targetText,
|
|
required String sourceLanguage,
|
|
required String targetLanguage,
|
|
required String mode,
|
|
}) {
|
|
try {
|
|
// 更新实时统计
|
|
realtimeStats.value.translationCount++;
|
|
realtimeStats.value.sourceTextCharacters += sourceText.length;
|
|
realtimeStats.value.ttsTextCharacters += targetText.length;
|
|
realtimeStats.value.currentLanguagePair =
|
|
'$sourceLanguage-$targetLanguage';
|
|
realtimeStats.value.currentMode = mode;
|
|
realtimeStats.refresh();
|
|
|
|
// 更新当日统计 - 使用多维度统计
|
|
final languagePair = '$sourceLanguage→$targetLanguage';
|
|
|
|
// 更新语言对统计
|
|
final newLanguagePairStats = Map<String, LanguagePairStats>.from(
|
|
todayStats.value.languagePairStats);
|
|
if (newLanguagePairStats.containsKey(languagePair)) {
|
|
final existing = newLanguagePairStats[languagePair]!;
|
|
newLanguagePairStats[languagePair] = LanguagePairStats(
|
|
languagePair: languagePair,
|
|
translationCount: existing.translationCount + 1,
|
|
sourceCharacters: existing.sourceCharacters + sourceText.length,
|
|
);
|
|
} else {
|
|
newLanguagePairStats[languagePair] = LanguagePairStats(
|
|
languagePair: languagePair,
|
|
translationCount: 1,
|
|
sourceCharacters: sourceText.length,
|
|
);
|
|
}
|
|
|
|
// 更新模式统计
|
|
final newModeStats =
|
|
Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
|
|
if (newModeStats.containsKey(mode)) {
|
|
final existing = newModeStats[mode]!;
|
|
newModeStats[mode] = existing.copyWith(
|
|
apiCalls: existing.apiCalls + 1,
|
|
sourceCharacters: existing.sourceCharacters + sourceText.length,
|
|
ttsCharacters: existing.ttsCharacters + targetText.length,
|
|
);
|
|
} else {
|
|
newModeStats[mode] = TranslationModeStats(
|
|
apiCalls: 1,
|
|
sourceCharacters: sourceText.length,
|
|
ttsCharacters: targetText.length,
|
|
);
|
|
}
|
|
|
|
todayStats.value = todayStats.value.copyWith(
|
|
translationApiCalls: todayStats.value.translationApiCalls + 1,
|
|
sourceTextCharacters:
|
|
todayStats.value.sourceTextCharacters + sourceText.length,
|
|
ttsTextCharacters:
|
|
todayStats.value.ttsTextCharacters + targetText.length,
|
|
languagePairStats: newLanguagePairStats,
|
|
modeStats: newModeStats,
|
|
);
|
|
|
|
_saveTodayStats();
|
|
|
|
Logger.info(
|
|
'记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符');
|
|
} catch (e) {
|
|
Logger.error('记录翻译API调用失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 记录ASR API调用 - 优化版(精确统计参数)
|
|
void recordAsrApiCall({
|
|
required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音)
|
|
required int totalDurationSeconds, // 总会话时长(包含静音)
|
|
required String language,
|
|
required String mode, // 翻译模式
|
|
String? audioFormat, // 音频格式(可选)
|
|
int? sampleRate, // 采样率(可选)
|
|
}) {
|
|
try {
|
|
// 更新实时统计(使用有效语音时长作为主要计费依据)
|
|
realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds;
|
|
realtimeStats.value.totalSessionDuration += totalDurationSeconds;
|
|
realtimeStats.refresh();
|
|
|
|
// 更新模式统计
|
|
final newModeStats =
|
|
Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
|
|
if (newModeStats.containsKey(mode)) {
|
|
final existing = newModeStats[mode]!;
|
|
newModeStats[mode] = existing.copyWith(
|
|
effectiveAudioDuration:
|
|
existing.effectiveAudioDuration + effectiveDurationSeconds,
|
|
);
|
|
} else {
|
|
newModeStats[mode] = TranslationModeStats(
|
|
effectiveAudioDuration: effectiveDurationSeconds,
|
|
);
|
|
}
|
|
|
|
// 更新当日统计
|
|
todayStats.value = todayStats.value.copyWith(
|
|
asrApiCalls: todayStats.value.asrApiCalls + 1,
|
|
effectiveAudioDuration:
|
|
todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
|
|
totalSessionDuration:
|
|
todayStats.value.totalSessionDuration + totalDurationSeconds,
|
|
modeStats: newModeStats,
|
|
);
|
|
|
|
_saveTodayStats();
|
|
|
|
Logger.info(
|
|
'ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode');
|
|
} 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':
|
|
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');
|
|
|
|
_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调用
|
|
void recordTtsApiCall({
|
|
required String text,
|
|
required int durationSeconds,
|
|
required String language,
|
|
}) {
|
|
try {
|
|
// 更新实时统计
|
|
realtimeStats.value.totalSessionDuration += durationSeconds;
|
|
realtimeStats.refresh();
|
|
|
|
// 更新当日统计(TTS按字符数计费,不是按时长)
|
|
todayStats.value = todayStats.value.copyWith(
|
|
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
|
|
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
|
|
totalSessionDuration:
|
|
todayStats.value.totalSessionDuration + durationSeconds,
|
|
);
|
|
|
|
_saveTodayStats();
|
|
|
|
Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒');
|
|
} catch (e) {
|
|
Logger.error('记录TTS API调用失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 记录AI摘要API调用
|
|
void recordAiSummaryApiCall() {
|
|
try {
|
|
// 更新当日统计
|
|
todayStats.value = todayStats.value.copyWith(
|
|
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
|
|
);
|
|
|
|
_saveTodayStats();
|
|
|
|
Logger.info('记录AI摘要API调用');
|
|
} catch (e) {
|
|
Logger.error('记录AI摘要API调用失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 开始新会话
|
|
void startNewSession() {
|
|
realtimeStats.value.reset();
|
|
|
|
// 更新当日会话数
|
|
todayStats.value = todayStats.value.copyWith(
|
|
sessionCount: todayStats.value.sessionCount + 1,
|
|
);
|
|
|
|
_saveTodayStats();
|
|
Logger.info('开始新的翻译会话');
|
|
}
|
|
|
|
/// 保存当日统计数据
|
|
void _saveTodayStats() {
|
|
try {
|
|
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());
|
|
} catch (e) {
|
|
Logger.error('保存当日统计数据失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 获取历史统计数据
|
|
Future<List<TranslationUsageStats>> getHistoryStats({
|
|
required DateTime startDate,
|
|
required DateTime endDate,
|
|
}) async {
|
|
try {
|
|
final List<TranslationUsageStats> historyStats = [];
|
|
|
|
for (DateTime date = startDate;
|
|
date.isBefore(endDate) || date.isAtSameMomentAs(endDate);
|
|
date = date.add(const Duration(days: 1))) {
|
|
final dateKey =
|
|
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
|
final statsData = _storage.read('${_dailyStatsKey}_$dateKey');
|
|
|
|
if (statsData != null) {
|
|
historyStats.add(TranslationUsageStats.fromJson(
|
|
Map<String, dynamic>.from(statsData)));
|
|
}
|
|
}
|
|
|
|
return historyStats;
|
|
} catch (e) {
|
|
Logger.error('获取历史统计数据失败: $e');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// 清理过期数据(保留最近90天)
|
|
void cleanupOldData() {
|
|
try {
|
|
final cutoffDate = DateTime.now().subtract(const Duration(days: 90));
|
|
final allKeys = _storage.getKeys();
|
|
|
|
for (final key in allKeys) {
|
|
if (key.startsWith(_dailyStatsKey)) {
|
|
final dateStr = key.replaceFirst('${_dailyStatsKey}_', '');
|
|
try {
|
|
final date = DateTime.parse(dateStr);
|
|
if (date.isBefore(cutoffDate)) {
|
|
_storage.remove(key);
|
|
Logger.info('清理过期统计数据: $key');
|
|
}
|
|
} catch (e) {
|
|
// 忽略解析错误的键
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Logger.error('清理过期数据失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 获取月度统计汇总
|
|
Future<TranslationUsageStats> getMonthlyStats(int year, int month) async {
|
|
try {
|
|
final startDate = DateTime(year, month, 1);
|
|
final endDate = DateTime(year, month + 1, 0); // 月末
|
|
|
|
final dailyStats = await getHistoryStats(
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
);
|
|
|
|
if (dailyStats.isEmpty) {
|
|
return TranslationUsageStats.empty(getCurrentUserId());
|
|
}
|
|
|
|
// 汇总月度数据
|
|
int totalTranslationCalls = 0;
|
|
int totalAsrCalls = 0;
|
|
int totalTtsCalls = 0;
|
|
int totalAiSummaryCalls = 0;
|
|
int totalSourceChars = 0;
|
|
int totalTargetChars = 0;
|
|
int totalRecordingDuration = 0;
|
|
int totalTtsPlayDuration = 0;
|
|
int totalSessions = 0;
|
|
//double totalCost = 0.0;
|
|
|
|
final Map<String, int> combinedLanguagePairUsage = {};
|
|
final Map<String, int> combinedModeUsage = {};
|
|
|
|
for (final stats in dailyStats) {
|
|
totalTranslationCalls += stats.translationApiCalls;
|
|
totalAsrCalls += stats.asrApiCalls;
|
|
totalTtsCalls += stats.ttsApiCalls;
|
|
totalAiSummaryCalls += stats.aiSummaryApiCalls;
|
|
totalSourceChars += stats.sourceCharacterCount;
|
|
totalTargetChars += stats.targetCharacterCount;
|
|
totalRecordingDuration += stats.audioRecordingDuration;
|
|
totalTtsPlayDuration += stats.ttsPlaybackDuration;
|
|
totalSessions += stats.sessionCount;
|
|
// totalCost += stats.estimatedCost;
|
|
|
|
// 合并语言对使用统计
|
|
stats.languagePairUsage.forEach((key, value) {
|
|
combinedLanguagePairUsage[key] =
|
|
(combinedLanguagePairUsage[key] ?? 0) + value;
|
|
});
|
|
|
|
// 合并模式使用统计
|
|
stats.modeUsage.forEach((key, value) {
|
|
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value;
|
|
});
|
|
}
|
|
|
|
return TranslationUsageStats(
|
|
id: 'monthly_${year}_$month',
|
|
date: startDate,
|
|
userId: getCurrentUserId(),
|
|
timeGranularity: 'month',
|
|
translationApiCalls: totalTranslationCalls,
|
|
asrApiCalls: totalAsrCalls,
|
|
ttsApiCalls: totalTtsCalls,
|
|
aiSummaryApiCalls: totalAiSummaryCalls,
|
|
sourceTextCharacters: totalSourceChars,
|
|
effectiveAudioDuration: totalRecordingDuration,
|
|
ttsTextCharacters: totalTargetChars,
|
|
totalSessionDuration: totalTtsPlayDuration,
|
|
sessionCount: totalSessions,
|
|
// estimatedCost: totalCost,
|
|
);
|
|
} catch (e) {
|
|
Logger.error('获取月度统计失败: $e');
|
|
return TranslationUsageStats.empty(getCurrentUserId());
|
|
}
|
|
}
|
|
}
|
|
|