新增:宠物头像接线——铅笔角标接上传、列表与详情展示真实头像(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>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
/// pets 域响应 / 请求模型(接口契约冻结稿 openapi.yaml v1.2.0,
|
||||
/// pets 域响应 / 请求模型(接口契约冻结稿 openapi.yaml v1.4.0,
|
||||
/// 字段名与后端逐字一致;枚举取值严格校验,未知值抛 [FormatException]
|
||||
/// 以便契约漂移在测试期暴露而非静默吞掉)。
|
||||
library;
|
||||
|
||||
import 'package:patbond_flutter/core/models/patch_field.dart';
|
||||
|
||||
export 'package:patbond_flutter/core/models/patch_field.dart';
|
||||
export 'package:patbond_flutter/core/models/cursor_page.dart';
|
||||
|
||||
/// 物种(创建即定,不可修改)。
|
||||
@@ -143,6 +146,7 @@ class Pet {
|
||||
required this.microchipNo,
|
||||
required this.sterilizedOn,
|
||||
required this.status,
|
||||
required this.avatarUrl,
|
||||
required this.myRole,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
@@ -164,6 +168,9 @@ class Pet {
|
||||
microchipNo: json['microchipNo'] as String?,
|
||||
sterilizedOn: _dateOrNull(json['sterilizedOn']),
|
||||
status: PetStatus.fromJson(json['status'] as String),
|
||||
// 契约 v1.4.0 新增:时效性预签名 GET URL,每次响应现签;
|
||||
// 不得持久化、过期即重取。响应**不含 avatarAssetId**(只写不读)。
|
||||
avatarUrl: json['avatarUrl'] as String?,
|
||||
myRole: PetRole.fromJson(json['myRole'] as String),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
@@ -184,6 +191,11 @@ class Pet {
|
||||
final String? microchipNo;
|
||||
final DateTime? sterilizedOn;
|
||||
final PetStatus status;
|
||||
|
||||
/// 头像预签名 GET URL;无头像 / asset 非 ready / 对象存储未配置均为 null。
|
||||
/// 「有头像」等价于本字段非 null(契约不外露 assetId)。
|
||||
final String? avatarUrl;
|
||||
|
||||
final PetRole myRole;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
@@ -230,8 +242,17 @@ class CreatePetRequest {
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新宠物请求(部分更新:缺席字段不变;不支持清空回 null;
|
||||
/// 品种对整体替换;species 不可改;version 乐观锁必填)。
|
||||
/// 更新宠物请求(部分更新:缺席字段不变;品种对整体替换;species 不可改;
|
||||
/// version 乐观锁必填)。
|
||||
///
|
||||
/// **两态与三态并存**:除 [avatarAssetId] 外的字段沿 M2 两态语义(缺省或
|
||||
/// null 皆为「不改」,不支持清空回 null——它们的 CHECK 约束本就不允许空值);
|
||||
/// [avatarAssetId] 是本 schema **唯一的三态字段**(契约 v1.4.0),因为
|
||||
/// 「删掉我设的那张头像」是一等公民操作,两态根本无法表达。
|
||||
///
|
||||
/// **权限随本次触及的字段变档**(ADR-022 + 服务端 `requiredLevel`):
|
||||
/// 只带 [avatarAssetId] 走 WRITE(owner + caregiver 均可);碰任一资料字段
|
||||
/// 即 MANAGE(仅 owner);混合请求取更严的一半(防「夹带改名」)。
|
||||
class UpdatePetRequest {
|
||||
const UpdatePetRequest({
|
||||
required this.version,
|
||||
@@ -245,6 +266,7 @@ class UpdatePetRequest {
|
||||
this.microchipNo,
|
||||
this.sterilizedOn,
|
||||
this.status,
|
||||
this.avatarAssetId = const PatchField<String>.absent(),
|
||||
});
|
||||
|
||||
final int version;
|
||||
@@ -259,19 +281,26 @@ class UpdatePetRequest {
|
||||
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,
|
||||
};
|
||||
/// 宠物头像 asset(两步上传产物,`purpose` 须为 `pet_avatar`)。三态。
|
||||
final PatchField<String> avatarAssetId;
|
||||
|
||||
Map<String, Object?> toJson() {
|
||||
final json = <String, Object?>{
|
||||
'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,
|
||||
};
|
||||
avatarAssetId.writeTo(json, 'avatarAssetId');
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
/// 品种目录项(只读字典)。
|
||||
|
||||
Reference in New Issue
Block a user