新增支付与钱包模块,并接入首页路由

引入真实收款与钱包页面替换首页占位入口,通过 RootView 接入 API,并支持银行卡 OSS 上传及二维码保存到相册。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-22 17:46:01 +08:00
parent d02fd2c069
commit 5d4613f317
23 changed files with 3117 additions and 9 deletions

View File

@ -0,0 +1,604 @@
//
// 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(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
}
}