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

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