Files
flutter-template/lib/core/error/exception_mapper.dart
T
SkyJourney b8b63282ee fix: 清除 platform-flutter 残留代码,重构文档与开发者体验
P0 代码修复:删除 imSdkAppId、清除内网 IP 与原项目域名、修正 USE_MOCK 默认值
P1 CLAUDE.md 重组:零容忍规则前置,新增 dart-defines 字段对照表
P2 文档优化:先跑再配的 setup 流程,新增 architecture.md 架构图
2026-05-14 13:21:06 +08:00

162 lines
5.5 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
///
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,
);
}
}