feat: 新增相册自动修图与OTG状态预览

支持相册修图配置、模板选择和传输模式选择;上传后自动提交修图并展示状态角标及精修预览。补充接口文档与相关测试。
This commit is contained in:
2026-08-27 16:03:40 +08:00
parent 9fce6ef713
commit d04641b623
17 changed files with 2523 additions and 26 deletions
@@ -5,6 +5,36 @@
import Foundation
/// 相册级自动 AI 修图配置,由服务端同步并在每次上传开始时固定快照。
struct TravelAlbumAutoRetouchConfiguration: Codable, Sendable, Equatable, Hashable {
let enabled: Bool
let refinedTemplateId: Int?
enum CodingKeys: String, CodingKey {
case enabled
case refinedTemplateId = "refined_template_id"
}
/// 默认关闭自动修图,用于兼容尚未返回配置字段的旧接口。
static let disabled = TravelAlbumAutoRetouchConfiguration(
enabled: false,
refinedTemplateId: nil
)
/// 创建经过规范化的自动修图配置。
init(enabled: Bool, refinedTemplateId: Int?) {
let validTemplateId = refinedTemplateId.flatMap { $0 > 0 ? $0 : nil }
self.enabled = enabled
self.refinedTemplateId = enabled ? validTemplateId : nil
}
/// 上传页紧凑设置项的展示文案。
var displayTitle: String { enabled ? "AI修图" : "不修图" }
/// 当前配置是否可以用于提交自动修图任务。
var isValid: Bool { !enabled || refinedTemplateId != nil }
}
/// 旅拍相册用户信息,对齐 Android `TravelAlbumUserEntity`。
struct TravelAlbumUser: Decodable, Sendable, Equatable, Hashable {
let id: Int
@@ -33,6 +63,7 @@ struct TravelAlbum: Decodable, Sendable, Equatable, Hashable, Identifiable {
let createdAt: String
let updatedAt: String
let user: TravelAlbumUser?
let autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration
enum CodingKeys: String, CodingKey {
case id
@@ -50,6 +81,31 @@ struct TravelAlbum: Decodable, Sendable, Equatable, Hashable, Identifiable {
case createdAt = "created_at"
case updatedAt = "updated_at"
case user
case autoRetouchConfiguration = "auto_retouch_config"
}
/// 解码相册详情;旧响应缺少自动修图配置时按关闭状态兼容。
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
storeUserId = try container.decode(Int.self, forKey: .storeUserId)
name = try container.decode(String.self, forKey: .name)
type = try container.decode(Int.self, forKey: .type)
orderNumber = try container.decode(String.self, forKey: .orderNumber)
materialNum = try container.decode(Int.self, forKey: .materialNum)
materialPrice = try container.decode(Int.self, forKey: .materialPrice)
materialPackagePrice = try container.decode(Int.self, forKey: .materialPackagePrice)
photoPrice = try container.decode(Int.self, forKey: .photoPrice)
coverUrl = try container.decode(String.self, forKey: .coverUrl)
userId = try container.decode(Int.self, forKey: .userId)
status = try container.decode(Int.self, forKey: .status)
createdAt = try container.decode(String.self, forKey: .createdAt)
updatedAt = try container.decode(String.self, forKey: .updatedAt)
user = try container.decodeIfPresent(TravelAlbumUser.self, forKey: .user)
autoRetouchConfiguration = try container.decodeIfPresent(
TravelAlbumAutoRetouchConfiguration.self,
forKey: .autoRetouchConfiguration
) ?? .disabled
}
init(
@@ -67,7 +123,8 @@ struct TravelAlbum: Decodable, Sendable, Equatable, Hashable, Identifiable {
status: Int = 0,
createdAt: String = "",
updatedAt: String = "",
user: TravelAlbumUser? = nil
user: TravelAlbumUser? = nil,
autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration = .disabled
) {
self.id = id
self.storeUserId = storeUserId
@@ -84,6 +141,7 @@ struct TravelAlbum: Decodable, Sendable, Equatable, Hashable, Identifiable {
self.createdAt = createdAt
self.updatedAt = updatedAt
self.user = user
self.autoRetouchConfiguration = autoRetouchConfiguration
}
/// 展示用手机号。
@@ -287,6 +345,7 @@ struct TravelAlbumCreateRequest: Encodable, Sendable, Equatable {
let materialPrice: Double?
let materialPackagePrice: Double?
let photoPrice: Double?
let autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration
enum CodingKeys: String, CodingKey {
case name
@@ -296,6 +355,67 @@ struct TravelAlbumCreateRequest: Encodable, Sendable, Equatable {
case materialPrice = "material_price"
case materialPackagePrice = "material_package_price"
case photoPrice = "photo_price"
case autoRetouchConfiguration = "auto_retouch_config"
}
private enum AutoRetouchConfigurationCodingKeys: String, CodingKey {
case enabled
case refinedTemplateId = "refined_template_id"
}
/// 创建相册请求;自动修图配置默认关闭以兼容现有调用方。
init(
name: String,
type: Int,
orderNumber: String?,
materialNum: Int?,
materialPrice: Double?,
materialPackagePrice: Double?,
photoPrice: Double?,
autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration = .disabled
) {
self.name = name
self.type = type
self.orderNumber = orderNumber
self.materialNum = materialNum
self.materialPrice = materialPrice
self.materialPackagePrice = materialPackagePrice
self.photoPrice = photoPrice
self.autoRetouchConfiguration = autoRetouchConfiguration
}
/// 编码创建参数;相册配置版本由服务端生成,不随创建请求上送。
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(type, forKey: .type)
try container.encodeIfPresent(orderNumber, forKey: .orderNumber)
try container.encodeIfPresent(materialNum, forKey: .materialNum)
try container.encodeIfPresent(materialPrice, forKey: .materialPrice)
try container.encodeIfPresent(materialPackagePrice, forKey: .materialPackagePrice)
try container.encodeIfPresent(photoPrice, forKey: .photoPrice)
var configuration = container.nestedContainer(
keyedBy: AutoRetouchConfigurationCodingKeys.self,
forKey: .autoRetouchConfiguration
)
try configuration.encode(autoRetouchConfiguration.enabled, forKey: .enabled)
try configuration.encodeIfPresent(
autoRetouchConfiguration.refinedTemplateId,
forKey: .refinedTemplateId
)
}
}
/// 更新相册自动修图配置的请求参数。
struct TravelAlbumAutoRetouchConfigurationRequest: Encodable, Sendable, Equatable {
let userEquityTravelId: Int
let enabled: Bool
let refinedTemplateId: Int?
enum CodingKeys: String, CodingKey {
case userEquityTravelId = "user_equity_travel_id"
case enabled
case refinedTemplateId = "refined_template_id"
}
}
@@ -337,6 +457,28 @@ struct TravelAlbumListResponse<Item: Decodable & Sendable & Equatable>: Decodabl
/// 旅拍相册创建响应。
struct TravelAlbumCreateResponse: Decodable, Sendable, Equatable {
let id: Int
let autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration
enum CodingKeys: String, CodingKey {
case id
case autoRetouchConfiguration = "auto_retouch_config"
}
/// 创建相册响应;后端暂未回传配置时按关闭状态兼容。
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
autoRetouchConfiguration = try container.decodeIfPresent(
TravelAlbumAutoRetouchConfiguration.self,
forKey: .autoRetouchConfiguration
) ?? .disabled
}
/// 创建相册响应测试数据。
init(id: Int, autoRetouchConfiguration: TravelAlbumAutoRetouchConfiguration = .disabled) {
self.id = id
self.autoRetouchConfiguration = autoRetouchConfiguration
}
}
/// 旅拍相册小程序码响应。
@@ -496,6 +638,7 @@ struct TravelAlbumAIRetouchRequest: Encodable, Sendable, Equatable {
let refinedTemplateId: Int
let atmosphereTemplateId: Int?
let coverTemplateId: Int?
let clientRequestId: String?
enum CodingKeys: String, CodingKey {
case userEquityTravelId = "user_equity_travel_id"
@@ -503,6 +646,24 @@ struct TravelAlbumAIRetouchRequest: Encodable, Sendable, Equatable {
case refinedTemplateId = "refined_template_id"
case atmosphereTemplateId = "atmosphere_template_id"
case coverTemplateId = "cover_template_id"
case clientRequestId = "client_request_id"
}
/// 创建首次 AI 修图请求;手动修图可不传幂等标识,自动修图必须传稳定标识。
init(
userEquityTravelId: Int,
materialIds: [Int],
refinedTemplateId: Int,
atmosphereTemplateId: Int?,
coverTemplateId: Int?,
clientRequestId: String? = nil
) {
self.userEquityTravelId = userEquityTravelId
self.materialIds = materialIds
self.refinedTemplateId = refinedTemplateId
self.atmosphereTemplateId = atmosphereTemplateId
self.coverTemplateId = coverTemplateId
self.clientRequestId = clientRequestId
}
}