// // CooperationOrderModels.swift // suixinkan // // Created by Codex on 2026/6/29. // import Foundation extension String { /// 非空时返回自身,否则返回 fallback。 func cooperationNonEmptyOrDefault(_ fallback: String) -> String { let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? fallback : trimmed } /// 获客员展示名:优先备注名,其次主名称,最后回退名称。 func saleUserDisplayName(primaryName: String, fallbackName: String = "") -> String { cooperationNonEmptyOrDefault(primaryName.cooperationNonEmptyOrDefault(fallbackName)) } } /// 带客单状态更新请求体。 struct ReferralOrderStatusUpdateRequest: Encodable { let status: Int } /// 合作获客员备注名更新请求体。 struct SaleUserUpdateRemarkRequest: Encodable { let saleUserId: Int let photogRemarkName: String enum CodingKeys: String, CodingKey { case saleUserId = "sale_user_id" case photogRemarkName = "photog_remark_name" } } /// 合作获客员列表响应。 struct CooperativeSalerListPayload: Decodable { let total: Int let list: [CooperativeSaler] let page: Int let pageSize: Int enum CodingKeys: String, CodingKey { case total case list case page case pageSize = "page_size" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) total = try container.decodeLossyInt(forKey: .total) ?? 0 list = try container.decodeIfPresent([CooperativeSaler].self, forKey: .list) ?? [] page = try container.decodeLossyInt(forKey: .page) ?? 0 pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? 0 } } /// 合作获客员实体。 struct CooperativeSaler: Decodable, Identifiable { let saleUserId: Int let bindingId: Int let name: String let salerName: String let photogRemarkName: String let phone: String let phoneMasked: String let salerPhone: String let shareCode: String let storeId: Int let bindTime: String let boundAt: String var id: Int { saleUserId } var displayId: Int { saleUserId } var displayName: String { photogRemarkName.saleUserDisplayName(primaryName: name, fallbackName: salerName) } var displayPhone: String { salerPhone.cooperationNonEmptyOrDefault(phone) } var displayBindTime: String { bindTime.cooperationNonEmptyOrDefault(boundAt) } enum CodingKeys: String, CodingKey { case saleUserId = "sale_user_id" case bindingId = "binding_id" case name case salerName = "saler_name" case photogRemarkName = "photog_remark_name" case phone case phoneMasked = "phone_masked" case salerPhone = "saler_phone" case shareCode = "share_code" case storeId = "store_id" case bindTime = "bind_time" case boundAt = "bound_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0 bindingId = try container.decodeLossyInt(forKey: .bindingId) ?? 0 name = try container.decodeLossyString(forKey: .name) salerName = try container.decodeLossyString(forKey: .salerName) photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName) phone = try container.decodeLossyString(forKey: .phone) phoneMasked = try container.decodeLossyString(forKey: .phoneMasked) salerPhone = try container.decodeLossyString(forKey: .salerPhone) shareCode = try container.decodeLossyString(forKey: .shareCode) storeId = try container.decodeLossyInt(forKey: .storeId) ?? 0 bindTime = try container.decodeLossyString(forKey: .bindTime) boundAt = try container.decodeLossyString(forKey: .boundAt) } /// 返回更新了备注名的副本,用于本地列表即时刷新。 func withPhotogRemarkName(_ remark: String) -> CooperativeSaler { CooperativeSaler( saleUserId: saleUserId, bindingId: bindingId, name: name, salerName: salerName, photogRemarkName: remark, phone: phone, phoneMasked: phoneMasked, salerPhone: salerPhone, shareCode: shareCode, storeId: storeId, bindTime: bindTime, boundAt: boundAt ) } init( saleUserId: Int, bindingId: Int, name: String, salerName: String, photogRemarkName: String, phone: String, phoneMasked: String, salerPhone: String, shareCode: String, storeId: Int, bindTime: String, boundAt: String ) { self.saleUserId = saleUserId self.bindingId = bindingId self.name = name self.salerName = salerName self.photogRemarkName = photogRemarkName self.phone = phone self.phoneMasked = phoneMasked self.salerPhone = salerPhone self.shareCode = shareCode self.storeId = storeId self.bindTime = bindTime self.boundAt = boundAt } } /// 获客员邀请信息,用于绑定确认页展示。 struct SaleUserInviteInfo: Decodable { let saleUserId: Int let isBound: Bool let photographerName: String let photographerPhone: String let salerName: String let salerPhone: String let shareCode: String let subtitle: String let title: String enum CodingKeys: String, CodingKey { case saleUserId = "sale_user_id" case isBound = "is_bound" case photographerName = "photographer_name" case photographerPhone = "photographer_phone" case salerName = "saler_name" case salerPhone = "saler_phone" case shareCode = "share_code" case subtitle case title } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0 isBound = try container.decodeLossyBool(forKey: .isBound) ?? false photographerName = try container.decodeLossyString(forKey: .photographerName) photographerPhone = try container.decodeLossyString(forKey: .photographerPhone) salerName = try container.decodeLossyString(forKey: .salerName) salerPhone = try container.decodeLossyString(forKey: .salerPhone) shareCode = try container.decodeLossyString(forKey: .shareCode) subtitle = try container.decodeLossyString(forKey: .subtitle) title = try container.decodeLossyString(forKey: .title) } init( saleUserId: Int, isBound: Bool = false, photographerName: String = "", photographerPhone: String = "", salerName: String = "", salerPhone: String = "", shareCode: String = "", subtitle: String = "", title: String = "" ) { self.saleUserId = saleUserId self.isBound = isBound self.photographerName = photographerName self.photographerPhone = photographerPhone self.salerName = salerName self.salerPhone = salerPhone self.shareCode = shareCode self.subtitle = subtitle self.title = title } } /// 获客员线索实体。 struct ReferralLead: Decodable, Identifiable { let id: Int let leadId: Int let userPhone: String let userMobileMasked: String let phone: String let remark: String let createdAt: String let imageUrls: [String] let images: [String] var displayId: String { let value = leadId > 0 ? leadId : id return "\(value)" } var displayPhone: String { userMobileMasked.cooperationNonEmptyOrDefault(userPhone.cooperationNonEmptyOrDefault(phone)) } var displayImages: [String] { imageUrls.isEmpty ? images : imageUrls } enum CodingKeys: String, CodingKey { case id case leadId = "lead_id" case userPhone = "user_phone" case userMobileMasked = "user_mobile_masked" case phone case remark case createdAt = "created_at" case imageUrls = "image_urls" case images } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 leadId = try container.decodeLossyInt(forKey: .leadId) ?? 0 userPhone = try container.decodeLossyString(forKey: .userPhone) userMobileMasked = try container.decodeLossyString(forKey: .userMobileMasked) phone = try container.decodeLossyString(forKey: .phone) remark = try container.decodeLossyString(forKey: .remark) createdAt = try container.decodeLossyString(forKey: .createdAt) imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? [] images = try container.decodeIfPresent([String].self, forKey: .images) ?? [] } } /// 获客订单实体。 struct AcquisitionOrder: Decodable, Identifiable { let id: Int let orderNumber: String let projectName: String let orderType: Int let orderTypeLabel: String let orderAmount: String let amount: String let actualPayAmount: String let createdAt: String let customerPhone: String let userMobileMasked: String let phone: String let orderStatusName: String let status: String let saleUserName: String let photogRemarkName: String let partnerName: String let partnerRole: String let remark: String let imageUrls: [String] var displayAmount: String { orderAmount.cooperationNonEmptyOrDefault(amount.cooperationNonEmptyOrDefault(actualPayAmount)) } var displayCustomerPhone: String { userMobileMasked.cooperationNonEmptyOrDefault(customerPhone.cooperationNonEmptyOrDefault(phone)) } var displayStatus: String { orderStatusName.cooperationNonEmptyOrDefault(status) } var displayPartner: String { let salerDisplayName = photogRemarkName.saleUserDisplayName(primaryName: saleUserName) if !salerDisplayName.isEmpty { return salerDisplayName } if !partnerName.isEmpty { return partnerName } return "" } enum CodingKeys: String, CodingKey { case id case orderNumber = "order_number" case projectName = "project_name" case orderType = "order_type" case orderTypeLabel = "order_type_label" case orderAmount = "order_amount" case amount case actualPayAmount = "actual_pay_amount" case createdAt = "created_at" case customerPhone = "customer_phone" case userMobileMasked = "user_mobile_masked" case phone case orderStatusName = "order_status_name" case status case saleUserName = "sale_user_name" case photogRemarkName = "photog_remark_name" case partnerName = "partner_name" case partnerRole = "partner_role" case remark case imageUrls = "image_urls" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 orderNumber = try container.decodeLossyString(forKey: .orderNumber) projectName = try container.decodeLossyString(forKey: .projectName) orderType = try container.decodeLossyInt(forKey: .orderType) ?? 0 orderTypeLabel = try container.decodeLossyString(forKey: .orderTypeLabel) orderAmount = try container.decodeLossyString(forKey: .orderAmount) amount = try container.decodeLossyString(forKey: .amount) actualPayAmount = try container.decodeLossyString(forKey: .actualPayAmount) createdAt = try container.decodeLossyString(forKey: .createdAt) customerPhone = try container.decodeLossyString(forKey: .customerPhone) userMobileMasked = try container.decodeLossyString(forKey: .userMobileMasked) phone = try container.decodeLossyString(forKey: .phone) orderStatusName = try container.decodeLossyString(forKey: .orderStatusName) status = try container.decodeLossyString(forKey: .status) saleUserName = try container.decodeLossyString(forKey: .saleUserName) photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName) partnerName = try container.decodeLossyString(forKey: .partnerName) partnerRole = try container.decodeLossyString(forKey: .partnerRole) remark = try container.decodeLossyString(forKey: .remark) imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? [] } } /// 带客单列表中的获客员摘要。 struct ReferralOrderSaler: Decodable { let saleUserId: Int let salerName: String let photogRemarkName: String let salerPhone: String var displayName: String { photogRemarkName.saleUserDisplayName(primaryName: salerName) } enum CodingKeys: String, CodingKey { case saleUserId = "sale_user_id" case salerName = "saler_name" case photogRemarkName = "photog_remark_name" case salerPhone = "saler_phone" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0 salerName = try container.decodeLossyString(forKey: .salerName) photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName) salerPhone = try container.decodeLossyString(forKey: .salerPhone) } } /// 可绑定带客单列表响应。 struct ReferralOrderListPayload: Decodable { let total: Int let list: [ReferralOrder] let page: Int let pageSize: Int let saler: ReferralOrderSaler? enum CodingKeys: String, CodingKey { case total case list case page case pageSize = "page_size" case saler } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) total = try container.decodeLossyInt(forKey: .total) ?? 0 list = try container.decodeIfPresent([ReferralOrder].self, forKey: .list) ?? [] page = try container.decodeLossyInt(forKey: .page) ?? 0 pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? 0 saler = try container.decodeIfPresent(ReferralOrderSaler.self, forKey: .saler) } } /// 可绑定带客单实体。 struct ReferralOrder: Decodable, Identifiable { let id: Int let referralOrderNo: String let orderNo: String let userMobile: String let remark: String let createdAt: String let status: Int let statusLabel: String let images: [String] var displayReferralOrderNo: String { referralOrderNo.cooperationNonEmptyOrDefault(orderNo) } enum CodingKeys: String, CodingKey { case id case referralOrderNo = "referral_order_no" case orderNo = "order_no" case userMobile = "user_mobile" case remark case createdAt = "created_at" case status case statusLabel = "status_label" case images } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 referralOrderNo = try container.decodeLossyString(forKey: .referralOrderNo) orderNo = try container.decodeLossyString(forKey: .orderNo) userMobile = try container.decodeLossyString(forKey: .userMobile) remark = try container.decodeLossyString(forKey: .remark) createdAt = try container.decodeLossyString(forKey: .createdAt) status = try container.decodeLossyInt(forKey: .status) ?? 0 statusLabel = try container.decodeLossyString(forKey: .statusLabel) images = try container.decodeIfPresent([String].self, forKey: .images) ?? [] } } /// listv2 已绑定订单来源 referral_order。 struct BoundReferralOrder: Decodable, Equatable, Hashable { let id: Int let orderNo: String let saleUserId: Int let salerName: String let photogRemarkName: String let salerPhone: String let displayText: String let userMobile: String let remark: String let bindOrderNumber: String let sourceType: Int let sourceTypeLabel: String let status: Int let statusLabel: String let createdAt: String var displaySalerName: String { photogRemarkName.saleUserDisplayName(primaryName: salerName) } enum CodingKeys: String, CodingKey { case id case orderNo = "order_no" case saleUserId = "sale_user_id" case salerName = "saler_name" case photogRemarkName = "photog_remark_name" case salerPhone = "saler_phone" case displayText = "display_text" case userMobile = "user_mobile" case remark case bindOrderNumber = "bind_order_number" case sourceType = "source_type" case sourceTypeLabel = "source_type_label" case status case statusLabel = "status_label" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 orderNo = try container.decodeLossyString(forKey: .orderNo) saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0 salerName = try container.decodeLossyString(forKey: .salerName) photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName) salerPhone = try container.decodeLossyString(forKey: .salerPhone) displayText = try container.decodeLossyString(forKey: .displayText) userMobile = try container.decodeLossyString(forKey: .userMobile) remark = try container.decodeLossyString(forKey: .remark) bindOrderNumber = try container.decodeLossyString(forKey: .bindOrderNumber) sourceType = try container.decodeLossyInt(forKey: .sourceType) ?? 0 sourceTypeLabel = try container.decodeLossyString(forKey: .sourceTypeLabel) status = try container.decodeLossyInt(forKey: .status) ?? 0 statusLabel = try container.decodeLossyString(forKey: .statusLabel) createdAt = try container.decodeLossyString(forKey: .createdAt) } } /// 扫码绑定获客员 Sheet 标识。 struct BindAcquirerSheetItem: Identifiable { let saleUserId: Int var id: Int { saleUserId } } /// 合作订单模块内部导航路由。 enum CooperationOrderRoute: Hashable { case acquirer case bind(saleUserId: Int) } private extension KeyedDecodingContainer { /// 将任意常见 JSON 值宽松解码成字符串。 func decodeLossyString(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 "" } /// 将 Int、Double、Bool 或数字字符串宽松解码成整数。 func decodeLossyInt(forKey key: Key) throws -> Int? { if let value = try? decodeIfPresent(Int.self, forKey: key) { return value } if let value = try? decodeIfPresent(String.self, forKey: key) { let text = value.trimmingCharacters(in: .whitespacesAndNewlines) if let intValue = Int(text) { return intValue } if let doubleValue = Double(text) { return Int(doubleValue) } } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) } if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value ? 1 : 0 } return nil } /// 将 Bool、Int 或字符串宽松解码成布尔值。 func decodeLossyBool(forKey key: Key) throws -> Bool? { if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return value != 0 } if let value = try? decodeIfPresent(String.self, forKey: key) { switch value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { case "1", "true", "yes": return true case "0", "false", "no": return false default: return nil } } return nil } }