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.
519 lines
18 KiB
519 lines
18 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';
|
|
|
|
/// 翻译使用量统计服务
|
|
class TranslationUsageService extends GetxService {
|
|
static const String _storageKey = 'translation_usage_stats';
|
|
static const String _dailyStatsKey = 'daily_translation_stats';
|
|
static const String _costConfigKey = 'translation_cost_config';
|
|
|
|
final GetStorage _storage = GetStorage();
|
|
|
|
// 实时统计数据
|
|
final Rx<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs;
|
|
|
|
// 当日统计数据
|
|
final Rx<TranslationUsageStats> todayStats = TranslationUsageStats.empty('').obs;
|
|
|
|
// 成本配置
|
|
final RxMap<String, double> costConfig = <String, double>{}.obs;
|
|
|
|
// 预警阈值
|
|
final RxDouble dailyCostThreshold = 10.0.obs; // 日成本预警阈值
|
|
final RxDouble monthlyCostThreshold = 300.0.obs; // 月成本预警阈值
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initializeService();
|
|
}
|
|
|
|
/// 初始化服务
|
|
void _initializeService() {
|
|
_loadCostConfig();
|
|
_loadTodayStats();
|
|
Logger.info('翻译使用量统计服务初始化完成');
|
|
}
|
|
|
|
/// 加载成本配置
|
|
void _loadCostConfig() {
|
|
try {
|
|
final configData = _storage.read(_costConfigKey);
|
|
if (configData != null) {
|
|
costConfig.value = Map<String, double>.from(configData);
|
|
} else {
|
|
// 设置默认成本配置(每千字符的成本,单位:元)
|
|
costConfig.value = {
|
|
'translation_per_1k_chars': 0.05, // 翻译:每千字符0.05元
|
|
'asr_per_minute': 0.02, // ASR:每分钟0.02元
|
|
'tts_per_1k_chars': 0.03, // TTS:每千字符0.03元
|
|
'ai_summary_per_request': 0.01, // AI摘要:每次请求0.01元
|
|
};
|
|
_saveCostConfig();
|
|
}
|
|
} catch (e) {
|
|
Logger.error('加载成本配置失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 保存成本配置
|
|
void _saveCostConfig() {
|
|
try {
|
|
_storage.write(_costConfigKey, costConfig.value);
|
|
} catch (e) {
|
|
Logger.error('保存成本配置失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 加载当日统计数据
|
|
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,
|
|
cost: existing.cost + _calculateTranslationCost(sourceText.length),
|
|
);
|
|
} else {
|
|
newLanguagePairStats[languagePair] = LanguagePairStats(
|
|
languagePair: languagePair,
|
|
translationCount: 1,
|
|
sourceCharacters: sourceText.length,
|
|
cost: _calculateTranslationCost(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,
|
|
cost: existing.cost + _calculateTranslationCost(sourceText.length),
|
|
);
|
|
} else {
|
|
newModeStats[mode] = TranslationModeStats(
|
|
apiCalls: 1,
|
|
sourceCharacters: sourceText.length,
|
|
ttsCharacters: targetText.length,
|
|
cost: _calculateTranslationCost(sourceText.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,
|
|
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
|
|
translationApiCalls: todayStats.value.translationApiCalls + 1,
|
|
sourceTextCharacters: todayStats.value.sourceTextCharacters + sourceText.length,
|
|
ttsTextCharacters: todayStats.value.ttsTextCharacters + targetText.length,
|
|
)),
|
|
);
|
|
|
|
_saveTodayStats();
|
|
_checkCostThreshold();
|
|
|
|
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,
|
|
cost: existing.cost + _calculateAsrCost(effectiveDurationSeconds),
|
|
);
|
|
} else {
|
|
newModeStats[mode] = TranslationModeStats(
|
|
effectiveAudioDuration: effectiveDurationSeconds,
|
|
cost: _calculateAsrCost(effectiveDurationSeconds),
|
|
);
|
|
}
|
|
|
|
// 更新当日统计
|
|
todayStats.value = todayStats.value.copyWith(
|
|
asrApiCalls: todayStats.value.asrApiCalls + 1,
|
|
effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
|
|
totalSessionDuration: todayStats.value.totalSessionDuration + totalDurationSeconds,
|
|
modeStats: newModeStats,
|
|
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
|
|
asrApiCalls: todayStats.value.asrApiCalls + 1,
|
|
effectiveAudioDuration: todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
|
|
)),
|
|
);
|
|
|
|
_saveTodayStats();
|
|
|
|
Logger.info('ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode');
|
|
} catch (e) {
|
|
Logger.error('记录ASR API调用失败: $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,
|
|
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
|
|
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
|
|
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
|
|
)),
|
|
);
|
|
|
|
_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,
|
|
estimatedCost: _calculateEstimatedCost(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('开始新的翻译会话');
|
|
}
|
|
|
|
/// 计算预估成本 - 优化版(基于精确计费参数)
|
|
double _calculateEstimatedCost(TranslationUsageStats stats) {
|
|
double cost = 0.0;
|
|
|
|
// 翻译成本:基于源文本字符数
|
|
cost += (stats.sourceTextCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05);
|
|
|
|
// ASR成本:基于有效音频时长
|
|
cost += (stats.effectiveAudioDuration / 60) * (costConfig['asr_per_minute'] ?? 0.02);
|
|
|
|
// TTS成本:基于TTS文本字符数
|
|
cost += (stats.ttsTextCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03);
|
|
|
|
// AI摘要成本(已废弃但保留兼容性)
|
|
cost += stats.aiSummaryApiCalls * (costConfig['ai_summary_per_request'] ?? 0.01);
|
|
|
|
return cost;
|
|
}
|
|
|
|
/// 计算翻译成本(单次调用)
|
|
double _calculateTranslationCost(int sourceCharacters) {
|
|
return (sourceCharacters / 1000) * (costConfig['translation_per_1k_chars'] ?? 0.05);
|
|
}
|
|
|
|
/// 计算ASR成本(单次调用)
|
|
double _calculateAsrCost(int durationSeconds) {
|
|
return (durationSeconds / 60) * (costConfig['asr_per_minute'] ?? 0.02);
|
|
}
|
|
|
|
/// 计算TTS成本(单次调用)
|
|
double _calculateTtsCost(int textCharacters) {
|
|
return (textCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03);
|
|
}
|
|
|
|
/// 保存当日统计数据
|
|
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');
|
|
}
|
|
}
|
|
|
|
/// 检查成本阈值
|
|
void _checkCostThreshold() {
|
|
final todayCost = todayStats.value.estimatedCost;
|
|
|
|
if (todayCost >= dailyCostThreshold.value) {
|
|
_showCostWarning('今日翻译成本已达到预警阈值:¥${todayCost.toStringAsFixed(2)}');
|
|
}
|
|
}
|
|
|
|
/// 显示成本预警
|
|
void _showCostWarning(String message) {
|
|
Get.snackbar(
|
|
'成本预警',
|
|
message,
|
|
snackPosition: SnackPosition.TOP,
|
|
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
|
|
colorText: Get.theme.colorScheme.error,
|
|
duration: const Duration(seconds: 5),
|
|
);
|
|
}
|
|
|
|
/// 获取历史统计数据
|
|
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');
|
|
}
|
|
}
|
|
|
|
/// 更新成本配置
|
|
void updateCostConfig(Map<String, double> newConfig) {
|
|
costConfig.value = newConfig;
|
|
_saveCostConfig();
|
|
|
|
// 重新计算当日成本
|
|
todayStats.value = todayStats.value.copyWith(
|
|
estimatedCost: _calculateEstimatedCost(todayStats.value),
|
|
);
|
|
_saveTodayStats();
|
|
|
|
Logger.info('更新成本配置完成');
|
|
}
|
|
|
|
/// 更新预警阈值
|
|
void updateThresholds({
|
|
double? dailyThreshold,
|
|
double? monthlyThreshold,
|
|
}) {
|
|
if (dailyThreshold != null) {
|
|
dailyCostThreshold.value = dailyThreshold;
|
|
}
|
|
if (monthlyThreshold != null) {
|
|
monthlyCostThreshold.value = monthlyThreshold;
|
|
}
|
|
|
|
// 保存阈值配置
|
|
_storage.write('daily_cost_threshold', dailyCostThreshold.value);
|
|
_storage.write('monthly_cost_threshold', monthlyCostThreshold.value);
|
|
|
|
Logger.info('更新预警阈值完成');
|
|
}
|
|
|
|
/// 获取月度统计汇总
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|