fix:参考金值,研听,研值工程通用通用工程公共能力,重点对比网络请求、登录认证、状态管理、本地存储、WebView、推送、版本更新、一键登录、主题/UI 等模块,确定后续统一整改方案

This commit is contained in:
jingyun committed 2026-09-10 09:52:36 +08:00
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),
),
);
+72 -138
View File
@@ -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);
}
}
+4 -3
View File
@@ -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;