// // WalletModels.swift // suixinkan // // Created by Codex on 2026/6/22. // import Foundation /// 钱包汇总响应实体,表示累计收益、当前余额和可提现金额。 struct WalletSummaryResponse: Decodable, Equatable { let amountTotal: String let amountCurrentBalance: String let amountWithdrawable: String enum CodingKeys: String, CodingKey { case amountTotal = "amount_total" case amountCurrentBalance = "amount_current_balance" case amountWithdrawable = "amount_withdrawable" } /// 创建钱包汇总响应实体,主要用于测试和空状态。 init(amountTotal: String = "0.00", amountCurrentBalance: String = "0.00", amountWithdrawable: String = "0.00") { self.amountTotal = amountTotal self.amountCurrentBalance = amountCurrentBalance self.amountWithdrawable = amountWithdrawable } /// 自定义解码,兼容金额字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) amountTotal = try container.decodeLossyString(forKey: .amountTotal) amountCurrentBalance = try container.decodeLossyString(forKey: .amountCurrentBalance) amountWithdrawable = try container.decodeLossyString(forKey: .amountWithdrawable) } } /// 钱包收益明细响应实体,表示日分组收益和分页总数。 struct WalletEarningDetailResponse: Decodable, Equatable { let totalAmount: String let totalPoints: Int let total: Int let list: [WalletEarningDetailGroup] enum CodingKeys: String, CodingKey { case totalAmount = "total_amount" case totalPoints = "total_points" case total case list } /// 创建收益明细响应实体,主要用于测试替身。 init(totalAmount: String = "0.00", totalPoints: Int = 0, total: Int = 0, list: [WalletEarningDetailGroup] = []) { self.totalAmount = totalAmount self.totalPoints = totalPoints self.total = total self.list = list } /// 自定义解码,兼容后端字段缺失和类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) totalAmount = try container.decodeLossyString(forKey: .totalAmount) totalPoints = try container.decodeLossyInt(forKey: .totalPoints) ?? 0 total = try container.decodeLossyInt(forKey: .total) ?? 0 list = try container.decodeIfPresent([WalletEarningDetailGroup].self, forKey: .list) ?? [] } } /// 钱包收益日分组实体,表示某一天的收益、积分和明细。 struct WalletEarningDetailGroup: Decodable, Equatable, Identifiable { let date: String let dayAmount: String let dayPoints: Int let items: [WalletEarningDetailItem] var id: String { date } enum CodingKeys: String, CodingKey { case date case dayAmount = "day_amount" case dayPoints = "day_points" case items } /// 创建收益日分组实体,主要用于测试替身。 init(date: String, dayAmount: String = "0.00", dayPoints: Int = 0, items: [WalletEarningDetailItem] = []) { self.date = date self.dayAmount = dayAmount self.dayPoints = dayPoints self.items = items } /// 自定义解码,兼容后端字段缺失和类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) date = try container.decodeLossyString(forKey: .date) dayAmount = try container.decodeLossyString(forKey: .dayAmount) dayPoints = try container.decodeLossyInt(forKey: .dayPoints) ?? 0 items = try container.decodeIfPresent([WalletEarningDetailItem].self, forKey: .items) ?? [] } } /// 钱包收益明细实体,表示一笔订单收益或积分来源。 struct WalletEarningDetailItem: Decodable, Equatable, Identifiable { let id: Int64 let amount: String let points: Int let type: Int let typeLabel: String let orderNumberSuffix: String let createdAt: String let withdrawLabel: String? let source: String? enum CodingKeys: String, CodingKey { case id case amount case points case type case typeLabel = "type_label" case orderNumberSuffix = "order_number_suffix" case createdAt = "created_at" case withdrawLabel = "withdraw_label" case source } /// 创建收益明细实体,主要用于测试替身和本地空状态。 init( id: Int64 = 0, amount: String = "", points: Int = 0, type: Int = 0, typeLabel: String = "", orderNumberSuffix: String = "", createdAt: String = "", withdrawLabel: String? = nil, source: String? = nil ) { self.id = id self.amount = amount self.points = points self.type = type self.typeLabel = typeLabel self.orderNumberSuffix = orderNumberSuffix self.createdAt = createdAt self.withdrawLabel = withdrawLabel self.source = source } /// 自定义解码,兼容字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = Int64(try container.decodeLossyInt(forKey: .id) ?? 0) amount = try container.decodeLossyString(forKey: .amount) points = try container.decodeLossyInt(forKey: .points) ?? 0 type = try container.decodeLossyInt(forKey: .type) ?? 0 typeLabel = try container.decodeLossyString(forKey: .typeLabel) orderNumberSuffix = try container.decodeLossyString(forKey: .orderNumberSuffix) createdAt = try container.decodeLossyString(forKey: .createdAt) withdrawLabel = try container.decodeIfPresent(String.self, forKey: .withdrawLabel) source = try container.decodeIfPresent(String.self, forKey: .source) } } /// 钱包提现记录响应实体,表示提现分页列表。 struct WalletWithdrawListResponse: Decodable, Equatable { let total: Int let item: [WalletWithdrawRecord] enum CodingKeys: String, CodingKey { case total case item } /// 创建提现记录响应实体,主要用于测试替身。 init(total: Int = 0, item: [WalletWithdrawRecord] = []) { self.total = total self.item = item } /// 自定义解码,兼容 total 字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) total = try container.decodeLossyInt(forKey: .total) ?? 0 item = try container.decodeIfPresent([WalletWithdrawRecord].self, forKey: .item) ?? [] } } /// 钱包提现记录实体,表示一笔提现申请。 struct WalletWithdrawRecord: Decodable, Equatable, Identifiable { let id: Int64 let amount: String let createdAt: String let statusLabel: String let expectedAt: String? let auditTime: String? let completedAt: String? enum CodingKeys: String, CodingKey { case id case amount case createdAt = "created_at" case statusLabel = "status_label" case expectedAt = "expected_at" case auditTime = "audit_time" case completedAt = "completed_at" } /// 自定义解码,兼容字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = Int64(try container.decodeLossyInt(forKey: .id) ?? 0) amount = try container.decodeLossyString(forKey: .amount) createdAt = try container.decodeLossyString(forKey: .createdAt) statusLabel = try container.decodeLossyString(forKey: .statusLabel) expectedAt = try container.decodeIfPresent(String.self, forKey: .expectedAt) auditTime = try container.decodeIfPresent(String.self, forKey: .auditTime) completedAt = try container.decodeIfPresent(String.self, forKey: .completedAt) } } /// 银行卡信息响应实体,包裹当前银行卡资料。 struct BankCardInfoResponse: Decodable, Equatable { let bankCard: WalletBankCardInfo? enum CodingKeys: String, CodingKey { case bankCard = "bank_card" } } /// 钱包银行卡实体,表示银行卡资料和审核状态。 struct WalletBankCardInfo: Decodable, Equatable, Identifiable { var id: String { cardNumber } let realName: String let bankName: String let branchName: String let cardNumber: String let frontUrl: String? let backUrl: String? let provinceCode: String? let cityCode: String? let rejectReason: String? let auditStatus: Int let auditStatusLabel: String enum CodingKeys: String, CodingKey { case realName = "real_name" case bankName = "bank_name" case branchName = "branch_name" case cardNumber = "card_number" case frontUrl = "front_url" case backUrl = "back_url" case provinceCode = "province_code" case cityCode = "city_code" case rejectReason = "reject_reason" case auditStatus = "audit_status" case auditStatusLabel = "audit_status_label" } /// 自定义解码,兼容审核状态字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) realName = try container.decodeLossyString(forKey: .realName) bankName = try container.decodeLossyString(forKey: .bankName) branchName = try container.decodeLossyString(forKey: .branchName) cardNumber = try container.decodeLossyString(forKey: .cardNumber) frontUrl = try container.decodeIfPresent(String.self, forKey: .frontUrl) backUrl = try container.decodeIfPresent(String.self, forKey: .backUrl) provinceCode = try container.decodeIfPresent(String.self, forKey: .provinceCode) cityCode = try container.decodeIfPresent(String.self, forKey: .cityCode) rejectReason = try container.decodeIfPresent(String.self, forKey: .rejectReason) auditStatus = try container.decodeLossyInt(forKey: .auditStatus) ?? 0 auditStatusLabel = try container.decodeLossyString(forKey: .auditStatusLabel) } } /// 银行列表响应实体,表示可选择银行名称。 struct BankListResponse: Decodable, Equatable { let banks: [String] enum CodingKeys: String, CodingKey { case banks } /// 创建银行列表响应实体,主要用于测试替身。 init(banks: [String] = []) { self.banks = banks } } /// 省市区节点实体,表示提现银行卡设置中的地区树。 struct AreaNode: Decodable, Equatable, Identifiable { let id: String let code: String let name: String let children: [AreaNode] enum CodingKeys: String, CodingKey { case id case code case name case children } /// 自定义解码,兼容 id/code 类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decodeLossyString(forKey: .code) let rawId = try container.decodeLossyString(forKey: .id).trimmingCharacters(in: .whitespacesAndNewlines) id = rawId.isEmpty ? code : rawId name = try container.decodeLossyString(forKey: .name) children = try container.decodeIfPresent([AreaNode].self, forKey: .children) ?? [] } } /// 提现信息响应实体,表示提现上限、手机号和银行卡摘要。 struct WithdrawInfoResponse: Decodable, Equatable { let amountWithdrawable: String let minWithdrawAmount: String let maxSingleWithdrawAmount: String let maxDailyWithdrawAmount: String let userPhone: String let bankCard: WithdrawBankCardInfo let withdrawInfo: [String] enum CodingKeys: String, CodingKey { case amountWithdrawable = "amount_withdrawable" case minWithdrawAmount = "min_withdraw_amount" case maxSingleWithdrawAmount = "max_single_withdraw_amount" case maxDailyWithdrawAmount = "max_daily_withdraw_amount" case userPhone = "user_phone" case bankCard = "bank_card" case withdrawInfo = "withdraw_info" } /// 自定义解码,兼容金额字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) amountWithdrawable = try container.decodeLossyString(forKey: .amountWithdrawable) minWithdrawAmount = try container.decodeLossyString(forKey: .minWithdrawAmount) maxSingleWithdrawAmount = try container.decodeLossyString(forKey: .maxSingleWithdrawAmount) maxDailyWithdrawAmount = try container.decodeLossyString(forKey: .maxDailyWithdrawAmount) userPhone = try container.decodeLossyString(forKey: .userPhone) bankCard = try container.decodeIfPresent(WithdrawBankCardInfo.self, forKey: .bankCard) ?? WithdrawBankCardInfo() withdrawInfo = try container.decodeIfPresent([String].self, forKey: .withdrawInfo) ?? [] } } /// 提现银行卡摘要实体,表示提现页面展示的银行卡信息。 struct WithdrawBankCardInfo: Decodable, Equatable { let realName: String let bankName: String let cardNumber: String enum CodingKeys: String, CodingKey { case realName = "real_name" case bankName = "bank_name" case cardNumber = "card_number" } /// 创建提现银行卡摘要,主要用于空状态兜底。 init(realName: String = "", bankName: String = "", cardNumber: String = "") { self.realName = realName self.bankName = bankName self.cardNumber = cardNumber } /// 自定义解码,兼容字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) realName = try container.decodeLossyString(forKey: .realName) bankName = try container.decodeLossyString(forKey: .bankName) cardNumber = try container.decodeLossyString(forKey: .cardNumber) } } /// 提现申请请求实体,表示金额和短信验证码。 struct WithdrawApplyRequest: Encodable { let amount: String let smsCode: String enum CodingKeys: String, CodingKey { case amount case smsCode = "sms_code" } } /// 银行卡更新请求实体,表示银行卡资料、证件图片 URL 和验证码。 struct UpdateBankInfoRequest: Encodable, Equatable { let realName: String let cardNumber: String let bankName: String let branchName: String let frontUrl: String let backUrl: String let provinceCode: String let cityCode: String let smsVerifyCode: String enum CodingKeys: String, CodingKey { case realName = "real_name" case cardNumber = "card_number" case bankName = "bank_name" case branchName = "branch_name" case frontUrl = "front_url" case backUrl = "back_url" case provinceCode = "province_code" case cityCode = "city_code" case smsVerifyCode = "sms_verify_code" } } /// 积分概览响应实体,表示当前积分可兑换状态。 struct PointOverviewResponse: Decodable, Equatable { let totalPoints: Int let availablePoints: Int let withdrawnPoints: Int let pendingPoints: Int let time: String enum CodingKeys: String, CodingKey { case totalPoints = "total_points" case availablePoints = "available_points" case withdrawnPoints = "withdrawn_points" case pendingPoints = "pending_points" case time } /// 创建积分概览响应实体,主要用于空状态和测试替身。 init(totalPoints: Int = 0, availablePoints: Int = 0, withdrawnPoints: Int = 0, pendingPoints: Int = 0, time: String = "") { self.totalPoints = totalPoints self.availablePoints = availablePoints self.withdrawnPoints = withdrawnPoints self.pendingPoints = pendingPoints self.time = time } /// 自定义解码,兼容积分字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) totalPoints = try container.decodeLossyInt(forKey: .totalPoints) ?? 0 availablePoints = try container.decodeLossyInt(forKey: .availablePoints) ?? 0 withdrawnPoints = try container.decodeLossyInt(forKey: .withdrawnPoints) ?? 0 pendingPoints = try container.decodeLossyInt(forKey: .pendingPoints) ?? 0 time = try container.decodeLossyString(forKey: .time) } } /// 积分兑换申请请求实体。 struct PointWithdrawApplyRequest: Encodable { let points: Int let remark: String } /// 积分兑换列表请求实体。 struct PointWithdrawListRequest: Encodable { let status: Int? let page: Int let pageSize: Int enum CodingKeys: String, CodingKey { case status case page case pageSize = "page_size" } } /// 积分兑换列表响应实体。 struct PointWithdrawListResponse: Decodable, Equatable { let total: Int let list: [PointWithdrawItem] enum CodingKeys: String, CodingKey { case total case list } /// 创建积分兑换列表响应实体,主要用于测试替身。 init(total: Int = 0, list: [PointWithdrawItem] = []) { self.total = total self.list = list } /// 自定义解码,兼容字段缺失和类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) total = try container.decodeLossyInt(forKey: .total) ?? 0 list = try container.decodeIfPresent([PointWithdrawItem].self, forKey: .list) ?? [] } } /// 积分兑换记录实体。 struct PointWithdrawItem: Decodable, Equatable, Identifiable { let id: Int let points: Int let amount: Double let status: Int let createdAt: String enum CodingKeys: String, CodingKey { case id case points case amount case status case createdAt = "created_at" } /// 自定义解码,兼容字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 points = try container.decodeLossyInt(forKey: .points) ?? 0 amount = try container.decodeLossyDouble(forKey: .amount) ?? 0 status = try container.decodeLossyInt(forKey: .status) ?? 0 createdAt = try container.decodeLossyString(forKey: .createdAt) } } /// 钱包本地路由实体,表示钱包内二级页面。 enum WalletRoute: Hashable, Identifiable { case withdrawApply case bankCardSettings case pointsRedemption case realNameAuth var id: String { switch self { case .withdrawApply: "withdrawApply" case .bankCardSettings: "bankCardSettings" case .pointsRedemption: "pointsRedemption" case .realNameAuth: "realNameAuth" } } } /// 提现资格决策实体,表示点击提现后的下一步页面或提示。 enum WalletWithdrawDecision: Equatable { case route(WalletRoute) case message(String) } /// 钱包筛选区间实体,表示收益明细查询时间范围。 enum WalletDateFilter: String, CaseIterable, Identifiable { case last7 case last30 case last180 var id: String { rawValue } /// 当前筛选区间的展示标题。 var title: String { switch self { case .last7: "近7日" case .last30: "近30日" case .last180: "近180日" } } /// 根据当前日期计算开始和结束日期。 func range(today: Date = Date(), calendar: Calendar = .current) -> (start: String, end: String) { let formatter = DateFormatter() formatter.calendar = calendar formatter.dateFormat = "yyyy-MM-dd" let days: Int switch self { case .last7: days = 6 case .last30: days = 29 case .last180: days = 179 } let startDate = calendar.date(byAdding: .day, value: -days, to: today) ?? today return (formatter.string(from: startDate), formatter.string(from: today)) } } private extension KeyedDecodingContainer { /// 将字符串、数字或空值宽松解码为字符串。 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 value.truncatingRemainder(dividingBy: 1) == 0 ? String(Int(value)) : String(value) } return "" } /// 将字符串或数字宽松解码为整数。 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) } return nil } /// 将字符串或数字宽松解码为小数。 func decodeLossyDouble(forKey key: Key) throws -> Double? { if let value = try decodeIfPresent(Double.self, forKey: key) { return value } if let value = try decodeIfPresent(Int.self, forKey: key) { return Double(value) } if let value = try decodeIfPresent(String.self, forKey: key) { return Double(value.trimmingCharacters(in: .whitespacesAndNewlines)) } return nil } }