/// 照片域模型(P3 计划 Task 2):管理端照片记录与照片枚举。 /// /// 契约来源:`packages/contracts/openapi.yaml` tag `photos`。 library; import '../albums/models.dart' show SignedUrl; /// 照片对外可见性。 /// /// 契约 `components.schemas.PhotoVisibility`:unpublished/published/hidden/ /// deleted;deleted 为软删除。管理端 PATCH /photos/{photoId} 仅允许在 /// hidden 与 published 间切换。 enum PhotoVisibility { unpublished('unpublished'), published('published'), hidden('hidden'), deleted('deleted'); const PhotoVisibility(this.wire); /// 线上(JSON)取值。 final String wire; static PhotoVisibility fromJson(String value) => values.firstWhere( (v) => v.wire == value, orElse: () => throw FormatException('未知 PhotoVisibility: $value'), ); } /// 照片服务端处理状态。 /// /// 契约 `components.schemas.PhotoProcessingState`(技术实施方案 §5.5 /// 「服务端处理」链路)。 enum PhotoProcessingState { pendingUpload('pending_upload'), uploaded('uploaded'), processing('processing'), ready('ready'), processingFailed('processing_failed'); const PhotoProcessingState(this.wire); /// 线上(JSON)取值。 final String wire; static PhotoProcessingState fromJson(String value) => values.firstWhere( (v) => v.wire == value, orElse: () => throw FormatException('未知 PhotoProcessingState: $value'), ); } /// 管理端照片记录。 /// /// 契约 `components.schemas.PhotoRecord`(技术实施方案 §4 photos 表字段)。 /// 作为 GET /albums/{albumId}/photos 条目时经 [AdminPhoto] 内嵌 uploader; /// PATCH /photos/{photoId}(operationId `updatePhoto`)的响应为裸 /// PhotoRecord。 class PhotoRecord { const PhotoRecord({ required this.photoId, required this.albumId, required this.categoryId, required this.uploaderId, required this.visibility, required this.processingState, required this.bytes, required this.width, required this.height, required this.shotAt, required this.uploadedAt, required this.publishedAt, required this.likeCount, }); factory PhotoRecord.fromJson(Map json) => PhotoRecord( photoId: json['photoId'] as String, albumId: json['albumId'] as String, categoryId: json['categoryId'] as String?, uploaderId: json['uploaderId'] as String, visibility: PhotoVisibility.fromJson(json['visibility'] as String), processingState: PhotoProcessingState.fromJson(json['processingState'] as String), bytes: json['bytes'] as int, width: json['width'] as int?, height: json['height'] as int?, shotAt: json['shotAt'] == null ? null : DateTime.parse(json['shotAt'] as String), uploadedAt: DateTime.parse(json['uploadedAt'] as String), publishedAt: json['publishedAt'] == null ? null : DateTime.parse(json['publishedAt'] as String), likeCount: json['likeCount'] as int, ); /// 照片 ID。 final String photoId; /// 所属相册 ID。 final String albumId; /// 分类 ID;null 表示未分类。 final String? categoryId; /// 上传人用户 ID。 final String uploaderId; /// 对外可见性。 final PhotoVisibility visibility; /// 服务端处理状态。 final PhotoProcessingState processingState; /// 原图字节数。 final int bytes; /// 原图宽度(像素);处理前为 null。 final int? width; /// 原图高度(像素);处理前为 null。 final int? height; /// 拍摄时间(EXIF);无有效值时为 null。 final DateTime? shotAt; /// 上传完成时间。 final DateTime uploadedAt; /// 发布时间;未发布为 null。 final DateTime? publishedAt; /// 累计点赞数。 final int likeCount; Map toJson() => { 'photoId': photoId, 'albumId': albumId, 'categoryId': categoryId, 'uploaderId': uploaderId, 'visibility': visibility.wire, 'processingState': processingState.wire, 'bytes': bytes, 'width': width, 'height': height, 'shotAt': shotAt?.toUtc().toIso8601String(), 'uploadedAt': uploadedAt.toUtc().toIso8601String(), 'publishedAt': publishedAt?.toUtc().toIso8601String(), 'likeCount': likeCount, }; } /// 管理列表条目内嵌的上传人脱敏信息。 /// /// 契约 `components.schemas.AdminPhotoUploader`(不含邮箱、手机等凭据字段)。 class AdminPhotoUploader { const AdminPhotoUploader({ required this.userId, required this.account, required this.displayName, }); factory AdminPhotoUploader.fromJson(Map json) => AdminPhotoUploader( userId: json['userId'] as String, account: json['account'] as String, displayName: json['displayName'] as String, ); /// 上传人用户 ID(与 PhotoRecord.uploaderId 一致)。 final String userId; /// 登录账号。 final String account; /// 展示姓名(来自用户资料)。 final String displayName; Map toJson() => { 'userId': userId, 'account': account, 'displayName': displayName, }; } /// 管理照片列表数据范围(UI-01,listAlbumPhotos scope 查询参数)。 /// /// all=本相册全部照片(缺省,仅 owner/admin);own=仅当前用户上传(任何 /// 有效成员,服务端过滤)。 enum AlbumPhotoScope { all('all'), own('own'); const AlbumPhotoScope(this.wire); /// 线上(JSON)取值。 final String wire; static AlbumPhotoScope fromJson(String value) => values.firstWhere( (v) => v.wire == value, orElse: () => throw FormatException('未知 AlbumPhotoScope: $value'), ); } /// 管理端照片预览(UI-01)。 /// /// 契约 `components.schemas.PhotoPreview`:thumb/browse 短期签名 URL。 /// 有权限(admin 全部 / 相册 owner 本相册 / 协作者仅自己上传)且有有效 /// 变体、未删除才签发;隐藏照片仅 admin/owner。无预览时条目 preview 为 /// null(PhotoRecord 记录本身不受影响)。 class PhotoPreview { const PhotoPreview({required this.thumb, required this.browse}); factory PhotoPreview.fromJson(Map json) => PhotoPreview( thumb: SignedUrl.fromJson(json['thumb'] as Map), browse: SignedUrl.fromJson(json['browse'] as Map), ); /// 缩略图签名 URL。 final SignedUrl thumb; /// 浏览图签名 URL。 final SignedUrl browse; Map toJson() => { 'thumb': thumb.toJson(), 'browse': browse.toJson(), }; } /// 管理端照片列表条目。 /// /// 契约 `components.schemas.AdminPhoto`(= PhotoRecord + uploader 内嵌 + /// preview 可选扩展),GET /albums/{albumId}/photos(operationId /// `listAlbumPhotos`)的条目。preview(UI-01):无权签发/无变体/已删除为 /// null;旧响应缺键同样解析为 null。 class AdminPhoto extends PhotoRecord { const AdminPhoto({ required this.uploader, required super.photoId, required super.albumId, required super.categoryId, required super.uploaderId, required super.visibility, required super.processingState, required super.bytes, required super.width, required super.height, required super.shotAt, required super.uploadedAt, required super.publishedAt, required super.likeCount, this.preview, }); factory AdminPhoto.fromJson(Map json) => AdminPhoto( uploader: AdminPhotoUploader.fromJson( json['uploader'] as Map, ), photoId: json['photoId'] as String, albumId: json['albumId'] as String, categoryId: json['categoryId'] as String?, uploaderId: json['uploaderId'] as String, visibility: PhotoVisibility.fromJson(json['visibility'] as String), processingState: PhotoProcessingState.fromJson(json['processingState'] as String), bytes: json['bytes'] as int, width: json['width'] as int?, height: json['height'] as int?, shotAt: json['shotAt'] == null ? null : DateTime.parse(json['shotAt'] as String), uploadedAt: DateTime.parse(json['uploadedAt'] as String), publishedAt: json['publishedAt'] == null ? null : DateTime.parse(json['publishedAt'] as String), likeCount: json['likeCount'] as int, preview: json['preview'] == null ? null : PhotoPreview.fromJson(json['preview'] as Map), ); /// 上传人脱敏信息。 final AdminPhotoUploader uploader; /// 受权限管理的照片预览(UI-01);无权/无变体/已删除为 null。 final PhotoPreview? preview; @override Map toJson() => { ...super.toJson(), 'uploader': uploader.toJson(), 'preview': preview?.toJson(), }; }