Files
flutter-template/lib/core/error/exception_mapper.dart
T
SkyJourney 61017f1c39 chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板
Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
2026-05-14 12:51:05 +08:00

162 lines
5.6 KiB
Dart
Raw 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.
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
///
/// 详见 docs/flutter-architecture-design.md §四.2。
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/TalkeruserMessage 由 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-10RSA 加密失败统一映射为 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,
);
}
}