Files
jiu/client/lib/providers/update_provider.dart
T
wangjia 3888cf2d65 feat(client): 慢请求监控(>500ms 记日志+上报);拆除列表 provider 的 _cache 失败兜底
- SlowRequestInterceptor:单次 API 调用超 500ms 即 debugPrint long-request
  日志并经 ErrorReporter 上报(error_type=slow_api,按 method+归一路径 5 分钟
  节流)。服务端 GIN 日志只见自身处理耗时,网络往返段只有客户端可观测。
- 拆除 10 个列表 provider 的 _cache 失败兜底:实测线上接口客户端视角典型
  50-120ms、最坏约 270ms(列表类全部 <500ms),失败静默端旧数据弊大于利
  (曾放大跨账号残留问题),改为明确进入错误态由用户重试。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:49:29 +08:00

182 lines
6.0 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:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../core/config/app_config.dart';
import '../core/config/app_constants.dart';
// ── 数据模型 ────────────────────────────────────────────────
class AppUpdateInfo {
final String latestVersion;
final int buildNumber;
final bool forceUpdate;
final String releaseNotes;
final Map<String, String> downloadUrls;
final bool hasUpdate;
const AppUpdateInfo({
required this.latestVersion,
required this.buildNumber,
required this.forceUpdate,
required this.releaseNotes,
required this.downloadUrls,
required this.hasUpdate,
});
}
// ── Provider ────────────────────────────────────────────────
/// null = 检查失败或无更新数据(不影响主流程)
/// AppUpdateInfo with hasUpdate=false = 已是最新版
/// AppUpdateInfo with hasUpdate=true = 有新版本
final updateProvider =
AsyncNotifierProvider<UpdateNotifier, AppUpdateInfo?>(UpdateNotifier.new);
class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
String get _checkUrl => AppConfig.versionUrl;
Timer? _timer;
bool _dismissed = false; // 用户已手动关闭非强制更新提示
final _dio = Dio(BaseOptions(
connectTimeout: AppConstants.updateCheckTimeout,
receiveTimeout: AppConstants.updateCheckTimeout,
));
@override
Future<AppUpdateInfo?> build() async {
ref.onDispose(() {
_timer?.cancel();
_dio.close(force: true);
});
// 启动后延迟首次检查(避免与登录请求竞争)
await Future.delayed(AppConstants.updateInitialDelay);
final result = await _check();
// 定期检查更新
_timer = Timer.periodic(AppConstants.updatePollInterval, (_) async {
_dismissed = false;
final r = await _check();
state = AsyncValue.data(r);
});
return result;
}
/// 手动触发一次检查(供设置页"检查更新"按钮调用)
Future<void> forceCheck() async {
_dismissed = false;
state = const AsyncValue.loading();
state = AsyncValue.data(await _check());
}
/// 用户点击"稍后再说"后调用,隐藏 banner(直到下次定时刷新)
void dismiss() {
_dismissed = true;
// 保留数据但 UI 通过 dismissed 状态判断是否显示
state = AsyncValue.data(state.valueOrNull);
}
bool get isDismissed => _dismissed;
Future<AppUpdateInfo?> _check() async {
try {
final resp = await _dio.get(_checkUrl);
final data = resp.data as Map<String, dynamic>;
final latestVersion = data['version'] as String? ?? '0.0.0';
final buildNumber = data['build_number'] as int? ?? 0;
final forceUpdate = data['force_update'] as bool? ?? false;
final releaseNotes = data['release_notes'] as String? ?? '';
final rawUrls = data['download_urls'] as Map<String, dynamic>? ?? {};
final downloadUrls =
rawUrls.map((k, v) => MapEntry(k, v?.toString() ?? ''));
final current = (await currentAppVersion()).substring(1); // 去 v 前缀
final hasUpdate = _isNewer(latestVersion, current);
return AppUpdateInfo(
latestVersion: latestVersion,
buildNumber: buildNumber,
forceUpdate: forceUpdate,
releaseNotes: releaseNotes,
downloadUrls: downloadUrls,
hasUpdate: hasUpdate,
);
} catch (_) {
return null; // 检查失败静默处理,不影响主业务
}
}
/// 语义化版本比较:latest > current → true
bool _isNewer(String latest, String current) {
final l = _parse(latest);
final c = _parse(current);
for (var i = 0; i < 3; i++) {
if (l[i] > c[i]) return true;
if (l[i] < c[i]) return false;
}
return false;
}
List<int> _parse(String v) {
// 每段取前导数字(容忍 1.1.4-dev / 1.1.4+7 等本地构建后缀)
final parts = v.split('.').map((s) {
final m = RegExp(r'^\d+').firstMatch(s);
return m == null ? 0 : int.parse(m.group(0)!);
}).toList();
while (parts.length < 3) parts.add(0);
return parts;
}
}
// ── 当前版本(单一真相)─────────────────────────────────────
// 优先编译期 APP_VERSION dart-defineCI 与本地脚本都传;本地构建不改
// pubspecPackageInfo 会是旧值);未定义时回退 PackageInfo。
const _definedVersion = String.fromEnvironment('APP_VERSION');
Future<String> currentAppVersion() async {
if (_definedVersion.isNotEmpty) {
return _definedVersion.startsWith('v')
? _definedVersion
: 'v$_definedVersion';
}
final info = await PackageInfo.fromPlatform();
return 'v${info.version}';
}
// ── 版本号 Provider(供状态栏 / 门店信息面板使用)──────────
final appVersionProvider = FutureProvider<String>((ref) async {
return currentAppVersion();
});
// ── 打开下载链接工具函数 ────────────────────────────────────
Future<void> launchUpdateUrl(Map<String, String> downloadUrls) async {
String? urlStr;
if (kIsWeb) {
urlStr = downloadUrls['web'];
} else if (Platform.isMacOS) {
urlStr = downloadUrls['macos'];
} else if (Platform.isWindows) {
urlStr = downloadUrls['windows'];
} else if (Platform.isIOS) {
urlStr = downloadUrls['ios'];
} else if (Platform.isAndroid) {
urlStr = downloadUrls['android'];
} else {
// Linux 等
urlStr = downloadUrls['web'];
}
if (urlStr == null || urlStr.isEmpty) return;
final uri = Uri.parse(urlStr);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}