Files
lixi eff3526840 新增:宠物头像接线——铅笔角标接上传、列表与详情展示真实头像(T3.5-09)
- `Pet.avatarUrl` 入模型;列表卡与详情头像改用真实预签名 URL(无图仍回退
  爪印占位——服务端对非 ready 的 asset 直接给 null,客户端只有一种占位)。
- 详情页铅笔角标自此有功能:接 `MediaUploader`(purpose=pet_avatar);已有
  头像时先给「更换 / 移除」二选一,移除即三态显式 null。
- 权限按 **WRITE 档**呈现(ADR-022 D3.5-3:owner + caregiver 可改头像,
  viewer 无入口),与资料编辑的 MANAGE 档刻意不同。因此纯头像 PATCH 只发
  `version` + `avatarAssetId`,**不夹带任何资料字段**——夹带会让服务端按更严
  的一半定档,caregiver 立刻 403。
- `UpdatePetRequest.avatarAssetId` 是该 schema 唯一的三态字段,其余字段保持
  M2 两态语义(它们的 CHECK 约束本就不允许空值,「清空」无意义)。
- 40902 冲突后重取档案让用户自行决定重来,不静默重放(头像是用户可见的
  覆盖操作,不该自动生效两次)。

测试 588 → 597(+9)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 15:37:25 +08:00

