fix account profile and improve operation logs

This commit is contained in:
ngfchl
2026-07-18 22:35:47 +08:00
parent 6b202475d1
commit dbacdb854f
9 changed files with 255 additions and 126 deletions
+96 -31
View File
@@ -1,5 +1,8 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/legacy.dart';
import '../core/logging/app_logger.dart';
import '../core/http/http_error.dart';
import '../core/storage/storage_manager.dart';
import '../api/guangya_api.dart';
@@ -75,12 +78,19 @@ class AuthState {
String get memberLevel =>
_findStringDeep(userInfo, ['vipName', 'memberName', 'memberLevelName']) ??
'普通会员';
_memberLevelFromAssets;
String get _memberLevelFromAssets {
final svipStatus = _findInt64Deep(userInfo, ['svipStatus']) ?? 0;
if (svipStatus > 0) return '超级会员';
final vipStatus = _findInt64Deep(userInfo, ['vipStatus']) ?? 0;
return vipStatus > 0 ? '会员' : '普通会员';
}
int? get capacity =>
_findInt64Deep(userInfo, ['capacity', 'totalCapacity']);
_findInt64Deep(userInfo, ['totalSpaceSize', 'capacity', 'totalCapacity']);
int? get usedCapacity =>
_findInt64Deep(userInfo, ['usedCapacity', 'usedSpace']);
_findInt64Deep(userInfo, ['usedSpaceSize', 'usedCapacity', 'usedSpace']);
String get capacityText {
if (capacity == null) return '空间信息暂不可用';
@@ -90,7 +100,9 @@ class AuthState {
// ── Static helpers ─────────────────────────────────────────────
static String? _findStringDeep(
Map<String, dynamic>? json, List<String> keys) {
Map<String, dynamic>? json,
List<String> keys,
) {
if (json == null) return null;
for (final key in keys) {
final v = json[key];
@@ -98,16 +110,17 @@ class AuthState {
}
for (final entry in json.entries) {
if (entry.value is Map<String, dynamic>) {
final found =
_findStringDeep(entry.value as Map<String, dynamic>, keys);
final found = _findStringDeep(
entry.value as Map<String, dynamic>,
keys,
);
if (found != null) return found;
}
}
return null;
}
static int? _findInt64Deep(
Map<String, dynamic>? json, List<String> keys) {
static int? _findInt64Deep(Map<String, dynamic>? json, List<String> keys) {
if (json == null) return null;
for (final key in keys) {
final v = json[key];
@@ -122,7 +135,13 @@ class AuthState {
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
if (bytes < 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
if (bytes < 1024 * 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024 * 1024)).toStringAsFixed(1)} TB';
}
return '${(bytes / (1024 * 1024 * 1024 * 1024 * 1024)).toStringAsFixed(1)} PB';
}
}
@@ -149,11 +168,14 @@ class AuthNotifier extends StateNotifier<AuthState> {
if (access.isNotEmpty || refresh != null) {
try {
AppLogger.info('Auth', '正在恢复登录状态');
await _api.refreshAccessToken();
await _saveTokens();
state = state.copyWith(isSignedIn: true);
await loadAccount();
AppLogger.info('Auth', '登录状态恢复完成');
} catch (_) {
AppLogger.warning('Auth', '登录状态恢复失败,已清除本地令牌');
state = state.copyWith(isSignedIn: false);
_api.clearTokens();
await _clearTokens();
@@ -163,10 +185,33 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
Future<void> loadAccount() async {
AppLogger.info('Auth', '正在获取账户资料与云盘空间信息');
final userInfoFuture = _api.userInfo();
final assetsFuture = _api.cloudAssets();
try {
final userInfo = await _api.userInfo();
state = state.copyWith(userInfo: userInfo);
final userInfo = await userInfoFuture;
Map<String, dynamic> assets = const {};
try {
assets = await assetsFuture;
} catch (_) {
AppLogger.warning('Auth', '云盘空间信息暂时不可用');
}
state = state.copyWith(userInfo: {...userInfo, 'assets': assets});
AppLogger.info('Auth', '账户资料与云盘空间信息获取完成');
} on DioException catch (error) {
await assetsFuture.catchError((_) => <String, dynamic>{});
final status = error.response?.statusCode;
if (status == 404 || status == 501) {
AppLogger.warning('Auth', '账户信息接口暂不可用 ($status),已保留登录态');
return;
}
final message = error.response?.data is Map
? extractHttpMessage(error.response?.data) ?? error.type.name
: error.type.name;
AppLogger.error('Auth', '加载账户信息失败', error: message);
state = state.copyWith(errorMessage: message);
} catch (e) {
AppLogger.error('Auth', '加载账户信息失败', error: e);
state = state.copyWith(errorMessage: e.toString());
}
}
@@ -183,8 +228,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
Future<void> sendVerificationCode() async {
state = state.copyWith(clearError: true);
final cleanPhone =
state.phoneNumber.replaceAll(RegExp(r'[^\d]'), '');
final cleanPhone = state.phoneNumber.replaceAll(RegExp(r'[^\d]'), '');
if (cleanPhone.length < 8) {
state = state.copyWith(errorMessage: '请输入有效的手机号');
return;
@@ -192,8 +236,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
try {
final initResult = await _api.loginSMSInit(cleanPhone);
final captcha =
AuthState._findStringDeep(initResult, ['captcha_token', 'captchaToken']);
final captcha = AuthState._findStringDeep(initResult, [
'captcha_token',
'captchaToken',
]);
if (captcha == null) {
if (AuthState._findStringDeep(initResult, ['url', 'verify_url']) !=
null) {
@@ -227,11 +273,14 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith(clearError: true);
try {
final verifyResult = await _api.loginSMSVerify(
state.verificationID, state.verificationCode);
final vToken = AuthState._findStringDeep(
verifyResult, ['verification_token', 'verificationToken']);
final code =
AuthState._findStringDeep(verifyResult, ['code']);
state.verificationID,
state.verificationCode,
);
final vToken = AuthState._findStringDeep(verifyResult, [
'verification_token',
'verificationToken',
]);
final code = AuthState._findStringDeep(verifyResult, ['code']);
if (vToken == null || code == null) throw Exception('验证码验证失败');
await _api.loginSMSSignIn(
@@ -254,11 +303,24 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith(clearError: true);
try {
final result = await _api.loginQRInit();
final token = AuthState._findStringDeep(
result, ['device_code', 'deviceCode']) ??
final token =
AuthState._findStringDeep(result, ['device_code', 'deviceCode']) ??
'';
final payload = AuthState._findStringDeep(
result, ['verification_uri_complete', 'verificationUriComplete', 'qr_url', 'qrUrl', 'url', 'verification_uri', 'verificationUri', 'qrcode', 'qrCode', 'code', 'qr_payload', 'qrPayload']) ??
final payload =
AuthState._findStringDeep(result, [
'verification_uri_complete',
'verificationUriComplete',
'qr_url',
'qrUrl',
'url',
'verification_uri',
'verificationUri',
'qrcode',
'qrCode',
'code',
'qr_payload',
'qrPayload',
]) ??
token;
state = state.copyWith(
qrPayload: payload,
@@ -273,24 +335,24 @@ class AuthNotifier extends StateNotifier<AuthState> {
void _startQRPolling() {
_qrPollingTimer?.cancel();
_qrPollingTimer =
Timer.periodic(const Duration(seconds: 5), (timer) async {
_qrPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
if (state.qrToken.isEmpty) {
timer.cancel();
return;
}
try {
final result = await _api.loginQRPoll(state.qrToken);
final accessToken = AuthState._findStringDeep(
result, ['access_token', 'accessToken']);
final accessToken = AuthState._findStringDeep(result, [
'access_token',
'accessToken',
]);
if (accessToken != null) {
timer.cancel();
state = state.copyWith(isSignedIn: true);
await _saveTokens();
await loadAccount();
} else {
final error =
AuthState._findStringDeep(result, ['error']);
final error = AuthState._findStringDeep(result, ['error']);
String? newStatus;
if (error == 'slow_down') {
newStatus = '轮询过快,请稍候';
@@ -322,7 +384,10 @@ class AuthNotifier extends StateNotifier<AuthState> {
Future<void> _saveTokens() async {
await StorageManager.set(StorageKeys.accessToken, _api.accessToken);
if (_api.refreshTokenValue != null) {
await StorageManager.set(StorageKeys.refreshToken, _api.refreshTokenValue!);
await StorageManager.set(
StorageKeys.refreshToken,
_api.refreshTokenValue!,
);
}
if (_api.tokenExpiresAt != null) {
await StorageManager.set(
+30 -19
View File
@@ -546,7 +546,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
void cancelDetailSync() {
_cancelDetailSync = true;
_appendScanLog('[同步识别][DEBUG] 已请求取消同步识别');
_appendScanLog('[同步识别][调试] 已请求取消同步识别');
}
Future<void> refreshGlobalCloudIndex({bool force = false}) async {
@@ -574,6 +574,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
_refreshingCloudIndex = true;
try {
AppLogger.info('CloudIndex', '开始刷新全盘文件索引');
final files = await _allGlobalRemoteFiles();
final folders = <String?, List<CloudFile>>{};
for (final file in files) {
@@ -584,6 +585,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
StorageKeys.cloudIndexLastUpdatedAt,
DateTime.now().millisecondsSinceEpoch.toString(),
);
AppLogger.info('CloudIndex', '全盘文件索引刷新完成,共 ${files.length}');
_appendScanLog('[云盘索引] 已刷新 ${files.length} 个文件与目录缓存');
} catch (error) {
_appendScanLog('[云盘索引] 刷新失败:$error', isError: true);
@@ -736,7 +738,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (_cancelDetailSync) break;
try {
_appendScanLog(
'[同步识别][DEBUG] 开始:${original.file.name} '
'[同步识别][调试] 开始:${original.file.name} '
'(fileId=${original.id}, gcid=${original.file.gcid ?? '-'})',
);
final latestFile = await _resolveCurrentCloudFile(
@@ -747,7 +749,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
throw StateError('云盘中未找到该文件');
}
_appendScanLog(
'[同步识别][DEBUG] 云盘已同步:fileId ${original.id} -> '
'[同步识别][调试] 云盘已同步:文件 ID ${original.id} -> '
'${latestFile.id},名称=${latestFile.name}gcid=${latestFile.gcid ?? '-'}',
);
synced.add(
@@ -763,7 +765,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
} catch (error) {
failures.add('${original.file.name}: $error');
_appendScanLog(
'[同步识别][DEBUG] 同步失败:${original.file.name}$error',
'[同步识别][调试] 同步失败:${original.file.name}$error',
isError: true,
);
}
@@ -796,7 +798,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
directoryName: _parentDirectoryName(fallback.file.cloudPath),
);
_appendScanLog(
'[同步识别][DEBUG] 解析:${fallback.file.name} -> '
'[同步识别][调试] 解析:${fallback.file.name} -> '
'标题=${parsed.title},年份=${parsed.year ?? '-'}'
'类型=${fallback.mediaKind?.name ?? 'automatic'}',
);
@@ -825,19 +827,19 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
MediaTMDBMatchRequest(items: [fallback], candidates: candidates),
);
_appendScanLog(
'[同步识别][DEBUG] 存在 ${candidates.length} 个 TMDB 候选,等待用户选择',
'[同步识别][调试] 存在 ${candidates.length} 个 TMDB 候选,等待用户选择',
);
}
}
if (updated.tmdbID != null) {
recognizedCount++;
_appendScanLog(
'[同步识别][DEBUG] TMDB 命中:${updated.title} '
'[同步识别][调试] TMDB 命中:${updated.title} '
'(tmdbId=${updated.tmdbID}, ${updated.year.isEmpty ? '年份未知' : updated.year})',
);
} else {
_appendScanLog(
'[同步识别][DEBUG] TMDB 未命中:${parsed.title} '
'[同步识别][调试] TMDB 未命中:${parsed.title} '
'(年份=${parsed.year ?? '-'})',
);
}
@@ -854,17 +856,17 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (updated.file.name != beforeName) {
renamedCount++;
_appendScanLog(
'[同步识别][DEBUG] 已规范命名:$beforeName -> ${updated.file.name}',
'[同步识别][调试] 已规范命名:$beforeName -> ${updated.file.name}',
);
} else if (updated.tmdbID != null) {
_appendScanLog('[同步识别][DEBUG] 跳过规范命名:未解析到分辨率或名称已符合规则');
_appendScanLog('[同步识别][调试] 跳过规范命名:未解析到分辨率或名称已符合规则');
}
updates.add(updated);
replacements['${original.libraryID}:${original.id}'] = updated;
} catch (error) {
failures.add('${original.file.name}: $error');
_appendScanLog(
'[同步识别][DEBUG] 识别失败:${original.file.name}$error',
'[同步识别][调试] 识别失败:${original.file.name}$error',
isError: true,
);
}
@@ -1584,6 +1586,8 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
const pageSize = 10000;
final values = <CloudFile>[];
for (var page = 0; !_cancelScan; page++) {
final typeLabel = resType == 2 ? '目录' : '文件';
AppLogger.info('CloudIndex', '正在获取全盘$typeLabel索引,第 ${page + 1}');
final response = await _api!.fsFiles(
parentID: '*',
page: page,
@@ -1594,6 +1598,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
);
final batch = _extractFiles(response);
values.addAll(batch);
AppLogger.info(
'CloudIndex',
'全盘$typeLabel索引第 ${page + 1} 页完成,获取 ${batch.length} 项,累计 ${values.length}',
);
final total = _findIntDeep(response, const [
'total',
'totalCount',
@@ -1667,6 +1675,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (cachedFolder != null) {
files = cachedFolder;
} else {
AppLogger.info('Media', '正在读取目录「${folder.path}」第 ${page + 1}');
final response = await _api!.fsFiles(
parentID: folder.id,
page: page,
@@ -1675,6 +1684,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
sortType: 0,
);
files = _extractFiles(response);
AppLogger.info(
'Media',
'目录「${folder.path}」第 ${page + 1} 页读取完成,获取 ${files.length}',
);
files = await _enrichAndCacheFiles(files);
folderSnapshot.addAll(files);
}
@@ -1867,9 +1880,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
} catch (error) {
// A rename can replace the cloud record. Locate its new ID below.
_appendScanLog(
'[同步识别][DEBUG] 旧 File ID 查询失败:${knownFile.id}$error;开始回退定位',
);
_appendScanLog('[同步识别][调试] 旧文件 ID 查询失败:${knownFile.id}$error;开始回退定位');
}
final cached = await FileMetadataCache.file(knownFile.id);
@@ -1881,7 +1892,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
cached.gcid == gcid &&
cached.id != knownFile.id) {
_appendScanLog(
'[同步识别][DEBUG] 从 GCID 缓存恢复新文件:${knownFile.id} -> ${cached.id}',
'[同步识别][调试] 从 GCID 缓存恢复新文件:${knownFile.id} -> ${cached.id}',
);
return _cacheResolvedCloudFile(
knownFile,
@@ -1953,7 +1964,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (located != null) return located;
}
_appendScanLog(
'[同步识别][DEBUG] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库',
'[同步识别][调试] 未定位到当前文件目录中的 GCID;停止自动查找,建议重新扫描媒体库',
isError: true,
);
return null;
@@ -1965,7 +1976,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}) async {
final gcid = knownFile.gcid?.trim();
if (gcid == null || gcid.isEmpty) return null;
_appendScanLog('[同步识别][DEBUG] 从当前文件目录扫描:parentId=$parentIDgcid=$gcid');
_appendScanLog('[同步识别][调试] 从当前文件目录扫描:父目录 ID=$parentIDGCID=$gcid');
final parentPath = _parentPath(knownFile.cloudPath);
List<CloudFile> children;
try {
@@ -1998,7 +2009,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
if (resolved.gcid == gcid) {
final exact = _withPath(resolved, '$parentPath/${resolved.name}');
_appendScanLog(
'[同步识别][DEBUG] 当前目录定位成功:${knownFile.id} -> '
'[同步识别][调试] 当前目录定位成功:${knownFile.id} -> '
'${exact.id},路径=${exact.cloudPath}',
);
return _cacheResolvedCloudFile(
@@ -2009,7 +2020,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
}
_appendScanLog(
'[同步识别][DEBUG] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次',
'[同步识别][调试] 当前文件目录未命中:已检查 ${children.length} 个条目,详情查询不超过 12 次',
);
return null;
}