/// 服务端错误信封 `{code, message, data}` 的业务码常量(接口契约冻结稿)。 abstract final class ApiCodes { static const ok = 0; /// 参数错误。 static const paramError = 40000; /// 用户名或密码错误。 static const badCredentials = 40100; /// access token 无效或过期。 static const accessTokenInvalid = 40101; /// refresh token 已失效。 static const refreshTokenInvalid = 40102; /// 用户名重复。 static const usernameTaken = 40900; /// 手机号重复。 static const phoneTaken = 40901; } /// API 调用的类型化异常。页面按类型映射为三层错误呈现 /// (字段级 / 表单横幅 / SnackBar,见 12 号组装稿 §4), /// 服务端原始 message 一律不直接透出给用户。 sealed class ApiException implements Exception { const ApiException(this.message); /// 服务端返回的原始 message(仅用于日志排查,不上屏)。 final String message; @override String toString() => '$runtimeType: $message'; } /// 系统错误:超时、断网、5xx、响应无法解析。归瞬态层(SnackBar + 重试)。 final class ApiNetworkException extends ApiException { const ApiNetworkException([super.message = '网络或服务不可用']); } /// 业务错误:错误信封 code != 0(40000/40100/40900/40901 等)。 final class ApiBusinessException extends ApiException { const ApiBusinessException({required this.code, required String message}) : super(message); final int code; } /// 限流(HTTP 429):映射为横幅「尝试次数过多,请稍后再试」。 final class ApiRateLimitException extends ApiException { const ApiRateLimitException([super.message = '请求过于频繁']); } /// 会话已失效:refresh token 失效(40102)或刷新重放后仍 401。 /// 抛出前本地会话已被清除,应用会自动回到登录页。 final class SessionExpiredException extends ApiException { const SessionExpiredException([super.message = '会话已失效']); }