d96e56f653
- upload_tasks 表(accountId 隔离 + 三个复合索引)、状态列枚举转换器、 datetime 文本落库(UTC 归一,亚秒精度) - 纯状态机(task_state.dart):合法迁移表 + canTransition/assertTransition, completed/cancelled 终态 - TaskStore 仓储:create(clientTaskId 同账号同相册应用侧唯一)、 updateState(迁移守护+旁路字段)、账号隔离读路径(硬守护)、 countsByState 徽章、cleanupCandidates/markCleanable(仅终态可清理—— 评审缺口 1 硬守护,未完成源文件永不清理) - 测试 26 例(状态机表驱动全边 + 守护/隔离/筛选/保留策略); sqlite3 3.x build hooks 在 flutter test 宿主与 Android APK 构建均验证 (libsqlite3.so 已打包全 ABI,运行时验证留 Task 11) Co-Authored-By: Claude Code <noreply@anthropic.com>
119 lines
4.7 KiB
Dart
119 lines
4.7 KiB
Dart
/// 本地上传任务状态机(P3 计划 Task 5):纯迁移表,无 drift 依赖。
|
||
///
|
||
/// 状态取值与合法迁移对齐技术实施方案 §5.5 与 P3 计划 Task 5/6:
|
||
/// - 准备:preparing → queued(入队)/ preparationFailed(准备失败,可回
|
||
/// preparing 重试);
|
||
/// - 传输主干:queued → uploading → uploadedConfirming → completed;
|
||
/// - 网络等待与重试:waitingNetwork / retryableFailed 可回 queued(重新
|
||
/// 出队)或直接 uploading(续传/重试);
|
||
/// - 终态:completed / cancelled,不接受任何后续迁移(completed 的源文件
|
||
/// 仅按保留策略清理,见 task_store.dart 的 cleanupCandidates 硬守护)。
|
||
///
|
||
/// 取消语义(方案 §5.5):未开始(preparing/preparationFailed/queued/
|
||
/// waitingNetwork/retryableFailed)→ cancelled 为确定语义;uploading 进行中
|
||
/// → cancelled 为"尽力"(写入标记,实际中断由引擎在下一个检查点兑现)。
|
||
library;
|
||
|
||
/// 上传任务状态(drift 表 `upload_tasks.state` 的取值)。
|
||
///
|
||
/// 注意与服务端会话状态(UploadSessionState:pending/confirmed/...)区分:
|
||
/// 本枚举是客户端本地任务生命周期,二者在 uploadedConfirming 阶段交汇。
|
||
enum UploadTaskState {
|
||
/// 准备中:选图/拍摄后复制源文件到受管目录、空间检查(Task 6)。
|
||
preparing('preparing'),
|
||
|
||
/// 准备失败(独立状态,可重试回 preparing):复制失败/磁盘不足等。
|
||
preparationFailed('preparation_failed'),
|
||
|
||
/// 已入队,等待传输引擎出队。
|
||
queued('queued'),
|
||
|
||
/// 传输中:POST /uploads → presign PUT → complete 的任一环节。
|
||
uploading('uploading'),
|
||
|
||
/// 字节已传完,等待/重试完成核验(POST complete)。
|
||
uploadedConfirming('uploaded_confirming'),
|
||
|
||
/// 终态:核验通过,服务端已受理(cleanup 仅针对此态与 cancelled)。
|
||
completed('completed'),
|
||
|
||
/// 网络不可达,指数退避等待恢复(nextRetryAt 记录下次尝试时间)。
|
||
waitingNetwork('waiting_network'),
|
||
|
||
/// 可重试失败(服务端 5xx/超时等),按退避策略回 queued 重试。
|
||
retryableFailed('retryable_failed'),
|
||
|
||
/// 终态:用户取消(uploading 态下为尽力取消)。
|
||
cancelled('cancelled');
|
||
|
||
const UploadTaskState(this.wire);
|
||
|
||
/// 落库(drift state 列)取值。
|
||
final String wire;
|
||
|
||
/// 解析落库取值;未知值 fail-fast(与 models.dart 的枚举解析约定一致)。
|
||
static UploadTaskState fromWire(String value) => values.firstWhere(
|
||
(v) => v.wire == value,
|
||
orElse: () => throw FormatException('未知 UploadTaskState: $value'),
|
||
);
|
||
|
||
/// 是否终态(不接受任何后续迁移)。
|
||
bool get isTerminal => this == completed || this == cancelled;
|
||
}
|
||
|
||
/// 合法迁移表(键=当前状态,值=可直接迁入的目标集合)。
|
||
const Map<UploadTaskState, Set<UploadTaskState>> kUploadTaskTransitions = {
|
||
UploadTaskState.preparing: {
|
||
UploadTaskState.queued,
|
||
UploadTaskState.preparationFailed,
|
||
UploadTaskState.cancelled,
|
||
},
|
||
UploadTaskState.preparationFailed: {
|
||
UploadTaskState.preparing,
|
||
UploadTaskState.cancelled,
|
||
},
|
||
UploadTaskState.queued: {
|
||
UploadTaskState.uploading,
|
||
UploadTaskState.waitingNetwork,
|
||
UploadTaskState.cancelled,
|
||
},
|
||
UploadTaskState.uploading: {
|
||
UploadTaskState.uploadedConfirming,
|
||
UploadTaskState.waitingNetwork,
|
||
UploadTaskState.retryableFailed,
|
||
// 尽力取消:见库级文档(进行中取消由引擎在检查点兑现)。
|
||
UploadTaskState.cancelled,
|
||
},
|
||
UploadTaskState.uploadedConfirming: {
|
||
UploadTaskState.completed,
|
||
UploadTaskState.retryableFailed,
|
||
UploadTaskState.waitingNetwork,
|
||
},
|
||
UploadTaskState.waitingNetwork: {
|
||
UploadTaskState.queued,
|
||
// 网络恢复直接续传(重试)。
|
||
UploadTaskState.uploading,
|
||
UploadTaskState.cancelled,
|
||
},
|
||
UploadTaskState.retryableFailed: {
|
||
UploadTaskState.queued,
|
||
UploadTaskState.cancelled,
|
||
},
|
||
// 终态:无出边(completed 的清理走 cleanedAt 标记,不是状态迁移)。
|
||
UploadTaskState.completed: {},
|
||
UploadTaskState.cancelled: {},
|
||
};
|
||
|
||
/// `from → to` 是否合法迁移(自迁移一律非法,状态未变无需写库)。
|
||
bool canTransition(UploadTaskState from, UploadTaskState to) =>
|
||
kUploadTaskTransitions[from]!.contains(to);
|
||
|
||
/// 断言 `from → to` 合法,否则抛 [StateError](携带合法目标集,便于排障)。
|
||
void assertTransition(UploadTaskState from, UploadTaskState to) {
|
||
if (!canTransition(from, to)) {
|
||
final legal = kUploadTaskTransitions[from]!;
|
||
final hint = legal.isEmpty ? '(终态,无合法后继)' : ',合法目标:$legal';
|
||
throw StateError('非法上传任务状态迁移:$from → $to$hint');
|
||
}
|
||
}
|