// // 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 [] } }