1. 新增工作台三栏布局模式,适配宽屏设备 2. 添加跨平台系统托盘支持,新增托盘图标资源 3. 修复工作台模式下导航返回异常问题 4. 统一JSON类型安全解析,替换硬类型转换 5. 增加macOS深度链接支持,统一渠道分发信息 6. 优化部分页面生命周期和状态加载逻辑 7. 移除废弃的nearby_connections依赖
494 lines
15 KiB
Dart
494 lines
15 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — Dio HTTP 缓存配置
|
||
/// 创建时间: 2026-05-27
|
||
/// 更新时间: 2026-06-19
|
||
/// 作用: 配置 dio_cache_interceptor 缓存策略
|
||
/// GET 请求默认缓存5分钟,特定接口可自定义
|
||
/// 排除需要实时数据的接口(登录、签到等)
|
||
/// 双层缓存: L1内存(快速) + L2 Hive持久化(重启不丢失)
|
||
/// 上次更新: 修复JSON类型安全问题,使用SafeJson.parseInt替代as int? ?? 0
|
||
/// ============================================================
|
||
|
||
import 'dart:async';
|
||
|
||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||
import 'package:xianyan/core/utils/safe_json.dart';
|
||
|
||
import '../utils/logger.dart';
|
||
import '../storage/hive_safe_access.dart';
|
||
|
||
// ============================================================
|
||
// Hive持久化缓存存储 (L2)
|
||
// ============================================================
|
||
|
||
/// 基于Hive的持久化缓存存储,App重启后缓存仍在
|
||
class HiveCacheStore extends CacheStore {
|
||
static const _boxName = 'dio_http_cache';
|
||
Box<dynamic>? _box;
|
||
Completer<void>? _initCompleter;
|
||
|
||
/// 获取Hive Box实例(通过 HiveSafeAccess 安全访问)
|
||
/// 捕获objective_c原生库加载失败等异常,降级为空缓存
|
||
Future<Box<dynamic>> _getBox() async {
|
||
if (_box != null && _box!.isOpen) return _box!;
|
||
if (_initCompleter != null) {
|
||
await _initCompleter!.future;
|
||
return _box!;
|
||
}
|
||
_initCompleter = Completer<void>();
|
||
try {
|
||
// 使用 HiveSafeAccess 安全打开 Box(带重试和缓存)
|
||
final box = await HiveSafeAccess.safeBox<dynamic>(name: _boxName);
|
||
_box = box as Box<dynamic>?;
|
||
_initCompleter!.complete();
|
||
Log.i('HiveCacheStore: Hive缓存Box已打开 (通过HiveSafeAccess)');
|
||
return _box!;
|
||
} on ArgumentError catch (e) {
|
||
// iOS模拟器objective_c库加载失败时降级处理
|
||
Log.e('HiveCacheStore: 原生库加载异常(通常为模拟器环境)', e);
|
||
// 返回一个内存中的临时Box作为降级
|
||
_box = await Hive.openBox<dynamic>('dio_http_cache_fallback');
|
||
_initCompleter!.complete();
|
||
return _box!;
|
||
} catch (e) {
|
||
_initCompleter!.completeError(e);
|
||
_initCompleter = null;
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
/// 序列化CacheResponse为Map
|
||
static Map<String, dynamic> _serialize(CacheResponse resp) {
|
||
return {
|
||
'cc_maxAge': resp.cacheControl.maxAge,
|
||
'cc_maxStale': resp.cacheControl.maxStale,
|
||
'cc_minFresh': resp.cacheControl.minFresh,
|
||
'cc_mustRevalidate': resp.cacheControl.mustRevalidate,
|
||
'cc_privacy': resp.cacheControl.privacy,
|
||
'cc_noCache': resp.cacheControl.noCache,
|
||
'cc_noStore': resp.cacheControl.noStore,
|
||
'cc_other': resp.cacheControl.other,
|
||
'content': resp.content,
|
||
'date': resp.date?.toIso8601String(),
|
||
'eTag': resp.eTag,
|
||
'expires': resp.expires?.toIso8601String(),
|
||
'headers': resp.headers,
|
||
'key': resp.key,
|
||
'lastModified': resp.lastModified,
|
||
'maxStale': resp.maxStale?.toIso8601String(),
|
||
'priority': resp.priority.index,
|
||
'requestDate': resp.requestDate.toIso8601String(),
|
||
'responseDate': resp.responseDate.toIso8601String(),
|
||
'url': resp.url,
|
||
'statusCode': resp.statusCode,
|
||
};
|
||
}
|
||
|
||
/// 反序列化Map为CacheResponse
|
||
static CacheResponse _deserialize(Map<dynamic, dynamic> raw) {
|
||
final map = Map<String, dynamic>.from(raw);
|
||
return CacheResponse(
|
||
cacheControl: CacheControl(
|
||
maxAge: SafeJson.parseInt(map['cc_maxAge'], -1),
|
||
maxStale: SafeJson.parseInt(map['cc_maxStale'], -1),
|
||
minFresh: SafeJson.parseInt(map['cc_minFresh'], -1),
|
||
mustRevalidate: map['cc_mustRevalidate'] as bool? ?? false,
|
||
privacy: map['cc_privacy'] as String?,
|
||
noCache: map['cc_noCache'] as bool? ?? false,
|
||
noStore: map['cc_noStore'] as bool? ?? false,
|
||
other: map['cc_other'] != null
|
||
? List<String>.from(map['cc_other'] as List)
|
||
: [],
|
||
),
|
||
content: map['content'] != null
|
||
? List<int>.from(map['content'] as List)
|
||
: null,
|
||
date: map['date'] != null
|
||
? DateTime.parse(map['date'] as String)
|
||
: null,
|
||
eTag: map['eTag'] as String?,
|
||
expires: map['expires'] != null
|
||
? DateTime.parse(map['expires'] as String)
|
||
: null,
|
||
headers: map['headers'] != null
|
||
? List<int>.from(map['headers'] as List)
|
||
: null,
|
||
key: map['key'] as String,
|
||
lastModified: map['lastModified'] as String?,
|
||
maxStale: map['maxStale'] != null
|
||
? DateTime.parse(map['maxStale'] as String)
|
||
: null,
|
||
priority: CachePriority.values[SafeJson.parseInt(map['priority'], 1)],
|
||
requestDate: DateTime.parse(map['requestDate'] as String),
|
||
responseDate: DateTime.parse(map['responseDate'] as String),
|
||
url: map['url'] as String,
|
||
statusCode: SafeJson.parseInt(map['statusCode'], 200),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Future<bool> exists(String key) async {
|
||
final box = await _getBox();
|
||
return box.containsKey(key);
|
||
}
|
||
|
||
@override
|
||
Future<CacheResponse?> get(String key) async {
|
||
final box = await _getBox();
|
||
final data = box.get(key);
|
||
if (data == null) return null;
|
||
try {
|
||
return _deserialize(data as Map);
|
||
} catch (e) {
|
||
Log.w('HiveCacheStore: 反序列化失败 key=$key, $e');
|
||
await box.delete(key);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<List<CacheResponse>> getFromPath(
|
||
RegExp pathPattern, {
|
||
Map<String, String?>? queryParams,
|
||
}) async {
|
||
final box = await _getBox();
|
||
final results = <CacheResponse>[];
|
||
for (final key in box.keys) {
|
||
final data = box.get(key);
|
||
if (data == null) continue;
|
||
try {
|
||
final resp = _deserialize(data as Map);
|
||
if (pathExists(resp.url, pathPattern, queryParams: queryParams)) {
|
||
results.add(resp);
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
return results;
|
||
}
|
||
|
||
@override
|
||
Future<void> set(CacheResponse response) async {
|
||
final box = await _getBox();
|
||
await box.put(response.key, _serialize(response));
|
||
}
|
||
|
||
@override
|
||
Future<void> delete(String key, {bool staleOnly = false}) async {
|
||
final box = await _getBox();
|
||
if (!staleOnly) {
|
||
await box.delete(key);
|
||
return;
|
||
}
|
||
final data = box.get(key);
|
||
if (data == null) return;
|
||
try {
|
||
final resp = _deserialize(data as Map);
|
||
if (resp.isStaled()) {
|
||
await box.delete(key);
|
||
}
|
||
} catch (_) {
|
||
await box.delete(key);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> deleteFromPath(
|
||
RegExp pathPattern, {
|
||
Map<String, String?>? queryParams,
|
||
}) async {
|
||
final box = await _getBox();
|
||
final keysToDelete = <dynamic>[];
|
||
for (final key in box.keys) {
|
||
final data = box.get(key);
|
||
if (data == null) continue;
|
||
try {
|
||
final resp = _deserialize(data as Map);
|
||
if (pathExists(resp.url, pathPattern, queryParams: queryParams)) {
|
||
keysToDelete.add(key);
|
||
}
|
||
} catch (_) {
|
||
keysToDelete.add(key);
|
||
}
|
||
}
|
||
if (keysToDelete.isNotEmpty) {
|
||
await box.deleteAll(keysToDelete);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> clean({
|
||
CachePriority priorityOrBelow = CachePriority.high,
|
||
bool staleOnly = false,
|
||
}) async {
|
||
final box = await _getBox();
|
||
if (!staleOnly && priorityOrBelow == CachePriority.high) {
|
||
await box.clear();
|
||
return;
|
||
}
|
||
final keysToDelete = <dynamic>[];
|
||
for (final key in box.keys) {
|
||
final data = box.get(key);
|
||
if (data == null) continue;
|
||
try {
|
||
final resp = _deserialize(data as Map);
|
||
if (resp.priority.index > priorityOrBelow.index) continue;
|
||
if (staleOnly && !resp.isStaled()) continue;
|
||
keysToDelete.add(key);
|
||
} catch (_) {
|
||
keysToDelete.add(key);
|
||
}
|
||
}
|
||
if (keysToDelete.isNotEmpty) {
|
||
await box.deleteAll(keysToDelete);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> close() async {
|
||
final box = _box;
|
||
if (box != null && box.isOpen) {
|
||
await box.close();
|
||
_box = null;
|
||
_initCompleter = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 双层缓存存储 (L1内存 + L2 Hive持久化)
|
||
// ============================================================
|
||
|
||
/// 双层缓存: L1内存缓存(快速命中) + L2 Hive持久化(重启不丢失)
|
||
///
|
||
/// 读取: L1 → L2 → miss (L2命中时回填L1)
|
||
/// 写入: 同时写入L1和L2
|
||
/// 删除/清理: 同时操作L1和L2
|
||
class DualCacheStore extends CacheStore {
|
||
final MemCacheStore _l1;
|
||
final HiveCacheStore _l2;
|
||
|
||
DualCacheStore(this._l1, this._l2);
|
||
|
||
@override
|
||
Future<bool> exists(String key) async {
|
||
if (await _l1.exists(key)) return true;
|
||
return await _l2.exists(key);
|
||
}
|
||
|
||
@override
|
||
Future<CacheResponse?> get(String key) async {
|
||
final l1Resp = await _l1.get(key);
|
||
if (l1Resp != null) return l1Resp;
|
||
|
||
final l2Resp = await _l2.get(key);
|
||
if (l2Resp != null) {
|
||
await _l1.set(l2Resp);
|
||
}
|
||
return l2Resp;
|
||
}
|
||
|
||
@override
|
||
Future<List<CacheResponse>> getFromPath(
|
||
RegExp pathPattern, {
|
||
Map<String, String?>? queryParams,
|
||
}) async {
|
||
return await _l2.getFromPath(pathPattern, queryParams: queryParams);
|
||
}
|
||
|
||
@override
|
||
Future<void> set(CacheResponse response) async {
|
||
await Future.wait([
|
||
_l1.set(response),
|
||
_l2.set(response),
|
||
]);
|
||
}
|
||
|
||
@override
|
||
Future<void> delete(String key, {bool staleOnly = false}) async {
|
||
await Future.wait([
|
||
_l1.delete(key, staleOnly: staleOnly),
|
||
_l2.delete(key, staleOnly: staleOnly),
|
||
]);
|
||
}
|
||
|
||
@override
|
||
Future<void> deleteFromPath(
|
||
RegExp pathPattern, {
|
||
Map<String, String?>? queryParams,
|
||
}) async {
|
||
await Future.wait([
|
||
_l1.deleteFromPath(pathPattern, queryParams: queryParams),
|
||
_l2.deleteFromPath(pathPattern, queryParams: queryParams),
|
||
]);
|
||
}
|
||
|
||
@override
|
||
Future<void> clean({
|
||
CachePriority priorityOrBelow = CachePriority.high,
|
||
bool staleOnly = false,
|
||
}) async {
|
||
await Future.wait([
|
||
_l1.clean(priorityOrBelow: priorityOrBelow, staleOnly: staleOnly),
|
||
_l2.clean(priorityOrBelow: priorityOrBelow, staleOnly: staleOnly),
|
||
]);
|
||
}
|
||
|
||
@override
|
||
Future<void> close() async {
|
||
await Future.wait([
|
||
_l1.close(),
|
||
_l2.close(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 缓存配置 — 统一管理 Dio HTTP 缓存策略
|
||
// ============================================================
|
||
|
||
class CacheConfig {
|
||
CacheConfig._();
|
||
|
||
/// 默认缓存时长: 5 分钟
|
||
static const Duration defaultMaxStale = Duration(minutes: 5);
|
||
|
||
/// 长缓存时长: 30 分钟(适合不常变化的数据)
|
||
static const Duration longMaxStale = Duration(minutes: 30);
|
||
|
||
/// 短缓存时长: 1 分钟(适合频繁更新的数据)
|
||
static const Duration shortMaxStale = Duration(minutes: 1);
|
||
|
||
// ============================================================
|
||
// 排除缓存的接口路径(需要实时数据)
|
||
// ============================================================
|
||
|
||
/// 不缓存的路径前缀列表
|
||
static const List<String> _excludedPaths = [
|
||
'/api/login',
|
||
'/api/register',
|
||
'/api/token/refresh',
|
||
'/api/signin',
|
||
'/api/signin/makeup',
|
||
'/api/user/profile',
|
||
'/api/coin/log',
|
||
'/api/message',
|
||
'/api/upload',
|
||
];
|
||
|
||
// ============================================================
|
||
// 自定义缓存时长的接口
|
||
// ============================================================
|
||
|
||
/// 特定接口的自定义缓存时长
|
||
static const Map<String, Duration> _customDurations = {
|
||
'/api/sentence': Duration(minutes: 10),
|
||
'/api/sentences': Duration(minutes: 10),
|
||
'/api/categories': Duration(minutes: 30),
|
||
'/api/daily': Duration(minutes: 5),
|
||
'/api/hitokoto': Duration(minutes: 3),
|
||
'/api/weather': Duration(minutes: 15),
|
||
'/api/poetry': Duration(minutes: 30),
|
||
};
|
||
|
||
// ============================================================
|
||
// CacheStore 单例
|
||
// ============================================================
|
||
|
||
static DualCacheStore? _store;
|
||
|
||
/// 获取双层缓存存储(L1内存 + L2 Hive持久化)
|
||
static DualCacheStore getStore() {
|
||
_store ??= DualCacheStore(MemCacheStore(), HiveCacheStore());
|
||
Log.i('CacheConfig: 双层缓存存储已初始化 (L1内存 + L2 Hive)');
|
||
return _store!;
|
||
}
|
||
|
||
// ============================================================
|
||
// CacheOptions 构建
|
||
// ============================================================
|
||
|
||
/// 构建默认缓存选项
|
||
static CacheOptions buildOptions() {
|
||
return CacheOptions(
|
||
store: getStore(),
|
||
hitCacheOnNetworkFailure: true,
|
||
policy: CachePolicy.forceCache,
|
||
maxStale: const Duration(minutes: 5),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// 请求级缓存策略
|
||
// ============================================================
|
||
|
||
/// 根据请求路径判断是否排除缓存
|
||
static bool shouldExclude(String path) {
|
||
for (final excluded in _excludedPaths) {
|
||
if (path.startsWith(excluded)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// 根据请求路径获取自定义缓存时长
|
||
static Duration? getCustomDuration(String path) {
|
||
for (final entry in _customDurations.entries) {
|
||
if (path.startsWith(entry.key)) return entry.value;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// 根据请求路径构建 CacheOptions
|
||
static CacheOptions? optionsForPath(
|
||
String path, {
|
||
required CacheOptions baseOptions,
|
||
}) {
|
||
if (shouldExclude(path)) return null;
|
||
|
||
final customDuration = getCustomDuration(path);
|
||
if (customDuration != null) {
|
||
return baseOptions.copyWith(maxStale: customDuration);
|
||
}
|
||
|
||
return baseOptions.copyWith(
|
||
policy: CachePolicy.refresh,
|
||
maxStale: const Duration(minutes: 5),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// 缓存管理
|
||
// ============================================================
|
||
|
||
/// 清除所有 HTTP 缓存
|
||
static Future<void> clearAll() async {
|
||
try {
|
||
final store = _store;
|
||
if (store != null) {
|
||
await store.clean();
|
||
Log.i('CacheConfig: HTTP缓存已清除(L1+L2)');
|
||
}
|
||
} catch (e) {
|
||
Log.e('CacheConfig: 清除HTTP缓存失败', e);
|
||
}
|
||
}
|
||
|
||
/// 清除过期缓存
|
||
static Future<void> cleanExpired() async {
|
||
try {
|
||
final store = _store;
|
||
if (store != null) {
|
||
await store.clean(staleOnly: true);
|
||
Log.i('CacheConfig: 过期HTTP缓存已清除(L1+L2)');
|
||
}
|
||
} catch (e) {
|
||
Log.e('CacheConfig: 清除过期缓存失败', e);
|
||
}
|
||
}
|
||
|
||
/// 强制刷新指定路径的缓存
|
||
static CacheOptions forceRefreshOptions({required CacheOptions baseOptions}) {
|
||
return baseOptions.copyWith(policy: CachePolicy.refresh);
|
||
}
|
||
}
|