flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
import 'package:isar_community/isar.dart';
part 'app_version_entity.g.dart';
/// ============================================================
/// 【RemoteVersionEntity】远端版本策略实体(Isar 集合 + API 响应模型)
///
/// 一、双重身份:
/// 1. Isar @collection → 可持久化到本地数据库,离线时读取缓存策略;
/// 2. fromJson 工厂 → 直接解析后端 GET /api/app/version 返回的 data。
///
/// 二、字段对照(后端 JSON → 本实体):
/// latestVersion → 最新对外展示版本号(如 "1.1.0")
/// latestBuild → 最新构建号(整型,用于版本比对)
/// minSupportBuild → 最低兼容构建号(本地 build < 此值 → 强制更新)
/// force → 是否强制更新(非最低兼容场景的开关)
/// updateDesc → 更新日志列表
/// androidUrl → 安卓下载地址
/// iosUrl → iOS App Store 链接
/// enableHotfix → 热更新开关(仅安卓生效,iOS 禁止下发 dart 代码)
///
/// 三、Isar 注意:
/// - 改字段后必须重跑 build_runner 重新生成 Schema;
/// - Isar 只在本文件被 native 端 import(条件导出),web 不会编译到。
/// ============================================================
@collection
class RemoteVersionEntity {
RemoteVersionEntity();
Id id = Isar.autoIncrement;
String? latestVersion;
int? latestBuild;
int? minSupportBuild;
bool? force;
/// 更新日志(`List<String>` Isar 原生支持)
List<String>? updateDesc;
String? androidUrl;
String? iosUrl;
bool? enableHotfix;
/// 从后端 JSON 解析(data 层,不含外层 code)
factory RemoteVersionEntity.fromJson(Map<String, dynamic> json) {
return RemoteVersionEntity()
..latestVersion = json['latestVersion'] as String?
..latestBuild = json['latestBuild'] as int?
..minSupportBuild = json['minSupportBuild'] as int?
..force = json['force'] as bool?
..updateDesc = (json['updateDesc'] as List<dynamic>?)
?.map((e) => e.toString())
.toList()
..androidUrl = json['androidUrl'] as String?
..iosUrl = json['iosUrl'] as String?
..enableHotfix = json['enableHotfix'] as bool?;
}
/// 判断是否需要强制更新
/// [localBuild] 本机构建号(package_info_plus 读取)
bool needsForceUpdate(int localBuild) {
final minBuild = minSupportBuild ?? 0;
if (localBuild < minBuild) return true;
if (localBuild < (latestBuild ?? 0) && (force ?? false)) return true;
return false;
}
/// 判断是否需要更新(含可选)
bool needsUpdate(int localBuild) {
return localBuild < (latestBuild ?? 0);
}
@override
String toString() =>
'RemoteVersion(latest=$latestVersion, build=$latestBuild, '
'minBuild=$minSupportBuild, force=$force, hotfix=$enableHotfix)';
}
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
/// ============================================================
/// 【AppVersionService + RemoteVersionEntity】版本服务 —— 平台分发入口
///
/// 用法:
/// import 'package:new_learn/core/version/app_version_service.dart';
/// final service = AppVersionService.instance;
/// await service.init();
/// final update = await service.checkForUpdate();
///
/// 一、为什么拆成条件导出(和 app_database.dart 同模式):
/// native 实现 import 了 isar_community + package_info_plus,
/// web 端没有 Isar 支持(编译不过),所以 web 用空实现。
///
/// 二、条件导出规则:
/// native(dart.library.io 为真)→ app_version_service_io.dart
/// web(dart.library.io 为假) → app_version_service_web.dart
///
/// 三、两个文件必须保持 API 一致(类名、方法签名完全相同),
/// 否则共享代码在某个平台编译不过。
/// ============================================================
library;
export 'app_version_service_web.dart'
if (dart.library.io) 'app_version_service_io.dart';
@@ -0,0 +1,169 @@
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:isar_community/isar.dart';
import 'package:new_learn/core/network/dio_client.dart';
import 'package:new_learn/core/storage/database/app_database_io.dart';
import 'app_version_entity.dart';
/// ============================================================
/// 【AppVersionService】版本核心服务 —— native 真实现(Android/iOS/桌面)
///
/// 一、职责:
/// 1. init():通过 package_info_plus 读取本机 versionName、buildNumber;
/// 2. fetchRemoteVersion():请求后端 GET /api/app/version,解析并存入 Isar;
/// 3. checkForUpdate():版本比对,返回 RemoteVersionEntity 或 null;
/// 4. showUpdateDialogIfNeeded():在页面 mounted 后唤起更新弹窗。
///
/// 二、隐私约束:
/// ⚠️ 严禁在隐私协议同意前调用 init() 或任何网络请求。
/// 只有 privacyAcceptedNotifier.value == true 后才能使用本服务。
///
/// 三、缓存策略:
/// 接口成功 → 解析 RemoteVersionEntity → Isar 写入缓存(覆盖旧数据);
/// 网络异常 → 从 Isar 读取上次缓存策略做降级判断。
///
/// 四、版本比对规则(全程用 buildNumber 整型比较):
/// localBuild < minSupportBuild → 强制更新
/// localBuild < latestBuild && force == true → 强制更新
/// localBuild < latestBuild && force == false → 可选更新
/// localBuild >= latestBuild → 无需更新
/// ============================================================
class AppVersionService {
AppVersionService._();
static final AppVersionService instance = AppVersionService._();
/// 本机版本名(如 "1.0.0")
String _versionName = '';
String get versionName => _versionName;
/// 本机构建号(如 1)
int _buildNumber = 0;
int get buildNumber => _buildNumber;
/// 最近一次获取到的远端版本信息(检查后可读取)
RemoteVersionEntity? _remoteVersion;
RemoteVersionEntity? get remoteVersion => _remoteVersion;
/// ① 初始化:读取本机版本信息
/// ⚠️ 必须在隐私协议同意后调用
Future<void> init() async {
try {
final info = await PackageInfo.fromPlatform();
_versionName = info.version;
_buildNumber = int.tryParse(info.buildNumber) ?? 0;
debugPrint(
'▶ 【版本】本机版本:$_versionName+$_buildNumber');
} catch (e) {
debugPrint('▶ 【版本】读取本机版本失败:$e');
}
}
/// ② 请求后端版本接口
/// 成功:解析并存入 Isar 缓存
/// 失败:从 Isar 缓存读取上次策略
Future<RemoteVersionEntity?> fetchRemoteVersion() async {
try {
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🎯 Mock 开关:改成 true 用模拟数据,false 请求真实接口
// 修改 _mockData 模拟不同场景,热重启即可
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// const bool mock = false;
const bool mock = true; // ← 取消注释这行,注释掉上面那行,开启 Mock
final dynamic data;
if (mock) {
// ====== 🎯 修改这里模拟不同场景 ======
// 当前 App 版本:pubspec.yaml → version: 1.0.0+1(buildNumber=1)
//
// 场景 A - 无需更新:latestBuild = 1
// 场景 B - 可选更新:latestBuild = 3, force = false
// 场景 C - 强制更新:latestBuild = 3, force = true
// 场景 D - 最低兼容强制更新:minSupportBuild = 5
data = {
"latestVersion": "1.1.1",
"latestBuild": 3,
"minSupportBuild": 1,
"force": false,
"updateDesc": [
"新增学习报告功能",
"优化视频播放体验",
"修复已知问题",
],
"androidUrl": "https://www.example.com/app-release.apk",
"iosUrl": "https://apps.apple.com/cn/example",
"enableHotfix": false,
};
// 模拟 500ms 网络延迟(和 Vue3 里 setTimeout 一个效果)
await Future.delayed(const Duration(milliseconds: 500));
} else {
data = await DioClient.instance.get('/api/app/version');
}
// 兼容 {code, data: {...}} 和直接返回 data 两种格式
final payload =
(data is Map<String, dynamic> && data.containsKey('data'))
? data['data'] as Map<String, dynamic>
: data as Map<String, dynamic>;
final entity = RemoteVersionEntity.fromJson(payload);
// 存入 Isar 缓存(先清旧数据再写新数据)
await _saveToCache(entity);
debugPrint('▶ 【版本】远端版本:$entity');
return entity;
} catch (e) {
debugPrint('▶ 【版本】请求远端版本失败:$e,尝试读取本地缓存');
return _loadFromCache();
}
}
/// ③ 版本比对,返回需要更新时的远端版本信息,无需更新返回 null
Future<RemoteVersionEntity?> checkForUpdate() async {
if (_buildNumber == 0) {
// 本机版本未初始化,先 init
await init();
}
final remote = await fetchRemoteVersion();
_remoteVersion = remote;
if (remote == null) return null;
if (!remote.needsUpdate(_buildNumber)) {
debugPrint('▶ 【版本】当前已是最新版本');
return null;
}
return remote;
}
/// 判断当前远端版本是否要求强制更新
bool isForceUpdate() {
final remote = _remoteVersion;
if (remote == null) return false;
return remote.needsForceUpdate(_buildNumber);
}
/// 存入 Isar 缓存(覆盖旧数据)
Future<void> _saveToCache(RemoteVersionEntity entity) async {
try {
final db = AppDatabase.instance;
await db.isar.writeTxn(() async {
// 清除旧缓存(只保留一条最新策略)
await db.remoteVersions.clear();
await db.remoteVersions.put(entity);
});
} catch (e) {
debugPrint('▶ 【版本】缓存远端版本失败:$e');
}
}
/// 从 Isar 缓存读取
Future<RemoteVersionEntity?> _loadFromCache() async {
try {
final db = AppDatabase.instance;
final all = await db.remoteVersions.where().findAll();
final cached = all.isNotEmpty ? all.first : null;
if (cached != null) {
debugPrint('▶ 【版本】使用缓存策略:$cached');
}
return cached;
} catch (e) {
debugPrint('▶ 【版本】读取缓存失败:$e');
return null;
}
}
}
@@ -0,0 +1,110 @@
import 'package:flutter/foundation.dart';
import 'package:new_learn/core/network/dio_client.dart';
/// ============================================================
/// 【AppVersionService】版本核心服务 —— web 空实现(Chrome/Edge 调试用)
///
/// web 端没有 package_info_plus(返回固定值)和 Isar(不支持缓存),
/// 只做基础网络请求,无缓存降级。
/// 生产走 Android/iOS,本文件仅供 Web 调试 UI 流程。
///
/// ⚠ 注意:本文件不 import isar 或 package_info_plus 的原生能力,
/// web 端编译不会报错。
/// ============================================================
/// 远端版本实体(web 简化版,非 Isar 集合)
class RemoteVersionEntity {
RemoteVersionEntity();
String? latestVersion;
int? latestBuild;
int? minSupportBuild;
bool? force;
List<String>? updateDesc;
String? androidUrl;
String? iosUrl;
bool? enableHotfix;
factory RemoteVersionEntity.fromJson(Map<String, dynamic> json) {
return RemoteVersionEntity()
..latestVersion = json['latestVersion'] as String?
..latestBuild = json['latestBuild'] as int?
..minSupportBuild = json['minSupportBuild'] as int?
..force = json['force'] as bool?
..updateDesc = (json['updateDesc'] as List<dynamic>?)
?.map((e) => e.toString())
.toList()
..androidUrl = json['androidUrl'] as String?
..iosUrl = json['iosUrl'] as String?
..enableHotfix = json['enableHotfix'] as bool?;
}
bool needsForceUpdate(int localBuild) {
final minBuild = minSupportBuild ?? 0;
if (localBuild < minBuild) return true;
if (localBuild < (latestBuild ?? 0) && (force ?? false)) return true;
return false;
}
bool needsUpdate(int localBuild) {
return localBuild < (latestBuild ?? 0);
}
@override
String toString() =>
'RemoteVersion(latest=$latestVersion, build=$latestBuild, '
'minBuild=$minSupportBuild, force=$force)';
}
class AppVersionService {
AppVersionService._();
static final AppVersionService instance = AppVersionService._();
String _versionName = '0.0.0';
String get versionName => _versionName;
int _buildNumber = 0;
int get buildNumber => _buildNumber;
RemoteVersionEntity? _remoteVersion;
RemoteVersionEntity? get remoteVersion => _remoteVersion;
/// web 端:package_info_plus 不可用,使用固定值
Future<void> init() async {
_versionName = '0.0.0';
_buildNumber = 0;
debugPrint('▶ 【版本】Web 调试模式:版本信息使用固定值');
}
/// web 端:无 Isar 缓存,网络失败直接返回 null
Future<RemoteVersionEntity?> fetchRemoteVersion() async {
try {
final data = await DioClient.instance.get('/api/app/version');
final payload =
(data is Map<String, dynamic> && data.containsKey('data'))
? data['data'] as Map<String, dynamic>
: data as Map<String, dynamic>;
final entity = RemoteVersionEntity.fromJson(payload);
debugPrint('▶ 【版本】远端版本:$entity');
return entity;
} catch (e) {
debugPrint('▶ 【版本】Web 请求远端版本失败:$e(无缓存降级)');
return null;
}
}
Future<RemoteVersionEntity?> checkForUpdate() async {
if (_buildNumber == 0) await init();
final remote = await fetchRemoteVersion();
_remoteVersion = remote;
if (remote == null) return null;
if (!remote.needsUpdate(_buildNumber)) return null;
return remote;
}
bool isForceUpdate() {
final remote = _remoteVersion;
if (remote == null) return false;
return remote.needsForceUpdate(_buildNumber);
}
}
@@ -0,0 +1,13 @@
/// ============================================================
/// 【RemoteVersionEntity】远端版本实体 —— 平台分发入口
///
/// native → app_version_entity.dart(Isar @collection)
/// web → app_version_service_web.dart(普通类,内联在 service web 文件中)
///
/// 和 app_version_service.dart 同模式:条件导出,调用方只 import 这一个文件。
/// ============================================================
library;
export 'app_version_service_web.dart'
// web 文件里定义了 RemoteVersionEntity 类,web 端直接用它
if (dart.library.io) 'app_version_entity.dart';