queue.dart 10.2 KB
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';

import 'package:archive/archive.dart';
import 'package:auto_track/auto_track/config/manager.dart';
import 'package:auto_track/auto_track/utils/track_model.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart' as path;
import 'package:sqflite/sqflite.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import '../log/logger.dart';

class AutoTrackQueue {
  static final AutoTrackQueue instance = AutoTrackQueue._();

  Database? database;
  Future<void> _lastTask = Future.value();

  void post(Future<void> Function() task) {
    _lastTask = _lastTask.then((_) => task());
  }

  AutoTrackQueue._() {
    httpClient.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true;

    //异步任务
    post(() async {
      await _checkInitDataBase();
    });
  }

  Future<void> _checkInitDataBase() async{
    if (database != null) {
      database = await openTrackDatabase();
    }
  }

  Future<Database> openTrackDatabase() async {
    final databasePath = await getDatabasesPath();
    final pathString = path.join(databasePath, 'track.db');
    return openDatabase(
      pathString,
      onCreate: (db, version) {
        return db.execute(
          "CREATE TABLE track("
          "id INTEGER PRIMARY KEY AUTOINCREMENT, "
          "event TEXT, "
          "date DATE"
          ")",
        );
      },
      version: 1,
    );
  }

  Timer? _timer;
  final httpClient = HttpClient();

  void appendQueue(TrackModel model) {
    post(() async {
      await _checkInitDataBase();
      await database!.insert(
          "track",
          Track(event: jsonEncode(model.toMap()), date: DateTime.now())
              .toMap());
    });
  }

  void start() {
    if (_timer != null) return;
    _timer = Timer.periodic(
        Duration(
            seconds: AutoTrackConfigManager.instance.config.uploadInterval ??
                10), (timer) {
      post(flush);
    });
  }

  void stop() {
    _timer?.cancel();
    _timer = null;
  }

