- 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:
@@ -0,0 +1,854 @@
|
||||
/// pets 域响应 / 请求模型(接口契约冻结稿 openapi.yaml v1.2.0,
|
||||
/// 字段名与后端逐字一致;枚举取值严格校验,未知值抛 [FormatException]
|
||||
/// 以便契约漂移在测试期暴露而非静默吞掉)。
|
||||
library;
|
||||
|
||||
/// 物种(创建即定,不可修改)。
|
||||
enum PetSpecies {
|
||||
dog,
|
||||
cat,
|
||||
other;
|
||||
|
||||
static PetSpecies fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'species');
|
||||
}
|
||||
|
||||
/// 性别。
|
||||
enum PetSex {
|
||||
male,
|
||||
female,
|
||||
unknown;
|
||||
|
||||
static PetSex fromJson(String value) => _enumFromJson(values, value, 'sex');
|
||||
}
|
||||
|
||||
/// 宠物状态(deleted 为内部软删态,接口永不返回)。
|
||||
enum PetStatus {
|
||||
active,
|
||||
lost,
|
||||
deceased,
|
||||
archived;
|
||||
|
||||
static PetStatus fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'status');
|
||||
}
|
||||
|
||||
/// 调用者对宠物的权限角色(客户端据此显隐写入口)。
|
||||
enum PetRole {
|
||||
owner,
|
||||
caregiver,
|
||||
viewer;
|
||||
|
||||
static PetRole fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'myRole');
|
||||
}
|
||||
|
||||
/// 体重记录来源。
|
||||
enum WeightSource {
|
||||
manual,
|
||||
clinic,
|
||||
device;
|
||||
|
||||
static WeightSource fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'source');
|
||||
}
|
||||
|
||||
/// 疫苗记录状态。
|
||||
enum VaccinationStatus {
|
||||
scheduled,
|
||||
completed,
|
||||
cancelled;
|
||||
|
||||
static VaccinationStatus fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'status');
|
||||
}
|
||||
|
||||
/// 健康事件六类类型。
|
||||
enum HealthEventType {
|
||||
medical,
|
||||
feeding,
|
||||
deworming,
|
||||
grooming,
|
||||
measurement,
|
||||
note;
|
||||
|
||||
static HealthEventType fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'eventType');
|
||||
}
|
||||
|
||||
/// 照护提醒四类类型。
|
||||
enum CareReminderType {
|
||||
deworming,
|
||||
checkup,
|
||||
medication,
|
||||
other;
|
||||
|
||||
static CareReminderType fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'reminderType');
|
||||
}
|
||||
|
||||
/// 照护提醒状态。
|
||||
enum CareReminderStatus {
|
||||
pending,
|
||||
completed,
|
||||
dismissed;
|
||||
|
||||
static CareReminderStatus fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'status');
|
||||
}
|
||||
|
||||
/// 摘要「下次接种」的取值来源。
|
||||
enum NextVaccinationSource {
|
||||
planned,
|
||||
nextDue;
|
||||
|
||||
static NextVaccinationSource fromJson(String value) =>
|
||||
_enumFromJson(values, value, 'source');
|
||||
}
|
||||
|
||||
T _enumFromJson<T extends Enum>(List<T> values, String raw, String field) {
|
||||
for (final value in values) {
|
||||
if (value.name == raw) return value;
|
||||
}
|
||||
throw FormatException('未知的 $field 取值:$raw');
|
||||
}
|
||||
|
||||
/// 日期字段(契约 format: date)序列化为 YYYY-MM-DD。
|
||||
String dateToJson(DateTime date) {
|
||||
final y = date.year.toString().padLeft(4, '0');
|
||||
final m = date.month.toString().padLeft(2, '0');
|
||||
final d = date.day.toString().padLeft(2, '0');
|
||||
return '$y-$m-$d';
|
||||
}
|
||||
|
||||
DateTime? _dateOrNull(Object? value) =>
|
||||
value == null ? null : DateTime.parse(value as String);
|
||||
|
||||
/// cursor 分页正典信封 `{items, nextCursor, hasMore}`(体重、健康事件)。
|
||||
class CursorPage<T> {
|
||||
const CursorPage({
|
||||
required this.items,
|
||||
required this.nextCursor,
|
||||
required this.hasMore,
|
||||
});
|
||||
|
||||
factory CursorPage.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Map<String, dynamic>) itemFromJson,
|
||||
) {
|
||||
return CursorPage(
|
||||
items: (json['items'] as List)
|
||||
.map((item) => itemFromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
// 不透明 base64url 游标,客户端不得解析;hasMore=false 时恒为 null。
|
||||
nextCursor: json['nextCursor'] as String?,
|
||||
hasMore: json['hasMore'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
final List<T> items;
|
||||
final String? nextCursor;
|
||||
final bool hasMore;
|
||||
}
|
||||
|
||||
/// 宠物档案(列表 / 详情 / 创建 / 更新统一响应形态)。
|
||||
/// breedId 与 customBreedName 恰有其一非空;breedDisplayName 随 breedId 存在。
|
||||
class Pet {
|
||||
const Pet({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.species,
|
||||
required this.breedId,
|
||||
required this.breedDisplayName,
|
||||
required this.customBreedName,
|
||||
required this.sex,
|
||||
required this.birthDate,
|
||||
required this.birthDateEstimated,
|
||||
required this.personality,
|
||||
required this.microchipNo,
|
||||
required this.sterilizedOn,
|
||||
required this.status,
|
||||
required this.myRole,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
factory Pet.fromJson(Map<String, dynamic> json) {
|
||||
return Pet(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
species: PetSpecies.fromJson(json['species'] as String),
|
||||
breedId: json['breedId'] as String?,
|
||||
breedDisplayName: json['breedDisplayName'] as String?,
|
||||
customBreedName: json['customBreedName'] as String?,
|
||||
sex: PetSex.fromJson(json['sex'] as String),
|
||||
birthDate: _dateOrNull(json['birthDate']),
|
||||
birthDateEstimated: json['birthDateEstimated'] as bool,
|
||||
personality: json['personality'] as String?,
|
||||
microchipNo: json['microchipNo'] as String?,
|
||||
sterilizedOn: _dateOrNull(json['sterilizedOn']),
|
||||
status: PetStatus.fromJson(json['status'] as String),
|
||||
myRole: PetRole.fromJson(json['myRole'] as String),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
version: json['version'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final PetSpecies species;
|
||||
final String? breedId;
|
||||
final String? breedDisplayName;
|
||||
final String? customBreedName;
|
||||
final PetSex sex;
|
||||
final DateTime? birthDate;
|
||||
final bool birthDateEstimated;
|
||||
final String? personality;
|
||||
final String? microchipNo;
|
||||
final DateTime? sterilizedOn;
|
||||
final PetStatus status;
|
||||
final PetRole myRole;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int version;
|
||||
}
|
||||
|
||||
/// 创建宠物请求。breedId 与 customBreedName 二选一且互斥(400/40000 兜底在服务端)。
|
||||
class CreatePetRequest {
|
||||
const CreatePetRequest({
|
||||
required this.name,
|
||||
required this.species,
|
||||
required this.sex,
|
||||
this.breedId,
|
||||
this.customBreedName,
|
||||
this.birthDate,
|
||||
this.birthDateEstimated,
|
||||
this.personality,
|
||||
this.microchipNo,
|
||||
this.sterilizedOn,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final PetSpecies species;
|
||||
final PetSex sex;
|
||||
final String? breedId;
|
||||
final String? customBreedName;
|
||||
final DateTime? birthDate;
|
||||
final bool? birthDateEstimated;
|
||||
final String? personality;
|
||||
final String? microchipNo;
|
||||
final DateTime? sterilizedOn;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'name': name,
|
||||
'species': species.name,
|
||||
'sex': sex.name,
|
||||
if (breedId != null) 'breedId': breedId,
|
||||
if (customBreedName != null) 'customBreedName': customBreedName,
|
||||
if (birthDate != null) 'birthDate': dateToJson(birthDate!),
|
||||
if (birthDateEstimated != null) 'birthDateEstimated': birthDateEstimated,
|
||||
if (personality != null) 'personality': personality,
|
||||
if (microchipNo != null) 'microchipNo': microchipNo,
|
||||
if (sterilizedOn != null) 'sterilizedOn': dateToJson(sterilizedOn!),
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新宠物请求(部分更新:缺席字段不变;不支持清空回 null;
|
||||
/// 品种对整体替换;species 不可改;version 乐观锁必填)。
|
||||
class UpdatePetRequest {
|
||||
const UpdatePetRequest({
|
||||
required this.version,
|
||||
this.name,
|
||||
this.breedId,
|
||||
this.customBreedName,
|
||||
this.sex,
|
||||
this.birthDate,
|
||||
this.birthDateEstimated,
|
||||
this.personality,
|
||||
this.microchipNo,
|
||||
this.sterilizedOn,
|
||||
this.status,
|
||||
});
|
||||
|
||||
final int version;
|
||||
final String? name;
|
||||
final String? breedId;
|
||||
final String? customBreedName;
|
||||
final PetSex? sex;
|
||||
final DateTime? birthDate;
|
||||
final bool? birthDateEstimated;
|
||||
final String? personality;
|
||||
final String? microchipNo;
|
||||
final DateTime? sterilizedOn;
|
||||
final PetStatus? status;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'version': version,
|
||||
if (name != null) 'name': name,
|
||||
if (breedId != null) 'breedId': breedId,
|
||||
if (customBreedName != null) 'customBreedName': customBreedName,
|
||||
if (sex != null) 'sex': sex!.name,
|
||||
if (birthDate != null) 'birthDate': dateToJson(birthDate!),
|
||||
if (birthDateEstimated != null) 'birthDateEstimated': birthDateEstimated,
|
||||
if (personality != null) 'personality': personality,
|
||||
if (microchipNo != null) 'microchipNo': microchipNo,
|
||||
if (sterilizedOn != null) 'sterilizedOn': dateToJson(sterilizedOn!),
|
||||
if (status != null) 'status': status!.name,
|
||||
};
|
||||
}
|
||||
|
||||
/// 品种目录项(只读字典)。
|
||||
class Breed {
|
||||
const Breed({
|
||||
required this.id,
|
||||
required this.species,
|
||||
required this.code,
|
||||
required this.displayName,
|
||||
});
|
||||
|
||||
factory Breed.fromJson(Map<String, dynamic> json) {
|
||||
return Breed(
|
||||
id: json['id'] as String,
|
||||
species: PetSpecies.fromJson(json['species'] as String),
|
||||
code: json['code'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final PetSpecies species;
|
||||
final String code;
|
||||
final String displayName;
|
||||
}
|
||||
|
||||
/// 体重记录(append-only,无乐观锁)。
|
||||
class WeightRecord {
|
||||
const WeightRecord({
|
||||
required this.id,
|
||||
required this.petId,
|
||||
required this.weightKg,
|
||||
required this.measuredAt,
|
||||
required this.source,
|
||||
required this.note,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory WeightRecord.fromJson(Map<String, dynamic> json) {
|
||||
return WeightRecord(
|
||||
id: json['id'] as String,
|
||||
petId: json['petId'] as String,
|
||||
// 服务端 numeric(6,2),整数值也可能以 int 下发。
|
||||
weightKg: (json['weightKg'] as num).toDouble(),
|
||||
measuredAt: DateTime.parse(json['measuredAt'] as String),
|
||||
source: WeightSource.fromJson(json['source'] as String),
|
||||
note: json['note'] as String?,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String petId;
|
||||
final double weightKg;
|
||||
final DateTime measuredAt;
|
||||
final WeightSource source;
|
||||
final String? note;
|
||||
final DateTime createdAt;
|
||||
}
|
||||
|
||||
/// 添加体重记录请求。
|
||||
class CreateWeightRequest {
|
||||
const CreateWeightRequest({
|
||||
required this.weightKg,
|
||||
required this.measuredAt,
|
||||
this.source,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final double weightKg;
|
||||
final DateTime measuredAt;
|
||||
final WeightSource? source;
|
||||
final String? note;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'weightKg': weightKg,
|
||||
'measuredAt': measuredAt.toIso8601String(),
|
||||
if (source != null) 'source': source!.name,
|
||||
if (note != null) 'note': note,
|
||||
};
|
||||
}
|
||||
|
||||
/// 疫苗目录项(只读字典)。
|
||||
class VaccineCatalogItem {
|
||||
const VaccineCatalogItem({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.species,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
factory VaccineCatalogItem.fromJson(Map<String, dynamic> json) {
|
||||
return VaccineCatalogItem(
|
||||
id: json['id'] as String,
|
||||
code: json['code'] as String,
|
||||
name: json['name'] as String,
|
||||
species: PetSpecies.fromJson(json['species'] as String),
|
||||
description: json['description'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String code;
|
||||
final String name;
|
||||
final PetSpecies species;
|
||||
final String? description;
|
||||
}
|
||||
|
||||
/// 疫苗记录。vaccineName 由目录解出(列表页免二次查字典)。
|
||||
class Vaccination {
|
||||
const Vaccination({
|
||||
required this.id,
|
||||
required this.petId,
|
||||
required this.vaccineId,
|
||||
required this.vaccineName,
|
||||
required this.seriesKey,
|
||||
required this.doseNo,
|
||||
required this.doseLabel,
|
||||
required this.status,
|
||||
required this.plannedOn,
|
||||
required this.administeredOn,
|
||||
required this.nextDueOn,
|
||||
required this.manufacturer,
|
||||
required this.batchNo,
|
||||
required this.notes,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
factory Vaccination.fromJson(Map<String, dynamic> json) {
|
||||
return Vaccination(
|
||||
id: json['id'] as String,
|
||||
petId: json['petId'] as String,
|
||||
vaccineId: json['vaccineId'] as String,
|
||||
vaccineName: json['vaccineName'] as String,
|
||||
seriesKey: json['seriesKey'] as String,
|
||||
doseNo: json['doseNo'] as int,
|
||||
doseLabel: json['doseLabel'] as String?,
|
||||
status: VaccinationStatus.fromJson(json['status'] as String),
|
||||
plannedOn: _dateOrNull(json['plannedOn']),
|
||||
administeredOn: _dateOrNull(json['administeredOn']),
|
||||
nextDueOn: _dateOrNull(json['nextDueOn']),
|
||||
manufacturer: json['manufacturer'] as String?,
|
||||
batchNo: json['batchNo'] as String?,
|
||||
notes: json['notes'] as String?,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
version: json['version'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String petId;
|
||||
final String vaccineId;
|
||||
final String vaccineName;
|
||||
final String seriesKey;
|
||||
final int doseNo;
|
||||
final String? doseLabel;
|
||||
final VaccinationStatus status;
|
||||
final DateTime? plannedOn;
|
||||
final DateTime? administeredOn;
|
||||
final DateTime? nextDueOn;
|
||||
final String? manufacturer;
|
||||
final String? batchNo;
|
||||
final String? notes;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int version;
|
||||
}
|
||||
|
||||
/// 创建疫苗记录请求(创建状态仅 scheduled / completed)。
|
||||
class CreateVaccinationRequest {
|
||||
const CreateVaccinationRequest({
|
||||
required this.vaccineId,
|
||||
required this.seriesKey,
|
||||
required this.doseNo,
|
||||
required this.status,
|
||||
this.doseLabel,
|
||||
this.plannedOn,
|
||||
this.administeredOn,
|
||||
this.nextDueOn,
|
||||
this.manufacturer,
|
||||
this.batchNo,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
final String vaccineId;
|
||||
final String seriesKey;
|
||||
final int doseNo;
|
||||
|
||||
/// 契约允许 scheduled / completed(cancelled 创建无业务意义,服务端 400)。
|
||||
final VaccinationStatus status;
|
||||
final String? doseLabel;
|
||||
final DateTime? plannedOn;
|
||||
final DateTime? administeredOn;
|
||||
final DateTime? nextDueOn;
|
||||
final String? manufacturer;
|
||||
final String? batchNo;
|
||||
final String? notes;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'vaccineId': vaccineId,
|
||||
'seriesKey': seriesKey,
|
||||
'doseNo': doseNo,
|
||||
'status': status.name,
|
||||
if (doseLabel != null) 'doseLabel': doseLabel,
|
||||
if (plannedOn != null) 'plannedOn': dateToJson(plannedOn!),
|
||||
if (administeredOn != null) 'administeredOn': dateToJson(administeredOn!),
|
||||
if (nextDueOn != null) 'nextDueOn': dateToJson(nextDueOn!),
|
||||
if (manufacturer != null) 'manufacturer': manufacturer,
|
||||
if (batchNo != null) 'batchNo': batchNo,
|
||||
if (notes != null) 'notes': notes,
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新疫苗记录请求(vaccineId / seriesKey / doseNo 不可改;version 必填)。
|
||||
class UpdateVaccinationRequest {
|
||||
const UpdateVaccinationRequest({
|
||||
required this.version,
|
||||
this.status,
|
||||
this.plannedOn,
|
||||
this.administeredOn,
|
||||
this.nextDueOn,
|
||||
this.doseLabel,
|
||||
this.manufacturer,
|
||||
this.batchNo,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
final int version;
|
||||
final VaccinationStatus? status;
|
||||
final DateTime? plannedOn;
|
||||
final DateTime? administeredOn;
|
||||
final DateTime? nextDueOn;
|
||||
final String? doseLabel;
|
||||
final String? manufacturer;
|
||||
final String? batchNo;
|
||||
final String? notes;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'version': version,
|
||||
if (status != null) 'status': status!.name,
|
||||
if (plannedOn != null) 'plannedOn': dateToJson(plannedOn!),
|
||||
if (administeredOn != null) 'administeredOn': dateToJson(administeredOn!),
|
||||
if (nextDueOn != null) 'nextDueOn': dateToJson(nextDueOn!),
|
||||
if (doseLabel != null) 'doseLabel': doseLabel,
|
||||
if (manufacturer != null) 'manufacturer': manufacturer,
|
||||
if (batchNo != null) 'batchNo': batchNo,
|
||||
if (notes != null) 'notes': notes,
|
||||
};
|
||||
}
|
||||
|
||||
/// 健康事件(六类;金额整数分传输,元/分换算留给 UI 层)。
|
||||
class HealthEvent {
|
||||
const HealthEvent({
|
||||
required this.id,
|
||||
required this.petId,
|
||||
required this.eventType,
|
||||
required this.occurredAt,
|
||||
required this.title,
|
||||
required this.notes,
|
||||
required this.amountCents,
|
||||
required this.createdByUserId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
factory HealthEvent.fromJson(Map<String, dynamic> json) {
|
||||
return HealthEvent(
|
||||
id: json['id'] as String,
|
||||
petId: json['petId'] as String,
|
||||
eventType: HealthEventType.fromJson(json['eventType'] as String),
|
||||
occurredAt: DateTime.parse(json['occurredAt'] as String),
|
||||
title: json['title'] as String,
|
||||
notes: json['notes'] as String?,
|
||||
amountCents: json['amountCents'] as int?,
|
||||
createdByUserId: json['createdByUserId'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
version: json['version'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String petId;
|
||||
final HealthEventType eventType;
|
||||
final DateTime occurredAt;
|
||||
final String title;
|
||||
final String? notes;
|
||||
|
||||
/// 金额(整数分,非负,可空)。DTO 层保持整数分不换算。
|
||||
final int? amountCents;
|
||||
final String createdByUserId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final int version;
|
||||
}
|
||||
|
||||
/// 添加健康事件请求(createdByUserId 取自 token,不收请求体)。
|
||||
class CreateHealthEventRequest {
|
||||
const CreateHealthEventRequest({
|
||||
required this.eventType,
|
||||
required this.occurredAt,
|
||||
required this.title,
|
||||
this.notes,
|
||||
this.amountCents,
|
||||
});
|
||||
|
||||
final HealthEventType eventType;
|
||||
final DateTime occurredAt;
|
||||
final String title;
|
||||
final String? notes;
|
||||
|
||||
/// 整数分;提交小数服务端 400/40000(不做静默截断)。
|
||||
final int? amountCents;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'eventType': eventType.name,
|
||||
'occurredAt': occurredAt.toIso8601String(),
|
||||
'title': title,
|
||||
if (notes != null) 'notes': notes,
|
||||
if (amountCents != null) 'amountCents': amountCents,
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新健康事件请求(仅 title / notes / amountCents 可编辑;version 必填)。
|
||||
class UpdateHealthEventRequest {
|
||||
const UpdateHealthEventRequest({
|
||||
required this.version,
|
||||
this.title,
|
||||
this.notes,
|
||||
this.amountCents,
|
||||
});
|
||||
|
||||
final int version;
|
||||
final String? title;
|
||||
final String? notes;
|
||||
final int? amountCents;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'version': version,
|
||||
if (title != null) 'title': title,
|
||||
if (notes != null) 'notes': notes,
|
||||
if (amountCents != null) 'amountCents': amountCents,
|
||||
};
|
||||
}
|
||||
|
||||
/// 照护提醒(无 version 字段;completedAt 非空当且仅当 status=completed)。
|
||||
class CareReminder {
|
||||
const CareReminder({
|
||||
required this.id,
|
||||
required this.petId,
|
||||
required this.reminderType,
|
||||
required this.title,
|
||||
required this.dueAt,
|
||||
required this.status,
|
||||
required this.completedAt,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory CareReminder.fromJson(Map<String, dynamic> json) {
|
||||
return CareReminder(
|
||||
id: json['id'] as String,
|
||||
petId: json['petId'] as String,
|
||||
reminderType: CareReminderType.fromJson(json['reminderType'] as String),
|
||||
title: json['title'] as String,
|
||||
dueAt: DateTime.parse(json['dueAt'] as String),
|
||||
status: CareReminderStatus.fromJson(json['status'] as String),
|
||||
completedAt: json['completedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['completedAt'] as String),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String petId;
|
||||
final CareReminderType reminderType;
|
||||
final String title;
|
||||
final DateTime dueAt;
|
||||
final CareReminderStatus status;
|
||||
final DateTime? completedAt;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
}
|
||||
|
||||
/// 创建照护提醒请求(创建恒为 pending,不收 status)。
|
||||
class CreateCareReminderRequest {
|
||||
const CreateCareReminderRequest({
|
||||
required this.reminderType,
|
||||
required this.title,
|
||||
required this.dueAt,
|
||||
});
|
||||
|
||||
final CareReminderType reminderType;
|
||||
final String title;
|
||||
final DateTime dueAt;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'reminderType': reminderType.name,
|
||||
'title': title,
|
||||
'dueAt': dueAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新提醒状态请求(状态流转专用:仅 status + completedAt)。
|
||||
class UpdateCareReminderRequest {
|
||||
const UpdateCareReminderRequest({required this.status, this.completedAt});
|
||||
|
||||
final CareReminderStatus status;
|
||||
|
||||
/// status=completed 时必填;其余状态禁带(违反 422/42202)。
|
||||
/// 由客户端提交(而非服务端 now()),允许补记实际完成时刻。
|
||||
final DateTime? completedAt;
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'status': status.name,
|
||||
if (completedAt != null) 'completedAt': completedAt!.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 摘要:最新体重(无记录 → 整体 null)。
|
||||
class SummaryLatestWeight {
|
||||
const SummaryLatestWeight({required this.weightKg, required this.measuredAt});
|
||||
|
||||
factory SummaryLatestWeight.fromJson(Map<String, dynamic> json) {
|
||||
return SummaryLatestWeight(
|
||||
weightKg: (json['weightKg'] as num).toDouble(),
|
||||
measuredAt: DateTime.parse(json['measuredAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
final double weightKg;
|
||||
final DateTime measuredAt;
|
||||
}
|
||||
|
||||
/// 摘要:疫苗进度(totalDoses=0 → 整体 null,不是 0/0)。
|
||||
class SummaryVaccinationProgress {
|
||||
const SummaryVaccinationProgress({
|
||||
required this.completedDoses,
|
||||
required this.totalDoses,
|
||||
});
|
||||
|
||||
factory SummaryVaccinationProgress.fromJson(Map<String, dynamic> json) {
|
||||
return SummaryVaccinationProgress(
|
||||
completedDoses: json['completedDoses'] as int,
|
||||
totalDoses: json['totalDoses'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
final int completedDoses;
|
||||
final int totalDoses;
|
||||
}
|
||||
|
||||
/// 摘要:下次接种(候选集空 → 整体 null;dueOn 可为过去日期——逾期针仍是下一针)。
|
||||
class SummaryNextVaccination {
|
||||
const SummaryNextVaccination({
|
||||
required this.vaccinationId,
|
||||
required this.vaccineId,
|
||||
required this.vaccineName,
|
||||
required this.doseNo,
|
||||
required this.doseLabel,
|
||||
required this.dueOn,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
factory SummaryNextVaccination.fromJson(Map<String, dynamic> json) {
|
||||
return SummaryNextVaccination(
|
||||
vaccinationId: json['vaccinationId'] as String,
|
||||
vaccineId: json['vaccineId'] as String,
|
||||
vaccineName: json['vaccineName'] as String,
|
||||
doseNo: json['doseNo'] as int,
|
||||
doseLabel: json['doseLabel'] as String?,
|
||||
dueOn: DateTime.parse(json['dueOn'] as String),
|
||||
source: NextVaccinationSource.fromJson(json['source'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
final String vaccinationId;
|
||||
final String vaccineId;
|
||||
final String vaccineName;
|
||||
final int doseNo;
|
||||
final String? doseLabel;
|
||||
final DateTime dueOn;
|
||||
final NextVaccinationSource source;
|
||||
}
|
||||
|
||||
/// 摘要:当月花费(恒非 null;无支出 amountCents=0;月边界随 tz 移动)。
|
||||
class SummaryMonthlyExpense {
|
||||
const SummaryMonthlyExpense({
|
||||
required this.month,
|
||||
required this.timezone,
|
||||
required this.amountCents,
|
||||
});
|
||||
|
||||
factory SummaryMonthlyExpense.fromJson(Map<String, dynamic> json) {
|
||||
return SummaryMonthlyExpense(
|
||||
month: json['month'] as String,
|
||||
timezone: json['timezone'] as String,
|
||||
amountCents: json['amountCents'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// ISO year-month(如 2026-09)。
|
||||
final String month;
|
||||
final String timezone;
|
||||
final int amountCents;
|
||||
}
|
||||
|
||||
/// 档案聚合摘要(四项聚合实时计算;前三项无记录时为 null,
|
||||
/// monthlyExpense 恒非 null——契约 PetSummary null 语义)。
|
||||
class PetSummary {
|
||||
const PetSummary({
|
||||
required this.petId,
|
||||
required this.latestWeight,
|
||||
required this.vaccinationProgress,
|
||||
required this.nextVaccination,
|
||||
required this.monthlyExpense,
|
||||
});
|
||||
|
||||
factory PetSummary.fromJson(Map<String, dynamic> json) {
|
||||
return PetSummary(
|
||||
petId: json['petId'] as String,
|
||||
latestWeight: json['latestWeight'] == null
|
||||
? null
|
||||
: SummaryLatestWeight.fromJson(
|
||||
json['latestWeight'] as Map<String, dynamic>,
|
||||
),
|
||||
vaccinationProgress: json['vaccinationProgress'] == null
|
||||
? null
|
||||
: SummaryVaccinationProgress.fromJson(
|
||||
json['vaccinationProgress'] as Map<String, dynamic>,
|
||||
),
|
||||
nextVaccination: json['nextVaccination'] == null
|
||||
? null
|
||||
: SummaryNextVaccination.fromJson(
|
||||
json['nextVaccination'] as Map<String, dynamic>,
|
||||
),
|
||||
monthlyExpense: SummaryMonthlyExpense.fromJson(
|
||||
json['monthlyExpense'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String petId;
|
||||
final SummaryLatestWeight? latestWeight;
|
||||
final SummaryVaccinationProgress? vaccinationProgress;
|
||||
final SummaryNextVaccination? nextVaccination;
|
||||
final SummaryMonthlyExpense monthlyExpense;
|
||||
}
|
||||
Reference in New Issue
Block a user