feat(contracts,api): UI-01 cover, preview, scope=own and embed config extensions
契约扩展(全部可选/可空,旧客户端零感知)+ cn/ov 实现 + e2e 权限矩阵:
- AlbumSummary += cover(kind=cover state=effective 最新行的短期签名 URL,
列表/详情按页批量组装非 N+1;未配置为 null)
- AdminPhoto += preview{thumb,browse}(变体齐备且未删除才签发;隐藏仅
admin/owner;协作者仅自己上传,scope=own 服务端过滤 uploaderId,
成员资格逐请求裁决)
- listAlbumPhotos += scope=all|own(缺省 all 行为不变:仅 owner/admin,
协作者 403;own 任何有效成员)
- EmbedSlotSettings += imageFit/offsetX/offsetBottom/zIndex(cn PATCH 校验
→ slot-state 投影 → ov 公开回读;settings 为 Json 列免迁移,读侧
normalizeStoredSettings 兜旧形状默认值)
- Flutter 模型同步(SignedUrl/PhotoPreview/AlbumPhotoScope,可空容错)
e2e:ui01-contracts 9 例(封面选取/字节取回/N+1 计数断言、preview 权限
矩阵、scope 过滤与 403/404/400)+ embed ⑨ 配置链路;全量 e2e 257 通过。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -70,10 +70,35 @@ enum AlbumStatus {
|
||||
);
|
||||
}
|
||||
|
||||
/// 私有对象的短期签名 URL。
|
||||
///
|
||||
/// 契约 `components.schemas.SignedUrl`(UI-01 起相册封面/照片预览共用):
|
||||
/// 不得写入持久分享链接,过期后经原入口(列表/详情重取)刷新。
|
||||
class SignedUrl {
|
||||
const SignedUrl({required this.url, required this.expiresAt});
|
||||
|
||||
factory SignedUrl.fromJson(Map<String, dynamic> json) => SignedUrl(
|
||||
url: json['url'] as String,
|
||||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||
);
|
||||
|
||||
/// 签名访问 URL。
|
||||
final String url;
|
||||
|
||||
/// URL 失效时间。
|
||||
final DateTime expiresAt;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'url': url,
|
||||
'expiresAt': expiresAt.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 相册列表条目(完整开关与多语言标题,管理列表无需二次请求详情)。
|
||||
///
|
||||
/// 契约 `components.schemas.AlbumSummary`(GET /albums,
|
||||
/// operationId `listAlbums` 的条目)。
|
||||
/// operationId `listAlbums` 的条目)。cover(UI-01 可选扩展):当前生效
|
||||
/// 封面的短期签名 URL;旧响应缺键或未配置封面为 null。
|
||||
class AlbumSummary {
|
||||
const AlbumSummary({
|
||||
required this.id,
|
||||
@@ -93,6 +118,7 @@ class AlbumSummary {
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.statsSyncedAt,
|
||||
this.cover,
|
||||
});
|
||||
|
||||
factory AlbumSummary.fromJson(Map<String, dynamic> json) => AlbumSummary(
|
||||
@@ -123,6 +149,9 @@ class AlbumSummary {
|
||||
statsSyncedAt: json['statsSyncedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['statsSyncedAt'] as String),
|
||||
cover: json['cover'] == null
|
||||
? null
|
||||
: SignedUrl.fromJson(json['cover'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
/// 相册 ID。
|
||||
@@ -176,6 +205,9 @@ class AlbumSummary {
|
||||
/// 统计副本最近同步时间(P2 A27);null 表示尚无回传应用。
|
||||
final DateTime? statsSyncedAt;
|
||||
|
||||
/// 相册封面签名 URL(UI-01);未配置封面或旧响应缺键为 null。
|
||||
final SignedUrl? cover;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'publicToken': publicToken,
|
||||
@@ -196,6 +228,7 @@ class AlbumSummary {
|
||||
'createdAt': createdAt.toUtc().toIso8601String(),
|
||||
'updatedAt': updatedAt.toUtc().toIso8601String(),
|
||||
'statsSyncedAt': statsSyncedAt?.toUtc().toIso8601String(),
|
||||
'cover': cover?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user