queue.dart 8.74 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:path/path.dart' as path;
import 'package:sqflite/sqflite.dart';

import '../log/logger.dart';

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

  late 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 {
      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) {
    if (database == null) return;
    post(() async {
      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 {
    AutoTrackLogger.getInstance().debug("@@@start flush");

    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);

    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) {
      return;
    }
    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';
          }
        }
        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,
          // "$carrier": "NONE",
          // "$os_version": "13",
          // "$model": "C310CS",
          // "$os": "Android",
          // "$screen_width": 1200,
          // "$brand": "BOE",
          // "$screen_height": 1920,
          // "$device_id": "afa4c7a98b3f6467",
          // "$app_name": "Ewin Reading",
          // "$lib_version": "5.3.3",
          // "$timezone_offset": -480,
          // "$app_id": "com.ewin.tech.reading",
          // "$mac": "020000000000",
          // "$manufacturer": "BOE",
          // "$sn": "C310CS014820000006",
          // "$wifi": true,
          // "$network_type": "WIFI",
          // "$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");
      }
    }
  }

  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}';
  }
}