Sync migrated iOS modules
This commit is contained in:
@ -0,0 +1,78 @@
|
||||
//
|
||||
// AccountSwitchViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 账号切换 ViewModel,管理可切换账号列表、选择状态和提交状态。
|
||||
final class AccountSwitchViewModel {
|
||||
private(set) var accounts: [AccountSwitchAccount] = []
|
||||
var selectedAccountId: String?
|
||||
private(set) var loading = false
|
||||
private(set) var switching = false
|
||||
private var didLoad = false
|
||||
|
||||
/// 当前选中的账号。
|
||||
var selectedAccount: AccountSwitchAccount? {
|
||||
accounts.first { $0.id == selectedAccountId }
|
||||
}
|
||||
|
||||
/// 拉取可切换账号列表,默认只加载一次。
|
||||
func load(api: ProfileAPI, force: Bool = false, currentAccountId: String? = nil) async throws {
|
||||
guard force || !didLoad else { return }
|
||||
didLoad = true
|
||||
loading = true
|
||||
defer { loading = false }
|
||||
|
||||
let response = try await api.switchableAccounts()
|
||||
accounts = response.accounts.filter { $0.businessUserId > 0 }
|
||||
selectedAccountId = accounts.first(where: { $0.isCurrent || isCurrent($0, currentAccountId: currentAccountId) })?.id
|
||||
?? accounts.first?.id
|
||||
}
|
||||
|
||||
/// 选择指定账号。
|
||||
func select(_ account: AccountSwitchAccount) {
|
||||
selectedAccountId = account.id
|
||||
}
|
||||
|
||||
/// 提交账号切换,调用 set-user 换取正式 token。
|
||||
func switchAccount(_ account: AccountSwitchAccount, api: AuthAPI) async throws -> V9AuthResponse {
|
||||
guard !switching else {
|
||||
throw AccountSwitchError.submitting
|
||||
}
|
||||
guard account.businessUserId > 0 else {
|
||||
throw AccountSwitchError.invalidAccount
|
||||
}
|
||||
|
||||
switching = true
|
||||
defer { switching = false }
|
||||
return try await api.setUser(account.toSetUserRequest())
|
||||
}
|
||||
|
||||
/// 判断账号是否与当前账号标识一致。
|
||||
func isCurrent(_ account: AccountSwitchAccount, currentAccountId: String?) -> Bool {
|
||||
guard let currentAccountId else { return false }
|
||||
return account.id == currentAccountId || String(account.businessUserId) == currentAccountId
|
||||
}
|
||||
}
|
||||
|
||||
/// 账号切换错误实体,表示重复提交或账号数据异常。
|
||||
enum AccountSwitchError: LocalizedError, Equatable {
|
||||
case submitting
|
||||
case invalidAccount
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .submitting:
|
||||
"账号正在切换,请稍候"
|
||||
case .invalidAccount:
|
||||
"账号信息异常,请重新选择账号"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,170 @@
|
||||
//
|
||||
// RealNameAuthViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 实名认证 ViewModel,管理认证资料表单、审核状态、短信验证码和提交状态。
|
||||
final class RealNameAuthViewModel {
|
||||
private(set) var info: RealNameInfo?
|
||||
var realName = ""
|
||||
var idCardNo = ""
|
||||
var smsCode = ""
|
||||
var startDate = Date()
|
||||
var endDate = Calendar.current.date(byAdding: .year, value: 10, to: Date()) ?? Date()
|
||||
var isLongValid = false
|
||||
var frontUrl = ""
|
||||
var backUrl = ""
|
||||
private(set) var loading = false
|
||||
private(set) var sendingCode = false
|
||||
private(set) var submitting = false
|
||||
var statusMessage: String?
|
||||
|
||||
/// 拉取实名认证信息,并回填表单。
|
||||
func load(api: ProfileAPI) async throws {
|
||||
guard !loading else { return }
|
||||
loading = true
|
||||
defer { loading = false }
|
||||
|
||||
let response = try await api.realNameInfo()
|
||||
apply(response.realNameInfo)
|
||||
}
|
||||
|
||||
/// 发送短信验证码。
|
||||
func sendCode(api: ProfileAPI) async throws {
|
||||
guard !sendingCode else { return }
|
||||
sendingCode = true
|
||||
defer { sendingCode = false }
|
||||
|
||||
try await api.realNameSmsVerifyCode()
|
||||
statusMessage = "验证码已发送"
|
||||
}
|
||||
|
||||
/// 提交实名认证资料,成功后刷新审核状态。
|
||||
func submit(api: ProfileAPI) async throws {
|
||||
if info?.verified == true {
|
||||
statusMessage = "已完成实名认证"
|
||||
return
|
||||
}
|
||||
if let validationMessage {
|
||||
throw RealNameValidationError.message(validationMessage)
|
||||
}
|
||||
|
||||
submitting = true
|
||||
defer { submitting = false }
|
||||
try await api.realNameSubmit(makeRequest())
|
||||
statusMessage = "已提交审核"
|
||||
try await load(api: api)
|
||||
}
|
||||
|
||||
/// 根据审核状态生成展示文案。
|
||||
func auditStatusText(_ status: Int) -> String {
|
||||
switch status {
|
||||
case 1: "待审核"
|
||||
case 2: "审核通过"
|
||||
case 3: "审核不通过"
|
||||
default: "未认证"
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前表单校验错误,nil 表示可以提交。
|
||||
var validationMessage: String? {
|
||||
if realName.trimmed.isEmpty { return "请输入真实姓名" }
|
||||
if idCardNo.trimmed.isEmpty { return "请输入身份证号" }
|
||||
if !Self.isValidMainlandIDCardNumber(idCardNo) { return "请输入有效的身份证号" }
|
||||
if info?.verified != true {
|
||||
if smsCode.trimmed.isEmpty { return "请输入短信验证码" }
|
||||
if frontUrl.trimmed.isEmpty { return "请填写身份证人像面图片 URL" }
|
||||
if backUrl.trimmed.isEmpty { return "请填写身份证国徽面图片 URL" }
|
||||
}
|
||||
if !isLongValid && endDate < startDate { return "证件结束日期不能早于起始日期" }
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 应用接口返回的实名认证信息。
|
||||
private func apply(_ info: RealNameInfo?) {
|
||||
self.info = info
|
||||
guard let info else { return }
|
||||
realName = info.realName
|
||||
idCardNo = info.idCardNo
|
||||
frontUrl = info.frontUrl ?? ""
|
||||
backUrl = info.backUrl ?? ""
|
||||
isLongValid = info.isLongValid
|
||||
startDate = Self.date(from: info.startDate) ?? startDate
|
||||
endDate = Self.date(from: info.endDate) ?? endDate
|
||||
}
|
||||
|
||||
/// 生成提交请求。
|
||||
private func makeRequest() -> RealNameAuthRequest {
|
||||
RealNameAuthRequest(
|
||||
realName: realName.trimmed,
|
||||
idCardNo: Self.normalizedIDCardNumber(idCardNo),
|
||||
smsVerifyCode: smsCode.trimmed,
|
||||
startDate: Self.dateFormatter.string(from: startDate),
|
||||
endDate: isLongValid ? "" : Self.dateFormatter.string(from: endDate),
|
||||
isLongValid: isLongValid ? 1 : 0,
|
||||
frontUrl: frontUrl.trimmed,
|
||||
backUrl: backUrl.trimmed
|
||||
)
|
||||
}
|
||||
|
||||
/// 规范化身份证号,去除空白并大写末位 X。
|
||||
nonisolated static func normalizedIDCardNumber(_ value: String) -> String {
|
||||
value.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
}
|
||||
|
||||
/// 校验大陆身份证号基础格式和校验位。
|
||||
nonisolated static func isValidMainlandIDCardNumber(_ value: String) -> Bool {
|
||||
let number = normalizedIDCardNumber(value)
|
||||
guard number.count == 18 else { return false }
|
||||
let chars = Array(number)
|
||||
guard chars.prefix(17).allSatisfy(\.isNumber) else { return false }
|
||||
guard chars[17].isNumber || chars[17] == "X" else { return false }
|
||||
|
||||
let weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
||||
let checkCodes = Array("10X98765432")
|
||||
let sum = zip(chars.prefix(17), weights).reduce(0) { partial, pair in
|
||||
partial + (pair.0.wholeNumberValue ?? 0) * pair.1
|
||||
}
|
||||
return chars[17] == checkCodes[sum % 11]
|
||||
}
|
||||
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter
|
||||
}()
|
||||
|
||||
/// 将 yyyy-MM-dd 文本解析为日期。
|
||||
private static func date(from value: String?) -> Date? {
|
||||
guard let value, !value.isEmpty else { return nil }
|
||||
return dateFormatter.date(from: value)
|
||||
}
|
||||
}
|
||||
|
||||
/// 实名认证校验错误实体,用于把表单错误统一抛给页面展示。
|
||||
enum RealNameValidationError: LocalizedError, Equatable {
|
||||
case message(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .message(let message):
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
/// 去除首尾空白后的文本。
|
||||
var trimmed: String {
|
||||
trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user