Files
huangguo_android/lib/tools_base/ad_manager.dart
T
2026-09-15 15:44:13 +07:00

199 lines
8.2 KiB
Dart

import 'dart:convert';
import 'package:hgdj/config/address.dart';
import 'package:hgdj/hj_model/home/module_detail_model.dart';
import 'package:hgdj/hj_model/splash/ads_model.dart';
import 'package:hgdj/hj_model/user/user_info_model.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/light_model.dart';
import 'package:hgdj/hj_utils/store_keys.dart';
import 'package:hgdj/hj_utils/text_util.dart';
import 'package:path/path.dart' as path;
/// 广告总管:广告数据(内存+本地缓存)、免广告策略、列表插广告
class AdManager {
static final AdManager _instance = AdManager._();
AdManager._();
factory AdManager() => _instance;
List<AdsInfoModel>? _adsList; // 广告内存缓存,splash 启动早期由 preload 预热
List<AnnounceInfoBean> announceList = []; // 文字/图片公告,随 /ping/domain 下发
bool _adFreeAlways = false; // 首次进 app 免广告
DateTime? _adFreeUntil; // 限时免广告的到期时间,null 为不限时免广告
/// 预热广告内存缓存:内存命中直接返回,否则从磁盘读
Future<void> preload() async {
if (_adsList?.isNotEmpty == true) return;
_adsList = _decodeAds(await lightKV.getString(StoreKeys.ADS_LIST));
}
/// 按 [position] 同步取广告,走内存缓存
List<AdsInfoModel> adsByType(int? position) =>
_filterByPosition(_adsList, position);
/// 按 [position] 异步取上次展示的广告,直接读磁盘(不走/不写内存缓存,splash 启动早期用)
Future<List<AdsInfoModel>> lastAdsByType(int? position) async {
if (position == null) return const [];
return _filterByPosition(
_decodeAds(await lightKV.getString(StoreKeys.ADS_LIST)), position);
}
/// 保存服务端下发的广告:cover 补全成完整地址后刷内存、写磁盘
Future<bool?> saveAds(List<AdsInfoModel>? ads) async {
for (final model in ads ?? const <AdsInfoModel>[]) {
final cover = model.cover;
// 完整链接只取 path 拼当前图片域名,相对路径直接拼
final subPath = cover?.startsWith('http') == true
? Uri.tryParse(cover!)?.path
: cover;
model.cover = path.join(Address.baseImagePath ?? '', subPath);
}
_adsList = ads;
try {
final jsonStr = json.encode(ads);
if (TextUtil.isEmpty(jsonStr)) return false;
return lightKV.setString(StoreKeys.ADS_LIST, jsonStr);
} catch (_) {
return false;
}
}
List<AdsInfoModel>? _decodeAds(String? jsonStr) {
if (TextUtil.isEmpty(jsonStr)) return null;
try {
return (jsonDecode(jsonStr!) as List?)
?.map((o) => AdsInfoModel.fromMap(o))
.toList();
} catch (_) {
return null;
}
}
List<AdsInfoModel> _filterByPosition(List<AdsInfoModel>? all, int? position) {
if (position == null || all == null || all.isEmpty) return const [];
return all.where((it) => it.position == position).toList();
}
/// 是否展示 ab 测广告,实时判定,限时免广告到点自动恢复展示
bool get showAbTestAd {
if (_adFreeAlways) return false;
return _adFreeUntil == null || DateTime.now().isAfter(_adFreeUntil!);
}
/// 按用户的广告 AB 类型刷新免广告策略
/// adverAbTestShowType: -1-首次进 app 免广告 0-所有广告都展示 其他N-注册后 N 分钟内免广告
/// 只能在登录成功后调用:AB 类型仅登录接口下发,拿 /mine/info 的结果刷新会把策略清空
Future<void> refresh(UserInfoModel? userInfo) async {
final type = userInfo?.adverAbTestShowType ?? 0;
_adFreeAlways = false;
_adFreeUntil = null; // 0 及其余取值:所有广告都展示
if (type == -1) {
// 首次免广告只给第一次进 app:用掉就记本地,第二次启动起照常展示
final used = await lightKV.getBool(StoreKeys.AD_FREE_FIRST_USED) ?? false;
_adFreeAlways = !used;
if (!used) await lightKV.setBool(StoreKeys.AD_FREE_FIRST_USED, true);
} else if (type > 0) {
// 注册时刻起算 type 分钟:注册时间是固定的绝对时刻,重进 app 既不会续期也不会丢剩余时长
// createdAt 是 UTC 串,缺时区标记时补 Z 再解析,否则会被当成本地时间差出时区偏移
final raw = userInfo?.createdAt ?? '';
final registeredAt = DateTime.tryParse(
raw.endsWith('Z') || raw.contains('+') ? raw : '${raw}Z')
?.toLocal();
_adFreeUntil = registeredAt?.add(Duration(minutes: type));
}
}
/// 专题列表插入广告:每隔 [adGap] 条普通内容插一条广告位
/// [modelArr] 原始列表(会被原地修改);[advList] 广告数据;[adGap] 间隔条数
void insertSectionAds(List<AllSection> modelArr, List<AdsInfoModel> advList,
{int adGap = 5}) {
if (advList.isEmpty || modelArr.isEmpty || !showAbTestAd) return;
int countGap = 0; // 距上一条广告已累计的普通内容数
modelArr.removeWhere((it) => it.isAdsArr()); // 先清掉旧广告,避免重复插入
for (int i = 0; i < modelArr.length; i++) {
AllSection model = modelArr[i];
if (countGap >= adGap) {
// 已够间隔,在当前位置插一条广告(已是广告位则跳过)
countGap = 0;
if (!model.isAdsArr()) {
AllSection adModel = AllSection();
adModel.adsInfoArr = advList;
modelArr.insert(i, adModel);
}
} else {
// 未够间隔:遇广告位重置计数,否则累加
countGap = model.isAdsArr() ? 0 : countGap + 1;
}
// 遍历到末尾且刚好满间隔,补一条广告收尾
if (countGap == adGap && i == modelArr.length - 1) {
AllSection adModel = AllSection();
adModel.adsInfoArr = advList;
modelArr.add(adModel);
break;
}
}
}
/// 视频流插入「单条随机广告」:每隔 [adGap] 条插一条,广告按 adIndex 轮播取用
void insertRandomAds(List<VideoModel> modelArr, List<AdsInfoModel> advList,
{int adGap = 5}) {
if (advList.isEmpty || modelArr.isEmpty || !showAbTestAd) return;
int countGap = 0; // 距上一条广告已累计的普通内容数
int adIndex = 0; // 当前取到第几条广告,与 advList 取模实现轮播
modelArr.removeWhere((it) => it.isAdsArr()); // 先清掉旧广告
for (int i = 0; i < modelArr.length; i++) {
VideoModel model = modelArr[i];
if (countGap >= adGap) {
// 够间隔,插一条随机广告(已是随机广告则跳过)
countGap = 0;
if (!model.isRandomAd()) {
VideoModel adModel = VideoModel();
adModel.randomAdsInfo = advList[adIndex % advList.length];
modelArr.insert(i, adModel);
adIndex++;
}
} else {
countGap = model.isAdsArr() ? 0 : countGap + 1;
}
// 末尾刚好满间隔,补一条收尾
if (countGap == adGap && i == modelArr.length - 1) {
VideoModel adModel = VideoModel();
adModel.randomAdsInfo = advList[adIndex % advList.length];
modelArr.add(adModel);
adIndex++;
break;
}
}
}
/// 视频流插入「整组广告」:每隔 [adGap] 条插一条,广告位携带整个 advList(与 [insertRandomAds] 的单条随机不同)
void insertGroupAds(List<VideoModel> modelArr, List<AdsInfoModel> advList,
{int adGap = 5}) {
if (advList.isEmpty || modelArr.isEmpty || !showAbTestAd) return;
int countGap = 0; // 距上一条广告已累计的普通内容数
modelArr.removeWhere((it) => it.isAdsArr()); // 先清掉旧广告
for (int i = 0; i < modelArr.length; i++) {
VideoModel model = modelArr[i];
if (countGap >= adGap) {
// 够间隔,插一条携带整组广告的广告位
countGap = 0;
if (!model.isRandomAd()) {
VideoModel adModel = VideoModel();
adModel.adsInfoArr = advList;
modelArr.insert(i, adModel);
}
} else {
countGap = model.isAdsArr() ? 0 : countGap + 1;
}
// 末尾刚好满间隔,补一条收尾
if (countGap == adGap && i == modelArr.length - 1) {
VideoModel adModel = VideoModel();
adModel.adsInfoArr = advList;
modelArr.add(adModel);
break;
}
}
}
}