  Future<void> flush() async {
    try {
      AutoTrackLogger.getInstance().debug("@@@start flush");
      await _checkInitDataBase();

      if (database == null) {
        AutoTrackLogger.getInstance().debug('数据库未初始化,跳过 flush');
        return;
      }

      final List<TrackModel> uploadList = [];

      List<Map<String, dynamic>> events = await database!.query("track",
          columns: ["id", "event", "date"], limit: 100);
      if (events.isEmpty) {
        AutoTrackLogger.getInstance().debug("@@@events is empty");
        return;
      }

      for (var event in events) {
        Track model = Track.fromMap(event);
        uploadList.add(TrackModel.fromMap(jsonDecode(model.event)));
      }

      final config = AutoTrackConfigManager.instance.config;
      final baseDeviceInfo = AutoTrackConfigManager.instance.baseDeviceInfo;
      final host = config.host;
      if (config.samplingRate != 1) {
        if (Random().nextDouble() > config.samplingRate) {
          // 不在采样范围不上传
          return;
        }
      }
      String? token = config.token;
      if (token == null) {
        AutoTrackConfigManager.instance.getToken(true);
        return;
      }
      ConnectivityResult connectivityResult =
          await Connectivity().checkConnectivity();

      Map<ConnectivityResult, String> map = {
        ConnectivityResult.mobile: 'MOBILE',
        ConnectivityResult.wifi: 'WIFI',
        ConnectivityResult.ethernet: 'ETHERNET',
        ConnectivityResult.vpn: 'VPN',
        ConnectivityResult.other: 'OTHER',
        ConnectivityResult.none: 'NONE',
      };

      if (host != null) {
        List<Map> datas = [];
        uploadList.forEach((event) {
          Random random = Random.secure();
          int id = random.nextInt(1 << 32); // 模拟 Java 的 nextInt()
          final t = DateTime.now().millisecondsSinceEpoch;

          String os = "";
          String os_version = "";
          String model = "";
          String manufacturer = "";

          if (baseDeviceInfo != null) {
            if (baseDeviceInfo is AndroidDeviceInfo) {
              os = 'android';
              os_version = baseDeviceInfo.version.release;
              model = baseDeviceInfo.model;
              manufacturer = baseDeviceInfo.manufacturer;
            } else if (baseDeviceInfo is IosDeviceInfo) {
              os = 'ios';
              os_version = baseDeviceInfo.systemVersion;
              model = baseDeviceInfo.model;
              manufacturer = 'apple';
            } else if (baseDeviceInfo is OhosDeviceInfo) {
              os = 'ohos';
              os_version = baseDeviceInfo.versionId ?? "";
              model = baseDeviceInfo.productModel ?? "";
              manufacturer = 'huawei';
            }
          }
          Size size = const Size(0, 0);
          if (config.buildContext != null) {
            size = Size(
                MediaQuery.of(config.buildContext!).size.width *
                    MediaQuery.of(config.buildContext!).devicePixelRatio,
                MediaQuery.of(config.buildContext!).size.width *
                    MediaQuery.of(config.buildContext!).devicePixelRatio);
          }

          final properties = {
            '\$os': os,
            '\$os_version': os_version,
            '\$model': model,
            '\$lib': "Flutter",
            '\$app_name': AutoTrackConfigManager.instance.appName,
            '\$app_version': AutoTrackConfigManager.instance.appVersion,
            '\$device_id': AutoTrackConfigManager.instance.deviceId,
            '\$timezone_offset': getZoneOffset(),
            "\$manufacturer": manufacturer,
            "\$screen_width": size.width.toInt(),
            "\$screen_height": size.width.toInt(),
            "\$app_id": AutoTrackConfigManager.instance.pkgName,
            "\$lib_version": "6.0.0",
            "\$network_type": map[connectivityResult],
            "\$wifi": connectivityResult == ConnectivityResult.wifi,

            // "$carrier": "NONE",
            // "$os_version": "13",
            // "$model": "C310CS",
            // "$brand": "BOE",
            // "$mac": "020000000000",
            // "$sn": "C310CS014820000006",
            // "$screen_orientation": "portrait",
            // "$screen_brightness": 204,
            // "$event_duration": 0,
            // "$lib_method": "autoTrack",
            // "$is_first_day": true
          };

          event.params.forEach((k, v) {
            properties[k] = v;
          });

          datas.add({
            '_track_id': id,
            'time': t,
            'type': 'track',
            'distinct_id':
                config.userId ?? AutoTrackConfigManager.instance.deviceId,
            'anonymous_id': AutoTrackConfigManager.instance.deviceId,
            'event': event.type,
            'properties': properties
          });

          AutoTrackLogger.getInstance().debug('upload => data => $datas');
        });

        try {
          final httpClient = HttpClient();
          final request = await httpClient.postUrl(Uri.parse(host + UPLOAD));

          request.headers
              .set(HttpHeaders.contentTypeHeader, "application/json");
          request.headers.set("token", token); // 设置 header

          // 对数据进行压缩并进行 Base64 编码
          final compressedData = encodeData(jsonEncode(datas));
          final jsonPayload = jsonEncode({"base64Str": compressedData});

          AutoTrackLogger.getInstance().debug("压缩数据:$jsonPayload");

          request.write(jsonPayload);
          final response = await request.close();

          final responseCode = response.statusCode;
          AutoTrackLogger.getInstance().debug("responseCode: $responseCode");

          final responseBody = await response.transform(utf8.decoder).join();

          if (responseCode >= HttpStatus.ok &&
              responseCode < HttpStatus.multipleChoices) {
            // 状态码 200 - 300 认为是成功

            AutoTrackLogger.getInstance().debug(
                'upload => success ret_code: $responseCode ret_content: $responseBody');

            try {
              final jsonResponse = jsonDecode(responseBody);
              if (jsonResponse["code"] == 4005) {
                AutoTrackConfigManager.instance.getToken(true);
              } else {
                //批量删除
                for (var event in events) {
                  await database!.delete("track",
                      where: "id = ?", whereArgs: [event['id']]);
                }
              }
            } catch (e) {
              AutoTrackLogger.getInstance().debug("JSON 解析错误: $e");
            }
          } else {
            AutoTrackLogger.getInstance().debug(
                'upload => fail ret_code: $responseCode ret_content: $responseBody');
          }
        } catch (e) {
          AutoTrackLogger.getInstance().debug("网络请求错误: $e");
        }
      }
    } catch (e) {
      AutoTrackLogger.getInstance().debug("上报出错");
    }
  }

  int getZoneOffset() {
    final now = DateTime.now();
    final localOffset = now.timeZoneOffset.inMinutes; // 获取时区偏移量(分钟)
    return -localOffset; // 取反,保持与 Java 代码一致
  }

  String encodeData(String rawMessage) {
    try {
      // 使用 GZip 压缩
      List<int> compressed = GZipEncoder().encode(utf8.encode(rawMessage))!;
      // Base64 编码
      return base64.encode(compressed);
    } catch (e) {
      throw FormatException('Invalid data: ${e.toString()}');
    }
  }
}

class Track {
  final int? id;
  final String event;
  final DateTime date;

  Track({
    this.id,
    required this.event,
    required this.date,
  });

  // 从 Map 中创建 Track 对象
  factory Track.fromMap(Map<String, dynamic> map) {
    return Track(
      id: map['id'] as int,
      event: map['event'] as String,
      date: DateTime.fromMillisecondsSinceEpoch(map['date'] as int),
    );
  }

  // 将 Track 对象转换为 Map
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'event': event,
      'date': date.millisecondsSinceEpoch,
    };
  }

  // 用于调试的 toString 方法
  @override
  String toString() {
    return 'Track{id: $id, event: $event, date: $date}';
  }
}