feat(mobile): api client, session storage and contract-aligned models
P3 计划 Task 2(apps/mobile):
- core/config.dart:API_BASE/VIEWER_BASE 经 --dart-define 覆盖,缺省
10.0.2.2 模拟器宿主回环;viewer 相册链接构造 {VIEWER_BASE}/a/{token}。
- core/error_codes.dart:errors.ts 17 码 1:1 镜像(含注释),另设 4 个
客户端本地伪码(NETWORK_ERROR/NETWORK_TIMEOUT/CANCELLED/HTTP_ERROR)。
- core/api_client.dart:dio 封装——Bearer 注入(登录端点豁免)、401
驱逐(清会话 + onUnauthorized 回调去重,App 壳注册跳 /login)、
DioException/错误体 {code,message,requestId} 归一 AppException、
通用游标分页助手;auth/albums/members/categories/admin/uploads/photos
全端点方法;riverpod provider 出口。
- core/session_store.dart:SessionStore 抽象 + flutter_secure_storage
实现(token/expiresAt/account 三键,encode/decode 纯函数可测)。
- features/{auth,albums,uploads,photos}/models.dart:手写 fromJson/
toJson,逐字段对齐 openapi.yaml(类注释标注契约 schema/operationId);
locale 按字符串透传(zh-CN 标识符不兼容),业务枚举 Dart 化并在
未知值 fail-fast。
- 测试 66 项:17 码表驱动 + 网络/超时/取消 + 兜底映射、游标边界
(含 CursorPageMeta 不变式违约容错)、模型往返(fixture 转写自
openapi.yaml schema 与 api e2e 断言,来源在测试头注释)、拦截器
行为(手写 FakeAdapter:Bearer 注入/401 驱逐/登录豁免/并发去重/
尽力而为 logout/refresh 滑动续期)。flutter analyze 零告警。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
/// 相册域模型(P3 计划 Task 2):相册、分类、成员、账号查询与请求体。
|
||||
///
|
||||
/// 契约来源:`packages/contracts/openapi.yaml` tag `albums`/`categories`/
|
||||
/// `members`/`admin`。
|
||||
library;
|
||||
|
||||
/// 相册多语言条目(创建/更新请求与响应共用)。
|
||||
///
|
||||
/// 契约 `components.schemas.AlbumTranslation`:未填的可选字段存储为 null
|
||||
/// (回读时 null 而非缺省键)。
|
||||
class AlbumTranslation {
|
||||
const AlbumTranslation({
|
||||
required this.locale,
|
||||
required this.title,
|
||||
this.description,
|
||||
this.venue,
|
||||
this.address,
|
||||
});
|
||||
|
||||
factory AlbumTranslation.fromJson(Map<String, dynamic> json) =>
|
||||
AlbumTranslation(
|
||||
locale: json['locale'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
venue: json['venue'] as String?,
|
||||
address: json['address'] as String?,
|
||||
);
|
||||
|
||||
/// 语言(契约 Locale enum:zh-CN | en;Dart 标识符不兼容连字符,
|
||||
/// 按 String 透传)。
|
||||
final String locale;
|
||||
|
||||
/// 相册标题。
|
||||
final String title;
|
||||
|
||||
/// 相册描述;未填为 null。
|
||||
final String? description;
|
||||
|
||||
/// 场地名称;未填为 null。
|
||||
final String? venue;
|
||||
|
||||
/// 场地地址;未填为 null。
|
||||
final String? address;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'locale': locale,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'venue': venue,
|
||||
'address': address,
|
||||
};
|
||||
}
|
||||
|
||||
/// 相册状态。
|
||||
///
|
||||
/// 契约 `AlbumSummary.status` enum:active=正常;offline=下线
|
||||
/// (停止公开访问与新授权签发)。
|
||||
enum AlbumStatus {
|
||||
active('active'),
|
||||
offline('offline');
|
||||
|
||||
const AlbumStatus(this.wire);
|
||||
|
||||
/// 线上(JSON)取值。
|
||||
final String wire;
|
||||
|
||||
static AlbumStatus fromJson(String value) => values.firstWhere(
|
||||
(v) => v.wire == value,
|
||||
orElse: () => throw FormatException('未知 AlbumStatus: $value'),
|
||||
);
|
||||
}
|
||||
|
||||
/// 相册列表条目(完整开关与多语言标题,管理列表无需二次请求详情)。
|
||||
///
|
||||
/// 契约 `components.schemas.AlbumSummary`(GET /albums,
|
||||
/// operationId `listAlbums` 的条目)。
|
||||
class AlbumSummary {
|
||||
const AlbumSummary({
|
||||
required this.id,
|
||||
required this.publicToken,
|
||||
required this.ownerId,
|
||||
required this.status,
|
||||
required this.defaultLocale,
|
||||
required this.timezone,
|
||||
required this.startAt,
|
||||
required this.endAt,
|
||||
required this.uploadEnabled,
|
||||
required this.publicEnabled,
|
||||
required this.downloadEnabled,
|
||||
required this.originalDownloadEnabled,
|
||||
required this.watermarkVersion,
|
||||
required this.translations,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.statsSyncedAt,
|
||||
});
|
||||
|
||||
factory AlbumSummary.fromJson(Map<String, dynamic> json) => AlbumSummary(
|
||||
id: json['id'] as String,
|
||||
publicToken: json['publicToken'] as String,
|
||||
ownerId: json['ownerId'] as String,
|
||||
status: AlbumStatus.fromJson(json['status'] as String),
|
||||
defaultLocale: json['defaultLocale'] as String,
|
||||
timezone: json['timezone'] as String,
|
||||
startAt: DateTime.parse(json['startAt'] as String),
|
||||
endAt: json['endAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['endAt'] as String),
|
||||
uploadEnabled: json['uploadEnabled'] as bool,
|
||||
publicEnabled: json['publicEnabled'] as bool,
|
||||
downloadEnabled: json['downloadEnabled'] as bool,
|
||||
originalDownloadEnabled: json['originalDownloadEnabled'] as bool,
|
||||
watermarkVersion: json['watermarkVersion'] as int,
|
||||
translations: (json['translations'] as List<dynamic>)
|
||||
.map(
|
||||
(dynamic e) => AlbumTranslation.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
statsSyncedAt: json['statsSyncedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['statsSyncedAt'] as String),
|
||||
);
|
||||
|
||||
/// 相册 ID。
|
||||
final String id;
|
||||
|
||||
/// 公开访问令牌(不可枚举,pattern ^[0-9a-z]{20,32}$)。
|
||||
final String publicToken;
|
||||
|
||||
/// 创建者用户 ID。
|
||||
final String ownerId;
|
||||
|
||||
/// 相册状态。
|
||||
final AlbumStatus status;
|
||||
|
||||
/// 缺省语言(契约 Locale enum:zh-CN | en)。
|
||||
final String defaultLocale;
|
||||
|
||||
/// 活动 IANA 时区。
|
||||
final String timezone;
|
||||
|
||||
/// 活动开始时间(UTC 存储,展示按 timezone)。
|
||||
final DateTime startAt;
|
||||
|
||||
/// 活动结束时间;null 表示未定。
|
||||
final DateTime? endAt;
|
||||
|
||||
/// 是否允许新上传任务。
|
||||
final bool uploadEnabled;
|
||||
|
||||
/// 是否公开可见。
|
||||
final bool publicEnabled;
|
||||
|
||||
/// 允许下载浏览版本。
|
||||
final bool downloadEnabled;
|
||||
|
||||
/// 允许下载原图。
|
||||
final bool originalDownloadEnabled;
|
||||
|
||||
/// 当前水印配置版本。
|
||||
final int watermarkVersion;
|
||||
|
||||
/// 各语言内容。
|
||||
final List<AlbumTranslation> translations;
|
||||
|
||||
/// 创建时间。
|
||||
final DateTime createdAt;
|
||||
|
||||
/// 最近更新时间。
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// 统计副本最近同步时间(P2 A27);null 表示尚无回传应用。
|
||||
final DateTime? statsSyncedAt;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'publicToken': publicToken,
|
||||
'ownerId': ownerId,
|
||||
'status': status.wire,
|
||||
'defaultLocale': defaultLocale,
|
||||
'timezone': timezone,
|
||||
'startAt': startAt.toUtc().toIso8601String(),
|
||||
'endAt': endAt?.toUtc().toIso8601String(),
|
||||
'uploadEnabled': uploadEnabled,
|
||||
'publicEnabled': publicEnabled,
|
||||
'downloadEnabled': downloadEnabled,
|
||||
'originalDownloadEnabled': originalDownloadEnabled,
|
||||
'watermarkVersion': watermarkVersion,
|
||||
'translations': translations
|
||||
.map((AlbumTranslation t) => t.toJson())
|
||||
.toList(),
|
||||
'createdAt': createdAt.toUtc().toIso8601String(),
|
||||
'updatedAt': updatedAt.toUtc().toIso8601String(),
|
||||
'statsSyncedAt': statsSyncedAt?.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 相册详情。
|
||||
///
|
||||
/// 契约 `components.schemas.Album`(= AlbumSummary + categories,按
|
||||
/// sortOrder 升序);POST /albums、GET/PATCH /albums/{albumId}
|
||||
/// (operationId `createAlbum` / `getAlbum` / `updateAlbum`)的响应。
|
||||
class Album extends AlbumSummary {
|
||||
const Album({
|
||||
required this.categories,
|
||||
required super.id,
|
||||
required super.publicToken,
|
||||
required super.ownerId,
|
||||
required super.status,
|
||||
required super.defaultLocale,
|
||||
required super.timezone,
|
||||
required super.startAt,
|
||||
required super.endAt,
|
||||
required super.uploadEnabled,
|
||||
required super.publicEnabled,
|
||||
required super.downloadEnabled,
|
||||
required super.originalDownloadEnabled,
|
||||
required super.watermarkVersion,
|
||||
required super.translations,
|
||||
required super.createdAt,
|
||||
required super.updatedAt,
|
||||
required super.statsSyncedAt,
|
||||
});
|
||||
|
||||
factory Album.fromJson(Map<String, dynamic> json) => Album(
|
||||
categories: (json['categories'] as List<dynamic>)
|
||||
.map(
|
||||
(dynamic e) => Category.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
id: json['id'] as String,
|
||||
publicToken: json['publicToken'] as String,
|
||||
ownerId: json['ownerId'] as String,
|
||||
status: AlbumStatus.fromJson(json['status'] as String),
|
||||
defaultLocale: json['defaultLocale'] as String,
|
||||
timezone: json['timezone'] as String,
|
||||
startAt: DateTime.parse(json['startAt'] as String),
|
||||
endAt: json['endAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['endAt'] as String),
|
||||
uploadEnabled: json['uploadEnabled'] as bool,
|
||||
publicEnabled: json['publicEnabled'] as bool,
|
||||
downloadEnabled: json['downloadEnabled'] as bool,
|
||||
originalDownloadEnabled: json['originalDownloadEnabled'] as bool,
|
||||
watermarkVersion: json['watermarkVersion'] as int,
|
||||
translations: (json['translations'] as List<dynamic>)
|
||||
.map(
|
||||
(dynamic e) => AlbumTranslation.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
statsSyncedAt: json['statsSyncedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['statsSyncedAt'] as String),
|
||||
);
|
||||
|
||||
/// 相册分类(按 sortOrder 升序;新相册为空数组)。
|
||||
final List<Category> categories;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() =>
|
||||
{...super.toJson(), 'categories': categories.map((c) => c.toJson()).toList()};
|
||||
}
|
||||
|
||||
/// 分类多语言名称条目。
|
||||
///
|
||||
/// 契约 `components.schemas.CategoryTranslation`。
|
||||
class CategoryTranslation {
|
||||
const CategoryTranslation({required this.locale, required this.name});
|
||||
|
||||
factory CategoryTranslation.fromJson(Map<String, dynamic> json) =>
|
||||
CategoryTranslation(
|
||||
locale: json['locale'] as String,
|
||||
name: json['name'] as String,
|
||||
);
|
||||
|
||||
/// 语言(契约 Locale enum:zh-CN | en)。
|
||||
final String locale;
|
||||
|
||||
/// 分类名称。
|
||||
final String name;
|
||||
|
||||
Map<String, dynamic> toJson() => {'locale': locale, 'name': name};
|
||||
}
|
||||
|
||||
/// 相册分类。
|
||||
///
|
||||
/// 契约 `components.schemas.Category`;POST /albums/{albumId}/categories、
|
||||
/// PATCH /categories/{categoryId}(operationId `createCategory` /
|
||||
/// `updateCategory`)的响应,亦内嵌于 [Album.categories]。
|
||||
class Category {
|
||||
const Category({
|
||||
required this.id,
|
||||
required this.albumId,
|
||||
required this.sortOrder,
|
||||
required this.translations,
|
||||
});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) => Category(
|
||||
id: json['id'] as String,
|
||||
albumId: json['albumId'] as String,
|
||||
sortOrder: json['sortOrder'] as int,
|
||||
translations: (json['translations'] as List<dynamic>)
|
||||
.map(
|
||||
(dynamic e) =>
|
||||
CategoryTranslation.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
/// 分类 ID。
|
||||
final String id;
|
||||
|
||||
/// 所属相册 ID。
|
||||
final String albumId;
|
||||
|
||||
/// 排序序号(升序)。
|
||||
final int sortOrder;
|
||||
|
||||
/// 各语言名称。
|
||||
final List<CategoryTranslation> translations;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'albumId': albumId,
|
||||
'sortOrder': sortOrder,
|
||||
'translations':
|
||||
translations.map((CategoryTranslation t) => t.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 成员角色。
|
||||
///
|
||||
/// 契约 `Member.role` enum:owner 由创建流程产生,P1a 仅 collaborator 可添加。
|
||||
enum MemberRole {
|
||||
owner('owner'),
|
||||
collaborator('collaborator');
|
||||
|
||||
const MemberRole(this.wire);
|
||||
|
||||
/// 线上(JSON)取值。
|
||||
final String wire;
|
||||
|
||||
static MemberRole fromJson(String value) => values.firstWhere(
|
||||
(v) => v.wire == value,
|
||||
orElse: () => throw FormatException('未知 MemberRole: $value'),
|
||||
);
|
||||
}
|
||||
|
||||
/// 成员状态。
|
||||
///
|
||||
/// 契约 `Member.status` enum:P1a 仅 active,预留扩展。
|
||||
enum MemberStatus {
|
||||
active('active');
|
||||
|
||||
const MemberStatus(this.wire);
|
||||
|
||||
/// 线上(JSON)取值。
|
||||
final String wire;
|
||||
|
||||
static MemberStatus fromJson(String value) => values.firstWhere(
|
||||
(v) => v.wire == value,
|
||||
orElse: () => throw FormatException('未知 MemberStatus: $value'),
|
||||
);
|
||||
}
|
||||
|
||||
/// 相册成员。
|
||||
///
|
||||
/// 契约 `components.schemas.Member`;POST /albums/{albumId}/members
|
||||
/// (operationId `addAlbumMember`)的响应、GET 同路径
|
||||
/// (`listAlbumMembers`)的条目。
|
||||
class Member {
|
||||
const Member({
|
||||
required this.userId,
|
||||
required this.account,
|
||||
required this.displayName,
|
||||
required this.role,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
factory Member.fromJson(Map<String, dynamic> json) => Member(
|
||||
userId: json['userId'] as String,
|
||||
account: json['account'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
role: MemberRole.fromJson(json['role'] as String),
|
||||
status: MemberStatus.fromJson(json['status'] as String),
|
||||
);
|
||||
|
||||
/// 用户 ID。
|
||||
final String userId;
|
||||
|
||||
/// 登录账号。
|
||||
final String account;
|
||||
|
||||
/// 展示姓名(来自用户资料同步)。
|
||||
final String displayName;
|
||||
|
||||
/// 相册角色。
|
||||
final MemberRole role;
|
||||
|
||||
/// 成员状态。
|
||||
final MemberStatus status;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'userId': userId,
|
||||
'account': account,
|
||||
'displayName': displayName,
|
||||
'role': role.wire,
|
||||
'status': status.wire,
|
||||
};
|
||||
}
|
||||
|
||||
/// 账号前缀查询条目(脱敏)。
|
||||
///
|
||||
/// 契约 `components.schemas.AdminUserLookupItem`;GET /admin/users
|
||||
/// (operationId `lookupUsersByAccount`)的条目,用于邀请协作者的
|
||||
/// 前置搜索。无游标(前缀搜索 + 小上限即取即用)。
|
||||
class AdminUserLookupItem {
|
||||
const AdminUserLookupItem({
|
||||
required this.id,
|
||||
required this.account,
|
||||
required this.displayName,
|
||||
});
|
||||
|
||||
factory AdminUserLookupItem.fromJson(Map<String, dynamic> json) =>
|
||||
AdminUserLookupItem(
|
||||
id: json['id'] as String,
|
||||
account: json['account'] as String,
|
||||
displayName: json['displayName'] as String,
|
||||
);
|
||||
|
||||
/// 用户 ID(用于 POST /albums/{albumId}/members 的 userId)。
|
||||
final String id;
|
||||
|
||||
/// 登录账号。
|
||||
final String account;
|
||||
|
||||
/// 展示姓名。
|
||||
final String displayName;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'account': account,
|
||||
'displayName': displayName,
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建相册请求体。
|
||||
///
|
||||
/// 契约 `components.schemas.AlbumCreateRequest`(operationId
|
||||
/// `createAlbum`)。初始默认值(服务端):uploadEnabled/publicEnabled=
|
||||
/// downloadEnabled=true、originalDownloadEnabled=false、status=active、
|
||||
/// watermarkVersion=0——故请求体不含 uploadEnabled/publicEnabled/status。
|
||||
class AlbumCreateRequest {
|
||||
const AlbumCreateRequest({
|
||||
required this.translations,
|
||||
required this.defaultLocale,
|
||||
required this.startAt,
|
||||
this.endAt,
|
||||
this.timezone = 'Asia/Shanghai',
|
||||
this.downloadEnabled = true,
|
||||
this.originalDownloadEnabled = false,
|
||||
});
|
||||
|
||||
/// 多语言内容;必须包含 defaultLocale 对应条目(minItems 1)。
|
||||
final List<AlbumTranslation> translations;
|
||||
|
||||
/// 缺省语言(契约 Locale enum:zh-CN | en)。
|
||||
final String defaultLocale;
|
||||
|
||||
/// 活动开始时间。
|
||||
final DateTime startAt;
|
||||
|
||||
/// 活动结束时间;null/缺省表示未定。
|
||||
final DateTime? endAt;
|
||||
|
||||
/// 活动 IANA 时区(契约缺省 Asia/Shanghai)。
|
||||
final String timezone;
|
||||
|
||||
/// 允许下载浏览版本(契约缺省 true)。
|
||||
final bool downloadEnabled;
|
||||
|
||||
/// 允许下载原图(契约缺省 false)。
|
||||
final bool originalDownloadEnabled;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'translations':
|
||||
translations.map((AlbumTranslation t) => t.toJson()).toList(),
|
||||
'defaultLocale': defaultLocale,
|
||||
'startAt': startAt.toUtc().toIso8601String(),
|
||||
'endAt': endAt?.toUtc().toIso8601String(),
|
||||
'timezone': timezone,
|
||||
'downloadEnabled': downloadEnabled,
|
||||
'originalDownloadEnabled': originalDownloadEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新相册请求体(部分更新:仅提交的字段被修改)。
|
||||
///
|
||||
/// 契约 `components.schemas.AlbumUpdateRequest`(operationId
|
||||
/// `updateAlbum`,minProperties 1)。translations 为全量替换(须包含
|
||||
/// defaultLocale 对应条目);[endAt] 传 null 清除——「未提供」与
|
||||
/// 「显式清除」经 [clearEndAt] 区分(Dart 可空字段无法自带该语义)。
|
||||
class AlbumUpdateRequest {
|
||||
const AlbumUpdateRequest({
|
||||
this.translations,
|
||||
this.defaultLocale,
|
||||
this.startAt,
|
||||
this.endAt,
|
||||
this.clearEndAt = false,
|
||||
this.timezone,
|
||||
this.uploadEnabled,
|
||||
this.publicEnabled,
|
||||
this.downloadEnabled,
|
||||
this.originalDownloadEnabled,
|
||||
});
|
||||
|
||||
/// 全量替换多语言内容(须包含 defaultLocale 对应条目)。
|
||||
final List<AlbumTranslation>? translations;
|
||||
|
||||
/// 缺省语言。
|
||||
final String? defaultLocale;
|
||||
|
||||
/// 活动开始时间。
|
||||
final DateTime? startAt;
|
||||
|
||||
/// 活动结束时间;与 [clearEndAt] 互斥使用。
|
||||
final DateTime? endAt;
|
||||
|
||||
/// 显式清除 endAt(序列化为 `"endAt": null`)。
|
||||
final bool clearEndAt;
|
||||
|
||||
/// 活动 IANA 时区。
|
||||
final String? timezone;
|
||||
|
||||
/// 是否允许新上传任务。
|
||||
final bool? uploadEnabled;
|
||||
|
||||
/// 是否公开可见。
|
||||
final bool? publicEnabled;
|
||||
|
||||
/// 允许下载浏览版本。
|
||||
final bool? downloadEnabled;
|
||||
|
||||
/// 允许下载原图。
|
||||
final bool? originalDownloadEnabled;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
if (translations != null)
|
||||
'translations':
|
||||
translations!.map((AlbumTranslation t) => t.toJson()).toList(),
|
||||
if (defaultLocale != null) 'defaultLocale': defaultLocale,
|
||||
if (startAt != null) 'startAt': startAt!.toUtc().toIso8601String(),
|
||||
if (endAt != null) 'endAt': endAt!.toUtc().toIso8601String(),
|
||||
if (clearEndAt) 'endAt': null,
|
||||
if (timezone != null) 'timezone': timezone,
|
||||
if (uploadEnabled != null) 'uploadEnabled': uploadEnabled,
|
||||
if (publicEnabled != null) 'publicEnabled': publicEnabled,
|
||||
if (downloadEnabled != null) 'downloadEnabled': downloadEnabled,
|
||||
if (originalDownloadEnabled != null)
|
||||
'originalDownloadEnabled': originalDownloadEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建分类请求体。
|
||||
///
|
||||
/// 契约 `components.schemas.CategoryCreateRequest`(operationId
|
||||
/// `createCategory`)。sortOrder 契约缺省 0——null 表示不提交(走服务端
|
||||
/// 缺省)。
|
||||
class CategoryCreateRequest {
|
||||
const CategoryCreateRequest({this.sortOrder, required this.translations});
|
||||
|
||||
/// 排序序号(升序);null 表示不提交(服务端缺省 0)。
|
||||
final int? sortOrder;
|
||||
|
||||
/// 分类多语言名称(minItems 1)。
|
||||
final List<CategoryTranslation> translations;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
if (sortOrder != null) 'sortOrder': sortOrder,
|
||||
'translations':
|
||||
translations.map((CategoryTranslation t) => t.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 更新分类请求体(部分更新;translations 为全量替换)。
|
||||
///
|
||||
/// 契约 `components.schemas.CategoryUpdateRequest`(operationId
|
||||
/// `updateCategory`,minProperties 1)。
|
||||
class CategoryUpdateRequest {
|
||||
const CategoryUpdateRequest({this.sortOrder, this.translations});
|
||||
|
||||
/// 排序序号(升序)。
|
||||
final int? sortOrder;
|
||||
|
||||
/// 全量替换多语言名称。
|
||||
final List<CategoryTranslation>? translations;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
if (sortOrder != null) 'sortOrder': sortOrder,
|
||||
if (translations != null)
|
||||
'translations':
|
||||
translations!.map((CategoryTranslation t) => t.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/// 认证域模型(P3 计划 Task 2)。
|
||||
///
|
||||
/// 契约来源:`packages/contracts/openapi.yaml` tag `auth`。
|
||||
library;
|
||||
|
||||
/// 密码登录响应。
|
||||
///
|
||||
/// 契约 `components.schemas.LoginResponse`(operationId `loginWithPassword`
|
||||
/// ,POST /auth/password → 200)。
|
||||
class LoginResponse {
|
||||
const LoginResponse({required this.sessionToken, required this.expiresAt});
|
||||
|
||||
factory LoginResponse.fromJson(Map<String, dynamic> json) => LoginResponse(
|
||||
sessionToken: json['sessionToken'] as String,
|
||||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||
);
|
||||
|
||||
/// 会话令牌(Bearer)。
|
||||
final String sessionToken;
|
||||
|
||||
/// 会话过期时间(date-time)。
|
||||
final DateTime expiresAt;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'sessionToken': sessionToken,
|
||||
'expiresAt': expiresAt.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 会话滑动续期响应。
|
||||
///
|
||||
/// 契约 `components.schemas.RefreshResponse`(operationId `refreshSession`
|
||||
/// ,POST /auth/refresh → 200):令牌不变,仅返回新的过期时间。
|
||||
class RefreshResponse {
|
||||
const RefreshResponse({required this.expiresAt});
|
||||
|
||||
factory RefreshResponse.fromJson(Map<String, dynamic> json) =>
|
||||
RefreshResponse(expiresAt: DateTime.parse(json['expiresAt'] as String));
|
||||
|
||||
/// 续期后的过期时间(date-time)。
|
||||
final DateTime expiresAt;
|
||||
|
||||
Map<String, dynamic> toJson() =>
|
||||
{'expiresAt': expiresAt.toUtc().toIso8601String()};
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/// 照片域模型(P3 计划 Task 2):管理端照片记录与照片枚举。
|
||||
///
|
||||
/// 契约来源:`packages/contracts/openapi.yaml` tag `photos`。
|
||||
library;
|
||||
|
||||
/// 照片对外可见性。
|
||||
///
|
||||
/// 契约 `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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() => {
|
||||
'userId': userId,
|
||||
'account': account,
|
||||
'displayName': displayName,
|
||||
};
|
||||
}
|
||||
|
||||
/// 管理端照片列表条目。
|
||||
///
|
||||
/// 契约 `components.schemas.AdminPhoto`(= PhotoRecord + uploader 内嵌),
|
||||
/// GET /albums/{albumId}/photos(operationId `listAlbumPhotos`)的条目。
|
||||
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,
|
||||
});
|
||||
|
||||
factory AdminPhoto.fromJson(Map<String, dynamic> json) => AdminPhoto(
|
||||
uploader: AdminPhotoUploader.fromJson(
|
||||
json['uploader'] as Map<String, dynamic>,
|
||||
),
|
||||
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,
|
||||
);
|
||||
|
||||
/// 上传人脱敏信息。
|
||||
final AdminPhotoUploader uploader;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {...super.toJson(), 'uploader': uploader.toJson()};
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/// 上传域模型(P3 计划 Task 2):直传会话、凭证与请求体。
|
||||
///
|
||||
/// 契约来源:`packages/contracts/openapi.yaml` tag `uploads`。
|
||||
library;
|
||||
|
||||
import '../photos/models.dart';
|
||||
|
||||
/// 上传会话状态(服务端视角;本地任务状态机见技术实施方案 §5.5、
|
||||
/// P3 计划 Task 5)。
|
||||
///
|
||||
/// 契约 `components.schemas.UploadSessionState`:会话没有 failed 状态——
|
||||
/// 完成核验失败属于单次请求错误(UPLOAD_VERIFY_FAILED),会话保持
|
||||
/// pending 可重试。
|
||||
enum UploadSessionState {
|
||||
pending('pending'),
|
||||
confirmed('confirmed'),
|
||||
cancelled('cancelled'),
|
||||
expired('expired');
|
||||
|
||||
const UploadSessionState(this.wire);
|
||||
|
||||
/// 线上(JSON)取值。
|
||||
final String wire;
|
||||
|
||||
static UploadSessionState fromJson(String value) => values.firstWhere(
|
||||
(v) => v.wire == value,
|
||||
orElse: () => throw FormatException('未知 UploadSessionState: $value'),
|
||||
);
|
||||
}
|
||||
|
||||
/// staging 对象的临时直传凭证。
|
||||
///
|
||||
/// 契约 `components.schemas.UploadCredential`:仅可写入指定 objectKey,
|
||||
/// 不可访问 originals/variants;过期后续签(renew)再重试。
|
||||
class UploadCredential {
|
||||
const UploadCredential({
|
||||
required this.mode,
|
||||
required this.url,
|
||||
required this.method,
|
||||
required this.headers,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
factory UploadCredential.fromJson(Map<String, dynamic> json) =>
|
||||
UploadCredential(
|
||||
mode: json['mode'] as String,
|
||||
url: json['url'] as String,
|
||||
method: json['method'] as String,
|
||||
headers: (json['headers'] as Map<dynamic, dynamic>)
|
||||
.map((dynamic k, dynamic v) => MapEntry(k as String, v as String)),
|
||||
expiresAt: DateTime.parse(json['expiresAt'] as String),
|
||||
);
|
||||
|
||||
/// 凭证模式;P1a 仅 presigned,预留 STS 扩展。
|
||||
final String mode;
|
||||
|
||||
/// 预签名上传 URL。
|
||||
final String url;
|
||||
|
||||
/// 上传 HTTP 方法(PUT)。
|
||||
final String method;
|
||||
|
||||
/// 上传时必须携带的请求头(如 Content-Type)。
|
||||
final Map<String, String> headers;
|
||||
|
||||
/// 凭证过期时间。
|
||||
final DateTime expiresAt;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'mode': mode,
|
||||
'url': url,
|
||||
'method': method,
|
||||
'headers': headers,
|
||||
'expiresAt': expiresAt.toUtc().toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 初始化上传任务请求体。
|
||||
///
|
||||
/// 契约 `components.schemas.UploadInitRequest`(operationId
|
||||
/// `initUpload`,POST /uploads):幂等初始化——相同 clientTaskId 且参数
|
||||
/// 一致时返回原会话,参数不一致 409(UPLOAD_SESSION_CONFLICT)。
|
||||
///
|
||||
/// 注:契约增补(P3 计划 Task 3)将顺带加入可选 sourceDeviceId
|
||||
/// (A19 设备来源语义);当前以契约现状为准,不含该字段。
|
||||
class UploadInitRequest {
|
||||
const UploadInitRequest({
|
||||
required this.albumId,
|
||||
required this.clientTaskId,
|
||||
required this.filename,
|
||||
required this.bytes,
|
||||
required this.contentType,
|
||||
this.categoryId,
|
||||
});
|
||||
|
||||
/// 目标相册 ID。
|
||||
final String albumId;
|
||||
|
||||
/// 目标分类 ID;缺省为相册默认分类。
|
||||
final String? categoryId;
|
||||
|
||||
/// 客户端任务幂等键;同一任务的重试必须复用(maxLength 128)。
|
||||
final String clientTaskId;
|
||||
|
||||
/// 原始文件名(仅元数据,maxLength 255)。
|
||||
final String filename;
|
||||
|
||||
/// 申明文件字节数;完成核验时逐一比对。
|
||||
final int bytes;
|
||||
|
||||
/// MIME 类型(白名单 image/jpeg | image/png | image/heic)。
|
||||
final String contentType;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'albumId': albumId,
|
||||
if (categoryId != null) 'categoryId': categoryId,
|
||||
'clientTaskId': clientTaskId,
|
||||
'filename': filename,
|
||||
'bytes': bytes,
|
||||
'contentType': contentType,
|
||||
};
|
||||
}
|
||||
|
||||
/// 初始化上传任务响应。
|
||||
///
|
||||
/// 契约 `components.schemas.UploadInitResponse`(operationId `initUpload`
|
||||
/// → 201;`renewUploadCredential` → 200 结构相同,credential 为新值、
|
||||
/// 其余字段回显原会话)。
|
||||
class UploadInitResponse {
|
||||
const UploadInitResponse({
|
||||
required this.uploadId,
|
||||
required this.photoId,
|
||||
required this.objectKey,
|
||||
required this.credential,
|
||||
});
|
||||
|
||||
factory UploadInitResponse.fromJson(Map<String, dynamic> json) =>
|
||||
UploadInitResponse(
|
||||
uploadId: json['uploadId'] as String,
|
||||
photoId: json['photoId'] as String,
|
||||
objectKey: json['objectKey'] as String,
|
||||
credential: UploadCredential.fromJson(
|
||||
json['credential'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
|
||||
/// 上传会话 ID。
|
||||
final String uploadId;
|
||||
|
||||
/// 预创建的照片 ID(重试复用,不重复建照片)。
|
||||
final String photoId;
|
||||
|
||||
/// staging 对象键
|
||||
/// (photo-live/{env}/staging/{uploadId}/{photoId}.{ext})。
|
||||
final String objectKey;
|
||||
|
||||
/// staging 对象的临时直传凭证。
|
||||
final UploadCredential credential;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'uploadId': uploadId,
|
||||
'photoId': photoId,
|
||||
'objectKey': objectKey,
|
||||
'credential': credential.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 完成核验响应。
|
||||
///
|
||||
/// 契约 `components.schemas.UploadCompleteResponse`(operationId
|
||||
/// `completeUpload`,POST /uploads/{uploadId}/complete → 200;幂等)。
|
||||
class UploadCompleteResponse {
|
||||
const UploadCompleteResponse({
|
||||
required this.photoId,
|
||||
required this.processingState,
|
||||
});
|
||||
|
||||
factory UploadCompleteResponse.fromJson(Map<String, dynamic> json) =>
|
||||
UploadCompleteResponse(
|
||||
photoId: json['photoId'] as String,
|
||||
processingState:
|
||||
PhotoProcessingState.fromJson(json['processingState'] as String),
|
||||
);
|
||||
|
||||
/// 照片 ID。
|
||||
final String photoId;
|
||||
|
||||
/// 核验通过后的处理状态(进入处理流水线)。
|
||||
final PhotoProcessingState processingState;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'photoId': photoId,
|
||||
'processingState': processingState.wire,
|
||||
};
|
||||
}
|
||||
|
||||
/// 会话关联照片的当前状态(会话记录内嵌,客户端据此展示服务端进度)。
|
||||
///
|
||||
/// 契约 `components.schemas.UploadSessionPhoto`。
|
||||
class UploadSessionPhoto {
|
||||
const UploadSessionPhoto({
|
||||
required this.photoId,
|
||||
required this.processingState,
|
||||
required this.visibility,
|
||||
});
|
||||
|
||||
factory UploadSessionPhoto.fromJson(Map<String, dynamic> json) =>
|
||||
UploadSessionPhoto(
|
||||
photoId: json['photoId'] as String,
|
||||
processingState:
|
||||
PhotoProcessingState.fromJson(json['processingState'] as String),
|
||||
visibility: PhotoVisibility.fromJson(json['visibility'] as String),
|
||||
);
|
||||
|
||||
/// 照片 ID。
|
||||
final String photoId;
|
||||
|
||||
/// 服务端处理状态。
|
||||
final PhotoProcessingState processingState;
|
||||
|
||||
/// 对外可见性。
|
||||
final PhotoVisibility visibility;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'photoId': photoId,
|
||||
'processingState': processingState.wire,
|
||||
'visibility': visibility.wire,
|
||||
};
|
||||
}
|
||||
|
||||
/// 上传会话记录。
|
||||
///
|
||||
/// 契约 `components.schemas.UploadSessionRecord`(GET /uploads,
|
||||
/// operationId `listUploads` 的条目):当前用户的上传会话历史(新在前),
|
||||
/// 用于任务队列恢复与跨设备状态展示(A19)。
|
||||
class UploadSessionRecord {
|
||||
const UploadSessionRecord({
|
||||
required this.uploadId,
|
||||
required this.albumId,
|
||||
required this.clientTaskId,
|
||||
required this.state,
|
||||
required this.expectedBytes,
|
||||
required this.contentType,
|
||||
required this.createdAt,
|
||||
required this.photo,
|
||||
});
|
||||
|
||||
factory UploadSessionRecord.fromJson(Map<String, dynamic> json) =>
|
||||
UploadSessionRecord(
|
||||
uploadId: json['uploadId'] as String,
|
||||
albumId: json['albumId'] as String,
|
||||
clientTaskId: json['clientTaskId'] as String,
|
||||
state: UploadSessionState.fromJson(json['state'] as String),
|
||||
expectedBytes: json['expectedBytes'] as int,
|
||||
contentType: json['contentType'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
photo: UploadSessionPhoto.fromJson(
|
||||
json['photo'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
|
||||
/// 上传会话 ID。
|
||||
final String uploadId;
|
||||
|
||||
/// 目标相册 ID。
|
||||
final String albumId;
|
||||
|
||||
/// 客户端任务幂等键。
|
||||
final String clientTaskId;
|
||||
|
||||
/// 会话状态。
|
||||
final UploadSessionState state;
|
||||
|
||||
/// 申明文件字节数。
|
||||
final int expectedBytes;
|
||||
|
||||
/// 申明 MIME 类型。
|
||||
final String contentType;
|
||||
|
||||
/// 会话创建时间。
|
||||
final DateTime createdAt;
|
||||
|
||||
/// 会话关联照片的当前状态。
|
||||
final UploadSessionPhoto photo;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'uploadId': uploadId,
|
||||
'albumId': albumId,
|
||||
'clientTaskId': clientTaskId,
|
||||
'state': state.wire,
|
||||
'expectedBytes': expectedBytes,
|
||||
'contentType': contentType,
|
||||
'createdAt': createdAt.toUtc().toIso8601String(),
|
||||
'photo': photo.toJson(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user