fix:第一轮跑

This commit is contained in:
jingyun committed 2026-08-24 19:07:31 +08:00
1 parent e834ec7586
commit 33235f450b
56 files changed
+4357 -210

No files matched your search

@@ -0,0 +1,32 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../mall/application/mall_tab_config.dart';
import '../data/ads_api_client.dart';
import 'splash_ad_controller.dart';
final adConfigForegroundRefresherProvider = Provider(
(ref) => AdConfigForegroundRefresher(
() => ref.invalidate(featuredAdsProvider(homeBannerAdPlacement)),
() => ref.read(splashAdControllerProvider).refresh(),
() => ref.read(mallTabConfigProvider.notifier).refresh(),
),
);
class AdConfigForegroundRefresher {
AdConfigForegroundRefresher(this._banner, this._splash, this._mall);
final void Function() _banner;
final Future<void> Function() _splash, _mall;
Future<void>? _running;
Future<void> refresh() =>
_running ??= _refresh().whenComplete(() => _running = null);
Future<void> _refresh() async {
try {
_banner();
} catch (_) {}
try {
await _splash();
} catch (_) {}
try {
await _mall();
} catch (_) {}
}
}
@@ -0,0 +1,128 @@
import 'dart:convert';
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 '../data/ads_api_client.dart';
class SplashAdEntry {
const SplashAdEntry(this.ad, this.dailyCap, this.assetPath, this.cachedAtMs);
final AdCreative ad;
final int dailyCap;
final String? assetPath;
final int cachedAtMs;
bool get valid =>
ad.isRenderable &&
DateTime.now().millisecondsSinceEpoch <
cachedAtMs + const Duration(hours: 24).inMilliseconds &&
(ad.endAtMs <= 0 || ad.endAtMs > DateTime.now().millisecondsSinceEpoch);
Map<String, dynamic> toJson() => {
'ad': ad.toJson(),
'dailyCap': dailyCap,
'assetPath': assetPath,
'cachedAtMs': cachedAtMs,
};
factory SplashAdEntry.fromJson(Map<String, dynamic> map) => SplashAdEntry(
AdCreative.fromJson(Map<String, dynamic>.from(map['ad'] as Map)),
(map['dailyCap'] as num?)?.toInt() ?? 0,
map['assetPath'] as String?,
(map['cachedAtMs'] as num?)?.toInt() ?? 0,
);
}
class SplashAdController {
SplashAdController(this._api);
final AdsApiClient _api;
static const _entryKey = 'ads.splash.entry';
static const _dailyKey = 'ads.splash.daily';
Future<SplashAdEntry?> coldLaunchDecision() async {
final raw = StorageService.to.preferences.getString(_entryKey);
if (raw == null) return null;
try {
final entry = SplashAdEntry.fromJson(
Map<String, dynamic>.from(jsonDecode(raw) as Map),
);
if (!entry.valid || !_underCap(entry) || !await _ready(entry)) {
return null;
}
return entry;
} catch (_) {
await clearCache();
return null;
}
}
bool _underCap(SplashAdEntry entry) {
final value = StorageService.to.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);
return map['date'] != today ||
entry.dailyCap <= 0 ||
((map['count'] as num?)?.toInt() ?? 0) < entry.dailyCap;
}
Future<bool> _ready(SplashAdEntry entry) async =>
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);
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(
_dailyKey,
jsonEncode({'date': today, 'count': count + 1}),
);
await _report(entry.ad, 'ad_impression');
}
Future<void> refresh() async {
try {
final result = await _api.getAdResult(splashAdPlacement);
if (!result.enabled || result.ad == null) {
await _clearEntry();
return;
}
final file = await DefaultCacheManager().getSingleFile(
result.ad!.imageUrl,
);
if (!await file.exists()) return;
await StorageService.to.preferences.setString(
_entryKey,
jsonEncode(
SplashAdEntry(
result.ad!,
result.dailyCap,
file.path,
DateTime.now().millisecondsSinceEpoch,
).toJson(),
),
);
} catch (_) {}
}
Future<void> _report(AdCreative ad, String event, {String? clickId}) async {
try {
await _api.report(ad, event, clickId: clickId);
} catch (_) {}
}
Future<void> skip(SplashAdEntry entry) => _report(entry.ad, 'ad_skip');
Future<void> clearCache() async {
await _clearEntry();
await StorageService.to.preferences.remove(_dailyKey);
}
Future<void> _clearEntry() async {
await StorageService.to.preferences.remove(_entryKey);
}
}
final splashAdControllerProvider = Provider(
(ref) => SplashAdController(ref.watch(adsApiClientProvider)),
);
@@ -0,0 +1,57 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../common/router/app_router.dart';
import '../data/ads_api_client.dart';
import '../presentation/splash_ad_dialog.dart';
import 'splash_ad_controller.dart';
final startupAdResolvedProvider = StateProvider<bool>((_) => false);
abstract final class StartupAdService {
static Future<void>? _running;
static Future<void> showIfAvailable(BuildContext context, WidgetRef ref) =>
_running ??= _show(context, ref);
static Future<void> _show(BuildContext context, WidgetRef ref) async {
final controller = ref.read(splashAdControllerProvider);
try {
final entry = await controller.coldLaunchDecision();
if (entry != null && context.mounted) {
final result = await showGeneralDialog<SplashAdResult>(
context: context,
useRootNavigator: true,
barrierDismissible: false,
barrierColor: Colors.black,
pageBuilder: (ctx, _, _) => SplashAdDialog(
entry: entry,
onShown: () => unawaited(controller.markShown(entry)),
onClick: () async {
final id = ref.read(adsApiClientProvider).newClickId();
final uri = entry.ad.clickUri(id);
if (uri != null) {
unawaited(
ref
.read(adsApiClientProvider)
.report(entry.ad, 'ad_click', clickId: id),
);
if (entry.ad.openMode == AdOpenMode.inApp) {
context.push(
'${AppRoutes.web}?url=${Uri.encodeComponent(uri.toString())}',
);
} else {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
},
),
);
if (result == SplashAdResult.skipped) unawaited(controller.skip(entry));
}
} finally {
ref.read(startupAdResolvedProvider.notifier).state = true;
unawaited(controller.refresh());
}
}
}