为旅拍 OTG 上传增加 client_photo_id,并按服务端已登记 ID 同步本地上传状态。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -5,6 +5,25 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 生成并规范化旅拍照片的客户端唯一标识。
|
||||
enum TravelAlbumClientPhotoID {
|
||||
/// 生成小写、带连字符的 UUID v4 字符串。
|
||||
static func make() -> String {
|
||||
UUID().uuidString.lowercased()
|
||||
}
|
||||
|
||||
/// 规范化服务端或本地保存的客户端照片 ID。
|
||||
static func normalize(_ value: String?) -> String? {
|
||||
guard let normalized = value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased(),
|
||||
!normalized.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
/// OTG 传输照片上传状态,对齐 Android `WiredTransferUploadStatus`。
|
||||
enum TravelAlbumOTGUploadStatus: String, Codable, Equatable, Sendable {
|
||||
case pending = "PENDING"
|
||||
@ -25,6 +44,7 @@ enum TravelAlbumOTGPhotoFormatOption: String, CaseIterable, Equatable, Sendable
|
||||
struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
let id: String
|
||||
let sourceId: String
|
||||
var clientPhotoId: String
|
||||
let fileName: String
|
||||
var localPath: String
|
||||
var thumbnailPath: String
|
||||
@ -42,6 +62,7 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
init(
|
||||
id: String,
|
||||
sourceId: String? = nil,
|
||||
clientPhotoId: String = TravelAlbumClientPhotoID.make(),
|
||||
fileName: String,
|
||||
localPath: String,
|
||||
thumbnailPath: String = "",
|
||||
@ -57,6 +78,7 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
) {
|
||||
self.id = id
|
||||
self.sourceId = sourceId ?? id
|
||||
self.clientPhotoId = TravelAlbumClientPhotoID.normalize(clientPhotoId) ?? TravelAlbumClientPhotoID.make()
|
||||
self.fileName = fileName
|
||||
self.localPath = localPath
|
||||
self.thumbnailPath = thumbnailPath
|
||||
@ -71,6 +93,31 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, sourceId, clientPhotoId, fileName, localPath, thumbnailPath, capturedAt
|
||||
case fileSizeBytes, status, progress, errorMessage, albumId, userId, remoteUrl, updatedAt
|
||||
}
|
||||
|
||||
/// 解码本地索引;旧版本缺少 `clientPhotoId` 时先保留为空,由 Store 一次性迁移并回写。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
sourceId = try container.decodeIfPresent(String.self, forKey: .sourceId) ?? id
|
||||
clientPhotoId = try container.decodeIfPresent(String.self, forKey: .clientPhotoId) ?? ""
|
||||
fileName = try container.decode(String.self, forKey: .fileName)
|
||||
localPath = try container.decode(String.self, forKey: .localPath)
|
||||
thumbnailPath = try container.decodeIfPresent(String.self, forKey: .thumbnailPath) ?? ""
|
||||
capturedAt = try container.decode(String.self, forKey: .capturedAt)
|
||||
fileSizeBytes = try container.decodeIfPresent(Int64.self, forKey: .fileSizeBytes) ?? 0
|
||||
status = try container.decode(TravelAlbumOTGUploadStatus.self, forKey: .status)
|
||||
progress = try container.decodeIfPresent(Int.self, forKey: .progress) ?? 0
|
||||
errorMessage = try container.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
albumId = try container.decode(Int.self, forKey: .albumId)
|
||||
userId = try container.decode(String.self, forKey: .userId)
|
||||
remoteUrl = try container.decodeIfPresent(String.self, forKey: .remoteUrl) ?? ""
|
||||
updatedAt = try container.decodeIfPresent(Int64.self, forKey: .updatedAt) ?? 0
|
||||
}
|
||||
|
||||
/// 把中断中的传输恢复为待上传,避免重进页面卡在上传中。
|
||||
func normalizedAfterInterruptedTransfer() -> TravelAlbumOTGPhotoRecord {
|
||||
guard status == .transferring || status == .uploading else { return self }
|
||||
@ -188,9 +235,17 @@ final class TravelAlbumOTGPhotoStore {
|
||||
let decoded = try? JSONDecoder().decode([TravelAlbumOTGPhotoRecord].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return decoded
|
||||
.filter { $0.albumId == albumId && $0.userId == context.userId }
|
||||
.map { $0.normalizedAfterInterruptedTransfer() }
|
||||
let scoped = decoded.filter { $0.albumId == albumId && $0.userId == context.userId }
|
||||
let migrated = scoped.map { record -> TravelAlbumOTGPhotoRecord in
|
||||
var copy = record
|
||||
copy.clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId)
|
||||
?? TravelAlbumClientPhotoID.make()
|
||||
return copy.normalizedAfterInterruptedTransfer()
|
||||
}
|
||||
if migrated.map(\.clientPhotoId) != scoped.map(\.clientPhotoId) {
|
||||
save(migrated, albumId: albumId)
|
||||
}
|
||||
return migrated
|
||||
.filter { isDisplayable($0, albumId: albumId) }
|
||||
.sorted { $0.capturedAt > $1.capturedAt }
|
||||
}
|
||||
@ -434,6 +489,40 @@ final class TravelAlbumOTGPhotoStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按服务端客户端照片 ID 快照回写本地持久状态。
|
||||
enum TravelAlbumOTGServerStatusPolicy {
|
||||
/// 只处理请求发起时已存在且期间未变化的非活动记录。
|
||||
static func reconcile(
|
||||
records: [TravelAlbumOTGPhotoRecord],
|
||||
baselineRecordsById: [String: TravelAlbumOTGPhotoRecord],
|
||||
activePhotoIds: Set<String>,
|
||||
serverClientPhotoIds: [String],
|
||||
now: Int64
|
||||
) -> [TravelAlbumOTGPhotoRecord] {
|
||||
let serverIds = Set(serverClientPhotoIds.compactMap(TravelAlbumClientPhotoID.normalize))
|
||||
return records.map { record in
|
||||
guard baselineRecordsById[record.id] == record,
|
||||
!activePhotoIds.contains(record.id) else {
|
||||
return record
|
||||
}
|
||||
var copy = record
|
||||
if let clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId),
|
||||
serverIds.contains(clientPhotoId) {
|
||||
copy.status = .uploaded
|
||||
copy.progress = 100
|
||||
copy.errorMessage = nil
|
||||
copy.updatedAt = now
|
||||
} else if record.status == .uploaded {
|
||||
copy.status = .pending
|
||||
copy.progress = 0
|
||||
copy.errorMessage = nil
|
||||
copy.updatedAt = now
|
||||
}
|
||||
return copy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容从 `otg_swift` 同步来的边拍边传写入调用。
|
||||
enum AlbumPhotoStorage {
|
||||
/// 写入原片数据到当前账号与相册隔离目录。
|
||||
|
||||
Reference in New Issue
Block a user