/// 服务端错误信封 `{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; /// 登录失败次数过多,账号临时锁定(HTTP 423,见 openapi.yaml)。 static const loginLocked = 42300; // ------ pets 域(契约 v1.2.0 冻结,M2 第二波定型 8 个)------ /// 对可见宠物无相应操作权限(viewer 写记录、caregiver/viewer 改档案)。 static const petAccessDenied = 40300; /// 宠物不存在 / 已软删 / 与调用者无关系(防枚举,三种情况响应一致)。 static const petNotFound = 40401; /// 记录不存在或记录所属宠物对调用者不可见(记录级防枚举)。 static const recordNotFound = 40402; /// 乐观锁版本冲突(提交的 version 已过期;提醒的条件更新守卫落空同码)。 static const versionConflict = 40902; /// 芯片号已被登记(跨用户唯一)。 static const microchipTaken = 40903; /// 同宠物同疫苗同系列同剂次的非 cancelled 疫苗记录已存在。 static const vaccinationDoseExists = 40904; /// 疫苗状态机 / 状态-日期规则违反(HTTP 422)。 static const vaccinationRuleViolation = 42201; /// 提醒状态机 / completed-completedAt 一致性违反(HTTP 422)。 static const careReminderRuleViolation = 42202; } /// 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 等)。 /// 领域层可按错误码细分子类型(见 features/pets/pet_exceptions.dart)。 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 = '会话已失效']); }