初始化
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user