Pārlūkot izejas kodu

feat: 添加统计日志上传

Tony 3 mēneši atpakaļ
vecāks
revīzija
e90494c4a1

+ 133 - 0
lib/app/controllers/windows/stat_log_entry.dart

@@ -0,0 +1,133 @@
+import 'dart:convert';
+
+/// 解析 JSON 字符串
+StatLogEntry statLogEntryFromJson(String str) =>
+    StatLogEntry.fromJson(json.decode(str));
+
+/// 序列化为 JSON 字符串
+String statLogEntryToJson(StatLogEntry data) => json.encode(data.toJson());
+
+class StatLogEntry {
+  final String? id;
+  final int? time;
+  final String? level;
+  final String? module;
+  final String? category;
+  final Fields? fields;
+
+  StatLogEntry({
+    this.id,
+    this.time,
+    this.level,
+    this.module,
+    this.category,
+    this.fields,
+  });
+
+  factory StatLogEntry.fromJson(Map<String, dynamic> json) => StatLogEntry(
+    id: json["id"],
+    time: json["time"],
+    level: json["level"],
+    module: json["module"],
+    category: json["category"],
+    fields: json["fields"] == null ? null : Fields.fromJson(json["fields"]),
+  );
+
+  Map<String, dynamic> toJson() => {
+    "id": id,
+    "time": time,
+    "level": level,
+    "module": module,
+    "category": category,
+    "fields": fields?.toJson(),
+  };
+}
+
+class Fields {
+  final int? code;
+  final String? boostSessionId;
+  final bool? success;
+  final int? locationId;
+  final List<ConnectionHistory>? connectionHistory;
+  final String? locationCode;
+  final int? generatedTime;
+
+  Fields({
+    this.code,
+    this.boostSessionId,
+    this.success,
+    this.locationId,
+    this.connectionHistory,
+    this.locationCode,
+    this.generatedTime,
+  });
+
+  factory Fields.fromJson(Map<String, dynamic> json) => Fields(
+    code: json["code"],
+    boostSessionId: json["boostSessionId"],
+    success: json["success"],
+    locationId: json["locationId"],
+    connectionHistory: json["connectionHistory"] == null
+        ? []
+        : List<ConnectionHistory>.from(
+            json["connectionHistory"]!.map(
+              (x) => ConnectionHistory.fromJson(x),
+            ),
+          ),
+    locationCode: json["locationCode"],
+    generatedTime: json["generatedTime"],
+  );
+
+  Map<String, dynamic> toJson() => {
+    "code": code,
+    "boostSessionId": boostSessionId,
+    "success": success,
+    "locationId": locationId,
+    "connectionHistory": connectionHistory == null
+        ? []
+        : List<dynamic>.from(connectionHistory!.map((x) => x.toJson())),
+    "locationCode": locationCode,
+    "generatedTime": generatedTime,
+  };
+}
+
+class ConnectionHistory {
+  final String? firstHop;
+  final int? duration;
+  final String? address;
+  final int? code;
+  final int? startTime;
+  final int? endTime;
+  final String? nodeId;
+
+  ConnectionHistory({
+    this.firstHop,
+    this.duration,
+    this.address,
+    this.code,
+    this.startTime,
+    this.endTime,
+    this.nodeId,
+  });
+
+  factory ConnectionHistory.fromJson(Map<String, dynamic> json) =>
+      ConnectionHistory(
+        firstHop: json["firstHop"],
+        duration: json["duration"],
+        address: json["address"],
+        code: json["code"],
+        startTime: json["startTime"],
+        endTime: json["endTime"],
+        nodeId: json["nodeId"],
+      );
+
+  Map<String, dynamic> toJson() => {
+    "firstHop": firstHop,
+    "duration": duration,
+    "address": address,
+    "code": code,
+    "startTime": startTime,
+    "endTime": endTime,
+    "nodeId": nodeId,
+  };
+}

+ 21 - 4
lib/app/controllers/windows/vpn_service.dart

@@ -94,8 +94,8 @@ class VpnService {
         _accessKeySecret,
       );
 
-      log(_tag, '[VpnService] url:$url');
-      log(_tag, '[VpnService] >>:$data');
+      log(_tag, 'url:$url');
+      log(_tag, '>>:$data');
 
       final response = await _rpcDio.post(
         url,
@@ -109,7 +109,7 @@ class VpnService {
         ),
       );
 
-      log(_tag, '[VpnService] <<:${response.data}');
+      log(_tag, '<<:${response.data}');
 
       if (response.statusCode != 200) {
         log(_tag, 'rpcPost HTTP error: ${response.statusCode}');
@@ -246,6 +246,23 @@ class VpnService {
     }
   }
 
