fix:第一轮跑
This commit is contained in:
1 parent
e834ec7586
commit
33235f450b
56 files changed
+4357
-210
No files matched your search
@@ -0,0 +1,294 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/network/dio_provider.dart';
|
||||
|
||||
const splashAdPlacement = 'splash';
|
||||
const homeBannerAdPlacement = 'home_banner';
|
||||
|
||||
enum AdMediaType { image, gif, video, unsupported }
|
||||
|
||||
enum AdOpenMode { external, inApp }
|
||||
|
||||
class AdCreative {
|
||||
const AdCreative({
|
||||
required this.adId,
|
||||
required this.placement,
|
||||
required this.targetApp,
|
||||
required this.title,
|
||||
required this.imageUrl,
|
||||
required this.mediaType,
|
||||
required this.h5Url,
|
||||
required this.trackingId,
|
||||
required this.countdownSeconds,
|
||||
required this.endAtMs,
|
||||
required this.trackedUrl,
|
||||
required this.openMode,
|
||||
});
|
||||
final String adId,
|
||||
placement,
|
||||
targetApp,
|
||||
title,
|
||||
imageUrl,
|
||||
h5Url,
|
||||
trackingId,
|
||||
trackedUrl;
|
||||
final AdMediaType mediaType;
|
||||
final int countdownSeconds, endAtMs;
|
||||
final AdOpenMode openMode;
|
||||
Uri? get mediaUri => _http(imageUrl);
|
||||
Uri? get targetUri => _http(trackedUrl) ?? _http(h5Url);
|
||||
bool get isRenderable =>
|
||||
adId.isNotEmpty &&
|
||||
mediaUri != null &&
|
||||
targetUri != null &&
|
||||
mediaType != AdMediaType.unsupported &&
|
||||
(endAtMs <= 0 || endAtMs > DateTime.now().millisecondsSinceEpoch);
|
||||
Uri? clickUri(String clickId) {
|
||||
final uri = targetUri;
|
||||
if (uri == null) return null;
|
||||
return uri.replace(
|
||||
queryParameters: {
|
||||
...uri.queryParameters,
|
||||
'ad_id': adId,
|
||||
'placement': placement,
|
||||
'tracking_id': trackingId,
|
||||
'click_id': clickId,
|
||||
'target_app': targetApp,
|
||||
'hook_type': placement,
|
||||
'ts': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'adId': adId,
|
||||
'placement': placement,
|
||||
'targetApp': targetApp,
|
||||
'title': title,
|
||||
'imageUrl': imageUrl,
|
||||
'mediaType': mediaType.index,
|
||||
'h5Url': h5Url,
|
||||
'trackingId': trackingId,
|
||||
'countdownSeconds': countdownSeconds,
|
||||
'endAtMs': endAtMs,
|
||||
'trackedUrl': trackedUrl,
|
||||
'openMode': openMode.index,
|
||||
};
|
||||
factory AdCreative.fromJson(Map<String, dynamic> value) => AdCreative(
|
||||
adId: '${value['adId'] ?? ''}',
|
||||
placement: '${value['placement'] ?? ''}',
|
||||
targetApp: '${value['targetApp'] ?? ''}',
|
||||
title: '${value['title'] ?? ''}',
|
||||
imageUrl: '${value['imageUrl'] ?? ''}',
|
||||
mediaType:
|
||||
AdMediaType.values[(value['mediaType'] as num?)?.toInt().clamp(
|
||||
0,
|
||||
AdMediaType.values.length - 1,
|
||||
) ??
|
||||
AdMediaType.unsupported.index],
|
||||
h5Url: '${value['h5Url'] ?? ''}',
|
||||
trackingId: '${value['trackingId'] ?? ''}',
|
||||
countdownSeconds: (value['countdownSeconds'] as num?)?.toInt() ?? 0,
|
||||
endAtMs: (value['endAtMs'] as num?)?.toInt() ?? 0,
|
||||
trackedUrl: '${value['trackedUrl'] ?? ''}',
|
||||
openMode: (value['openMode'] as num?)?.toInt() == AdOpenMode.inApp.index
|
||||
? AdOpenMode.inApp
|
||||
: AdOpenMode.external,
|
||||
);
|
||||
factory AdCreative.fromProto(ProtoMessage fields) => AdCreative(
|
||||
adId: fields.string(1),
|
||||
placement: fields.string(2),
|
||||
targetApp: fields.string(3),
|
||||
title: fields.string(4),
|
||||
imageUrl: fields.string(6),
|
||||
mediaType: switch (fields.integer(7)) {
|
||||
1 => AdMediaType.image,
|
||||
2 => AdMediaType.gif,
|
||||
3 => AdMediaType.video,
|
||||
_ => AdMediaType.unsupported,
|
||||
},
|
||||
h5Url: fields.string(9),
|
||||
trackingId: fields.string(10),
|
||||
countdownSeconds: fields.integer(11),
|
||||
endAtMs: fields.integer(13),
|
||||
trackedUrl: fields.string(14),
|
||||
openMode: fields.integer(15) == 2 ? AdOpenMode.inApp : AdOpenMode.external,
|
||||
);
|
||||
}
|
||||
|
||||
class GetAdResult {
|
||||
const GetAdResult(this.enabled, this.ad, this.dailyCap);
|
||||
final bool enabled;
|
||||
final AdCreative? ad;
|
||||
final int dailyCap;
|
||||
}
|
||||
|
||||
class FeaturedAdsState {
|
||||
const FeaturedAdsState(this.enabled, this.ads, {this.trackingId});
|
||||
final bool enabled;
|
||||
final List<AdCreative> ads;
|
||||
final String? trackingId;
|
||||
bool get hasAds => enabled && ads.isNotEmpty;
|
||||
}
|
||||
|
||||
class AdsApiClient {
|
||||
AdsApiClient(this._dio);
|
||||
final Dio _dio;
|
||||
Future<GetAdResult> getAdResult(String placement) async {
|
||||
final response = await _dio.get<List<int>>(
|
||||
'/api/v1/ads',
|
||||
queryParameters: {'placement': placement},
|
||||
options: protobufOptions(),
|
||||
);
|
||||
final root = ProtoMessage(response.data ?? const []);
|
||||
final creative = root.message(2);
|
||||
final ad = AdCreative.fromProto(creative);
|
||||
return GetAdResult(
|
||||
root.integer(1) != 0,
|
||||
ad.isRenderable ? ad : null,
|
||||
root.integer(3),
|
||||
);
|
||||
}
|
||||
|
||||
Future<FeaturedAdsState> listAds(String placement) async {
|
||||
final response = await _dio.get<List<int>>(
|
||||
'/api/v1/ads/list',
|
||||
queryParameters: {'placement': placement},
|
||||
options: protobufOptions(),
|
||||
);
|
||||
final root = ProtoMessage(response.data ?? const []);
|
||||
final ads = root
|
||||
.messages(2)
|
||||
.map(AdCreative.fromProto)
|
||||
.where((ad) => ad.isRenderable)
|
||||
.toList(growable: false);
|
||||
return FeaturedAdsState(
|
||||
root.integer(1) != 0,
|
||||
ads,
|
||||
trackingId: root.string(3).isEmpty ? null : root.string(3),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> report(
|
||||
AdCreative ad,
|
||||
String event, {
|
||||
String? clickId,
|
||||
String? trackingId,
|
||||
}) => _dio.post<List<int>>(
|
||||
'/api/v1/ads/events',
|
||||
data: _Writer()
|
||||
.string(1, event)
|
||||
.string(2, ad.placement)
|
||||
.string(3, ad.adId)
|
||||
.string(4, trackingId ?? ad.trackingId)
|
||||
.string(5, clickId ?? '')
|
||||
.integer(6, DateTime.now().millisecondsSinceEpoch)
|
||||
.toBytes(),
|
||||
options: protobufOptions(),
|
||||
);
|
||||
String newClickId() => const Uuid().v4();
|
||||
}
|
||||
|
||||
final adsApiClientProvider = Provider(
|
||||
(ref) => AdsApiClient(ref.watch(dioProvider)),
|
||||
);
|
||||
final featuredAdsProvider = FutureProvider.autoDispose
|
||||
.family<FeaturedAdsState, String>(
|
||||
(ref, placement) => ref.watch(adsApiClientProvider).listAds(placement),
|
||||
);
|
||||
|
||||
Uri? _http(String raw) {
|
||||
final uri = Uri.tryParse(raw.trim());
|
||||
return uri != null &&
|
||||
uri.host.isNotEmpty &&
|
||||
(uri.scheme == 'https' || uri.scheme == 'http')
|
||||
? uri
|
||||
: null;
|
||||
}
|
||||
|
||||
class ProtoMessage {
|
||||
ProtoMessage(this.bytes);
|
||||
final List<int> bytes;
|
||||
Map<int, List<Object>>? _cache;
|
||||
Map<int, List<Object>> get _fields => _cache ??= _read();
|
||||
int integer(int tag) => (_fields[tag]?.lastOrNull as int?) ?? 0;
|
||||
String string(int tag) {
|
||||
final value = _fields[tag]?.lastOrNull;
|
||||
return value is List<int> ? String.fromCharCodes(value) : '';
|
||||
}
|
||||
|
||||
ProtoMessage message(int tag) {
|
||||
final value = _fields[tag]?.lastOrNull;
|
||||
return ProtoMessage(value is List<int> ? value : const []);
|
||||
}
|
||||
|
||||
List<ProtoMessage> messages(int tag) => (_fields[tag] ?? const [])
|
||||
.whereType<List<int>>()
|
||||
.map(ProtoMessage.new)
|
||||
.toList();
|
||||
Map<int, List<Object>> _read() {
|
||||
final out = <int, List<Object>>{};
|
||||
var i = 0;
|
||||
int v() {
|
||||
var n = 0, s = 0;
|
||||
while (i < bytes.length) {
|
||||
final b = bytes[i++];
|
||||
n |= (b & 127) << s;
|
||||
if (b < 128) return n;
|
||||
s += 7;
|
||||
}
|
||||
throw const FormatException('Invalid protobuf');
|
||||
}
|
||||
|
||||
while (i < bytes.length) {
|
||||
final key = v(), tag = key >> 3;
|
||||
Object value;
|
||||
if ((key & 7) == 0) {
|
||||
value = v();
|
||||
} else if ((key & 7) == 2) {
|
||||
final l = v(), end = i + l;
|
||||
if (end > bytes.length) throw const FormatException('Invalid protobuf');
|
||||
value = bytes.sublist(i, end);
|
||||
i = end;
|
||||
} else {
|
||||
throw const FormatException('Unsupported protobuf');
|
||||
}
|
||||
(out[tag] ??= []).add(value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
extension on List<Object> {
|
||||
Object? get lastOrNull => isEmpty ? null : last;
|
||||
}
|
||||
|
||||
class _Writer {
|
||||
final b = BytesBuilder(copy: false);
|
||||
_Writer string(int t, String value) {
|
||||
final d = value.codeUnits;
|
||||
v((t << 3) | 2);
|
||||
v(d.length);
|
||||
b.add(d);
|
||||
return this;
|
||||
}
|
||||
|
||||
_Writer integer(int t, int value) {
|
||||
v(t << 3);
|
||||
v(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
Uint8List toBytes() => b.toBytes();
|
||||
void v(int n) {
|
||||
while (n > 127) {
|
||||
b.addByte((n & 127) | 128);
|
||||
n >>= 7;
|
||||
}
|
||||
b.addByte(n);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user