新增线下收款记录和 ai 修图优化

This commit is contained in:
han xin
2026-08-21 15:51:52 +08:00
parent 6fe9928b49
commit 5cf4409bad
40 changed files with 5482 additions and 56 deletions
@@ -0,0 +1,248 @@
//
// OfflineCollectionViewModels.swift
// suixinkan
//
import Foundation
/// 收款首页线下收款区域的 ViewModel。
final class OfflineCollectionHomeViewModel {
private(set) var todaySummary: OfflineDailySummary
private(set) var overdueSummaries: [OfflineDailySummary] = []
let context: OfflineCollectionContext
let service: OfflineCollectionMockService
var onStateChange: (() -> Void)?
/// 使用当前账号上下文创建首页 ViewModel。
init(
context: OfflineCollectionContext = .current(),
service: OfflineCollectionMockService = .shared
) {
self.context = context
self.service = service
todaySummary = OfflineDailySummary.make(businessDate: service.todayBusinessDate, records: [])
load()
}
/// 当前今日营业日。
var todayBusinessDate: String { service.todayBusinessDate }
/// 最早一个需要处理的逾期营业日。
var earliestOverdueBusinessDate: String? { overdueSummaries.first?.businessDate }
/// 历史逾期记录的合计笔数。
var overdueRecordCount: Int { overdueSummaries.reduce(0) { $0 + $1.pendingCount } }
/// 历史逾期记录的合计待补缴金额。
var overdueAmountFen: Int { overdueSummaries.reduce(0) { $0 + $1.pendingAmountFen } }
/// 从本地 Mock 服务重新读取今日和逾期汇总。
func load() {
todaySummary = service.getDailySummary(date: todayBusinessDate, context: context)
overdueSummaries = service.getOverdueSummaries(context: context)
onStateChange?()
}
}
/// 线下收款登记页 ViewModel,管理表单校验、提交防重和成功反馈。
final class OfflineCollectionRegistrationViewModel {
private(set) var amountText = ""
private(set) var paymentMethod: OfflineCollectionPaymentMethod? = .wechat
private(set) var isSubmitting = false
private(set) var receipt: OfflineCollectionRegistrationReceipt?
let context: OfflineCollectionContext
let service: OfflineCollectionMockService
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
var onRegistrationSuccess: ((OfflineCollectionRegistrationReceipt) -> Void)?
/// 使用指定收款上下文和 Mock 服务创建登记 ViewModel。
init(
context: OfflineCollectionContext = .current(),
service: OfflineCollectionMockService = .shared
) {
self.context = context
self.service = service
}
/// 当前金额和收款方式是否允许提交。
var canSubmit: Bool {
!isSubmitting && OfflineCollectionMoney.parseFen(amountText) != nil && paymentMethod != nil
}
/// 更新金额输入,仅接受数字、单个小数点和最多两位小数。
func updateAmount(_ value: String) {
guard OfflineCollectionMoney.acceptsEditingText(value) else { return }
amountText = value
receipt = nil
onStateChange?()
}
/// 选择线下收款方式。
func selectPaymentMethod(_ method: OfflineCollectionPaymentMethod) {
paymentMethod = method
receipt = nil
onStateChange?()
}
/// 提交一笔线下收款登记,提交中的重复调用会被忽略。
func submit() async {
guard !isSubmitting else { return }
guard let amountFen = OfflineCollectionMoney.parseFen(amountText) else {
onShowMessage?(OfflineCollectionError.invalidAmount.localizedDescription)
return
}
guard let paymentMethod else {
onShowMessage?("请选择收款方式")
return
}
isSubmitting = true
onStateChange?()
defer {
isSubmitting = false
onStateChange?()
}
do {
let result = try await service.createOfflineCollection(
amountFen: amountFen,
paymentMethod: paymentMethod,
context: context
)
receipt = result
onRegistrationSuccess?(result)
} catch {
onShowMessage?(error.localizedDescription)
}
}
/// 登记成功后开始下一笔,清空金额并保留本次收款方式。
func startAnotherRegistration() {
amountText = ""
receipt = nil
onStateChange?()
}
}
/// 日清页的补缴处理状态。
enum OfflineSettlementPresentationState: Sendable, Equatable {
case idle
case processing
case success(OfflineSettlementBatch)
case failed(String)
}
/// 线下收款日清页 ViewModel,负责单营业日汇总、快照冻结和全额补缴。
final class OfflineCollectionDailyViewModel {
private(set) var businessDate: String
private(set) var summary: OfflineDailySummary
private(set) var records: [OfflineCollectionRecord] = []
private(set) var batches: [OfflineSettlementBatch] = []
private(set) var availableBusinessDates: [String] = []
private(set) var settlementState: OfflineSettlementPresentationState = .idle
private(set) var preparedRequest: OfflineSettlementRequest?
let context: OfflineCollectionContext
let service: OfflineCollectionMockService
var onStateChange: (() -> Void)?
/// 使用指定营业日、收款上下文和 Mock 服务创建日清 ViewModel。
init(
businessDate: String,
context: OfflineCollectionContext = .current(),
service: OfflineCollectionMockService = .shared
) {
self.businessDate = businessDate
self.context = context
self.service = service
summary = OfflineDailySummary.make(businessDate: businessDate, records: [])
load()
}
/// 当前查看的是否为今日营业日。
var isToday: Bool { businessDate == service.todayBusinessDate }
/// 当前是否可以发起补缴。
var canSettle: Bool {
summary.pendingAmountFen > 0 && summary.pendingCount > 0 && settlementState != .processing
}
/// 从本地 Mock 服务加载当前营业日的全部数据。
func load() {
records = service.getDailyRecords(date: businessDate, context: context)
summary = service.getDailySummary(date: businessDate, context: context)
batches = service.getSettlementBatches(date: businessDate, context: context)
availableBusinessDates = service.getAvailableBusinessDates(context: context)
onStateChange?()
}
/// 切换至已存在数据或今日的营业日。
func selectBusinessDate(_ date: String) {
guard availableBusinessDates.contains(date), settlementState != .processing else { return }
businessDate = date
preparedRequest = nil
settlementState = .idle
load()
}
/// 按页面当前待补缴记录冻结一份不可编辑的补缴快照。
func prepareSettlement() throws -> OfflineSettlementRequest {
guard canSettle else { throw OfflineCollectionError.noPendingRecords }
let pendingRecords = records.filter { $0.status == .pending || $0.status == .overdue }
let amountFen = pendingRecords.reduce(0) { $0 + $1.amountFen }
guard pendingRecords.count == summary.pendingCount, amountFen == summary.pendingAmountFen else {
throw OfflineCollectionError.summaryMismatch
}
let request = OfflineSettlementRequest(
businessDate: businessDate,
recordIds: pendingRecords.map(\.id),
amountFen: amountFen,
clientRequestId: "LOCAL-\(UUID().uuidString.uppercased())"
)
preparedRequest = request
return request
}
/// 取消未提交的补缴确认快照。
func cancelPreparedSettlement() {
guard settlementState != .processing else { return }
preparedRequest = nil
}
/// 提交已冻结的补缴快照,失败后保留快照以便原样重试。
func confirmSettlement() async {
guard settlementState != .processing else { return }
guard let request = preparedRequest else {
settlementState = .failed(OfflineCollectionError.noPendingRecords.localizedDescription)
onStateChange?()
return
}
settlementState = .processing
onStateChange?()
do {
let batch = try await service.settleDailyCollections(request: request, context: context)
preparedRequest = nil
settlementState = .success(batch)
load()
} catch {
settlementState = .failed(error.localizedDescription)
load()
}
}
/// 将成功或失败反馈恢复为可操作状态。
func clearSettlementFeedback() {
guard settlementState != .processing else { return }
settlementState = .idle
onStateChange?()
}
/// 开发环境下将下一次 Mock 补缴设为失败。
func simulateNextSettlementFailure() {
service.simulateNextSettlementFailure()
}
}