将项目管理、排期管理与摄影师邀请流程从占位页迁移为完整实现,包含项目图片 OSS 上传与共享业务模型。 Co-authored-by: Cursor <cursoragent@cursor.com>
441 lines
15 KiB
Swift
441 lines
15 KiB
Swift
//
|
||
// ProjectModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/24.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 项目详情实体,表示摄影师项目或店铺项目详情页需要展示的完整业务字段。
|
||
struct PhotographerProjectDetailResponse: Decodable, Identifiable, Hashable {
|
||
let id: Int
|
||
let scenicId: Int
|
||
let cover: String
|
||
let coverProject: String
|
||
let coverCarousel: [String]
|
||
let name: String
|
||
let type: Int
|
||
let typeName: String
|
||
let status: Int
|
||
let statusName: String
|
||
let price: String
|
||
let otPrice: String
|
||
let priceDeposit: String
|
||
let description: String
|
||
let attrLabel: [String]
|
||
let projectContent: String
|
||
let sold: Int
|
||
let unitName: String
|
||
let materialNum: Int
|
||
let photoNum: Int
|
||
let videoNum: Int
|
||
let createdAt: String
|
||
let updatedAt: String
|
||
let creatorName: String
|
||
let creatorPhone: String
|
||
let creatorRoleName: String
|
||
let auditRemark: String
|
||
let photogList: [ProjectPhotographerBrief]
|
||
let scenicList: [ProjectScenicSpotBrief]
|
||
|
||
/// 字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case scenicId = "scenic_id"
|
||
case cover
|
||
case coverProject = "cover_project"
|
||
case coverCarousel = "cover_carousel"
|
||
case name
|
||
case type
|
||
case typeName = "type_name"
|
||
case status
|
||
case statusName = "status_name"
|
||
case price
|
||
case otPrice = "ot_price"
|
||
case priceDeposit = "price_deposit"
|
||
case description
|
||
case attrLabel = "attr_label"
|
||
case projectContent = "project_content"
|
||
case sold
|
||
case unitName = "unit_name"
|
||
case materialNum = "material_num"
|
||
case photoNum = "photo_num"
|
||
case videoNum = "video_num"
|
||
case createdAt = "created_at"
|
||
case updatedAt = "updated_at"
|
||
case creatorName = "creator_name"
|
||
case creatorPhone = "creator_phone"
|
||
case creatorRoleName = "creator_role_name"
|
||
case auditRemark = "audit_remark"
|
||
case photogList = "photog_list"
|
||
case scenicList = "scenic_list"
|
||
}
|
||
|
||
/// 宽松解码项目详情字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||
scenicId = try container.decodeProjectLossyInt(forKey: .scenicId) ?? 0
|
||
cover = try container.decodeProjectLossyString(forKey: .cover)
|
||
coverProject = try container.decodeProjectLossyString(forKey: .coverProject)
|
||
coverCarousel = try container.decodeProjectLossyStringArray(forKey: .coverCarousel)
|
||
name = try container.decodeProjectLossyString(forKey: .name)
|
||
type = try container.decodeProjectLossyInt(forKey: .type) ?? 0
|
||
typeName = try container.decodeProjectLossyString(forKey: .typeName)
|
||
status = try container.decodeProjectLossyInt(forKey: .status) ?? 0
|
||
statusName = try container.decodeProjectLossyString(forKey: .statusName)
|
||
price = try container.decodeProjectLossyString(forKey: .price)
|
||
otPrice = try container.decodeProjectLossyString(forKey: .otPrice)
|
||
priceDeposit = try container.decodeProjectLossyString(forKey: .priceDeposit)
|
||
description = try container.decodeProjectLossyString(forKey: .description)
|
||
attrLabel = try container.decodeProjectLossyStringArray(forKey: .attrLabel)
|
||
projectContent = try container.decodeProjectLossyString(forKey: .projectContent)
|
||
sold = try container.decodeProjectLossyInt(forKey: .sold) ?? 0
|
||
unitName = try container.decodeProjectLossyString(forKey: .unitName)
|
||
materialNum = try container.decodeProjectLossyInt(forKey: .materialNum) ?? 0
|
||
photoNum = try container.decodeProjectLossyInt(forKey: .photoNum) ?? 0
|
||
videoNum = try container.decodeProjectLossyInt(forKey: .videoNum) ?? 0
|
||
createdAt = try container.decodeProjectLossyString(forKey: .createdAt)
|
||
updatedAt = try container.decodeProjectLossyString(forKey: .updatedAt)
|
||
creatorName = try container.decodeProjectLossyString(forKey: .creatorName)
|
||
creatorPhone = try container.decodeProjectLossyString(forKey: .creatorPhone)
|
||
creatorRoleName = try container.decodeProjectLossyString(forKey: .creatorRoleName)
|
||
auditRemark = try container.decodeProjectLossyString(forKey: .auditRemark)
|
||
photogList = (try? container.decode([ProjectPhotographerBrief].self, forKey: .photogList)) ?? []
|
||
scenicList = (try? container.decode([ProjectScenicSpotBrief].self, forKey: .scenicList)) ?? []
|
||
}
|
||
}
|
||
|
||
/// 项目摄影师摘要实体,表示项目详情中的摄影师信息。
|
||
struct ProjectPhotographerBrief: Decodable, Identifiable, Hashable {
|
||
let id: Int
|
||
let photogUid: Int
|
||
let nickname: String
|
||
let avatar: String
|
||
let orderNum: Int
|
||
let completedOrderCount: Int
|
||
|
||
/// 字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case photogUid = "photog_uid"
|
||
case nickname
|
||
case avatar
|
||
case orderNum = "order_num"
|
||
case completedOrderCount = "completed_order_count"
|
||
}
|
||
|
||
/// 宽松解码摄影师摘要。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||
photogUid = try container.decodeProjectLossyInt(forKey: .photogUid) ?? 0
|
||
nickname = try container.decodeProjectLossyString(forKey: .nickname)
|
||
avatar = try container.decodeProjectLossyString(forKey: .avatar)
|
||
orderNum = try container.decodeProjectLossyInt(forKey: .orderNum) ?? 0
|
||
completedOrderCount = try container.decodeProjectLossyInt(forKey: .completedOrderCount) ?? 0
|
||
}
|
||
}
|
||
|
||
/// 项目打卡点摘要实体,表示项目关联的景点或打卡点。
|
||
struct ProjectScenicSpotBrief: Decodable, Identifiable, Hashable {
|
||
let id: Int
|
||
let name: String
|
||
|
||
/// 字段映射。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case name
|
||
}
|
||
|
||
/// 宽松解码打卡点摘要。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||
name = try container.decodeProjectLossyString(forKey: .name)
|
||
}
|
||
}
|
||
|
||
/// 摄影师项目创建或编辑请求实体。
|
||
struct ProjectCreateRequest: Encodable, Equatable {
|
||
let id: Int?
|
||
let type: Int
|
||
let scenicId: Int
|
||
let name: String
|
||
let price: String
|
||
let otPrice: String?
|
||
let coverProject: String
|
||
let coverCarousel: [String]?
|
||
let description: String
|
||
let attrLabel: [String]?
|
||
let extra: ProjectCreateExtra
|
||
let allowNFC: Int = 1
|
||
|
||
/// 请求字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case type
|
||
case scenicId = "scenic_id"
|
||
case name
|
||
case price
|
||
case otPrice = "ot_price"
|
||
case coverProject = "cover_project"
|
||
case coverCarousel = "cover_carousel"
|
||
case description
|
||
case attrLabel = "attr_label"
|
||
case extra
|
||
case allowNFC = "allow_nfc"
|
||
}
|
||
}
|
||
|
||
/// 摄影师项目扩展配置实体,表示交付数量、押金和关联打卡点/摄影师。
|
||
struct ProjectCreateExtra: Encodable, Equatable {
|
||
let priceDeposit: String
|
||
let materialNum: Int
|
||
let photoNum: Int
|
||
let videoNum: Int
|
||
let scenicSpotId: [Int]
|
||
let photogUid: [Int]
|
||
|
||
/// 请求字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case priceDeposit = "price_deposit"
|
||
case materialNum = "material_num"
|
||
case photoNum = "photo_num"
|
||
case videoNum = "video_num"
|
||
case scenicSpotId = "scenic_spot_id"
|
||
case photogUid = "photog_uid"
|
||
}
|
||
}
|
||
|
||
/// 项目删除请求实体。
|
||
struct ProjectDeleteRequest: Encodable, Equatable {
|
||
let id: Int
|
||
}
|
||
|
||
/// 店铺项目可管理景区实体。
|
||
struct StoreManagerScenicItem: Decodable, Identifiable, Hashable {
|
||
let id: Int
|
||
let name: String
|
||
|
||
/// 字段映射。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case name
|
||
}
|
||
|
||
/// 宽松解码店铺项目景区。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||
name = try container.decodeProjectLossyString(forKey: .name)
|
||
}
|
||
}
|
||
|
||
/// 店铺项目套餐实体,表示多点位项目的套餐价格配置。
|
||
struct StoreManagerPackageItem: Codable, Hashable {
|
||
let materialNum: Int
|
||
let price: String
|
||
|
||
/// 请求字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case materialNum = "material_num"
|
||
case price
|
||
}
|
||
}
|
||
|
||
/// 店铺项目创建请求实体,用于多点位项目。
|
||
struct StoreManagerCreateRequest: Encodable, Equatable {
|
||
let name: String
|
||
let storeId: Int?
|
||
let type: Int
|
||
let description: String
|
||
let coverProject: String?
|
||
let coverCarousel: [String]?
|
||
let projectRule: String?
|
||
let scenicId: [Int]
|
||
let settleSpotNum: Int
|
||
let price: Double
|
||
let priceMaterial: Double
|
||
let pricePhoto: Double
|
||
let priceVideo: Double
|
||
let priceMaterialAll: Double?
|
||
let packageList: [StoreManagerPackageItem]?
|
||
let userId: Int
|
||
let singleSpotMaterialNum: Int
|
||
let singleSpotPhotoNum: Int
|
||
let singleSpotVideoNum: Int
|
||
|
||
/// 请求字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case name
|
||
case storeId = "store_id"
|
||
case type
|
||
case description
|
||
case coverProject = "cover_project"
|
||
case coverCarousel = "cover_carousel"
|
||
case projectRule = "project_rule"
|
||
case scenicId = "scenic_id"
|
||
case settleSpotNum = "settle_spot_num"
|
||
case price
|
||
case priceMaterial = "price_material"
|
||
case pricePhoto = "price_photo"
|
||
case priceVideo = "price_video"
|
||
case priceMaterialAll = "price_material_all"
|
||
case packageList = "package"
|
||
case userId = "user_id"
|
||
case singleSpotMaterialNum = "single_spot_material_num"
|
||
case singleSpotPhotoNum = "single_spot_photo_num"
|
||
case singleSpotVideoNum = "single_spot_video_num"
|
||
}
|
||
}
|
||
|
||
/// 店铺项目更新请求实体,用于多点位项目。
|
||
struct StoreManagerUpdateRequest: Encodable, Equatable {
|
||
let id: Int
|
||
let name: String
|
||
let type: Int
|
||
let description: String
|
||
let coverProject: String?
|
||
let coverCarousel: [String]?
|
||
let projectRule: String?
|
||
let scenicId: [Int]
|
||
let settleSpotNum: Int
|
||
let price: Double
|
||
let priceMaterial: Double
|
||
let pricePhoto: Double
|
||
let priceVideo: Double
|
||
let priceMaterialAll: Double?
|
||
let packageList: [StoreManagerPackageItem]?
|
||
let userId: Int
|
||
let singleSpotMaterialNum: Int
|
||
let singleSpotPhotoNum: Int
|
||
let singleSpotVideoNum: Int
|
||
|
||
/// 请求字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case name
|
||
case type
|
||
case description
|
||
case coverProject = "cover_project"
|
||
case coverCarousel = "cover_carousel"
|
||
case projectRule = "project_rule"
|
||
case scenicId = "scenic_id"
|
||
case settleSpotNum = "settle_spot_num"
|
||
case price
|
||
case priceMaterial = "price_material"
|
||
case pricePhoto = "price_photo"
|
||
case priceVideo = "price_video"
|
||
case priceMaterialAll = "price_material_all"
|
||
case packageList = "package"
|
||
case userId = "user_id"
|
||
case singleSpotMaterialNum = "single_spot_material_num"
|
||
case singleSpotPhotoNum = "single_spot_photo_num"
|
||
case singleSpotVideoNum = "single_spot_video_num"
|
||
}
|
||
}
|
||
|
||
/// 店铺押金项目创建请求实体。
|
||
struct StoreManagerOfflineCreateRequest: Encodable, Equatable {
|
||
let name: String
|
||
let scenicId: Int
|
||
let storeId: Int
|
||
let description: String
|
||
let price: Double
|
||
let coverProject: String
|
||
let coverCarousel: [String]
|
||
|
||
/// 请求字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case name
|
||
case scenicId = "scenic_id"
|
||
case storeId = "store_id"
|
||
case description
|
||
case price
|
||
case coverProject = "cover_project"
|
||
case coverCarousel = "cover_carousel"
|
||
}
|
||
}
|
||
|
||
/// 店铺押金项目更新请求实体。
|
||
struct StoreManagerOfflineUpdateRequest: Encodable, Equatable {
|
||
let id: Int
|
||
let name: String
|
||
let scenicId: Int
|
||
let description: String
|
||
let price: Double
|
||
let coverProject: String?
|
||
let coverCarousel: [String]?
|
||
let storeId: Int?
|
||
|
||
/// 请求字段映射,兼容后端下划线命名。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case name
|
||
case scenicId = "scenic_id"
|
||
case description
|
||
case price
|
||
case coverProject = "cover_project"
|
||
case coverCarousel = "cover_carousel"
|
||
case storeId = "store_id"
|
||
}
|
||
}
|
||
|
||
/// 项目本地待上传图片实体,保存 PhotosPicker 读取后的图片数据。
|
||
struct ProjectLocalImage: Identifiable, Equatable {
|
||
let id = UUID()
|
||
let data: Data
|
||
let fileName: String
|
||
let previewURL: String?
|
||
}
|
||
|
||
private extension KeyedDecodingContainer {
|
||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||
func decodeProjectLossyString(forKey key: Key) throws -> String {
|
||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||
return value
|
||
}
|
||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||
return String(value)
|
||
}
|
||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||
return String(value)
|
||
}
|
||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||
return value ? "1" : "0"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
/// 将 String、Double 和 Int 宽松解码为 Int。
|
||
func decodeProjectLossyInt(forKey key: Key) throws -> Int? {
|
||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||
return value
|
||
}
|
||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||
return Int(value)
|
||
}
|
||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return Int(text) ?? Double(text).map(Int.init)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// 将数组或逗号分隔字符串宽松解码为字符串数组。
|
||
func decodeProjectLossyStringArray(forKey key: Key) throws -> [String] {
|
||
if let values = try? decodeIfPresent([String].self, forKey: key) {
|
||
return values
|
||
}
|
||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||
return value
|
||
.components(separatedBy: CharacterSet(charactersIn: ",,\n"))
|
||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||
.filter { !$0.isEmpty }
|
||
}
|
||
return []
|
||
}
|
||
}
|