新增: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
+83
View File
@@ -0,0 +1,83 @@
import 'package:patbond_flutter/core/network/api_exception.dart';
/// pets 域类型化业务异常(契约 v1.2.0 定型的 8 个新错误码,
/// 21 号收口报告 §1)。全部继承 [ApiBusinessException]
/// 既有按基类捕获的通用错误处理不受影响。
/// 40300:对可见宠物无相应操作权限(viewer 写记录、caregiver/viewer 改档案)。
final class PetAccessDeniedException extends ApiBusinessException {
const PetAccessDeniedException({required super.message})
: super(code: ApiCodes.petAccessDenied);
}
/// 40401:宠物不存在 / 已软删 / 与调用者无关系(防枚举,三种情况响应一致)。
final class PetNotFoundException extends ApiBusinessException {
const PetNotFoundException({required super.message})
: super(code: ApiCodes.petNotFound);
}
/// 40402:记录不存在或记录所属宠物对调用者不可见(记录级防枚举)。
final class PetRecordNotFoundException extends ApiBusinessException {
const PetRecordNotFoundException({required super.message})
: super(code: ApiCodes.recordNotFound);
}
/// 40902:乐观锁版本冲突(version 过期;提醒的条件更新守卫落空同码)。
/// 客户端处理:刷新取新数据后重提。
final class PetVersionConflictException extends ApiBusinessException {
const PetVersionConflictException({required super.message})
: super(code: ApiCodes.versionConflict);
}
/// 40903:芯片号已被登记(跨用户唯一)。
final class MicrochipTakenException extends ApiBusinessException {
const MicrochipTakenException({required super.message})
: super(code: ApiCodes.microchipTaken);
}
/// 40904:同宠物同疫苗同系列同剂次的非 cancelled 疫苗记录已存在
/// (cancel 后同剂次可重新登记)。
final class VaccinationDoseExistsException extends ApiBusinessException {
const VaccinationDoseExistsException({required super.message})
: super(code: ApiCodes.vaccinationDoseExists);
}
/// 42201:疫苗状态机非法迁移或状态-日期规则违反(HTTP 422)。
final class VaccinationRuleException extends ApiBusinessException {
const VaccinationRuleException({required super.message})
: super(code: ApiCodes.vaccinationRuleViolation);
}
/// 42202:提醒状态机非法迁移或 completed-completedAt 一致性违反(HTTP 422)。
final class CareReminderRuleException extends ApiBusinessException {
const CareReminderRuleException({required super.message})
: super(code: ApiCodes.careReminderRuleViolation);
}
/// 把通用业务异常按 pets 域错误码升格为类型化异常;
/// 未覆盖的码(40000 参数错误等)原样返回,沿用通用处理。
ApiBusinessException mapPetBusinessException(ApiBusinessException error) {
return switch (error.code) {
ApiCodes.petAccessDenied => PetAccessDeniedException(
message: error.message,
),
ApiCodes.petNotFound => PetNotFoundException(message: error.message),
ApiCodes.recordNotFound => PetRecordNotFoundException(
message: error.message,
),
ApiCodes.versionConflict => PetVersionConflictException(
message: error.message,
),
ApiCodes.microchipTaken => MicrochipTakenException(message: error.message),
ApiCodes.vaccinationDoseExists => VaccinationDoseExistsException(
message: error.message,
),
ApiCodes.vaccinationRuleViolation => VaccinationRuleException(
message: error.message,
),
ApiCodes.careReminderRuleViolation => CareReminderRuleException(
message: error.message,
),
_ => error,
};
}