Introduce real payment collection and wallet screens to replace home menu placeholders, wire APIs through RootView, and support bank card OSS uploads plus QR code saving to the photo library. Co-authored-by: Cursor <cursoragent@cursor.com>
183 lines
6.4 KiB
Swift
183 lines
6.4 KiB
Swift
//
|
||
// PaymentModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/22.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 收款码响应实体,表示静态收款码 URL 和动态收款码基础 URL。
|
||
struct PayCodeResponse: Decodable, Equatable {
|
||
let staticPayUrl: String
|
||
let dynamicPayUrl: String
|
||
|
||
/// 收款码 JSON 字段映射。
|
||
enum CodingKeys: String, CodingKey {
|
||
case staticPayUrl = "static_pay_url"
|
||
case dynamicPayUrl = "dynamic_pay_url"
|
||
}
|
||
|
||
/// 创建收款码响应实体,主要用于测试替身。
|
||
init(staticPayUrl: String = "", dynamicPayUrl: String = "") {
|
||
self.staticPayUrl = staticPayUrl
|
||
self.dynamicPayUrl = dynamicPayUrl
|
||
}
|
||
|
||
/// 自定义解码,兼容后端把 URL 字段返回为数字或空值。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
staticPayUrl = try container.decodeLossyString(forKey: .staticPayUrl)
|
||
dynamicPayUrl = try container.decodeLossyString(forKey: .dynamicPayUrl)
|
||
}
|
||
}
|
||
|
||
/// 收款记录响应实体,包含后端日汇总和原始记录列表。
|
||
struct PaymentCollectionRecordResponse: Decodable, Equatable {
|
||
let analyse: [PaymentCollectionRecordAnalyseItem]
|
||
let list: [PaymentCollectionRecordItem]
|
||
|
||
/// 收款记录响应 JSON 字段映射。
|
||
enum CodingKeys: String, CodingKey {
|
||
case analyse
|
||
case list
|
||
}
|
||
|
||
/// 创建收款记录响应实体,主要用于测试替身。
|
||
init(analyse: [PaymentCollectionRecordAnalyseItem] = [], list: [PaymentCollectionRecordItem] = []) {
|
||
self.analyse = analyse
|
||
self.list = list
|
||
}
|
||
|
||
/// 自定义解码,兼容后端字段缺失。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
analyse = try container.decodeIfPresent([PaymentCollectionRecordAnalyseItem].self, forKey: .analyse) ?? []
|
||
list = try container.decodeIfPresent([PaymentCollectionRecordItem].self, forKey: .list) ?? []
|
||
}
|
||
}
|
||
|
||
/// 收款日汇总实体,表示某一天的收款笔数和金额。
|
||
struct PaymentCollectionRecordAnalyseItem: Decodable, Equatable, Identifiable {
|
||
let date: String
|
||
let orderCount: Int
|
||
let orderAmountSum: String
|
||
|
||
var id: String { date }
|
||
|
||
/// 收款日汇总 JSON 字段映射。
|
||
enum CodingKeys: String, CodingKey {
|
||
case date
|
||
case orderCount = "order_count"
|
||
case orderAmountSum = "order_amount_sum"
|
||
}
|
||
|
||
/// 创建收款日汇总实体,主要用于列表兜底和测试。
|
||
init(date: String, orderCount: Int, orderAmountSum: String) {
|
||
self.date = date
|
||
self.orderCount = orderCount
|
||
self.orderAmountSum = orderAmountSum
|
||
}
|
||
|
||
/// 自定义解码,兼容金额和数量字段类型不稳定。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
date = try container.decodeLossyString(forKey: .date)
|
||
orderCount = try container.decodeLossyInt(forKey: .orderCount) ?? 0
|
||
orderAmountSum = try container.decodeLossyString(forKey: .orderAmountSum)
|
||
}
|
||
}
|
||
|
||
/// 收款记录实体,表示一次扫码收款订单。
|
||
struct PaymentCollectionRecordItem: Decodable, Equatable, Identifiable {
|
||
let orderNumber: String
|
||
let userPhone: String
|
||
let orderAmount: String
|
||
let createDate: String
|
||
let createTime: String
|
||
|
||
var id: String {
|
||
orderNumber.isEmpty ? "\(createDate)-\(createTime)-\(orderAmount)" : orderNumber
|
||
}
|
||
|
||
/// 收款记录 JSON 字段映射。
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case userPhone = "user_phone"
|
||
case orderAmount = "order_amount"
|
||
case createDate = "create_date"
|
||
case createTime = "create_time"
|
||
}
|
||
|
||
/// 创建收款记录实体,主要用于测试替身。
|
||
init(orderNumber: String, userPhone: String, orderAmount: String, createDate: String, createTime: String) {
|
||
self.orderNumber = orderNumber
|
||
self.userPhone = userPhone
|
||
self.orderAmount = orderAmount
|
||
self.createDate = createDate
|
||
self.createTime = createTime
|
||
}
|
||
|
||
/// 自定义解码,兼容订单号、手机号和金额字段类型不稳定。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
orderNumber = try container.decodeLossyString(forKey: .orderNumber)
|
||
userPhone = try container.decodeLossyString(forKey: .userPhone)
|
||
orderAmount = try container.decodeLossyString(forKey: .orderAmount)
|
||
createDate = try container.decodeLossyString(forKey: .createDate)
|
||
createTime = try container.decodeLossyString(forKey: .createTime)
|
||
}
|
||
}
|
||
|
||
/// 收款记录分组实体,表示一个日期下的汇总和明细。
|
||
struct PaymentCollectionRecordGroup: Equatable, Identifiable {
|
||
let analyse: PaymentCollectionRecordAnalyseItem
|
||
let items: [PaymentCollectionRecordItem]
|
||
|
||
var id: String { analyse.id }
|
||
}
|
||
|
||
/// 收款状态实体,表示当前动态收款轮询结果。
|
||
enum PaymentCollectionStatus: Equatable {
|
||
case idle
|
||
case waiting
|
||
case success(PaymentCollectionRecordItem)
|
||
case failed(String)
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|