import Foundation /// AI 修图任务筛选分组,对应任务列表接口的 `status_group`。 enum TravelAlbumAIJobFilter: String, CaseIterable, Sendable, Hashable { case all case inProgress = "in_progress" case completed case failed var title: String { switch self { case .all: "全部" case .inProgress: "进行中" case .completed: "已完成" case .failed: "失败" } } } /// AI 修图任务及子任务状态;未知值安全降级,避免新增后端状态导致整页解码失败。 enum TravelAlbumAIJobStatus: Sendable, Hashable { case queued case processing case succeeded case partiallySucceeded case failed case canceled case unknown(String) init(rawValue: String) { switch rawValue { case "queued": self = .queued case "processing": self = .processing case "succeeded": self = .succeeded case "partially_succeeded": self = .partiallySucceeded case "failed": self = .failed case "canceled": self = .canceled default: self = .unknown(rawValue) } } var rawValue: String { switch self { case .queued: "queued" case .processing: "processing" case .succeeded: "succeeded" case .partiallySucceeded: "partially_succeeded" case .failed: "failed" case .canceled: "canceled" case .unknown(let value): value } } var isInProgress: Bool { self == .queued || self == .processing } var isTerminal: Bool { !isInProgress && !isUnknown } private var isUnknown: Bool { if case .unknown = self { true } else { false } } var title: String { switch self { case .queued: "排队中" case .processing: "修图中" case .succeeded: "已完成" case .partiallySucceeded: "部分完成" case .failed: "失败" case .canceled: "已取消" case .unknown: "状态更新中" } } } extension TravelAlbumAIJobStatus: Decodable { init(from decoder: Decoder) throws { self.init(rawValue: (try? decoder.singleValueContainer().decode(String.self)) ?? "") } } /// AI 修图输出类型。 enum TravelAlbumAIJobOutputType: Sendable, Hashable { case refined case atmosphere case cover case unknown(String) init(rawValue: String) { switch rawValue { case "refined": self = .refined case "atmosphere": self = .atmosphere case "cover": self = .cover default: self = .unknown(rawValue) } } var title: String { switch self { case .refined: "原图精修" case .atmosphere: "氛围感" case .cover: "封面" case .unknown: "其他结果" } } var previewKind: TravelAlbumPreviewAssetKind? { switch self { case .refined: .retouched case .atmosphere: .atmosphere case .cover: .cover case .unknown: nil } } } extension TravelAlbumAIJobOutputType: Decodable { init(from decoder: Decoder) throws { self.init(rawValue: (try? decoder.singleValueContainer().decode(String.self)) ?? "") } } /// AI 修图子任务数量进度。 struct TravelAlbumAIJobProgress: Decodable, Sendable, Equatable, Hashable { let total: Int let queued: Int let processing: Int let succeeded: Int let failed: Int let canceled: Int var completed: Int { min(total, succeeded + failed + canceled) } var fraction: Double { total > 0 ? min(1, Double(completed) / Double(total)) : 0 } } /// AI 修图提交成功后返回的任务摘要。 struct TravelAlbumAIJobSubmission: Decodable, Sendable, Equatable { let aiRetouchBatchId: Int let userEquityTravelId: Int let status: TravelAlbumAIJobStatus let progress: TravelAlbumAIJobProgress let createdAt: String enum CodingKeys: String, CodingKey { case aiRetouchBatchId = "ai_retouch_batch_id" case userEquityTravelId = "user_equity_travel_id" case status, progress case createdAt = "created_at" } } /// 任务所属相册摘要。 struct TravelAlbumAIJobAlbum: Decodable, Sendable, Equatable, Hashable { let id: Int let name: String let userPhone: String let coverURL: String enum CodingKeys: String, CodingKey { case id, name case userPhone = "user_phone" case coverURL = "cover_url" } } /// 任务计划输出数量。 struct TravelAlbumAIJobOutput: Decodable, Sendable, Equatable, Hashable { let type: TravelAlbumAIJobOutputType let count: Int } /// 任务列表缩略图。 struct TravelAlbumAIJobPreviewImage: Decodable, Sendable, Equatable, Hashable { let materialId: Int let thumbnailURL: String enum CodingKeys: String, CodingKey { case materialId = "material_id" case thumbnailURL = "thumbnail_url" } } /// AI 修图任务列表项。 struct TravelAlbumAIJobSummary: Decodable, Sendable, Equatable, Hashable, Identifiable { var id: Int { aiRetouchBatchId } let aiRetouchBatchId: Int let userEquityTravelId: Int let scope: String let status: TravelAlbumAIJobStatus let album: TravelAlbumAIJobAlbum let sourceCount: Int let outputs: [TravelAlbumAIJobOutput] let previewImages: [TravelAlbumAIJobPreviewImage] let progress: TravelAlbumAIJobProgress let estimatedFinishAt: String? let failureSummary: String? let createdAt: String let startedAt: String? let finishedAt: String? enum CodingKeys: String, CodingKey { case aiRetouchBatchId = "ai_retouch_batch_id" case userEquityTravelId = "user_equity_travel_id" case scope, status, album, outputs, progress case sourceCount = "source_count" case previewImages = "preview_images" case estimatedFinishAt = "estimated_finish_at" case failureSummary = "failure_summary" case createdAt = "created_at" case startedAt = "started_at" case finishedAt = "finished_at" } var displayFailureSummary: String? { guard status == .failed || status == .partiallySucceeded || progress.failed > 0 else { return nil } let value = failureSummary?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return value.isEmpty ? "部分照片处理失败,点击查看原因" : value } } /// AI 修图任务游标分页响应。 struct TravelAlbumAIJobListResponse: Decodable, Sendable, Equatable { let items: [TravelAlbumAIJobSummary] let nextCursor: String? let hasMore: Bool enum CodingKeys: String, CodingKey { case items case nextCursor = "next_cursor" case hasMore = "has_more" } } /// AI 修图额度结算信息。 struct TravelAlbumAIJobQuotaSettlement: Decodable, Sendable, Equatable { let status: String let reservedUnits: Int let consumedUnits: Int let releasedUnits: Int let coverUnits: Int enum CodingKeys: String, CodingKey { case status case reservedUnits = "reserved_units" case consumedUnits = "consumed_units" case releasedUnits = "released_units" case coverUnits = "cover_units" } } /// AI 修图目标的来源素材。 struct TravelAlbumAIJobSourceMaterial: Decodable, Sendable, Equatable, Hashable { let id: Int let fileName: String let thumbnailURL: String enum CodingKeys: String, CodingKey { case id case fileName = "file_name" case thumbnailURL = "thumbnail_url" } } /// AI 修图目标使用的模板摘要。 struct TravelAlbumAIJobTemplate: Decodable, Sendable, Equatable, Hashable { let id: Int let name: String } /// AI 修图成功结果资源。 struct TravelAlbumAIJobResultAsset: Decodable, Sendable, Equatable, Hashable { let id: Int let materialId: Int let url: String let thumbnailURL: String enum CodingKeys: String, CodingKey { case id case materialId = "material_id" case url case thumbnailURL = "thumbnail_url" } } /// 可直接展示给用户的 AI 修图失败信息。 struct TravelAlbumAIJobError: Decodable, Sendable, Equatable, Hashable { let code: String let message: String let retryable: Bool var displayMessage: String { let value = message.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? "处理失败,请前往相册重新修图" : value } } /// 单个输出目标的处理明细。 struct TravelAlbumAIJobTarget: Decodable, Sendable, Equatable, Hashable, Identifiable { var id: Int { targetId } let targetId: Int let sourceMaterial: TravelAlbumAIJobSourceMaterial? let inputMaterialIds: [Int] let outputType: TravelAlbumAIJobOutputType let template: TravelAlbumAIJobTemplate? let status: TravelAlbumAIJobStatus let resultAsset: TravelAlbumAIJobResultAsset? let error: TravelAlbumAIJobError? let createdAt: String let startedAt: String? let finishedAt: String? enum CodingKeys: String, CodingKey { case targetId = "target_id" case sourceMaterial = "source_material" case inputMaterialIds = "input_material_ids" case outputType = "output_type" case template, status, error case resultAsset = "result_asset" case createdAt = "created_at" case startedAt = "started_at" case finishedAt = "finished_at" } var displayFailureMessage: String? { guard status == .failed else { return nil } return error?.displayMessage ?? "处理失败,请前往相册重新修图" } } /// AI 修图任务完整详情。 struct TravelAlbumAIJobDetail: Decodable, Sendable, Equatable { let aiRetouchBatchId: Int let userEquityTravelId: Int let scope: String let status: TravelAlbumAIJobStatus let album: TravelAlbumAIJobAlbum let sourceCount: Int let outputs: [TravelAlbumAIJobOutput] let progress: TravelAlbumAIJobProgress let quotaSettlement: TravelAlbumAIJobQuotaSettlement let targets: [TravelAlbumAIJobTarget] let estimatedFinishAt: String? let createdAt: String let startedAt: String? let finishedAt: String? let durationSeconds: Int? enum CodingKeys: String, CodingKey { case aiRetouchBatchId = "ai_retouch_batch_id" case userEquityTravelId = "user_equity_travel_id" case scope, status, album, outputs, progress, targets case sourceCount = "source_count" case quotaSettlement = "quota_settlement" case estimatedFinishAt = "estimated_finish_at" case createdAt = "created_at" case startedAt = "started_at" case finishedAt = "finished_at" case durationSeconds = "duration_seconds" } } /// AI 修图任务日期展示工具。 enum TravelAlbumAIJobDateFormatter { private static let internetFormatter = ISO8601DateFormatter() private static let preciseFormatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter }() static func date(_ value: String?) -> Date? { guard let value, !value.isEmpty else { return nil } return preciseFormatter.date(from: value) ?? internetFormatter.date(from: value) } static func display(_ value: String?) -> String { guard let date = date(value) else { return "--" } return date.formatted(.dateTime.month().day().hour().minute()) } static func time(_ value: String?) -> String { guard let date = date(value) else { return "--" } return date.formatted(.dateTime.hour().minute()) } }