api_statistics.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import 'dart:async';
  2. import 'dart:collection';
  3. import 'dart:convert';
  4. import 'dart:io';
  5. import 'package:get/get.dart';
  6. import 'package:path_provider/path_provider.dart';
  7. import 'package:uuid/uuid.dart';
  8. import '../app/api/core/api_core_paths.dart';
  9. import '../app/constants/configs.dart';
  10. import '../app/constants/enums.dart';
  11. import '../app/controllers/api_controller.dart';
  12. import '../app/data/sp/ix_sp.dart';
  13. import 'log/logger.dart';
  14. /// API请求统计记录
  15. class ApiStatRecord {
  16. final String id;
  17. final String domain;
  18. final String path;
  19. final bool success;
  20. final int errorCode;
  21. final String errorMessage;
  22. final int apiRequestTime;
  23. final int apiResponseTime;
  24. ApiStatRecord({
  25. required this.id,
  26. required this.domain,
  27. required this.path,
  28. required this.success,
  29. required this.errorCode,
  30. required this.errorMessage,
  31. required this.apiRequestTime,
  32. required this.apiResponseTime,
  33. });
  34. /// 从JSON创建
  35. factory ApiStatRecord.fromJson(Map<String, dynamic> json) {
  36. return ApiStatRecord(
  37. id: json['id'] ?? '',
  38. domain: json['domain'] ?? '',
  39. path: json['path'] ?? '',
  40. success: json['success'] ?? false,
  41. errorCode: json['errorCode'] ?? 0,
  42. errorMessage: json['errorMessage'] ?? '',
  43. apiRequestTime: json['apiRequestTime'] ?? 0,
  44. apiResponseTime: json['apiResponseTime'] ?? 0,
  45. );
  46. }
  47. /// 转换为JSON
  48. Map<String, dynamic> toJson() {
  49. return {
  50. 'id': id,
  51. 'domain': domain,
  52. 'path': path,
  53. 'success': success,
  54. 'errorCode': errorCode,
  55. 'errorMessage': errorMessage,
  56. 'apiRequestTime': apiRequestTime,
  57. 'apiResponseTime': apiResponseTime,
  58. };
  59. }
  60. @override
  61. String toString() {
  62. return jsonEncode(toJson());
  63. }
  64. }
  65. /// API统计管理器
  66. ///
  67. /// 功能:
  68. /// - 记录 API 请求的成功/失败统计
  69. /// - 最大保存 100 条记录(FIFO)
  70. /// - 支持文件持久化存储
  71. /// - 批量写入优化(每 10 条)
  72. class ApiStatistics {
  73. // 单例模式
  74. static final ApiStatistics _instance = ApiStatistics._internal();
  75. factory ApiStatistics() => _instance;
  76. ApiStatistics._internal();
  77. static ApiStatistics get instance => _instance;
  78. static const String TAG = 'ApiStatistics';
  79. // 最大保存记录数
  80. static const int maxRecords = 100;
  81. // 批量写入阈值
  82. static const int _batchWriteThreshold = 10;
  83. // 文件名
  84. static const String _fileName = 'api_statistics.json';
  85. // 使用Queue来保持FIFO顺序
  86. final Queue<ApiStatRecord> _records = Queue<ApiStatRecord>();
  87. // UUID生成器
  88. final Uuid _uuid = const Uuid();
  89. // 操作锁,防止在遍历时修改
  90. bool _isOperating = false;
  91. // 是否已初始化
  92. bool _isInitialized = false;
  93. // 未保存的记录数
  94. int _unsavedCount = 0;
  95. // 是否正在写入文件
  96. bool _isWriting = false;
  97. // 文件路径缓存
  98. String? _filePath;
  99. // 禁用的日志模块缓存
  100. Set<String> _disabledModules = {};
  101. /// 初始化(需要在 App 启动时调用)
  102. Future<void> initialize() async {
  103. if (_isInitialized) return;
  104. try {
  105. // 获取文件路径
  106. _filePath = await _getFilePath();
  107. // 从文件加载数据
  108. await _loadFromFile();
  109. _isInitialized = true;
  110. log(TAG, 'ApiStatistics initialized, loaded ${_records.length} records');
  111. } catch (e) {
  112. log(TAG, 'ApiStatistics initialize error: $e');
  113. }
  114. }
  115. /// 更新禁用模块列表(在 launch 数据更新后调用)
  116. void updateDisabledModules() {
  117. try {
  118. final launch = IXSP.getLaunch();
  119. _disabledModules = (launch?.appConfig?.disabledLogModules ?? []).toSet();
  120. log(TAG, 'ApiStatistics disabled modules updated: $_disabledModules');
  121. } catch (e) {
  122. log(TAG, 'ApiStatistics updateDisabledModules error: $e');
  123. }
  124. }
  125. /// 检查模块是否被禁用
  126. bool _isModuleDisabled(String path) {
  127. var moduleName = LogModule.NM_ApiOtherLog.name;
  128. if (path == ApiCorePaths.launch) {
  129. moduleName = LogModule.NM_ApiLaunchLog.name;
  130. } else if (path == ApiCorePaths.getDispatchInfo) {
  131. moduleName = LogModule.NM_ApiRouterLog.name;
  132. }
  133. return _disabledModules.contains(moduleName);
  134. }
  135. /// 获取文件路径
  136. Future<String> _getFilePath() async {
  137. if (_filePath != null) return _filePath!;
  138. final directory = await getApplicationDocumentsDirectory();
  139. _filePath = '${directory.path}/$_fileName';
  140. return _filePath!;
  141. }
  142. /// 从文件加载数据
  143. Future<void> _loadFromFile() async {
  144. try {
  145. final filePath = await _getFilePath();
  146. final file = File(filePath);
  147. if (!await file.exists()) {
  148. log(TAG, 'ApiStatistics file not exists, skip loading');
  149. return;
  150. }
  151. final content = await file.readAsString();
  152. if (content.isEmpty) return;
  153. final List<dynamic> jsonList = jsonDecode(content);
  154. _records.clear();
  155. for (final json in jsonList) {
  156. if (json is Map<String, dynamic>) {
  157. _records.addLast(ApiStatRecord.fromJson(json));
  158. }
  159. }
  160. // 确保不超过最大数量
  161. while (_records.length > maxRecords) {
  162. _records.removeFirst();
  163. }
  164. log(TAG, 'ApiStatistics loaded ${_records.length} records from file');
  165. } catch (e) {
  166. log(TAG, 'ApiStatistics load error: $e');
  167. }
  168. }
  169. /// 保存到文件
  170. Future<void> _saveToFile() async {
  171. if (_isWriting) return;
  172. if (_unsavedCount == 0) return;
  173. _isWriting = true;
  174. try {
  175. // 更新禁用模块列表
  176. updateDisabledModules();
  177. final filePath = await _getFilePath();
  178. final file = File(filePath);
  179. // 过滤掉被禁用模块的记录
  180. _filterDisabledRecords();
  181. // 获取记录副本
  182. final records = getRecordsJson();
  183. // 写入文件
  184. await file.writeAsString(jsonEncode(records));
  185. _unsavedCount = 0;
  186. log(TAG, 'ApiStatistics saved ${records.length} records to file');
  187. } catch (e) {
  188. log(TAG, 'ApiStatistics save error: $e');
  189. } finally {
  190. _isWriting = false;
  191. }
  192. }
  193. /// 过滤掉被禁用模块的记录
  194. void _filterDisabledRecords() {
  195. if (_disabledModules.isEmpty) return;
  196. final toRemove = <ApiStatRecord>[];
  197. for (final record in _records) {
  198. if (_isModuleDisabled(record.path)) {
  199. toRemove.add(record);
  200. }
  201. }
  202. for (final record in toRemove) {
  203. _records.remove(record);
  204. }
  205. if (toRemove.isNotEmpty) {
  206. log(
  207. TAG,
  208. 'ApiStatistics filtered out ${toRemove.length} disabled module records',
  209. );
  210. }
  211. }
  212. /// 检查是否需要保存
  213. void _checkAndSave() {
  214. if (_unsavedCount >= _batchWriteThreshold) {
  215. _saveToFile();
  216. }
  217. }
  218. /// 添加一条成功记录
  219. void addSuccess({
  220. required String domain,
  221. required String path,
  222. required int apiRequestTime,
  223. required int apiResponseTime,
  224. int errorCode = 200,
  225. }) {
  226. _addRecord(
  227. domain: domain,
  228. path: path,
  229. success: true,
  230. errorCode: errorCode,
  231. errorMessage: '',
  232. apiRequestTime: apiRequestTime,
  233. apiResponseTime: apiResponseTime,
  234. );
  235. }
  236. /// 添加一条失败记录
  237. void addFailure({
  238. required String domain,
  239. required String path,
  240. required int apiRequestTime,
  241. required int apiResponseTime,
  242. required int errorCode,
  243. required String errorMessage,
  244. }) {
  245. _addRecord(
  246. domain: domain,
  247. path: path,
  248. success: false,
  249. errorCode: errorCode,
  250. errorMessage: errorMessage,
  251. apiRequestTime: apiRequestTime,
  252. apiResponseTime: apiResponseTime,
  253. );
  254. }
  255. /// 内部添加记录方法(同步,线程安全)
  256. void _addRecord({
  257. required String domain,
  258. required String path,
  259. required bool success,
  260. required int errorCode,
  261. required String errorMessage,
  262. required int apiRequestTime,
  263. required int apiResponseTime,
  264. }) {
  265. // 如果正在进行其他操作,延迟添加
  266. if (_isOperating) {
  267. scheduleMicrotask(
  268. () => _addRecord(
  269. domain: domain,
  270. path: path,
  271. success: success,
  272. errorCode: errorCode,
  273. errorMessage: errorMessage,
  274. apiRequestTime: apiRequestTime,
  275. apiResponseTime: apiResponseTime,
  276. ),
  277. );
  278. return;
  279. }
  280. final record = ApiStatRecord(
  281. id: _uuid.v4(),
  282. domain: domain,
  283. path: path,
  284. success: success,
  285. errorCode: errorCode,
  286. errorMessage: errorMessage,
  287. apiRequestTime: apiRequestTime,
  288. apiResponseTime: apiResponseTime,
  289. );
  290. _records.addLast(record);
  291. _unsavedCount++;
  292. // 超出最大数量时,移除最早的记录
  293. while (_records.length > maxRecords) {
  294. _records.removeFirst();
  295. }
  296. // 检查是否需要保存
  297. _checkAndSave();
  298. }
  299. /// 获取所有记录(返回副本,避免并发修改问题)
  300. List<ApiStatRecord> getRecords() {
  301. _isOperating = true;
  302. try {
  303. return List<ApiStatRecord>.from(_records);
  304. } finally {
  305. _isOperating = false;
  306. }
  307. }
  308. /// 获取所有记录的JSON列表(返回副本)
  309. List<Map<String, dynamic>> getRecordsJson() {
  310. _isOperating = true;
  311. try {
  312. return _records.map((r) => r.toJson()).toList();
  313. } finally {
  314. _isOperating = false;
  315. }
  316. }
  317. /// 获取所有记录的JSON字符串
  318. String getRecordsJsonString({bool pretty = false}) {
  319. final json = getRecordsJson();
  320. if (pretty) {
  321. return const JsonEncoder.withIndent(' ').convert(json);
  322. }
  323. return jsonEncode(json);
  324. }
  325. /// 获取成功记录数
  326. int get successCount {
  327. final records = getRecords();
  328. return records.where((r) => r.success).length;
  329. }
  330. /// 获取失败记录数
  331. int get failureCount {
  332. final records = getRecords();
  333. return records.where((r) => !r.success).length;
  334. }
  335. /// 获取总记录数
  336. int get totalCount => _records.length;
  337. /// 获取成功率
  338. double get successRate {
  339. final total = totalCount;
  340. if (total == 0) return 0.0;
  341. return successCount / total;
  342. }
  343. /// 获取平均响应时间(毫秒)
  344. double get averageResponseTime {
  345. final records = getRecords();
  346. if (records.isEmpty) return 0.0;
  347. final totalTime = records.fold<int>(0, (sum, r) => sum + r.apiResponseTime);
  348. return totalTime / records.length;
  349. }
  350. /// 清空所有记录
  351. Future<void> clear() async {
  352. _isOperating = true;
  353. try {
  354. _records.clear();
  355. _unsavedCount = 0;
  356. // 清空文件
  357. await _saveToFile();
  358. } finally {
  359. _isOperating = false;
  360. }
  361. }
  362. /// 移除已上传的记录,保留上传过程中新增的记录
  363. Future<void> _removeUploadedRecords(
  364. List<ApiStatRecord> uploadedRecords,
  365. ) async {
  366. _isOperating = true;
  367. try {
  368. // 获取已上传记录的 ID 集合
  369. final uploadedIds = uploadedRecords.map((r) => r.id).toSet();
  370. // 移除已上传的记录
  371. _records.removeWhere((r) => uploadedIds.contains(r.id));
  372. _unsavedCount = _records.length;
  373. // 保存剩余记录到文件(包括上传过程中新增的记录)
  374. await _forceSaveToFile();
  375. } finally {
  376. _isOperating = false;
  377. }
  378. }
  379. /// 强制保存到文件(忽略 _unsavedCount 检查)
  380. Future<void> _forceSaveToFile() async {
  381. if (_isWriting) return;
  382. _isWriting = true;
  383. try {
  384. final filePath = await _getFilePath();
  385. final file = File(filePath);
  386. final records = _records.map((r) => r.toJson()).toList();
  387. await file.writeAsString(jsonEncode(records));
  388. _unsavedCount = 0;
  389. log(TAG, 'ApiStatistics force saved ${records.length} records to file');
  390. } catch (e) {
  391. log(TAG, 'ApiStatistics force save error: $e');
  392. } finally {
  393. _isWriting = false;
  394. }
  395. }
  396. /// 获取最近N条记录(返回副本)
  397. List<ApiStatRecord> getRecentRecords(int count) {
  398. final records = getRecords();
  399. if (count >= records.length) {
  400. return records;
  401. }
  402. return records.sublist(records.length - count);
  403. }
  404. /// 获取统计摘要
  405. Map<String, dynamic> getSummary() {
  406. final records = getRecords();
  407. final total = records.length;
  408. final success = records.where((r) => r.success).length;
  409. final failure = total - success;
  410. final rate = total > 0 ? success / total : 0.0;
  411. final avgTime = total > 0
  412. ? records.fold<int>(0, (sum, r) => sum + r.apiResponseTime) / total
  413. : 0.0;
  414. return {
  415. 'totalCount': total,
  416. 'successCount': success,
  417. 'failureCount': failure,
  418. 'successRate': '${(rate * 100).toStringAsFixed(2)}%',
  419. 'averageResponseTime': '${avgTime.toStringAsFixed(2)}ms',
  420. };
  421. }
  422. /// 强制保存到文件(App 进入后台或退出时调用)
  423. Future<void> flush() async {
  424. await _saveToFile();
  425. }
  426. /// 释放资源(App 退出时调用)
  427. Future<void> dispose() async {
  428. await _saveToFile();
  429. _isInitialized = false;
  430. }
  431. /// App 进入后台时调用(不阻塞,fire-and-forget)
  432. void onAppPaused() {
  433. // 异步保存,不等待结果
  434. _saveToFile();
  435. }
  436. /// App 进入前台时调用(不阻塞,fire-and-forget)
  437. Future<List<Map<String, dynamic>>> onAppResumed() async {
  438. await _loadFromFile();
  439. return await _uploadAndClear();
  440. }
  441. /// 上传统计数据并清空
  442. Future<List<Map<String, dynamic>>> _uploadAndClear() async {
  443. try {
  444. // 先更新禁用模块列表
  445. updateDisabledModules();
  446. final records = getRecords();
  447. if (records.isEmpty) return [];
  448. // 检查模块是否禁用
  449. final isLaunchDisabled = _disabledModules.contains(
  450. LogModule.NM_ApiLaunchLog.name,
  451. );
  452. final isRouterDisabled = _disabledModules.contains(
  453. LogModule.NM_ApiRouterLog.name,
  454. );
  455. final isOtherDisabled = _disabledModules.contains(
  456. LogModule.NM_ApiOtherLog.name,
  457. );
  458. // 过滤掉禁用模块的记录
  459. final enabledRecords = records.where((record) {
  460. if (record.path == ApiCorePaths.launch) {
  461. return !isLaunchDisabled;
  462. } else if (record.path == ApiCorePaths.getDispatchInfo) {
  463. return !isRouterDisabled;
  464. } else {
  465. return !isOtherDisabled;
  466. }
  467. }).toList();
  468. // 移除已上传的记录,保留上传过程中新增的记录(如上传接口本身的记录)
  469. await _removeUploadedRecords(records);
  470. log(
  471. TAG,
  472. 'ApiStatistics uploaded ${enabledRecords.length}/${records.length} records '
  473. '(launch disabled: $isLaunchDisabled, router disabled: $isRouterDisabled)',
  474. );
  475. // 合并上传(一次接口调用)
  476. if (enabledRecords.isNotEmpty) {
  477. final logs = _formatLogsForUpload(enabledRecords);
  478. log(TAG, 'ApiStatistics upload ${logs.length} records');
  479. return logs;
  480. }
  481. return [];
  482. } catch (e) {
  483. log(TAG, 'ApiStatistics upload error: $e');
  484. return [];
  485. }
  486. }
  487. /// 格式化日志数据用于上传
  488. List<Map<String, dynamic>> _formatLogsForUpload(List<ApiStatRecord> records) {
  489. return records.map((record) {
  490. var moduleName = LogModule.NM_ApiOtherLog.name;
  491. if (record.path == ApiCorePaths.launch) {
  492. moduleName = LogModule.NM_ApiLaunchLog.name;
  493. } else if (record.path == ApiCorePaths.getDispatchInfo) {
  494. moduleName = LogModule.NM_ApiRouterLog.name;
  495. }
  496. return {
  497. 'id': record.id,
  498. 'time': record.apiRequestTime,
  499. 'level': LogLevel.info.name,
  500. 'module': moduleName,
  501. 'category': Configs.productCode,
  502. 'fields': record.toJson(),
  503. };
  504. }).toList();
  505. }
  506. }