import 'package:dio/dio.dart'; import 'package:sunny_mochi/core/crypto/rsa_helper.dart' show RsaEncryptionException; import 'package:sunny_mochi/core/error/failures.dart'; import 'package:sunny_mochi/core/network/response_code.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'exception_mapper.g.dart'; /// 全局 [ExceptionMapper] 注入点。测试时用 `overrideWithValue` 替换为 stub。 @Riverpod(keepAlive: true) ExceptionMapper exceptionMapper(Ref ref) => const ExceptionMapper(); /// 把底层异常(DioException / DriftException / 其他)映射为应用层 [Failure]。 /// /// - 网络/HTTP → NetworkFailure / ServerFailure / AuthFailure /// - 业务 envelope retCode 非成功 → ServerFailure / AuthFailure /// - 数据库异常 → CacheFailure /// - 兜底 → UnknownFailure /// class ExceptionMapper { const ExceptionMapper(); Failure fromDio(DioException e) { return switch (e.type) { DioExceptionType.connectionTimeout || DioExceptionType.sendTimeout || DioExceptionType.receiveTimeout => NetworkFailure( message: '网络超时,请稍后重试', kind: NetworkFailureKind.timeout, cause: e, stackTrace: e.stackTrace, ), DioExceptionType.connectionError => NetworkFailure( message: '网络不可用,请检查连接', kind: NetworkFailureKind.noNetwork, cause: e, stackTrace: e.stackTrace, ), DioExceptionType.badCertificate => NetworkFailure( message: '证书校验失败,可能存在中间人风险', kind: NetworkFailureKind.badCertificate, cause: e, stackTrace: e.stackTrace, ), DioExceptionType.cancel => NetworkFailure( message: '请求已取消', kind: NetworkFailureKind.cancelled, cause: e, stackTrace: e.stackTrace, ), DioExceptionType.badResponse => _fromHttpStatus(e), DioExceptionType.unknown => _fromUnknownDio(e), }; } Failure _fromHttpStatus(DioException e) { final status = e.response?.statusCode; if (status == 401) { // 三态分类(与 TokenRefreshInterceptor 协作 — 反馈文档 §"Token 注销逻辑的编排"): // 1. _auth_refresh_failed → refresh 用尽,必须重新登录 // 2. _auth_not_refreshable → 业务 401(如权限不足),不应跳登录 // 3. 其他 → 真 token 过期但未触发 refresh(理论不应到这里),按 unauthorized 兜底 final extra = e.requestOptions.extra; if (extra['_auth_refresh_failed'] == true) { return AuthFailure( message: '登录状态已失效,请重新登录', kind: AuthFailureKind.refreshFailed, cause: e, stackTrace: e.stackTrace, ); } if (extra['_auth_not_refreshable'] == true) { return AuthFailure( message: _businessAuthMessage(e) ?? '当前账号无访问权限', kind: AuthFailureKind.forbidden, cause: e, stackTrace: e.stackTrace, ); } return AuthFailure( message: '登录已过期,请重新登录', cause: e, stackTrace: e.stackTrace, ); } if (status == 403) { return AuthFailure( message: '无访问权限', kind: AuthFailureKind.forbidden, cause: e, stackTrace: e.stackTrace, ); } // message 保留技术细节供 Sentry/Talker;userMessage 由 ServerFailure getter 遮蔽 return ServerFailure( message: e.message ?? 'HTTP $status', statusCode: status, cause: e, stackTrace: e.stackTrace, ); } /// 从响应 body 中提取业务侧 message,给业务 401 一个更具体的提示。 String? _businessAuthMessage(DioException e) { final body = e.response?.data; if (body is! Map) return null; final raw = body['retMsg'] ?? body['msg'] ?? body['message']; final msg = raw is String ? raw : raw?.toString(); return (msg != null && msg.isNotEmpty) ? msg : null; } Failure _fromUnknownDio(DioException e) { final inner = e.error; if (inner is Failure) return inner; return UnknownFailure( message: e.message ?? '未知错误', cause: e, stackTrace: e.stackTrace, ); } /// 把业务 envelope 错误码映射为 Failure。 Failure fromBusinessCode(String? code, String? message) { final friendly = ResponseCode.friendlyMessage(code, message ?? '请求失败'); if (ResponseCode.isUnauthorized(code)) { return AuthFailure(message: friendly); } return ServerFailure(message: friendly, code: code); } /// Drift / 文件 IO 等本地异常 → CacheFailure。 Failure fromCache(Object error, [StackTrace? st]) { return CacheFailure( message: '本地数据访问失败:$error', cause: error, stackTrace: st, ); } /// 兜底(任意未识别异常)。 Failure fromUnknown(Object error, [StackTrace? st]) { if (error is Failure) return error; if (error is DioException) return fromDio(error); // R-ROB-2 / 2026-05-10:RSA 加密失败统一映射为 AuthFailure(cryptoFailed), // 避免上层得到不友好的 'RsaEncryptionException: ...' 兜底文案 if (error is RsaEncryptionException) { return AuthFailure( message: '密码加密失败,请重试', kind: AuthFailureKind.cryptoFailed, cause: error, stackTrace: st, ); } return UnknownFailure( message: error.toString(), cause: error, stackTrace: st, ); } }