为旅拍 OTG 上传增加 client_photo_id,并按服务端已登记 ID 同步本地上传状态。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-16 19:13:19 +08:00
parent 3d5ad8a614
commit b019e1e494
11 changed files with 323 additions and 115 deletions

View File

@ -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 {
///

View File

@ -44,6 +44,9 @@ final class TravelAlbumOTGUploader: TravelAlbumOTGUploading {
FileManager.default.fileExists(atPath: record.localPath) else {
throw TravelAlbumOTGUploadError.localFileMissing
}
guard let clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId) else {
throw TravelAlbumOTGUploadError.clientPhotoIdMissing
}
let data = try Data(contentsOf: URL(fileURLWithPath: record.localPath))
let remoteURL = try await uploader.uploadTravelAlbumMaterial(
@ -56,7 +59,8 @@ final class TravelAlbumOTGUploader: TravelAlbumOTGUploading {
TravelAlbumUploadMaterialRequest(
userEquityTravelId: record.albumId,
fileName: record.fileName,
fileUrl: remoteURL
fileUrl: remoteURL,
clientPhotoId: clientPhotoId
)
)
}
@ -66,6 +70,7 @@ final class TravelAlbumOTGUploader: TravelAlbumOTGUploading {
enum TravelAlbumOTGUploadError: LocalizedError, Equatable {
case invalidAlbum
case localFileMissing
case clientPhotoIdMissing
var errorDescription: String? {
switch self {
@ -73,6 +78,8 @@ enum TravelAlbumOTGUploadError: LocalizedError, Equatable {
return "请先选择有效的相册"
case .localFileMissing:
return "本地文件不存在,无法上传"
case .clientPhotoIdMissing:
return "照片标识缺失,请重新导入后重试"
}
}
}