新增线下收款记录和 ai 修图优化

This commit is contained in:
han xin
2026-08-21 15:51:52 +08:00
parent 6fe9928b49
commit 5cf4409bad
40 changed files with 5482 additions and 56 deletions
@@ -56,6 +56,10 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
let albumId: Int
let userId: String
var remoteUrl: String
var materialId: Int?
var autoRetouchState: TravelAlbumAutoRetouchState
var autoRetouchTemplateId: String?
var retouchedPath: String
var updatedAt: Int64
/// 创建 OTG 本地照片记录。
@@ -74,6 +78,10 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
albumId: Int,
userId: String,
remoteUrl: String = "",
materialId: Int? = nil,
autoRetouchState: TravelAlbumAutoRetouchState = .none,
autoRetouchTemplateId: String? = nil,
retouchedPath: String = "",
updatedAt: Int64 = Int64(Date().timeIntervalSince1970 * 1000)
) {
self.id = id
@@ -90,12 +98,17 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
self.albumId = albumId
self.userId = userId
self.remoteUrl = remoteUrl
self.materialId = materialId
self.autoRetouchState = autoRetouchState
self.autoRetouchTemplateId = autoRetouchTemplateId
self.retouchedPath = retouchedPath
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
case fileSizeBytes, status, progress, errorMessage, albumId, userId, remoteUrl, materialId
case autoRetouchState, autoRetouchTemplateId, retouchedPath, updatedAt
}
/// 解码本地索引;旧版本缺少 `clientPhotoId` 时先保留为空,由 Store 一次性迁移并回写。
@@ -115,16 +128,30 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
albumId = try container.decode(Int.self, forKey: .albumId)
userId = try container.decode(String.self, forKey: .userId)
remoteUrl = try container.decodeIfPresent(String.self, forKey: .remoteUrl) ?? ""
materialId = try container.decodeIfPresent(Int.self, forKey: .materialId)
autoRetouchState = try container.decodeIfPresent(
TravelAlbumAutoRetouchState.self,
forKey: .autoRetouchState
) ?? .none
autoRetouchTemplateId = try container.decodeIfPresent(String.self, forKey: .autoRetouchTemplateId)
retouchedPath = try container.decodeIfPresent(String.self, forKey: .retouchedPath) ?? ""
updatedAt = try container.decodeIfPresent(Int64.self, forKey: .updatedAt) ?? 0
}
/// 把中断中的传输恢复为待上传,避免重进页面卡在上传中。
func normalizedAfterInterruptedTransfer() -> TravelAlbumOTGPhotoRecord {
guard status == .transferring || status == .uploading else { return self }
let interruptedUpload = status == .transferring || status == .uploading
let interruptedRetouch = autoRetouchState == .processing
guard interruptedUpload || interruptedRetouch else { return self }
var copy = self
copy.status = .pending
copy.progress = 0
copy.errorMessage = nil
if interruptedUpload {
copy.status = copy.materialId == nil ? .pending : .uploaded
copy.progress = copy.materialId == nil ? 0 : 100
copy.errorMessage = nil
}
if interruptedRetouch {
copy.autoRetouchState = copy.materialId == nil ? .none : .failed
}
copy.updatedAt = Int64(Date().timeIntervalSince1970 * 1000)
return copy
}
@@ -242,7 +269,7 @@ final class TravelAlbumOTGPhotoStore {
?? TravelAlbumClientPhotoID.make()
return copy.normalizedAfterInterruptedTransfer()
}
if migrated.map(\.clientPhotoId) != scoped.map(\.clientPhotoId) {
if migrated != scoped {
save(migrated, albumId: albumId)
}
return migrated
@@ -333,6 +360,22 @@ final class TravelAlbumOTGPhotoStore {
return url
}
/// 写入自动 AI 修图结果,返回当前相册预览目录中的文件 URL。
func writeRetouchedImage(_ data: Data, filename: String, albumId: Int) throws -> URL {
let directory = try previewsDirectory(albumId: albumId)
let sanitized = PTPHelper.sanitizeFilename(filename)
let stem = (sanitized as NSString).deletingPathExtension
let preferredName = "\(stem)_retouched.jpg"
var candidate = directory.appendingPathComponent(preferredName)
var counter = 1
while fileManager.fileExists(atPath: candidate.path), counter < 10_000 {
candidate = directory.appendingPathComponent("\(stem)_retouched_\(counter).jpg")
counter += 1
}
try data.write(to: candidate, options: .atomic)
return candidate
}
/// 返回写入索引用的相对路径。
func relativePath(for url: URL, albumId: Int) -> String {
let path = url.standardizedFileURL.path
@@ -382,6 +425,9 @@ final class TravelAlbumOTGPhotoStore {
if let thumbnailURL = absoluteURL(for: record.thumbnailPath, albumId: albumId) {
resolved.thumbnailPath = thumbnailURL.path
}
if let retouchedURL = absoluteURL(for: record.retouchedPath, albumId: albumId) {
resolved.retouchedPath = retouchedURL.path
}
return resolved
}
@@ -433,7 +479,7 @@ final class TravelAlbumOTGPhotoStore {
}
private func deleteFiles(_ record: TravelAlbumOTGPhotoRecord) {
[record.localPath, record.thumbnailPath].forEach { path in
[record.localPath, record.thumbnailPath, record.retouchedPath].forEach { path in
guard !path.isEmpty else { return }
guard let url = absoluteURL(for: path, albumId: record.albumId) else { return }
try? fileManager.removeItem(at: url)
@@ -463,6 +509,11 @@ final class TravelAlbumOTGPhotoStore {
? relativePath(for: URL(fileURLWithPath: record.thumbnailPath), albumId: albumId)
: record.thumbnailPath
}
if !record.retouchedPath.isEmpty {
normalized.retouchedPath = record.retouchedPath.hasPrefix("/")
? relativePath(for: URL(fileURLWithPath: record.retouchedPath), albumId: albumId)
: record.retouchedPath
}
return normalized
}