You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

362 lines
12 KiB

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:http/http.dart' as http;
import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/models/user_Info.dart';
import '../../../routes/app_routes.dart';
import '../../../data/services/network/api.dart';
import '../../../data/services/network/dio_manager.dart';
import '../../../data/models/channel_app_model.dart';
import '../../../data/services/version_update_service.dart';
import '../../../modules/common/widgets/version_update_dialog.dart';
import '../../../modules/common/widgets/privacy_policy_dialog.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/db/sqflite_api.dart';
class SplashController extends GetxController {
final GetStorage _storage = GetStorage();
static const String _tag = 'SplashCtrl';
final versionService = VersionUpdateService.to;
// 进度条动画控制
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 隐私政策同意状态 - 新增
final _hasAcceptedPrivacy = false.obs;
RxBool get hasAcceptedPrivacy => _hasAcceptedPrivacy;
// 使用一个反应式变量来决定是否可以导航,替代所有布尔标记
final _canNavigate = false.obs;
@override
void onInit() {
super.onInit();
// 初始化隐私政策状态
_hasAcceptedPrivacy.value =
_storage.read<bool>('hasAcceptedPrivacy') ?? false;
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航
once(_canNavigate, (_) => _navigateToTargetPage());
// 启动整个初始化流程
_initializeApp();
}
/// 启动并协调所有初始化任务
Future<void> _initializeApp() async {
// 步骤 1: 检查并等待隐私政策同意
final hasAccepted = await _checkAndRequestPrivacy();
if (!hasAccepted) {
// 如果用户拒绝隐私政策,应用会退出,后续代码不执行
return;
}
// 步骤 3: 并行执行所有启动任务,并等待它们全部完成
// Future.wait 会等待数组中所有的 Future 都完成后才继续
await Future.wait([
_startLoadingAnimation(), // 任务1: 最小动画时长
_performParallelTasks(), // 任务2: 其他所有后台任务
]);
// 步骤 4: 所有任务(动画和后台)都已完成,触发导航
_canNavigate.value = true;
}
/// 切换隐私政策同意状态 - 新增方法
// void togglePrivacyAcceptance() {
// _hasAcceptedPrivacy.value = !_hasAcceptedPrivacy.value;
// _storage.write('hasAcceptedPrivacy', _hasAcceptedPrivacy.value);
// }
/// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作
/// 返回用户是否同意
Future<bool> _checkAndRequestPrivacy() async {
if (_hasAcceptedPrivacy.value) {
return true;
}
final completer = Completer<bool>();
WidgetsBinding.instance.addPostFrameCallback((_) {
PrivacyPolicyDialog.show(
onAccept: () {
_storage.write('hasAcceptedPrivacy', true);
_hasAcceptedPrivacy.value = true;
Get.back(); // 关闭弹窗
if (!completer.isCompleted) completer.complete(true);
},
onReject: () {
if (!completer.isCompleted) completer.complete(false);
exit(0); // 退出应用
},
canDismiss: false,
);
});
return completer.future;
}
/// 网络连接检查 - 阻塞版本
Future<void> _checkNetworkStatus() async {
while (true) {
// 无限循环直到网络检查成功
try {
final connectivityResult = await Connectivity().checkConnectivity();
final hasConnectivity =
!connectivityResult.contains(ConnectivityResult.none);
// 条件变更:满足“有网络连接”或“能访问至少一个测试地址”之一即通过
if (hasConnectivity) {
Logger.d(_tag, '网络检查通过:检测到设备网络连接');
break;
}
// 若设备层面无网络标识,再尝试外网连通性
final hasInternet = await _checkInternetConnection();
if (hasInternet) {
Logger.d(_tag, '网络检查通过:可访问外网测试地址');
break;
}
// 两个条件都不满足,提示并重试
await _showNoNetworkDialogAndWait();
continue;
} catch (e) {
Logger.e(_tag, '网络检查失败: $e');
Logger.e(_tag, '网络检查异常: $e');
await _showNoNetworkDialogAndWait();
continue;
}
}
}
/// 检查实际的网络连通性
Future<bool> _checkInternetConnection() async {
// 业务服务器探测跟随 .env 的 SERVER_URL(同 doc_url_helper 的做法),
// 别再写死域名 —— 原先写死的是上一代服务器的两个地址,换服务器后必然超时,
// 每次启动白等两轮 5s 才 fallback 到 baidu/google。
final testUrls = [
'${DioManager().baseUrl}/ping', // 当前业务服务器
'https://www.baidu.com', // 百度
'https://www.google.com', // Google
];
for (final url in testUrls) {
try {
final response = await http.get(
Uri.parse(url.trim()), // 去除空格
headers: {'User-Agent': 'Eaimar-App'}, // 有助于后端识别
).timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
Logger.d(_tag, '网络连接检查成功: $url');
return true;
}
} on TimeoutException {
Logger.w(_tag, '网络连接超时: $url');
continue;
} on Exception catch (e) {
Logger.w(_tag, '网络连接失败: $url, 错误: $e');
continue;
}
}
Logger.e(_tag, '所有网络连接检查均失败');
return false;
}
/// 动画任务:返回一个在动画结束时完成的 Future
Future<void> _startLoadingAnimation() {
const totalDuration = 1500; // 动画总时长1.5秒
const steps = totalDuration ~/ 30;
const stepValue = 1.0 / steps;
final completer = Completer<void>();
Timer.periodic(const Duration(milliseconds: 30), (timer) {
if (_loadingProgress.value < 1.0) {
_loadingProgress.value += stepValue;
} else {
_loadingProgress.value = 1.0;
timer.cancel();
if (!completer.isCompleted) completer.complete();
}
});
return completer.future;
}
/// 将所有并行的后台任务打包成一个 Future
Future<void> _performParallelTasks() async {
//步骤2:检查网络连接状态
await _checkNetworkStatus();
// 同样使用 Future.wait,确保此函数在所有子任务完成后才结束
// 这里原先还会先弹 ATT 追踪授权框(app_tracking_transparency)。已移除:
// 本 App 从不读取 IDFA,也没有任何广告/归因 SDK,弹框纯粹劝退用户,
// 还会让 App Store Connect 要求申报「用于追踪的数据类型」。详见 CLAUDE.md。
await _performVersionCheckAsync();
await _validateTokenAsync();
//await Future.wait([
//_validateTokenAsync(),
//_performVersionCheckAsync(),
//]);
Logger.d(_tag, '所有后台任务已完成');
}
/// 异步验证Token和加载应用配置
Future<void> _validateTokenAsync() async {
try {
final token = _storage.read('logintoken');
if (token != null && token.isNotEmpty) {
final isLoggedIn = await getLoginToken();
if (isLoggedIn) {
// 只有登录成功,才获取App配置
await getAppConfig();
// ⚠️ 会议模板**不放在启动关键路径上**:它只被会议模板页读取
// (落 sqlite,由 SqfliteApi.getMeetingTemplateList 消费),
// 启动时没有任何地方要用。原来 await 它等于让每次冷启动的白屏
// 多背一次网络往返——而 _initializeApp 要等这一整串跑完才允许导航。
// 改成后台拉:模板照常更新,启动不再等它。
unawaited(_initMeetingTemplate());
} else {
await User.clearUserSession(); //
}
}
} catch (e) {
Logger.e(_tag, 'Token验证或配置加载失败: $e');
await User.clearUserSession();
}
}
/// 异步执行版本检查
Future<void> _performVersionCheckAsync() async {
try {
final versionService = VersionUpdateService.to;
final updateInfo = await versionService.checkForUpdate();
if (updateInfo.hasUpdate) {
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}');
// 推荐更新被用户「稍后提醒」过:同一版本 24 小时内不再弹。强更不受影响。
if (updateInfo.updateType == UpdateType.optional &&
versionService.isSnoozed(updateInfo.latestVersion)) {
Logger.i(_tag, '该版本已被稍后提醒,本次不弹');
return;
}
// 等待到下一微任务,降低与其它 UI 的并发竞争
await Future.delayed(Duration.zero);
try {
await VersionUpdateDialog.show(updateInfo);
} catch (e) {
Logger.e(_tag, '更新弹窗显示失败: $e');
}
} else {
Logger.i(_tag, '当前已是最新版本');
}
} catch (e) {
Logger.e(_tag, '版本检查失败: $e');
}
}
/// 根据最终状态跳转到目标页面
Future<void> _navigateToTargetPage() async {
// 导航时直接检查最终的数据状态,而不是依赖中间变量
final token = _storage.read('logintoken');
final isAppConfigInitialized = AppConfig.isInitialized();
// 检查登录状态和配置初始化状态
if (token != null && token.isNotEmpty && isAppConfigInitialized) {
// 登录成功 → 直接进入首页
Get.offAllNamed(Routes.home);
} else {
_storage.remove('meeting_upload_list');
_storage.remove('operation_records');
_storage.remove('operation_delids');
Get.offAllNamed(Routes.login);
}
}
/// 异步获取登录Token
Future<bool> getLoginToken() async {
try {
final appVersion = versionService.cachedAppVersion;
final channel = versionService.cachedChannel;
Logger.i(_tag, 'Token登录请求: version=$appVersion, channel=$channel');
final response = await Api.tokenlogin({
'version': appVersion,
'channel': channel,
});
if (response?['user'] != null && response['user'] != '') {
final user = User.fromJson(response);
await _storage.write('user_info', user.toJson());
return true;
}
return false;
} catch (e) {
Logger.e(_tag, 'Token获取失败: $e');
return false;
}
}
Future<bool> getAppConfig() async {
try {
final response = await Api.getappconfig();
if (response == null) return false;
final resp = UserGetAppConfigResp.fromJson(response);
AppConfig.initialize(resp);
return true;
} catch (e) {
Logger.e(_tag, 'getAppConfig 获取失败: $e');
return false;
}
}
Future _initMeetingTemplate() async {
try {
var response = await Api.getEchomeetTemplates();
if (response != null) {
await SqfliteApi.initMeetingTemplate(response['templates']);
}
} catch (e) {
Logger.e(_tag, 'initMeetingTemplate 获取失败: $e');
}
}
}
/// 显示无网络连接对话框并等待用户操作(阻塞版本)
Future<void> _showNoNetworkDialogAndWait() async {
final completer = Completer<void>();
WidgetsBinding.instance.addPostFrameCallback((_) {
Get.dialog(
AlertDialog(
title: Row(
children: [
const Icon(Icons.wifi_off, color: Colors.orange),
const SizedBox(width: 8),
Text('networkError'.tr), // 无网络连接
],
),
content: Text('networkConnectionFailed'.tr), // 网络连接失败,请检查网络设置
actions: [
TextButton(
onPressed: () {
Get.back(); // 关闭弹窗
completer.complete(); // 完成Future,继续执行
},
child: Text('retry'.tr), // 重试
),
],
),
barrierDismissible: false, // 不允许点击外部关闭
);
});
// 等待用户点击重试按钮
return completer.future;
}