Files
photo-live/apps/mobile/lib/features/uploads/task_state.dart
T
浅念 d96e56f653 feat(mobile): drift upload task store with state machine
- 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>
2026-09-19 02:12:39 +08:00

119 lines
4.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 本地上传任务状态机(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');
}
}