import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:cloudreve4_flutter/core/utils/app_logger.dart'; import 'package:cloudreve4_flutter/core/utils/avatar_utils.dart'; /// 头像缓存服务 /// 通过 Dio 下载头像,保存为本地文件 {userId}_avatar.webp /// 服务器头像 200 优先,否则使用 Gravatar (identicon 兜底) class AvatarCacheService extends ChangeNotifier { static AvatarCacheService? _instance; static AvatarCacheService get instance { _instance ??= AvatarCacheService._(); return _instance!; } AvatarCacheService._(); Directory? _cacheDir; final Map _pathCache = {}; final Map _sizeCache = {}; final Set _loadingSet = {}; /// 初始化缓存目录 Future init() async { final appDir = await getApplicationSupportDirectory(); _cacheDir = Directory('${appDir.path}/avatar_cache'); if (!_cacheDir!.existsSync()) { _cacheDir!.createSync(recursive: true); } _scanExistingFiles(); } void _scanExistingFiles() { if (_cacheDir == null) return; for (final entity in _cacheDir!.listSync()) { if (entity is File && entity.path.endsWith('_avatar.webp')) { final name = entity.uri.pathSegments.last; final userId = name.replaceAll('_avatar.webp', ''); _pathCache[userId] = entity.path; try { _sizeCache[userId] = entity.lengthSync(); } catch (_) {} } } } String _filePath(String userId) => '${_cacheDir!.path}/${userId}_avatar.webp'; /// 头像缓存文件是否存在(同步) bool avatarIsExist(String userId) { if (userId.isEmpty) return false; final cached = _pathCache[userId]; if (cached != null && File(cached).existsSync()) return true; return File(_filePath(userId)).existsSync(); } /// 检查服务器头像是否有更新(HEAD 请求比对 Content-Length) /// 返回 true 表示已更新并重新缓存,false 表示无需更新或检查失败 Future avatarIsUpdated(String userId, String baseUrl, String token) async { if (userId.isEmpty) return false; if (!avatarIsExist(userId)) { // 本地无缓存,直接走 getAvatar return await getAvatar(userId, baseUrl: baseUrl, token: token); } try { final url = AvatarUtils.getServerAvatarUrl(baseUrl, userId); // 使用独立 Dio 实例,避免 ApiService 拦截器干扰 final dio = Dio(); final response = await dio.head( url, options: Options( headers: {'Authorization': 'Bearer $token'}, validateStatus: (s) => s != null && s < 500, followRedirects: false, ), ); // 非 200(包括 302 指向 Gravatar)视为无自定义头像,无需比对更新 if (response.statusCode != 200) return false; final remoteSize = response.headers.value('content-length'); if (remoteSize == null) return false; final remoteBytes = int.tryParse(remoteSize) ?? -1; final localBytes = _sizeCache[userId] ?? (File(_filePath(userId)).existsSync() ? File(_filePath(userId)).lengthSync() : -1); AppLogger.d('头像更新检查 ($userId): 本地=$localBytes, 远程=$remoteBytes'); if (remoteBytes != localBytes) { // 大小不一致,重新获取 return await getAvatar(userId, baseUrl: baseUrl, token: token); } return false; } catch (e) { AppLogger.d('头像更新检查失败 ($userId): $e'); return false; } } /// 获取头像:并发请求服务器头像 + Gravatar,服务器 200 优先,否则用 Gravatar /// 返回 true 表示成功获取并缓存,false 表示全部失败 Future getAvatar(String userId, {String? baseUrl, String? token, String? email}) async { if (userId.isEmpty || _cacheDir == null) return false; // 防止同一 userId 并发 if (_loadingSet.contains(userId)) return false; _loadingSet.add(userId); try { final futures = >[]; // 1. 服务器头像 if (baseUrl != null && baseUrl.isNotEmpty && token != null && token.isNotEmpty) { futures.add(_fetchServerAvatar(userId, baseUrl, token)); } // 2. Gravatar (identicon 兜底) if (email != null && email.isNotEmpty) { futures.add(_fetchGravatar(userId, email)); } if (futures.isEmpty) return false; // 并发请求 final results = await Future.wait(futures); final serverResult = results.whereType<_ServerAvatarResult>().firstOrNull; final gravatarResult = results.whereType<_GravatarResult>().firstOrNull; // 服务器头像 200 优先 if (serverResult != null && serverResult.statusCode == 200 && serverResult.bytes != null) { _saveFile(userId, serverResult.bytes!); AppLogger.d('头像获取成功 (服务器, $userId), 大小=${serverResult.bytes!.length}'); notifyListeners(); return true; } // 服务器 404/302 或失败,用 Gravatar if (gravatarResult != null && gravatarResult.bytes != null) { _saveFile(userId, gravatarResult.bytes!); AppLogger.d('头像获取成功 (Gravatar, $userId), 大小=${gravatarResult.bytes!.length}'); notifyListeners(); return true; } AppLogger.d('头像获取失败 ($userId): 服务器状态=${serverResult?.statusCode}, Gravatar=${gravatarResult != null ? '失败' : '未请求'}'); return false; } finally { _loadingSet.remove(userId); } } Future<_AvatarResult> _fetchServerAvatar(String userId, String baseUrl, String token) async { try { final url = AvatarUtils.getServerAvatarUrl(baseUrl, userId); // 使用独立 Dio 实例,避免 ApiService 拦截器干扰 final dio = Dio(); final response = await dio.get>( url, options: Options( responseType: ResponseType.bytes, headers: {'Authorization': 'Bearer $token'}, validateStatus: (s) => s != null && s < 500, // 不跟随重定向:302 指向官方 Gravatar,跟随会绕过镜像加速 followRedirects: false, ), ); return _ServerAvatarResult(response.statusCode ?? 0, response.data); } catch (e) { AppLogger.d('请求服务器头像异常 ($userId): $e'); return _ServerAvatarResult(0, null); } } Future<_AvatarResult> _fetchGravatar(String userId, String email) async { try { final url = await AvatarUtils.getGravatarUrl(email, size: 400); final dio = Dio(); final response = await dio.get>( url, options: Options( responseType: ResponseType.bytes, validateStatus: (s) => s != null && s < 500, ), ); if (response.statusCode == 200 && response.data != null && response.data!.isNotEmpty) { return _GravatarResult(response.data!); } return _GravatarResult(null); } catch (e) { AppLogger.d('请求Gravatar头像异常 ($userId): $e'); return _GravatarResult(null); } } void _saveFile(String userId, List bytes) { if (_cacheDir == null) return; final path = _filePath(userId); final file = File(path); file.writeAsBytesSync(bytes); _pathCache[userId] = path; _sizeCache[userId] = bytes.length; } /// 获取已缓存的头像文件(同步,快速) File? getCachedFile(String userId) { if (_cacheDir == null || userId.isEmpty) return null; final cached = _pathCache[userId]; if (cached != null && File(cached).existsSync()) { return File(cached); } final path = _filePath(userId); if (File(path).existsSync()) { _pathCache[userId] = path; return File(path); } _pathCache.remove(userId); _sizeCache.remove(userId); return null; } /// 批量检查头像是否需要更新(限制并发,避免密集 IO/网络阻塞主线程) Future batchCheckUpdates( List userIds, { required String baseUrl, required String token, int concurrency = 3, }) async { var index = 0; final completer = Completer(); var activeCount = 0; var completedCount = 0; final total = userIds.length; void scheduleNext() { while (activeCount < concurrency && index < total) { final userId = userIds[index++]; activeCount++; avatarIsUpdated(userId, baseUrl, token).whenComplete(() { activeCount--; completedCount++; if (completedCount >= total && !completer.isCompleted) { completer.complete(); } else { scheduleNext(); } }); } if (completedCount >= total && !completer.isCompleted) { completer.complete(); } } scheduleNext(); await completer.future; } /// 清除指定用户的头像缓存(头像上传/切换后调用) Future evictCache(String userId) async { _pathCache.remove(userId); _sizeCache.remove(userId); final path = _filePath(userId); final file = File(path); if (file.existsSync()) { await file.delete(); } notifyListeners(); } /// 清除所有头像缓存(登出时调用) Future clearAllCache() async { _pathCache.clear(); _sizeCache.clear(); if (_cacheDir != null && _cacheDir!.existsSync()) { await _cacheDir!.delete(recursive: true); _cacheDir!.createSync(recursive: true); } notifyListeners(); } } /// 请求结果基类 abstract class _AvatarResult {} class _ServerAvatarResult extends _AvatarResult { final int statusCode; final List? bytes; _ServerAvatarResult(this.statusCode, this.bytes); } class _GravatarResult extends _AvatarResult { final List? bytes; _GravatarResult(this.bytes); }