389 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
		
		
			
		
	
	
			389 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
| 
								 | 
							
								import 'dart:io';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/account/account_data_store.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/firebase/remoteconfig/remote_config_manager.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_app/property/settings/guru_settings.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/device/device_utils.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/extensions/extensions.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/log/log.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:guru_utils/random/random_utils.dart';
							 | 
						|||
| 
								 | 
							
								import 'package:json_annotation/json_annotation.dart';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								part 'abtest_model.g.dart';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								class ConditionOpt {
							 | 
						|||
| 
								 | 
							
								  static const equals = "eq";
							 | 
						|||
| 
								 | 
							
								  static const greaterThan = "gt";
							 | 
						|||
| 
								 | 
							
								  static const greaterThanOrEquals = "gte";
							 | 
						|||
| 
								 | 
							
								  static const lessThan = "lt";
							 | 
						|||
| 
								 | 
							
								  static const lessThanOrEquals = "lte";
							 | 
						|||
| 
								 | 
							
								  static const notEquals = "ne";
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  static bool evaluate<T extends Comparable>(T value, T target, String opt) {
							 | 
						|||
| 
								 | 
							
								    final result = value.compareTo(target);
							 | 
						|||
| 
								 | 
							
								    switch (opt) {
							 | 
						|||
| 
								 | 
							
								      case ConditionOpt.equals:
							 | 
						|||
| 
								 | 
							
								        return result == 0;
							 | 
						|||
| 
								 | 
							
								      case ConditionOpt.greaterThan:
							 | 
						|||
| 
								 | 
							
								        return result > 0;
							 | 
						|||
| 
								 | 
							
								      case ConditionOpt.greaterThanOrEquals:
							 | 
						|||
| 
								 | 
							
								        return result >= 0;
							 | 
						|||
| 
								 | 
							
								      case ConditionOpt.lessThan:
							 | 
						|||
| 
								 | 
							
								        return result < 0;
							 | 
						|||
| 
								 | 
							
								      case ConditionOpt.lessThanOrEquals:
							 | 
						|||
| 
								 | 
							
								        return result <= 0;
							 | 
						|||
| 
								 | 
							
								      case ConditionOpt.notEquals:
							 | 
						|||
| 
								 | 
							
								        return result != 0;
							 | 
						|||
| 
								 | 
							
								      default:
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								abstract class ABTestFilter {
							 | 
						|||
| 
								 | 
							
								  static const platform = 1;
							 | 
						|||
| 
								 | 
							
								  static const version = 2;
							 | 
						|||
| 
								 | 
							
								  static const country = 3;
							 | 
						|||
| 
								 | 
							
								  static const newUser = 4;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  final int type;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const ABTestFilter(this.type);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  bool filter();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory ABTestFilter.fromJson(Map<String, dynamic> json) {
							 | 
						|||
| 
								 | 
							
								    final type = json["type"] = json["type"] ?? 0;
							 | 
						|||
| 
								 | 
							
								    switch (type) {
							 | 
						|||
| 
								 | 
							
								      case ABTestFilter.platform:
							 | 
						|||
| 
								 | 
							
								        return PlatformFilter.fromJson(json);
							 | 
						|||
| 
								 | 
							
								      case ABTestFilter.country:
							 | 
						|||
| 
								 | 
							
								        return CountryFilter.fromJson(json);
							 | 
						|||
| 
								 | 
							
								      case ABTestFilter.version:
							 | 
						|||
| 
								 | 
							
								        return VersionFilter.fromJson(json);
							 | 
						|||
| 
								 | 
							
								      case ABTestFilter.newUser:
							 | 
						|||
| 
								 | 
							
								        return NewUserFilter.fromJson(json);
							 | 
						|||
| 
								 | 
							
								      default:
							 | 
						|||
| 
								 | 
							
								        throw UnimplementedError("Unknown ABTestFilter type: $type");
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => toJson()..addAll({"type": type});
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								abstract class ABTestCondition {
							 | 
						|||
| 
								 | 
							
								  bool validate();
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								/// 为了后面可以做一些定制,因此这里按平台进行区分
							 | 
						|||
| 
								 | 
							
								@JsonSerializable()
							 | 
						|||
| 
								 | 
							
								class AndroidCondition extends ABTestCondition {
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "opt")
							 | 
						|||
| 
								 | 
							
								  final String? opt;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "sdk")
							 | 
						|||
| 
								 | 
							
								  final int? sdkInt;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  AndroidCondition({this.opt, this.sdkInt});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory AndroidCondition.fromJson(Map<String, dynamic> json) => _$AndroidConditionFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$AndroidConditionToJson(this);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() {
							 | 
						|||
| 
								 | 
							
								    final versionOpt = opt;
							 | 
						|||
| 
								 | 
							
								    final targetVersion = sdkInt;
							 | 
						|||
| 
								 | 
							
								    if (versionOpt != null && targetVersion != null) {
							 | 
						|||
| 
								 | 
							
								      final versionCode = DeviceUtils.peekOSVersion();
							 | 
						|||
| 
								 | 
							
								      // 操作系统版本号获取失败,直接返回false
							 | 
						|||
| 
								 | 
							
								      if (versionCode == -1) {
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    // 方便后面扩展其它字段
							 | 
						|||
| 
								 | 
							
								    return true;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								@JsonSerializable()
							 | 
						|||
| 
								 | 
							
								class IosCondition extends ABTestCondition {
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "opt")
							 | 
						|||
| 
								 | 
							
								  final String? opt;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "ver")
							 | 
						|||
| 
								 | 
							
								  final int? version; // 这里只记录大版本号
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  IosCondition({this.opt, this.version});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory IosCondition.fromJson(Map<String, dynamic> json) => _$IosConditionFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$IosConditionToJson(this);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool validate() {
							 | 
						|||
| 
								 | 
							
								    final versionOpt = opt;
							 | 
						|||
| 
								 | 
							
								    final targetVersion = version;
							 | 
						|||
| 
								 | 
							
								    if (versionOpt != null && targetVersion != null) {
							 | 
						|||
| 
								 | 
							
								      final versionCode = DeviceUtils.peekOSVersion();
							 | 
						|||
| 
								 | 
							
								      // 操作系统版本号获取失败,直接返回false
							 | 
						|||
| 
								 | 
							
								      if (versionCode == -1) {
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      if (!ConditionOpt.evaluate<int>(versionCode, targetVersion, versionOpt)) {
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    // 方便后面扩展其它字段
							 | 
						|||
| 
								 | 
							
								    return true;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								@JsonSerializable()
							 | 
						|||
| 
								 | 
							
								class PlatformFilter extends ABTestFilter {
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "ac")
							 | 
						|||
| 
								 | 
							
								  final AndroidCondition? androidCondition;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "ic")
							 | 
						|||
| 
								 | 
							
								  final IosCondition? iosCondition;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  PlatformFilter({this.androidCondition, this.iosCondition}) : super(ABTestFilter.platform);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool filter() {
							 | 
						|||
| 
								 | 
							
								    // 如果配了 Platform Filter, 如果指定平台没有 condition, 则默认为true
							 | 
						|||
| 
								 | 
							
								    if (Platform.isAndroid) {
							 | 
						|||
| 
								 | 
							
								      return androidCondition?.validate() != false;
							 | 
						|||
| 
								 | 
							
								    } else if (Platform.isIOS) {
							 | 
						|||
| 
								 | 
							
								      return iosCondition?.validate() != false;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return false;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory PlatformFilter.fromJson(Map<String, dynamic> json) => _$PlatformFilterFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$PlatformFilterToJson(this);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								@JsonSerializable(constructor: "_")
							 | 
						|||
| 
								 | 
							
								class VersionFilter extends ABTestFilter {
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "opt")
							 | 
						|||
| 
								 | 
							
								  final String opt;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "mmp")
							 | 
						|||
| 
								 | 
							
								  final String mmp; // major.minor.patch
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionFilter._(this.opt, this.mmp) : super(ABTestFilter.version);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionFilter.equals(this.mmp)
							 | 
						|||
| 
								 | 
							
								      : opt = ConditionOpt.equals,
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.version);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionFilter.greaterThan(this.mmp)
							 | 
						|||
| 
								 | 
							
								      : opt = ConditionOpt.greaterThan,
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.version);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionFilter.greaterThanOrEquals(this.mmp)
							 | 
						|||
| 
								 | 
							
								      : opt = ConditionOpt.greaterThanOrEquals,
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.version);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionFilter.lessThan(this.mmp)
							 | 
						|||
| 
								 | 
							
								      : opt = ConditionOpt.lessThan,
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.version);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionFilter.lessThanOrEquals(this.mmp)
							 | 
						|||
| 
								 | 
							
								      : opt = ConditionOpt.lessThanOrEquals,
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.version);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  VersionFilter.notEquals(this.mmp)
							 | 
						|||
| 
								 | 
							
								      : opt = ConditionOpt.notEquals,
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.version);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool filter() {
							 | 
						|||
| 
								 | 
							
								    final version = GuruSettings.instance.version.get();
							 | 
						|||
| 
								 | 
							
								    Log.d("[$runtimeType] $version $opt $mmp");
							 | 
						|||
| 
								 | 
							
								    return ConditionOpt.evaluate<String>(version, mmp, opt);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'VersionValidator{opt: $opt, mmp: $mmp}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory VersionFilter.fromJson(Map<String, dynamic> json) => _$VersionFilterFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$VersionFilterToJson(this);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								@JsonSerializable(constructor: "_")
							 | 
						|||
| 
								 | 
							
								class CountryFilter extends ABTestFilter {
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "included", defaultValue: {})
							 | 
						|||
| 
								 | 
							
								  final Set<String> included;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "excluded", defaultValue: {})
							 | 
						|||
| 
								 | 
							
								  final Set<String> excluded;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  CountryFilter._(this.included, this.excluded) : super(ABTestFilter.country);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  CountryFilter.included(this.included)
							 | 
						|||
| 
								 | 
							
								      : excluded = {},
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.country);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  CountryFilter.excluded(this.excluded)
							 | 
						|||
| 
								 | 
							
								      : included = {},
							 | 
						|||
| 
								 | 
							
								        super(ABTestFilter.country);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool filter() {
							 | 
						|||
| 
								 | 
							
								    final String countryCode = Platform.localeName.split('_').safeLast?.toLowerCase() ?? "";
							 | 
						|||
| 
								 | 
							
								    Log.d("[$runtimeType] $countryCode included: $included excluded: $excluded");
							 | 
						|||
| 
								 | 
							
								    if (countryCode.isEmpty) {
							 | 
						|||
| 
								 | 
							
								      return false;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// 如果excluded不为空,证明存在排除选项,该validate将只判断所有excluded中的逻辑,
							 | 
						|||
| 
								 | 
							
								// 将不会在判断included中的逻辑
							 | 
						|||
| 
								 | 
							
								    if (excluded.isNotEmpty) {
							 | 
						|||
| 
								 | 
							
								      return !excluded.contains(countryCode);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (included.contains(countryCode)) {
							 | 
						|||
| 
								 | 
							
								      return true;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return false;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory CountryFilter.fromJson(Map<String, dynamic> json) => _$CountryFilterFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$CountryFilterToJson(this);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								@JsonSerializable()
							 | 
						|||
| 
								 | 
							
								class NewUserFilter extends ABTestFilter {
							 | 
						|||
| 
								 | 
							
								  NewUserFilter() : super(ABTestFilter.newUser);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  bool filter() {
							 | 
						|||
| 
								 | 
							
								    final version = GuruSettings.instance.version.get();
							 | 
						|||
| 
								 | 
							
								    final fiv = GuruSettings.instance.firstInstallVersion.get();
							 | 
						|||
| 
								 | 
							
								    return fiv.startsWith(version);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory NewUserFilter.fromJson(Map<String, dynamic> json) => _$NewUserFilterFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$NewUserFilterToJson(this);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								@JsonSerializable()
							 | 
						|||
| 
								 | 
							
								class ABTestAudience {
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "filters")
							 | 
						|||
| 
								 | 
							
								  final List<ABTestFilter> filters;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "variant", defaultValue: 2)
							 | 
						|||
| 
								 | 
							
								  final int variant;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  ABTestAudience({required this.filters, this.variant = 2});
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory ABTestAudience.fromJson(Map<String, dynamic> json) => _$ABTestAudienceFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$ABTestAudienceToJson(this);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'ABTestAudience{filters: $filters, variant: $variant}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  bool validate() {
							 | 
						|||
| 
								 | 
							
								    for (var filter in filters) {
							 | 
						|||
| 
								 | 
							
								      if (!filter.filter()) {
							 | 
						|||
| 
								 | 
							
								        return false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return true;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								@JsonSerializable()
							 | 
						|||
| 
								 | 
							
								class ABTestExperiment {
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "name")
							 | 
						|||
| 
								 | 
							
								  final String name;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "start_ts", defaultValue: 0)
							 | 
						|||
| 
								 | 
							
								  final int startTs;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "end_ts", defaultValue: 0)
							 | 
						|||
| 
								 | 
							
								  final int endTs;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(name: "audience")
							 | 
						|||
| 
								 | 
							
								  final ABTestAudience audience;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  ABTestExperiment(
							 | 
						|||
| 
								 | 
							
								      {required String name, required this.startTs, required this.endTs, required this.audience})
							 | 
						|||
| 
								 | 
							
								      : name = _validExperimentName(name);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @override
							 | 
						|||
| 
								 | 
							
								  String toString() {
							 | 
						|||
| 
								 | 
							
								    return 'ABTestExperiment{name: $name, startTs: $startTs, endTs: $endTs, audience: $audience}';
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  static String _validExperimentName(String experimentName) {
							 | 
						|||
| 
								 | 
							
								    if (experimentName.contains(RemoteConfigManager.invalidABKeyRegExp)) {
							 | 
						|||
| 
								 | 
							
								      Log.w("abName($experimentName) use invalid key! $experimentName! replace invalid char to _");
							 | 
						|||
| 
								 | 
							
								      experimentName = experimentName.replaceAll(RemoteConfigManager.invalidABKeyRegExp, "_");
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      if (experimentName.length > 20) {
							 | 
						|||
| 
								 | 
							
								        experimentName = experimentName.substring(0, 20);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return experimentName;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  factory ABTestExperiment.fromJson(Map<String, dynamic> json) => _$ABTestExperimentFromJson(json);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  Map<String, dynamic> toJson() => _$ABTestExperimentToJson(this);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  bool isExpired() {
							 | 
						|||
| 
								 | 
							
								    final now = DateTime.now().millisecondsSinceEpoch;
							 | 
						|||
| 
								 | 
							
								    return now < startTs || now > endTs;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  bool isMatchAudience() {
							 | 
						|||
| 
								 | 
							
								    return audience.validate();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  @JsonKey(includeToJson: false)
							 | 
						|||
| 
								 | 
							
								  String? _variantName;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  String get variantName =>
							 | 
						|||
| 
								 | 
							
								      (_variantName ??= _toVariantName(RandomUtils.nextInt(audience.variant)));
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  static const _originalVariant = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  String _toVariantName(int value) {
							 | 
						|||
| 
								 | 
							
								    String codes = "";
							 | 
						|||
| 
								 | 
							
								    int nv = value;
							 | 
						|||
| 
								 | 
							
								    while (true) {
							 | 
						|||
| 
								 | 
							
								      final nextNum = nv ~/ _originalVariant.length;
							 | 
						|||
| 
								 | 
							
								      if (nextNum <= 0) {
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      codes = "${_originalVariant[nv % _originalVariant.length]}$codes";
							 | 
						|||
| 
								 | 
							
								      nv = nextNum;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    final tailIndex = nv % _originalVariant.length;
							 | 
						|||
| 
								 | 
							
								    if (tailIndex >= 0) {
							 | 
						|||
| 
								 | 
							
								      codes = "${_originalVariant[tailIndex]}$codes";
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								    return codes.toString();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 |