fix:参考金值,研听,研值工程通用通用工程公共能力,重点对比网络请求、登录认证、状态管理、本地存储、WebView、推送、版本更新、一键登录、主题/UI 等模块,确定后续统一整改方案
This commit is contained in:
1 parent
6997a65c96
commit
d364c6d908
46 files changed
+2060
-395
No files matched your search
@@ -4,7 +4,7 @@ import 'dart:io' show File;
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../common/storage/storage_service.dart';
|
||||
import '../../../common/storage/preferences_storage.dart';
|
||||
import '../data/ads_api_client.dart';
|
||||
|
||||
class SplashAdEntry {
|
||||
@@ -33,12 +33,13 @@ class SplashAdEntry {
|
||||
}
|
||||
|
||||
class SplashAdController {
|
||||
SplashAdController(this._api);
|
||||
SplashAdController(this._api, this._preferences);
|
||||
final AdsApiClient _api;
|
||||
final PreferencesStorage _preferences;
|
||||
static const _entryKey = 'ads.splash.entry';
|
||||
static const _dailyKey = 'ads.splash.daily';
|
||||
Future<SplashAdEntry?> coldLaunchDecision() async {
|
||||
final raw = StorageService.to.preferences.getString(_entryKey);
|
||||
final raw = _preferences.getString(_entryKey);
|
||||
if (raw == null) return null;
|
||||
try {
|
||||
final entry = SplashAdEntry.fromJson(
|
||||
@@ -55,7 +56,7 @@ class SplashAdController {
|
||||
}
|
||||
|
||||
bool _underCap(SplashAdEntry entry) {
|
||||
final value = StorageService.to.preferences.getString(_dailyKey);
|
||||
final value = _preferences.getString(_dailyKey);
|
||||
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||
if (value == null) return true;
|
||||
final map = Map<String, dynamic>.from(jsonDecode(value) as Map);
|
||||
@@ -68,13 +69,13 @@ class SplashAdController {
|
||||
entry.assetPath != null && await File(entry.assetPath!).exists();
|
||||
Future<void> markShown(SplashAdEntry entry) async {
|
||||
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||
final raw = StorageService.to.preferences.getString(_dailyKey);
|
||||
final raw = _preferences.getString(_dailyKey);
|
||||
var count = 0;
|
||||
if (raw != null) {
|
||||
final m = Map<String, dynamic>.from(jsonDecode(raw) as Map);
|
||||
if (m['date'] == today) count = (m['count'] as num?)?.toInt() ?? 0;
|
||||
}
|
||||
await StorageService.to.preferences.setString(
|
||||
await _preferences.setString(
|
||||
_dailyKey,
|
||||
jsonEncode({'date': today, 'count': count + 1}),
|
||||
);
|
||||
@@ -92,7 +93,7 @@ class SplashAdController {
|
||||
result.ad!.imageUrl,
|
||||
);
|
||||
if (!await file.exists()) return;
|
||||
await StorageService.to.preferences.setString(
|
||||
await _preferences.setString(
|
||||
_entryKey,
|
||||
jsonEncode(
|
||||
SplashAdEntry(
|
||||
@@ -115,14 +116,17 @@ class SplashAdController {
|
||||
Future<void> skip(SplashAdEntry entry) => _report(entry.ad, 'ad_skip');
|
||||
Future<void> clearCache() async {
|
||||
await _clearEntry();
|
||||
await StorageService.to.preferences.remove(_dailyKey);
|
||||
await _preferences.remove(_dailyKey);
|
||||
}
|
||||
|
||||
Future<void> _clearEntry() async {
|
||||
await StorageService.to.preferences.remove(_entryKey);
|
||||
await _preferences.remove(_entryKey);
|
||||
}
|
||||
}
|
||||
|
||||
final splashAdControllerProvider = Provider(
|
||||
(ref) => SplashAdController(ref.watch(adsApiClientProvider)),
|
||||
(ref) => SplashAdController(
|
||||
ref.watch(adsApiClientProvider),
|
||||
ref.watch(preferencesStorageProvider),
|
||||
),
|
||||
);
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
// ignore: unused_import — compute() is referenced by generated Retrofit code.
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../../common/network/dio_provider.dart';
|
||||
import '../../../models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||
import '../../../models/pb/jinzhi/v1/ads.pb.dart'
|
||||
show GetAdReply, ListAdsReply, ReportAdEventReply, ReportAdEventRequest;
|
||||
|
||||
part 'ads_api_client.g.dart';
|
||||
|
||||
const splashAdPlacement = 'splash';
|
||||
const homeBannerAdPlacement = 'home_banner';
|
||||
@@ -100,24 +106,26 @@ class AdCreative {
|
||||
? 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,
|
||||
factory AdCreative.fromProto(pb.AdCreative creative) => AdCreative(
|
||||
adId: creative.adId,
|
||||
placement: creative.placement,
|
||||
targetApp: creative.targetApp,
|
||||
title: creative.title,
|
||||
imageUrl: creative.imageUrl,
|
||||
mediaType: switch (creative.mediaType) {
|
||||
pb.MediaType.MEDIA_TYPE_IMAGE => AdMediaType.image,
|
||||
pb.MediaType.MEDIA_TYPE_GIF => AdMediaType.gif,
|
||||
pb.MediaType.MEDIA_TYPE_VIDEO => 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,
|
||||
h5Url: creative.h5Url,
|
||||
trackingId: creative.trackingId,
|
||||
countdownSeconds: creative.countdownSeconds,
|
||||
endAtMs: creative.endAtMs.toInt(),
|
||||
trackedUrl: creative.trackedUrl,
|
||||
openMode: creative.openMode == pb.AdOpenMode.AD_OPEN_MODE_IN_APP
|
||||
? AdOpenMode.inApp
|
||||
: AdOpenMode.external,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,41 +144,49 @@ class FeaturedAdsState {
|
||||
bool get hasAds => enabled && ads.isNotEmpty;
|
||||
}
|
||||
|
||||
@RestApi()
|
||||
abstract class AdsBackendApiClient {
|
||||
factory AdsBackendApiClient(Dio dio, {String? baseUrl}) =
|
||||
_AdsBackendApiClient;
|
||||
|
||||
@GET('/api/v1/ads')
|
||||
Future<pb.GetAdReply> getAd({@Query('placement') required String placement});
|
||||
|
||||
@GET('/api/v1/ads/list')
|
||||
Future<pb.ListAdsReply> listAds({
|
||||
@Query('placement') required String placement,
|
||||
});
|
||||
|
||||
@POST('/api/v1/ads/events')
|
||||
Future<pb.ReportAdEventReply> reportAdEvent(
|
||||
@Body() pb.ReportAdEventRequest body,
|
||||
);
|
||||
}
|
||||
|
||||
class AdsApiClient {
|
||||
AdsApiClient(this._dio);
|
||||
final Dio _dio;
|
||||
AdsApiClient(this._api);
|
||||
final AdsBackendApiClient _api;
|
||||
|
||||
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);
|
||||
final reply = await _api.getAd(placement: placement);
|
||||
final ad = reply.hasAd() ? AdCreative.fromProto(reply.ad) : null;
|
||||
return GetAdResult(
|
||||
root.integer(1) != 0,
|
||||
ad.isRenderable ? ad : null,
|
||||
root.integer(3),
|
||||
reply.enabled,
|
||||
ad?.isRenderable == true ? ad : null,
|
||||
reply.dailyCap,
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
final reply = await _api.listAds(placement: placement);
|
||||
final ads = reply.ads
|
||||
.map(AdCreative.fromProto)
|
||||
.where((ad) => ad.isRenderable)
|
||||
.toList(growable: false);
|
||||
return FeaturedAdsState(
|
||||
root.integer(1) != 0,
|
||||
reply.enabled,
|
||||
ads,
|
||||
trackingId: root.string(3).isEmpty ? null : root.string(3),
|
||||
trackingId: reply.trackingId.isEmpty ? null : reply.trackingId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,23 +195,24 @@ class AdsApiClient {
|
||||
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(),
|
||||
);
|
||||
}) async {
|
||||
await _api.reportAdEvent(
|
||||
pb.ReportAdEventRequest(
|
||||
eventName: event,
|
||||
placement: ad.placement,
|
||||
adId: ad.adId,
|
||||
trackingId: trackingId ?? ad.trackingId,
|
||||
clickId: clickId ?? '',
|
||||
clientTsMs: Int64(DateTime.now().millisecondsSinceEpoch),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String newClickId() => const Uuid().v4();
|
||||
}
|
||||
|
||||
final adsApiClientProvider = Provider(
|
||||
(ref) => AdsApiClient(ref.watch(dioProvider)),
|
||||
(ref) => AdsApiClient(AdsBackendApiClient(ref.watch(backendDioProvider))),
|
||||
);
|
||||
final featuredAdsProvider = FutureProvider.autoDispose
|
||||
.family<FeaturedAdsState, String>(
|
||||
@@ -210,86 +227,3 @@ Uri? _http(String raw) {
|
||||
? 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> ? utf8.decode(value, allowMalformed: true) : '';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../../common/widgets/app_network_image.dart';
|
||||
import '../data/ads_api_client.dart';
|
||||
|
||||
/// 统一渲染广告图片/GIF/视频;开屏可优先使用已缓存的本地素材。
|
||||
@@ -86,12 +87,12 @@ class _AdMediaState extends State<AdMedia> {
|
||||
errorBuilder: (_, _, _) => _error(),
|
||||
);
|
||||
}
|
||||
return Image.network(
|
||||
widget.ad.imageUrl,
|
||||
return AppNetworkImage(
|
||||
imageUrl: widget.ad.imageUrl,
|
||||
fit: widget.fit,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
errorBuilder: (_, _, _) => _error(),
|
||||
errorWidget: _error(),
|
||||
);
|
||||
}
|
||||
final c = player;
|
||||
|
||||
Reference in new issue
Block a user