api_statistics.dart 15 KB

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