Files
huangguo_android/lib/hj_page/splash/splash_logic.dart
T
2026-09-15 15:44:13 +07:00

297 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hgdj/config/address.dart';
import 'package:hgdj/config/config.dart';
import 'package:hgdj/hj_model/home/plate_model.dart';
import 'package:hgdj/hj_model/splash/ads_model.dart';
import 'package:hgdj/hj_model/splash/cut_info.dart';
import 'package:hgdj/hj_model/splash/domain_source_model.dart';
import 'package:hgdj/hj_model/user/user_info_model.dart';
import 'package:hgdj/hj_page/main_page/main_logic.dart';
import 'package:hgdj/hj_page/main_page/main_page.dart';
import 'package:hgdj/hj_page/pre_sale/limit_time_provider.dart';
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
import 'package:hgdj/hj_utils/api_service/common_service.dart';
import 'package:hgdj/hj_utils/detect_line_manager.dart';
import 'package:hgdj/hj_utils/local_server.dart';
import 'package:hgdj/hj_utils/local_server_guard.dart';
import 'package:hgdj/hj_utils/text_util.dart';
import 'package:hgdj/hj_utils/version_util.dart';
import 'package:hgdj/tools_base/ad_manager.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:hgdj/tools_base/net/api_exception.dart';
import 'package:hgdj/tools_base/net/net_manager.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
import 'package:path/path.dart' as path;
import '../../alert/splash/line_error_dialog.dart';
import '../../alert/splash/update_dialog.dart';
import '../../hj_utils/free_play_manager.dart';
import '../../tools_base/loading/loading_alert_widget.dart';
import '../../track_event_manager/device_service.dart';
import '../live/live_main_page.dart';
import '../main_page/provider/msg_provider.dart';
//启动页
class SplashLogic extends GetxController {
// ===== 开屏广告 =====
List<AdsInfoModel>? adsList; //开屏广告,没有就直接过
bool isAdLoaded = false; //广告数据已就位(可能为空),页面据此决定要不要挂广告层
bool isAdDone = false; //广告已放完/跳过,只等数据就绪
// ===== 启动数据 =====
bool isDataReady = false; //模块列表已落地
bool _isExiting = false; //强更被拒、已请求退出 app:后续流程一律不再放行首页
@override
onReady() {
super.onReady();
_start();
}
Future _start() async {
netManager.clearUserAgent(); //清除老的ua
await AdManager().preload(); //预热广告内存缓存,后续统一用 AdManager().adsByType
_loadAds(isLast: true); //获取上次启动的广告
final line = await _detectLine(); //选线
//拿不到线路就到此为止:要么用户放弃了重试,要么弹框回调里重试成功、
//已经自己走了一遍 _initByLine。这里再调一次会重复初始化
if (TextUtil.isEmpty(line)) return;
_initByLine(line!); //初始化流程
}
//选到线路后的完整启动流程
Future _initByLine(String line) async {
//1. 初始化网络层
_initNet(line);
//2.先登录,后升级,不然渠道一升级就没有量了,先登录才能获取配置信息
await _devLogin();
// 模块列表只依赖登录拿到的 token,请求先发出去跟下面的配置/更新检查并行跑,
// 但结果留到更新检查之后才放行(见末尾)——强更期间不能让首页变成可跳
// ignore():先占个监听位,免得错误在 await 到之前被当成未捕获异常上报;真正的错误照旧在 await 处抛
final tagTask = CommonService.getTagMarks()..ignore();
//2. 获取远程配置
final versions = await _fetchConfig();
PreSaleProvider().refreshConfig();
MineMsgProvider().startPayTimer(); //冷启动 /ping/domain 下发支付分层配置后启动倒计时
FreePlayManager().refresh(); //免费次数进首页才用得到,不 await、不挡启动
await _checkUpdate(versions); //强更时会停在启动页,必须挡在放行之前
if (_isExiting) return; //强更被拒,app 正在退出,别再往下放行
_initServer(); // 初始化localserver缓存
await _waitModules(tagTask); //放行跳首页
}
///isLast = true 取出上次展示的广告
Future _loadAds({bool isLast = true}) async {
final list =
isLast ? await AdManager().lastAdsByType(1) : AdManager().adsByType(1);
if (AdManager().showAbTestAd) adsList = list;
isAdLoaded = true;
update();
}
///一直检查网络直到成功
Future<String?> _detectLine() async {
LoadingAlertWidget.show(title: "选线中...");
final net = await Connectivity().checkConnectivity();
// 有网络,开始选线
if (net != ConnectivityResult.none) {
final line = await DetectLineManager().detectLineOnce();
LoadingAlertWidget.cancel();
if (TextUtil.isNotEmpty(line)) return line;
}
await LineErrorDialog.show(callback: () async {
final line = await DetectLineManager().detectLineOnce();
LoadingAlertWidget.cancel();
if (TextUtil.isNotEmpty(line)) _initByLine(line); //初始化流程
});
LoadingAlertWidget.cancel();
return null;
}
void _initNet(String host) {
Address.baseHost = host;
Address.baseApiPath = path.join(Address.baseHost ?? "", Address.apiPrefix);
netManager.init(Address.baseApiPath ?? "");
}
///登录
Future<UserInfoModel?> _devLogin() async {
String paste = "";
//2.获取渠道码
final channel = await DeviceInfoService.getChannel();
if (TextUtil.isNotEmpty(channel)) {
final traceId = await DeviceInfoService.fetchTraceId();
final cutInfo = CutInfo()
..dc = channel
..tid = traceId;
paste = json.encode(cutInfo.toJson());
} else {
//1.获取粘贴板(走缓存,避免 iOS 多次弹系统读取提示)
final clipText = await DeviceInfoService.getClipboardTextCached();
if (TextUtil.isNotEmpty(clipText)) {
paste = clipText!;
}
}
final deviceId = DeviceInfoService.deviceId;
await netManager.refreshUserAgent();
//登录 + 重试
while (true) {
final userInfo = await globalStore.loginByDevice(deviceId, paste: paste);
if (userInfo != null) return userInfo; //免广告策略已在 globalStore 登录成功时算好
final retry = await CommonAlert.show(
content: "登录失败,是否重试?",
confirmText: '重试',
);
if (retry != true) return null;
}
}
//拉远端配置并落地,失败弹框重试;用户放弃就返回空版本列表(等于不检查更新)
Future<List<CheckVersionInfo>> _fetchConfig() async {
List<CheckVersionInfo>? versions;
while (versions == null) {
try {
final info = await CommonService.fetchRemoteConfig();
if (info == null) {
// 配置拉取失败(无 token / 网络异常 / 返回体异常),走下面 catch 的重试逻辑
throw Exception('获取远端配置为空');
}
await _applyDomains(info);
await AdManager().saveAds(info.ads?.adsInfoList);
await _loadAds(isLast: false);
Config.aiUndressPrice = info.aiUndressPrice ?? '';
Config.aiVideoPrice = info.aiImageToVideoPrice ?? '';
Config.aiDrawPrice = info.aiTextToImagePrice ?? '';
Config.aiNovelPrice = info.aiTextToNovelPrice ?? '';
Config.isStoreOpen = info.storeIsOpen ?? false;
Config.jgArea = info.jgArea;
versions = info.ver ?? [];
//直播模块开关
LiveMainPage.broadcast = info.broadcast ?? false;
//预售相关
presaleProvider.advanceStatus = info.advanceStatus;
presaleProvider.advancePage = info.advancePage;
//幸运抽奖相关
presaleProvider.luckyDrawH5 = info.luckyDrawH5;
presaleProvider.luckyDrawIcon = info.luckyDrawIcon;
//限时活动相关
limitTimeProvider.bannerJump = info.bannerJump;
Config.proxyBanner = info.proxyBannerJump;
Config.hotWords = info.hotSearchTerms ?? [];
Config.searchHints = info.searchHintWord ?? [];
Config.darkWebVipId = info.darkWebVipId;
Config.darkWebVipName = info.darkWebVipName;
Config.shortDramaCardId = info.shortDramaCardId;
} catch (e) {
debugLog('getRemoteConfig', e.toString());
final retry = await CommonAlert.show(
content: "获取配置信息失败${(e is ApiException) ? e.code : 1500},是否重试?",
confirmText: '重试',
);
if (retry != true) break;
}
}
return versions ?? [];
}
//域名/公告落地
Future _applyDomains(DomainSourceModel info) async {
//域名列表信息
for (final source in info.sourceList!) {
switch (source.type) {
case "IMAGE":
Address.baseImagePath = source.domain![0].url;
break;
case "VID":
Address.cdnAddressLists = source.domain!;
Address.cdnAddress = Address.cdnAddressLists[0].url;
break;
case "GUIDE":
Address.groundUrl = source.domain![0].url;
break;
case 'AUDIO':
Address.audioCdnAddress = source.domain?.firstOrNull?.url;
break;
}
}
AdManager().announceList = info.ads?.announList ?? [];
}
///检查是否需要更新
Future _checkUpdate(List<CheckVersionInfo> versions) async {
//存内存,设置页的「检查更新」直接读这份比对,不再发请求
saveVersion(versions);
final target = checkUpdate();
if (target == null) return;
// 用户是否点了更新
final isUpdate = await UpdateDialog.show(target);
if (isUpdate != true && target.forcedUpdate == true) {
//SystemNavigator.pop 只 finish Activityengine 还能活几百毫秒,
//这期间后面的流程会照跑、够把首页放行出去,所以打标记让调用方直接收尾
_isExiting = true;
await SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
}
//本地缓存服务:守护进程被内部 Timer 持有,不用再存字段
Future _initServer() async {
final server = CacheServer(cacheManager: null, openSubManager: true);
await LocalServerGuard(server).run();
//其余任意文件拦截
server.addReqFilter(LOCAL_ALL_FILTER, Address.baseImagePath!);
}
//模块数据落地 → 放行跳首页。请求在登录后就发出去了,这里只等结果。
//拿不到模块一律不放行:首页 tab 全靠它,宁可停在启动页重试,也不进只剩"最新"的降级首页。
//弹框只给"确定"一个选择,且不判返回值——点遮罩关掉也照样重试,不给静默卡死的出口
Future _waitModules(Future<HomePlateModel?> task) async {
var pending = task;
while (true) {
HomePlateModel? model;
try {
model = await pending;
} catch (e) {
//网络异常和 fromJson 异常都被网络层吞成 null,走不到这;兜的是请求前置(取token/签名)抛的异常
debugLog('getTagMarks', e.toString());
}
if (model != null) {
Config.plateModule = model;
isDataReady = true;
_tryJump();
return;
}
await CommonAlert.show(content: "获取模块信息失败,请重试", showCancel: false);
pending = CommonService.getTagMarks(); //旧 Future 已完成,重试必须重新发请求
}
}
//广告结束:标记可跳转、刷新出过渡遮罩,再尝试进首页
void onAdFinish() {
isAdDone = true;
update();
_tryJump();
}
//广告和数据两边都就绪才进首页
void _tryJump() {
//opaque 在 offAll 里默认 false,不显式打开会在转场时透出下层
if (isAdDone && isDataReady)
Get.offAll(() => const MainPage(),
binding: MainPageBinding(), opaque: true);
}
}