新增: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
+96
View File
@@ -0,0 +1,96 @@
import 'package:flutter/foundation.dart';
import 'package:patbond_flutter/core/network/api_exception.dart';
import 'package:patbond_flutter/features/pets/pet_models.dart';
import 'package:patbond_flutter/features/pets/pets_repository.dart';
/// 网络数据四态(第 9 节硬要求:loading / empty / error / retry
/// data 态含空列表判定,页面据 [PetsLoadPhase] + 数据渲染四态)。
enum PetsLoadPhase { initial, loading, ready, error }
/// pets feature 状态控制器(开发计划 §4.2 分层:
/// Page/Widget → PetsController → PetsRepository → ApiClient)。
///
/// 宠物档案状态自此从 `AppState` 拆出:本 feature 不读写 AppState 的
/// 宠物/疫苗 demo 数据;服务端是唯一事实来源,内存副本仅作展示缓存
/// (无本地持久化——档案数据以 [refresh] 为失效/刷新策略)。
///
/// 本单只承载列表 + 详情主链路状态;体重/疫苗/事件/提醒的页面级
/// 状态由 T2-12~14 按页面直接经 Repository 取数或扩展本控制器。
class PetsController extends ChangeNotifier {
PetsController({required this._repository});
final PetsRepository _repository;
PetsLoadPhase _phase = PetsLoadPhase.initial;
List<Pet> _pets = const [];
ApiException? _lastError;
bool _disposed = false;
PetsLoadPhase get phase => _phase;
/// 当前用户可见宠物(服务端 created_at DESC 排序原样保留)。
List<Pet> get pets => _pets;
/// ready 且列表为空 → 空态(新用户建档引导)。
bool get isEmpty => _phase == PetsLoadPhase.ready && _pets.isEmpty;
/// 最近一次加载失败的类型化错误(error 态时非 null)。
ApiException? get lastError => _lastError;
/// 加载 / 重试宠物列表。错误不外抛,收敛为 error 态供页面渲染 + retry。
Future<void> refresh() async {
_phase = PetsLoadPhase.loading;
_lastError = null;
_notify();
try {
_pets = await _repository.listPets();
_phase = PetsLoadPhase.ready;
} on ApiException catch (error) {
_lastError = error;
_phase = PetsLoadPhase.error;
}
_notify();
}
/// 创建宠物:成功后就地插入列表头(服务端 created_at DESC,新建最前),
/// 失败按类型化异常外抛给表单层处理(40903 芯片号冲突等)。
Future<Pet> createPet(CreatePetRequest request) async {
final pet = await _repository.createPet(request);
_pets = [pet, ..._pets];
if (_phase != PetsLoadPhase.ready) _phase = PetsLoadPhase.ready;
_notify();
return pet;
}
/// 拉取单只宠物详情并同步列表内存副本。
Future<Pet> getPet(String petId) async {
final pet = await _repository.getPet(petId);
_replaceInList(pet);
return pet;
}
/// 更新宠物档案:成功后同步列表副本;40902 版本冲突等外抛,
/// 页面提示后应调用 [getPet] 刷新取新 version 重提。
Future<Pet> updatePet(String petId, UpdatePetRequest request) async {
final pet = await _repository.updatePet(petId, request);
_replaceInList(pet);
return pet;
}
void _replaceInList(Pet pet) {
final index = _pets.indexWhere((item) => item.id == pet.id);
if (index == -1) return;
_pets = [..._pets]..[index] = pet;
_notify();
}
void _notify() {
if (!_disposed) notifyListeners();
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}