Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,537 @@
//
// WalletViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import Foundation
@MainActor
/// 便
protocol WalletRealNameServing {
///
func realNameInfo() async throws -> RealNameInfoResponse
}
extension ProfileAPI: WalletRealNameServing {}
/// Tab
enum WalletLedgerTab: String, CaseIterable, Identifiable {
case earnings
case withdraws
var id: String { rawValue }
/// Tab
var title: String {
switch self {
case .earnings: "收益明细"
case .withdraws: "提现记录"
}
}
}
@MainActor
/// ViewModel
final class WalletViewModel {
var onChange: (() -> Void)?
var summary: WalletSummaryResponse? { didSet { onChange?() } }
var pointsOverview: PointOverviewResponse? { didSet { onChange?() } }
var selectedTab: WalletLedgerTab = .earnings { didSet { onChange?() } }
var selectedFilter: WalletDateFilter = .last30 { didSet { onChange?() } }
var earningsGroups: [WalletEarningDetailGroup] = [] { didSet { onChange?() } }
var withdrawRecords: [WalletWithdrawRecord] = [] { didSet { onChange?() } }
var isLoadingSummary = false { didSet { onChange?() } }
var isLoadingList = false { didSet { onChange?() } }
var isCheckingWithdraw = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
private let pageSize = 20
private var earningsPage = 1
private var earningsTotal = 0
private var withdrawPage = 1
private var withdrawTotal = 0
///
var withdrawableText: String {
"¥ \(Self.moneyText(summary?.amountWithdrawable))"
}
///
var totalAmountText: String {
"¥ \(Self.moneyText(summary?.amountTotal))"
}
///
var currentBalanceText: String {
"¥ \(Self.moneyText(summary?.amountCurrentBalance))"
}
///
var canLoadMoreEarnings: Bool {
earningsGroups.flatMap(\.items).count < earningsTotal
}
///
var canLoadMoreWithdraws: Bool {
withdrawRecords.count < withdrawTotal
}
///
func loadInitial(api: WalletServing, staffId: Int?) async {
isLoadingSummary = true
errorMessage = nil
async let summaryResult = api.walletSummary(type: 0)
async let pointsResult: PointOverviewResponse? = {
guard let staffId else { return nil }
return try? await api.pointOverview(staffId: staffId)
}()
do {
summary = try await summaryResult
pointsOverview = await pointsResult
} catch {
errorMessage = error.localizedDescription
}
isLoadingSummary = false
await reloadList(api: api)
}
/// Tab
func reloadList(api: WalletServing) async {
switch selectedTab {
case .earnings:
earningsPage = 1
await loadEarnings(api: api, reset: true)
case .withdraws:
withdrawPage = 1
await loadWithdraws(api: api, reset: true)
}
}
/// Tab
func selectTab(_ tab: WalletLedgerTab, api: WalletServing) async {
guard selectedTab != tab else { return }
selectedTab = tab
await reloadList(api: api)
}
///
func selectFilter(_ filter: WalletDateFilter, api: WalletServing) async {
guard selectedFilter != filter else { return }
selectedFilter = filter
if selectedTab == .earnings {
await reloadList(api: api)
}
}
///
func loadMore(api: WalletServing) async {
switch selectedTab {
case .earnings where canLoadMoreEarnings:
earningsPage += 1
await loadEarnings(api: api, reset: false)
case .withdraws where canLoadMoreWithdraws:
withdrawPage += 1
await loadWithdraws(api: api, reset: false)
default:
break
}
}
///
func resolveWithdrawDecision(profileAPI: WalletRealNameServing, walletAPI: WalletServing) async -> WalletWithdrawDecision {
isCheckingWithdraw = true
defer { isCheckingWithdraw = false }
do {
let realName = try await profileAPI.realNameInfo().realNameInfo
guard let realName else {
return .route(.realNameAuth)
}
switch realName.auditStatus {
case 2:
let bankCard = try await walletAPI.bankCardInfo().bankCard
guard let bankCard else {
return .route(.bankCardSettings)
}
if bankCard.auditStatus == 2 {
return .route(.withdrawApply)
}
if bankCard.auditStatus == 3 {
return .route(.bankCardSettings)
}
return .message("银行卡审核中,请审核通过后再试")
case 3:
return .route(.realNameAuth)
default:
return .message("实名认证审核中,请审核通过后再试")
}
} catch {
return .message(error.localizedDescription)
}
}
///
private func loadEarnings(api: WalletServing, reset: Bool) async {
isLoadingList = true
errorMessage = nil
defer { isLoadingList = false }
let range = selectedFilter.range()
do {
let response = try await api.walletEarningDetail(
startDate: range.start,
endDate: range.end,
page: earningsPage,
pageSize: pageSize
)
earningsTotal = response.total
earningsGroups = reset ? response.list : earningsGroups + response.list
} catch {
if !reset { earningsPage = max(1, earningsPage - 1) }
if reset { earningsGroups = [] }
errorMessage = error.localizedDescription
}
}
///
private func loadWithdraws(api: WalletServing, reset: Bool) async {
isLoadingList = true
errorMessage = nil
defer { isLoadingList = false }
do {
let response = try await api.walletWithdrawList(page: withdrawPage, pageSize: pageSize)
withdrawTotal = response.total
withdrawRecords = reset ? response.item : withdrawRecords + response.item
} catch {
if !reset { withdrawPage = max(1, withdrawPage - 1) }
if reset { withdrawRecords = [] }
errorMessage = error.localizedDescription
}
}
/// 0.00
static func moneyText(_ rawValue: String?) -> String {
let text = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard let decimal = Decimal(string: text), decimal > 0 else {
return "0.00"
}
return NSDecimalNumber(decimal: decimal).stringValue
}
}
@MainActor
/// ViewModel
final class WithdrawApplyViewModel {
var onChange: (() -> Void)?
var info: WithdrawInfoResponse? { didSet { onChange?() } }
var amountText = "" { didSet { onChange?() } }
var smsCode = "" { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isSubmitting = false { didSet { onChange?() } }
var smsCountdown = 0 { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
///
func load(api: WalletServing) async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
info = try await api.withdrawInfo()
} catch {
errorMessage = error.localizedDescription
}
}
///
func sendSms(api: WalletServing) async {
guard smsCountdown == 0 else { return }
do {
try await api.withdrawSendSms()
smsCountdown = 60
} catch {
errorMessage = error.localizedDescription
}
}
///
func submit(api: WalletServing) async -> Bool {
guard !isSubmitting else { return false }
guard let amount = PaymentCollectionViewModel.normalizedMoney(amountText) else {
errorMessage = "请输入有效提现金额"
return false
}
guard let info else {
errorMessage = "提现信息未加载"
return false
}
guard (Decimal(string: amount) ?? 0) <= (Decimal(string: info.amountWithdrawable) ?? 0) else {
errorMessage = "提现金额不能超过可提现金额"
return false
}
guard !smsCode.walletTrimmed.isEmpty else {
errorMessage = "请输入短信验证码"
return false
}
isSubmitting = true
defer { isSubmitting = false }
do {
try await api.withdrawApply(amount: amount, smsCode: smsCode.walletTrimmed)
amountText = ""
smsCode = ""
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
}
@MainActor
/// ViewModel
final class WithdrawalSettingsViewModel {
var onChange: (() -> Void)?
var bankCard: WalletBankCardInfo? { didSet { onChange?() } }
var banks: [String] = [] { didSet { onChange?() } }
var areas: [AreaNode] = [] { didSet { onChange?() } }
var realName = "" { didSet { onChange?() } }
var cardNumber = "" { didSet { onChange?() } }
var bankName = "" { didSet { onChange?() } }
var branchName = "" { didSet { onChange?() } }
var provinceCode = "" { didSet { onChange?() } }
var cityCode = "" { didSet { onChange?() } }
var smsCode = "" { didSet { onChange?() } }
var frontImageData: Data? { didSet { onChange?() } }
var backImageData: Data? { didSet { onChange?() } }
var frontImageURL = "" { didSet { onChange?() } }
var backImageURL = "" { didSet { onChange?() } }
var uploadProgress = 0 { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isSubmitting = false { didSet { onChange?() } }
var smsCountdown = 0 { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
///
func load(api: WalletServing) async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
async let bankCardResult = api.bankCardInfo()
async let bankListResult = api.bankList()
async let areaResult = api.areas()
do {
let (cardResponse, bankResponse, areaResponse) = try await (bankCardResult, bankListResult, areaResult)
bankCard = cardResponse.bankCard
banks = bankResponse.banks
areas = areaResponse
apply(cardResponse.bankCard)
} catch {
errorMessage = error.localizedDescription
}
}
///
func sendSms(api: WalletServing) async {
guard smsCountdown == 0 else { return }
do {
try await api.bankCardVerifyCode()
smsCountdown = 60
} catch {
errorMessage = error.localizedDescription
}
}
///
func submit(api: WalletServing, uploader: OSSUploadServing, scenicId: Int?) async -> Bool {
guard !isSubmitting else { return false }
guard let scenicId else {
errorMessage = "请先选择景区"
return false
}
guard validateForm() else { return false }
isSubmitting = true
uploadProgress = 0
defer { isSubmitting = false }
do {
let frontURL = try await resolvedImageURL(
currentURL: frontImageURL,
data: frontImageData,
fileName: "bank_card_front.jpg",
uploader: uploader,
scenicId: scenicId
)
let backURL = try await resolvedImageURL(
currentURL: backImageURL,
data: backImageData,
fileName: "bank_card_back.jpg",
uploader: uploader,
scenicId: scenicId
)
let request = UpdateBankInfoRequest(
realName: realName.walletTrimmed,
cardNumber: cardNumber.walletTrimmed,
bankName: bankName.walletTrimmed,
branchName: branchName.walletTrimmed,
frontUrl: frontURL,
backUrl: backURL,
provinceCode: provinceCode.walletTrimmed,
cityCode: cityCode.walletTrimmed,
smsVerifyCode: smsCode.walletTrimmed
)
try await api.updateBankInfo(request)
frontImageURL = frontURL
backImageURL = backURL
frontImageData = nil
backImageData = nil
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func apply(_ info: WalletBankCardInfo?) {
guard let info else { return }
realName = info.realName
cardNumber = info.cardNumber
bankName = info.bankName
branchName = info.branchName
provinceCode = info.provinceCode ?? ""
cityCode = info.cityCode ?? ""
frontImageURL = info.frontUrl ?? ""
backImageURL = info.backUrl ?? ""
}
///
private func validateForm() -> Bool {
if realName.walletTrimmed.isEmpty { errorMessage = "请输入持卡人姓名"; return false }
if cardNumber.walletTrimmed.isEmpty { errorMessage = "请输入银行卡号"; return false }
if bankName.walletTrimmed.isEmpty { errorMessage = "请选择开户银行"; return false }
if branchName.walletTrimmed.isEmpty { errorMessage = "请输入开户支行"; return false }
if provinceCode.walletTrimmed.isEmpty || cityCode.walletTrimmed.isEmpty { errorMessage = "请选择开户地区"; return false }
if frontImageURL.walletTrimmed.isEmpty && frontImageData == nil { errorMessage = "请选择银行卡正面照片"; return false }
if backImageURL.walletTrimmed.isEmpty && backImageData == nil { errorMessage = "请选择银行卡反面照片"; return false }
if smsCode.walletTrimmed.isEmpty { errorMessage = "请输入短信验证码"; return false }
return true
}
/// URL OSS
private func resolvedImageURL(
currentURL: String,
data: Data?,
fileName: String,
uploader: OSSUploadServing,
scenicId: Int
) async throws -> String {
guard let data else { return currentURL }
return try await uploader.uploadBankCardImage(data: data, fileName: fileName, scenicId: scenicId) { [weak self] progress in
self?.uploadProgress = progress
}
}
}
@MainActor
/// ViewModel
final class PointsRedemptionViewModel {
var onChange: (() -> Void)?
var overview = PointOverviewResponse() { didSet { onChange?() } }
var records: [PointWithdrawItem] = [] { didSet { onChange?() } }
var pointsText = "" { didSet { onChange?() } }
var isLoading = false { didSet { onChange?() } }
var isSubmitting = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
private let pageSize = 20
private var page = 1
private var total = 0
///
var canLoadMore: Bool {
records.count < total
}
///
func load(api: WalletServing, staffId: Int?) async {
guard let staffId else {
errorMessage = "缺少账号信息"
return
}
isLoading = true
errorMessage = nil
page = 1
defer { isLoading = false }
do {
async let overviewResult = api.pointOverview(staffId: staffId)
async let listResult = api.pointWithdrawList(status: nil, page: page, pageSize: pageSize)
overview = try await overviewResult
let list = try await listResult
total = list.total
records = list.list
} catch {
errorMessage = error.localizedDescription
}
}
///
func loadMore(api: WalletServing) async {
guard canLoadMore else { return }
page += 1
do {
let response = try await api.pointWithdrawList(status: nil, page: page, pageSize: pageSize)
total = response.total
records += response.list
} catch {
page = max(1, page - 1)
errorMessage = error.localizedDescription
}
}
///
func fillAllPoints() {
pointsText = String(overview.withdrawnPoints)
}
///
func submit(api: WalletServing, staffId: Int?) async -> Bool {
guard !isSubmitting else { return false }
guard let points = Int(pointsText.walletTrimmed), points > 0 else {
errorMessage = "请输入有效兑换积分"
return false
}
guard points <= overview.withdrawnPoints else {
errorMessage = "兑换积分不能超过可兑换积分"
return false
}
guard let staffId else {
errorMessage = "缺少账号信息"
return false
}
isSubmitting = true
defer { isSubmitting = false }
do {
try await api.pointWithdrawApply(points: points, remark: "积分提现申请")
pointsText = ""
await load(api: api, staffId: staffId)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
}
private extension String {
/// 使 fileprivate
var walletTrimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
}