+  Future<Map<String, dynamic>?> queryStat() async {
+    try {
+      final result = await _rpcPost('stat', {
+        "stat_types": [1, 2, 3],
+      });
+      if (!result.success) {
+        log(_tag, 'queryStat error: ${result.message}');
+        return null;
+      }
+      final json = result.data as Map<String, dynamic>;
+      return json['data'];
+    } catch (e) {
+      log(_tag, 'queryStat error: $e');
+      return null;
+    }
+  }
+
   Future<void> _startup({bool debug = false}) async {
     log(_tag, 'startup called. debug: $debug');
     _vpnDebug = debug;
@@ -276,7 +293,7 @@ class VpnService {
                 }
                 break;
               case VpnMessageType.heartbeat:
-                log(_tag, 'heartbeat message got');
+                //log(_tag, 'heartbeat message got');
                 // 应答心跳
                 webSocket.sink.add(
                   VpnMessage.create(VpnMessageType.heartbeat, null).toJson(),

+ 81 - 9
lib/app/controllers/windows_core_api.dart

@@ -7,6 +7,7 @@ import 'package:get/get.dart';
 import 'package:nomo/app/controllers/windows/window_service.dart';
 import 'package:path/path.dart' as path;
 import 'package:path_provider/path_provider.dart';
+import 'package:uuid/uuid.dart';
 
 import '../../config/translations/strings_enum.dart';
 import '../../utils/log/logger.dart';
@@ -16,6 +17,7 @@ import '../constants/errors.dart';
 import 'base_core_api.dart';
 import 'windows/menu_base/src/menu.dart';
 import 'windows/menu_base/src/menu_item.dart';
+import 'windows/stat_log_entry.dart';
 import 'windows/vpn_service.dart';
 
 /// Windows 实现
@@ -46,6 +48,11 @@ class WindowsCoreApi implements BaseCoreApi {
   int _sessionCountUp = 0; // session计时
   bool _isCountdown = false; // 汇报计时类型 倒计时还是正计时
 
+  int _locationId = 0;
+  String _locationCode = '';
+  String _boostSessionId = '';
+  int _peekTimeInterval = 0;
+
   /// 内部构造方法,供 BaseCoreApi 工厂使用
   factory WindowsCoreApi.create() => WindowsCoreApi._();
 
@@ -150,16 +157,82 @@ class WindowsCoreApi implements BaseCoreApi {
       '{"type":"vpn_status","status":2,"code":0,"message":""}',
     );
 
-    // TODO: 汇报日志
-    // _eventController.add(
-    //   '{"type":"boost_result","locationCode":"US","nodeId":"xxx","success":true}',
-    // );
+    // 获取统计
+    _vpn.queryStat().then((stat) {
+      log(_tag, 'stat: $stat');
+      try {
+        final ts = DateTime.now().millisecondsSinceEpoch;
+        final connectionHistory = (stat?['connectionHistory'] as List?)?.last;
+
+        final param = statLogEntryToJson(
+          StatLogEntry(
+            id: Uuid().v4(),
+            time: ts,
+            level: 'info',
+            module: 'NM_BoostResult',
+            category: 'nomo',
+            fields: Fields(
+              code: 0,
+              boostSessionId: _boostSessionId,
+              success: true,
+              locationId: _locationId,
+              locationCode: _locationCode,
+              generatedTime: ts,
+              connectionHistory: connectionHistory,
+            ),
+          ),
+        );
+
+        final nodeId = connectionHistory['nodeId'] ?? 0;
+        final msg =
+            '{"type":"boost_result","locationCode":"$_locationCode","nodeId":"$nodeId","success":true, "param": "$param"}';
+
+        _eventController.add(msg);
+      } catch (e) {
+        log(_tag, 'parse stat json error: $e');
+      }
+    });
   }
 
   void _handleStateError(int code) {
     _eventController.add(
       '{"type":"vpn_status","status":3,"code":$code,"message":""}',
     );
+    // 获取统计
+    _vpn.queryStat().then((stat) {
+      log(_tag, 'stat: $stat');
+      try {
+        final ts = DateTime.now().millisecondsSinceEpoch;
+        final connectionHistory = (stat?['connectionHistory'] as List?)?.last;
+
+        final param = statLogEntryToJson(
+          StatLogEntry(
+            id: Uuid().v4(),
+            time: ts,
+            level: 'info',
+            module: 'NM_BoostResult',
+            category: 'nomo',
+            fields: Fields(
+              code: code,
+              boostSessionId: _boostSessionId,
+              success: false,
+              locationId: _locationId,
+              locationCode: _locationCode,
+              generatedTime: ts,
+              connectionHistory: connectionHistory,
+            ),
+          ),
+        );
+
+        final nodeId = connectionHistory['nodeId'] ?? 0;
+        final msg =
+            '{"type":"boost_result","locationCode":"$_locationCode","nodeId":"$nodeId","success":false, "param": "$param"}';
+
+        _eventController.add(msg);
+      } catch (e) {
+        log(_tag, 'parse stat json error: $e');
+      }
+    });
     _vpn.stop();
   }
 
@@ -176,11 +249,6 @@ class WindowsCoreApi implements BaseCoreApi {
         '{"type":"vpn_status","status":0,"code":0,"message":""}',
       );
     }
-
-    // TODO: 汇报日志
-    // _eventController.add(
-    //   '{"type":"boost_result","locationCode":"US","nodeId":"xxx","success":true}',
-    // );
   }
 
   void _checkMembershipRemaining() {
@@ -265,6 +333,10 @@ class WindowsCoreApi implements BaseCoreApi {
     // 记录会员剩余时间
     _remainTime = remainTime;
     _isCountdown = isCountdown;
+    _locationId = locationId;
+    _locationCode = locationCode;
+    _boostSessionId = sessionId;
+    _peekTimeInterval = peekTimeInterval;
 
     String geoPath = await _getGeoDirectory();