Files
xianyan/lib/core/network/cache_config.dart
Developer 83720002e6 feat: 新增工作台模式、系统托盘,修复多平台兼容性问题
1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
2026-06-19 06:43:55 +08:00

494 lines
15 KiB
Dart
Raw Permalink 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.
/// ============================================================
/// 闲言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);
}
}