337 lines
9.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// pets 域测试样本(契约 openapi.yaml v1.4.0 各 schema 全字段 JSON
/// 与共享假仓库。
library;
import 'package:patbond_flutter/features/pets/pet_models.dart';
import 'package:patbond_flutter/features/pets/pets_repository.dart';
Map<String, Object?> okListEnvelope(List<Object?> data) => {
'code': 0,
'message': 'ok',
'data': data,
};
Map<String, dynamic> samplePetJson({
Map<String, Object?> overrides = const {},
}) => {
'id': 'p-1',
'name': '豆豆',
'species': 'dog',
'breedId': 'b-1',
'breedDisplayName': '柴犬',
'customBreedName': null,
'sex': 'male',
'birthDate': '2024-03-15',
'birthDateEstimated': false,
'personality': null,
'microchipNo': null,
'sterilizedOn': null,
'status': 'active',
'avatarUrl': null,
'myRole': 'owner',
'createdAt': '2026-09-01T10:00:00+08:00',
'updatedAt': '2026-09-02T10:00:00+08:00',
'version': 3,
...overrides,
};
Map<String, dynamic> sampleWeightJson() => {
'id': 'w-1',
'petId': 'p-1',
'weightKg': 4.35,
'measuredAt': '2026-09-07T09:00:00+08:00',
'source': 'manual',
'note': null,
'createdAt': '2026-09-07T09:01:00+08:00',
};
Map<String, dynamic> sampleVaccinationJson() => {
'id': 'vx-1',
'petId': 'p-1',
'vaccineId': 'v-1',
'vaccineName': '狂犬疫苗',
'seriesKey': 'rabies-initial',
'doseNo': 1,
'doseLabel': null,
'status': 'scheduled',
'plannedOn': '2026-10-01',
'administeredOn': null,
'nextDueOn': null,
'manufacturer': null,
'batchNo': null,
'notes': null,
'createdAt': '2026-09-01T10:00:00+08:00',
'updatedAt': '2026-09-01T10:00:00+08:00',
'version': 1,
};
Map<String, dynamic> sampleHealthEventJson() => {
'id': 'e-1',
'petId': 'p-1',
'eventType': 'medical',
'occurredAt': '2026-09-05T14:00:00+08:00',
'title': '皮肤检查',
'notes': null,
'amountCents': 12850,
'createdByUserId': 'u-1',
'createdAt': '2026-09-05T14:05:00+08:00',
'updatedAt': '2026-09-05T14:05:00+08:00',
'version': 1,
};
Map<String, dynamic> sampleCareReminderJson() => {
'id': 'r-1',
'petId': 'p-1',
'reminderType': 'checkup',
'title': '年度体检',
'dueAt': '2026-10-01T00:00:00+08:00',
'status': 'pending',
'completedAt': null,
'createdAt': '2026-09-01T10:00:00+08:00',
'updatedAt': '2026-09-01T10:00:00+08:00',
};
Map<String, dynamic> samplePetSummaryJson() => {
'petId': 'p-1',
'latestWeight': {'weightKg': 4.35, 'measuredAt': '2026-09-07T09:00:00+08:00'},
'vaccinationProgress': {'completedDoses': 2, 'totalDoses': 3},
'nextVaccination': {
'vaccinationId': 'vx-3',
'vaccineId': 'v-1',
'vaccineName': '狂犬疫苗',
'doseNo': 3,
'doseLabel': null,
'dueOn': '2026-08-01',
'source': 'planned',
},
'monthlyExpense': {
'month': '2026-09',
'timezone': 'Asia/Shanghai',
'amountCents': 12850,
},
};
Map<String, dynamic> sampleBreedJson({
String id = 'b-1',
String species = 'dog',
String code = 'shiba',
String displayName = '柴犬',
}) => {'id': id, 'species': species, 'code': code, 'displayName': displayName};
/// 快速构造 Petwidget/controller 测试共用)。
Pet buildPet(
String id, {
String name = '豆豆',
int version = 1,
Map<String, Object?> overrides = const {},
}) => Pet.fromJson(
samplePetJson(
overrides: {'id': id, 'name': name, 'version': version, ...overrides},
),
);
Map<String, dynamic> sampleVaccineCatalogJson({
String id = 'v-1',
String code = 'rabies',
String name = '狂犬疫苗',
String species = 'dog',
}) => {
'id': id,
'code': code,
'name': name,
'species': species,
'description': null,
};
/// 快速构造体重记录(widget 测试共用)。
WeightRecord buildWeight(
String id, {
double weightKg = 4.35,
String measuredAt = '2026-09-07T09:00:00+08:00',
Map<String, Object?> overrides = const {},
}) => WeightRecord.fromJson({
...sampleWeightJson(),
'id': id,
'weightKg': weightKg,
'measuredAt': measuredAt,
...overrides,
});
/// 快速构造疫苗记录。
Vaccination buildVaccination(
String id, {
Map<String, Object?> overrides = const {},
}) =>
Vaccination.fromJson({...sampleVaccinationJson(), 'id': id, ...overrides});
/// 快速构造健康事件。
HealthEvent buildHealthEvent(
String id, {
Map<String, Object?> overrides = const {},
}) =>
HealthEvent.fromJson({...sampleHealthEventJson(), 'id': id, ...overrides});
/// 快速构造照护提醒。
CareReminder buildReminder(
String id, {
Map<String, Object?> overrides = const {},
}) => CareReminder.fromJson({
...sampleCareReminderJson(),
'id': id,
...overrides,
});
/// 快速构造摘要(缺省为「三聚合齐备」样本;overrides 可置 null 验证空态)。
PetSummary buildSummary({Map<String, Object?> overrides = const {}}) =>
PetSummary.fromJson({...samplePetSummaryJson(), ...overrides});
/// 假仓库:各方法可注入行为;listPets 默认空列表,其余未注入的方法抛
/// UnimplementedError22 号报告 §7widget 测试注入假仓库的共享实现)。
class FakePetsRepository implements PetsRepository {
Future<List<Pet>> Function()? listPetsHandler;
Future<Pet> Function(CreatePetRequest)? createPetHandler;
Future<Pet> Function(String)? getPetHandler;
Future<Pet> Function(String, UpdatePetRequest)? updatePetHandler;
Future<List<Breed>> Function(PetSpecies?)? listBreedsHandler;
Future<CursorPage<WeightRecord>> Function(String, int?, String?)?
listWeightsHandler;
Future<WeightRecord> Function(String, CreateWeightRequest)?
createWeightHandler;
Future<List<VaccineCatalogItem>> Function(PetSpecies?)?
listVaccineCatalogHandler;
Future<List<Vaccination>> Function(String)? listVaccinationsHandler;
Future<Vaccination> Function(String, CreateVaccinationRequest)?
createVaccinationHandler;
Future<Vaccination> Function(String, UpdateVaccinationRequest)?
updateVaccinationHandler;
Future<CursorPage<HealthEvent>> Function(String, int?, String?)?
listHealthEventsHandler;
Future<HealthEvent> Function(String, CreateHealthEventRequest)?
createHealthEventHandler;
Future<HealthEvent> Function(String, UpdateHealthEventRequest)?
updateHealthEventHandler;
Future<List<CareReminder>> Function(String, CareReminderStatus?)?
listCareRemindersHandler;
Future<CareReminder> Function(String, CreateCareReminderRequest)?
createCareReminderHandler;
Future<CareReminder> Function(String, UpdateCareReminderRequest)?
updateCareReminderHandler;
Future<PetSummary> Function(String, String?)? getPetSummaryHandler;
@override
Future<List<Pet>> listPets() =>
listPetsHandler?.call() ?? Future.value(const []);
@override
Future<Pet> createPet(CreatePetRequest request) => createPetHandler!(request);
@override
Future<Pet> getPet(String petId) => getPetHandler!(petId);
@override
Future<Pet> updatePet(String petId, UpdatePetRequest request) =>
updatePetHandler!(petId, request);
@override
Future<List<Breed>> listBreeds({PetSpecies? species}) =>
listBreedsHandler?.call(species) ??
Future.value([Breed.fromJson(sampleBreedJson())]);
@override
Future<CursorPage<WeightRecord>> listWeights(
String petId, {
int? limit,
String? cursor,
}) => listWeightsHandler!(petId, limit, cursor);
@override
Future<WeightRecord> createWeight(
String petId,
CreateWeightRequest request,
) => createWeightHandler!(petId, request);
@override
Future<List<VaccineCatalogItem>> listVaccineCatalog({PetSpecies? species}) =>
listVaccineCatalogHandler?.call(species) ??
Future.value([VaccineCatalogItem.fromJson(sampleVaccineCatalogJson())]);
@override
Future<List<Vaccination>> listVaccinations(String petId) =>
listVaccinationsHandler!(petId);
@override
Future<Vaccination> createVaccination(
String petId,
CreateVaccinationRequest request,
) => createVaccinationHandler!(petId, request);
@override
Future<Vaccination> updateVaccination(
String vaccinationId,
UpdateVaccinationRequest request,
) => updateVaccinationHandler!(vaccinationId, request);
@override
Future<CursorPage<HealthEvent>> listHealthEvents(
String petId, {
int? limit,
String? cursor,
}) => listHealthEventsHandler!(petId, limit, cursor);
@override
Future<HealthEvent> createHealthEvent(
String petId,
CreateHealthEventRequest request,
) => createHealthEventHandler!(petId, request);
@override
Future<HealthEvent> updateHealthEvent(
String eventId,
UpdateHealthEventRequest request,
) => updateHealthEventHandler!(eventId, request);
@override
Future<List<CareReminder>> listCareReminders(
String petId, {
CareReminderStatus? status,
}) =>
listCareRemindersHandler?.call(petId, status) ??
// 缺省空列表:既有详情页测试不必逐个注入。
Future.value(const []);
@override
Future<CareReminder> createCareReminder(
String petId,
CreateCareReminderRequest request,
) => createCareReminderHandler!(petId, request);
@override
Future<CareReminder> updateCareReminder(
String reminderId,
UpdateCareReminderRequest request,
) => updateCareReminderHandler!(reminderId, request);
@override
Future<PetSummary> getPetSummary(String petId, {String? tz}) =>
getPetSummaryHandler?.call(petId, tz) ??
// 缺省给「无任何记录」空摘要(契约 null 语义),既有详情页测试
// 不必逐个注入。
Future.value(
PetSummary.fromJson({
'petId': petId,
'latestWeight': null,
'vaccinationProgress': null,
'nextVaccination': null,
'monthlyExpense': {
'month': '2026-09',
'timezone': 'UTC',
'amountCents': 0,
},
}),
);
@override
dynamic noSuchMethod(Invocation invocation) =>
throw UnimplementedError('${invocation.memberName}');
}