新增线下收款记录和 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
@@ -63,7 +63,7 @@ final class WiredCameraTransferViewModel {
private(set) var sonyMTPHint: String?
private(set) var isContentCatalogReady = false
let retouchOption = "不修图"
private(set) var autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration
private(set) var photoFormatOption: TravelAlbumOTGPhotoFormatOption = .jpg
private(set) var transferMode: TravelAlbumOTGTransferMode = .liveUpload {
didSet { notifyStateChanged() }
@@ -80,6 +80,9 @@ final class WiredCameraTransferViewModel {
private let api: any TravelAlbumServing
private let appStore: AppStore
private let userDefaults: UserDefaults
private let autoRetouchConfigurationStore: any TravelAlbumAutoRetouchConfigurationStoring
private let autoRetouchProcessor: any TravelAlbumAutoRetouchProcessing
private let aiEditResultStore: TravelAlbumAIEditResultStore
private var currentDriver: CameraDriver?
private var persistedRecordsById: [String: TravelAlbumOTGPhotoRecord] = [:]
@@ -100,7 +103,10 @@ final class WiredCameraTransferViewModel {
uploader: (any TravelAlbumOTGUploading)? = nil,
api: (any TravelAlbumServing)? = nil,
appStore: AppStore = .shared,
userDefaults: UserDefaults = .standard
userDefaults: UserDefaults = .standard,
autoRetouchConfigurationStore: any TravelAlbumAutoRetouchConfigurationStoring = TravelAlbumAutoRetouchConfigurationStore.shared,
autoRetouchProcessor: (any TravelAlbumAutoRetouchProcessing)? = nil,
aiEditResultStore: TravelAlbumAIEditResultStore? = nil
) {
self.albumId = albumId
self.albumTitle = albumTitle.isEmpty ? "有线传输" : albumTitle
@@ -113,6 +119,10 @@ final class WiredCameraTransferViewModel {
self.api = api ?? NetworkServices.shared.travelAlbumAPI
self.appStore = appStore
self.userDefaults = userDefaults
self.autoRetouchConfigurationStore = autoRetouchConfigurationStore
self.autoRetouchProcessor = autoRetouchProcessor ?? TravelAlbumAutoRetouchProcessor()
self.aiEditResultStore = aiEditResultStore ?? .shared
self.autoRetouchConfiguration = autoRetouchConfigurationStore.configuration(albumID: albumId)
// 从相册管理选择模式进入时,以本次选择为准;其他旧入口继续沿用上次记录。
self.transferMode = initialTransferMode ?? Self.persistedTransferMode(in: userDefaults)
if let initialTransferMode {
@@ -218,6 +228,11 @@ final class WiredCameraTransferViewModel {
transferMode.title
}
/// 自动 AI 修图设置项展示文案。
var retouchOption: String {
autoRetouchConfiguration.uploadOptionTitle
}
/// 指定上传弹窗选项。
var specifyUploadOptions: [TravelAlbumOTGSpecifyUploadOption] {
TravelAlbumOTGSpecifyUploadOption.allCases
@@ -306,6 +321,15 @@ final class WiredCameraTransferViewModel {
userDefaults.set(mode.rawValue, forKey: Self.transferModeDefaultsKey)
}
/// 更新当前相册的自动 AI 修图配置,只影响尚未开始的上传任务。
func updateAutoRetouchConfiguration(_ configuration: TravelAlbumAutoRetouchConfiguration) {
let normalized = configuration.isValid ? configuration : .disabled
guard normalized != autoRetouchConfiguration else { return }
autoRetouchConfiguration = normalized
autoRetouchConfigurationStore.save(normalized, albumID: albumId)
notifyStateChanged()
}
/// 切换上传格式。
func selectPhotoFormat(_ option: TravelAlbumOTGPhotoFormatOption) {
guard photoFormatOption != option else { return }
@@ -467,8 +491,31 @@ final class WiredCameraTransferViewModel {
startUploadByIds([photoId], emptyMessage: "本地文件不存在,无法重传")
}
/// 使用该照片上次失败时的模板重试自动修图,不重复上传原图。
func retryAutoRetouch(photoId: String) {
guard let record = persistedRecordsById[photoId],
record.autoRetouchState == .failed,
let materialId = record.materialId,
let templateID = record.autoRetouchTemplateId,
let preset = TravelAlbumEditPreset.autoRetouchOptions.first(where: { $0.id == templateID }) else {
showMessage("暂无可重试的修图任务")
return
}
Task {
await processAutoRetouch(
photoId: photoId,
sourceRecord: record,
materialId: materialId,
preset: preset
)
}
}
/// 删除单张本地照片。
func deletePhoto(photoId: String) {
if let materialId = persistedRecordsById[photoId]?.materialId {
aiEditResultStore.remove(materialID: materialId)
}
storage.remove(albumId: albumId, photoId: photoId)
persistedRecordsById.removeValue(forKey: photoId)
selectedPhotoIds.remove(photoId)
@@ -478,6 +525,9 @@ final class WiredCameraTransferViewModel {
/// 清空当前相册本地 OTG 缓存。
func clearLocalAlbumCache() {
persistedRecordsById.values.compactMap(\.materialId).forEach { materialId in
aiEditResultStore.remove(materialID: materialId)
}
storage.clearAlbum(albumId: albumId)
persistedRecordsById = [:]
photos = []
@@ -492,9 +542,27 @@ final class WiredCameraTransferViewModel {
private func loadPersistedPhotos() {
let records = storage.load(albumId: albumId)
persistedRecordsById = Dictionary(uniqueKeysWithValues: records.map { ($0.id, $0) })
restorePersistedAutoRetouchResults(records)
applyMergedPhotos()
}
private func restorePersistedAutoRetouchResults(_ records: [TravelAlbumOTGPhotoRecord]) {
records.forEach { record in
guard record.autoRetouchState == .completed,
let materialId = record.materialId,
let templateID = record.autoRetouchTemplateId,
let resultURL = storage.absoluteURL(for: record.retouchedPath, albumId: albumId),
let resultData = try? Data(contentsOf: resultURL) else {
return
}
_ = aiEditResultStore.completeFromImageData(
materialID: materialId,
presetID: templateID,
editedImageData: resultData
)
}
}
private func syncServerUploadStatuses() {
serverStatusSyncTask?.cancel()
guard albumId > 0 else { return }
@@ -593,6 +661,8 @@ final class WiredCameraTransferViewModel {
}
private func uploadPhoto(id: String) async {
// 每张照片开始上传时固定一次配置,避免修图过程中切换模板导致结果不一致。
let retouchConfiguration = autoRetouchConfiguration
do {
let record = try await localRecordForUpload(id: id)
updateRecord(id: id, status: .uploading, progress: max(record.progress, 1), error: nil)
@@ -602,13 +672,89 @@ final class WiredCameraTransferViewModel {
) { [weak self] progress in
self?.updateRecord(id: id, status: .uploading, progress: progress, error: nil)
}
updateRecord(id: id, status: .uploaded, progress: 100, error: nil, remoteUrl: material.fileUrl)
// 上传期间用户可能删除照片,删除后不再回写本地任务或发起修图。
guard persistedRecordsById[id] != nil else { return }
updateRecord(
id: id,
status: retouchConfiguration.template == nil ? .uploaded : .uploading,
progress: 100,
error: nil,
remoteUrl: material.fileUrl,
materialId: material.id
)
guard let preset = retouchConfiguration.template else { return }
await processAutoRetouch(
photoId: id,
sourceRecord: record,
materialId: material.id,
preset: preset
)
} catch {
updateRecord(id: id, status: .failed, progress: 0, error: error.localizedDescription)
showMessage(error.localizedDescription)
}
}
/// 对已完成原图上传的照片执行本地演示修图,修图失败不回退上传成功状态。
private func processAutoRetouch(
photoId: String,
sourceRecord: TravelAlbumOTGPhotoRecord,
materialId: Int,
preset: TravelAlbumEditPreset
) async {
let resolvedRecord = storage.recordWithResolvedFilePaths(sourceRecord, albumId: albumId)
let sourceURL = URL(fileURLWithPath: resolvedRecord.localPath)
updateAutoRetouchRecord(
id: photoId,
state: .processing,
templateId: preset.id,
materialId: materialId,
status: .uploading,
error: nil
)
aiEditResultStore.startProcessing(materialIDs: [materialId])
do {
let resultData = try await autoRetouchProcessor.process(sourceURL: sourceURL, preset: preset)
guard persistedRecordsById[photoId] != nil else {
aiEditResultStore.remove(materialID: materialId)
return
}
let resultURL = try storage.writeRetouchedImage(
resultData,
filename: sourceRecord.fileName,
albumId: albumId
)
guard aiEditResultStore.completeFromImageData(
materialID: materialId,
presetID: preset.id,
editedImageData: resultData
) else {
throw TravelAlbumAutoRetouchError.invalidImageData
}
updateAutoRetouchRecord(
id: photoId,
state: .completed,
templateId: preset.id,
materialId: materialId,
status: .uploaded,
error: nil,
retouchedPath: storage.relativePath(for: resultURL, albumId: albumId)
)
} catch {
aiEditResultStore.failProcessing(materialID: materialId)
updateAutoRetouchRecord(
id: photoId,
state: .failed,
templateId: preset.id,
materialId: materialId,
status: .uploaded,
error: error.localizedDescription
)
showMessage("修图失败,可点击更多重试")
}
}
private func localRecordForUpload(id: String) async throws -> TravelAlbumOTGPhotoRecord {
if let record = persistedRecordsById[id],
!record.localPath.isEmpty,
@@ -623,7 +769,8 @@ final class WiredCameraTransferViewModel {
status: TravelAlbumOTGUploadStatus,
progress: Int,
error: String?,
remoteUrl: String? = nil
remoteUrl: String? = nil,
materialId: Int? = nil
) {
var record = persistedRecordsById[id]
if record == nil, let item = photos.first(where: { $0.id == id }) {
@@ -642,7 +789,10 @@ final class WiredCameraTransferViewModel {
errorMessage: error,
albumId: albumId,
userId: appStore.session.userId,
remoteUrl: remoteUrl ?? item.remoteUrl
remoteUrl: remoteUrl ?? item.remoteUrl,
materialId: materialId,
autoRetouchState: item.autoRetouchState,
autoRetouchTemplateId: item.autoRetouchTemplateId
)
}
guard var record else { return }
@@ -652,6 +802,35 @@ final class WiredCameraTransferViewModel {
if let remoteUrl {
record.remoteUrl = remoteUrl
}
if let materialId {
record.materialId = materialId
}
record.updatedAt = Int64(Date().timeIntervalSince1970 * 1000)
persistedRecordsById[id] = record
storage.upsert(record, albumId: albumId)
applyMergedPhotos()
}
/// 单独更新修图阶段,保留已完成的原图上传信息。
private func updateAutoRetouchRecord(
id: String,
state: TravelAlbumAutoRetouchState,
templateId: String,
materialId: Int,
status: TravelAlbumOTGUploadStatus,
error: String?,
retouchedPath: String? = nil
) {
guard var record = persistedRecordsById[id] else { return }
record.status = status
record.progress = 100
record.errorMessage = error
record.materialId = materialId
record.autoRetouchState = state
record.autoRetouchTemplateId = templateId
if let retouchedPath {
record.retouchedPath = retouchedPath
}
record.updatedAt = Int64(Date().timeIntervalSince1970 * 1000)
persistedRecordsById[id] = record
storage.upsert(record, albumId: albumId)