新增:pets feature 数据层(T2-11,契约 v1.2.0 全 18 操作)
CI / flutter-gates (push) Successful in 1m12s

- pet_models.dart:pets 域 DTO 逐字段对齐冻结契约(宠物/品种/体重/
  疫苗目录/疫苗/健康事件/照护提醒/聚合摘要 + cursor 分页信封),
  枚举严格解析,请求体部分更新语义(缺席字段不出现)
- pets_repository.dart:PetsRepository 抽象 + ApiPetsRepository,
  12 路径 18 操作全覆盖;四个 POST 携带 Idempotency-Key;
  复用既有 ApiClient token 拦截与单飞刷新重放
- pet_exceptions.dart:新 8 错误码映射为类型化异常
  (40300/40401/40402/40902/40903/40904/42201/42202),
  仍可按 ApiBusinessException 基类捕获
- pets_controller.dart:宠物档案状态自 AppState 拆出
  (Controller → Repository → API Client 分层,四态就绪供 T2-12)
- money.dart:元/分换算工具(DTO 层保持整数分,UI 层 T2-14 使用)
- api_client.dart:新增 patbondPetApiBaseUrl(:8083,--dart-define
  可覆盖)与 query 参数支持;api_exception.dart 补 pets 域错误码
- 测试 64 → 126:DTO 映射、错误类型化映射、请求线路
  (路径/方法/鉴权/分页/幂等键/tz)、控制器状态机、金额换算

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 11:22:24 +08:00
parent 33b993ca0c
commit 7fb9031f8d
12 changed files with 2665 additions and 1 deletions
+13
View File
@@ -17,6 +17,14 @@ const String patbondUserApiBaseUrl = String.fromEnvironment(
defaultValue: 'http://127.0.0.1:8082',
);
/// pet 服务基地址(/api/v1/pets、/api/v1/breeds、/api/v1/vaccine-catalog 等
/// pets 域 12 路径):沿用 auth :8081 / user :8082 的分端口直连模式,
/// `--dart-define=PATBOND_PET_API_BASE_URL=...` 注入。
const String patbondPetApiBaseUrl = String.fromEnvironment(
'PATBOND_PET_API_BASE_URL',
defaultValue: 'http://127.0.0.1:8083',
);
/// 构建全局共用的 Dio 实例。
///
/// `validateStatus` 放行所有状态码:错误信封由 [ApiClient] 统一解析成
@@ -82,6 +90,7 @@ class ApiClient {
String path, {
String method = 'POST',
Object? body,
Map<String, Object?>? query,
Map<String, Object?>? headers,
bool requiresAuth = false,
}) async {
@@ -89,6 +98,7 @@ class ApiClient {
path,
method: method,
body: body,
query: query,
headers: headers,
requiresAuth: requiresAuth,
);
@@ -99,6 +109,7 @@ class ApiClient {
path,
method: method,
body: body,
query: query,
headers: headers,
requiresAuth: requiresAuth,
);
@@ -116,12 +127,14 @@ class ApiClient {
required String method,
required bool requiresAuth,
Object? body,
Map<String, Object?>? query,
Map<String, Object?>? headers,
}) async {
try {
return await _dio.request<Object?>(
path,
data: body,
queryParameters: query,
options: Options(
method: method,
headers: headers,
+28 -1
View File
@@ -22,6 +22,32 @@ abstract final class ApiCodes {
/// 登录失败次数过多,账号临时锁定(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 调用的类型化异常。页面按类型映射为三层错误呈现
@@ -43,7 +69,8 @@ final class ApiNetworkException extends ApiException {
}
/// 业务错误:错误信封 code != 040000/40100/40900/40901 等)。
final class ApiBusinessException extends ApiException {
/// 领域层可按错误码细分子类型(见 features/pets/pet_exceptions.dart)。
class ApiBusinessException extends ApiException {
const ApiBusinessException({required this.code, required String message})
: super(message);