import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import '../config/app_config.dart'; /// App 版本更新信息(GET /api/v1/app/update 响应 data;无记录时为空信息) class AppUpdateInfo { final String version; final String notes; const AppUpdateInfo({required this.version, required this.notes}); /// 服务器无版本记录时返回空信息,调用方视为无需更新 bool get isEmpty => version.isEmpty; } /// 启动时版本更新检查:仅 Android 检查;服务器版本高于本地版本即强制更新 /// (无普通/强制之分)。 class UpdateChecker { final String baseUrl; final http.Client _client; UpdateChecker({String? baseUrl, http.Client? client}) : baseUrl = baseUrl ?? AppConfig.apiBaseUrl, _client = client ?? http.Client(); /// 下载地址:固定静态路径(管理端上传的 APK 覆盖保存为固定文件) static String downloadUrl({String? baseUrl}) => '${baseUrl ?? AppConfig.apiBaseUrl}/download/observer-latest.apk'; /// 拉取服务器最新版本;非 Android 或网络异常时返回空信息,不阻塞启动 Future fetch() async { if (!Platform.isAndroid) { return const AppUpdateInfo(version: '', notes: ''); } try { final res = await _client .get(Uri.parse('$baseUrl/api/v1/app/update')) .timeout(const Duration(seconds: 8)); // 服务器 Content-Type 无 charset,http 包默认按 latin1 解码会乱码 → 显式 utf8 final body = jsonDecode(utf8.decode(res.bodyBytes)) as Map; final data = body['data'] as Map? ?? const {}; return AppUpdateInfo( version: data['version'] as String? ?? '', notes: data['notes'] as String? ?? '', ); } catch (_) { return const AppUpdateInfo(version: '', notes: ''); } } /// 是否需要更新:服务器版本高于「已装版本与已确认接受版本」中的较大者。 /// APK 版本号不递增时,用户点过「立即更新」后 accepted 追上服务器版本, /// 已更新完成再次启动也不会反复提示。 static bool needsUpdate(String server, String installed, String accepted) { if (server.isEmpty || installed.isEmpty) return false; return isNewer(server, installed) || isNewer(server, accepted); } /// 语义化版本号比较:a > b 返回 true。按数字段比较(1.10.0 > 1.9.9), /// 任一版本号解析失败时视为相等(不触发更新)。 static bool isNewer(String a, String b) { final pa = _parse(a); final pb = _parse(b); if (pa == null || pb == null) return false; for (var i = 0; i < 3; i++) { if (pa[i] != pb[i]) return pa[i] > pb[i]; } return false; } static List? _parse(String v) { final parts = v.split('.'); if (parts.length != 3) return null; final nums = []; for (final p in parts) { final n = int.tryParse(p); if (n == null) return null; nums.add(n); } return nums; } }