Files
2026-06-26 14:33:31 +08:00

231 lines
8.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// RealNameAuthViewModel.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import Foundation
@MainActor
/// ViewModel
final class RealNameAuthViewModel {
var onChange: (() -> Void)?
private(set) var info: RealNameInfo? { didSet { onChange?() } }
var realName = "" { didSet { onChange?() } }
var idCardNo = "" { didSet { onChange?() } }
var smsCode = "" { didSet { onChange?() } }
var startDate = Date() { didSet { onChange?() } }
var endDate = Calendar.current.date(byAdding: .year, value: 10, to: Date()) ?? Date() { didSet { onChange?() } }
var isLongValid = false { didSet { onChange?() } }
var frontUrl = "" { didSet { onChange?() } }
var backUrl = "" { didSet { onChange?() } }
private(set) var pendingFrontImageData: Data? { didSet { onChange?() } }
private(set) var pendingBackImageData: Data? { didSet { onChange?() } }
private(set) var frontUploadProgress: Int? { didSet { onChange?() } }
private(set) var backUploadProgress: Int? { didSet { onChange?() } }
private var pendingFrontFileName: String?
private var pendingBackFileName: String?
private(set) var loading = false { didSet { onChange?() } }
private(set) var sendingCode = false { didSet { onChange?() } }
private(set) var submitting = false { didSet { onChange?() } }
var statusMessage: String? { didSet { onChange?() } }
///
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 prepareIdentityImage(data: Data, side: RealNameImageSide, timestamp: TimeInterval = Date().timeIntervalSince1970) throws {
let processed = try RealNameImageProcessor.process(data: data, side: side, timestamp: timestamp)
switch side {
case .front:
pendingFrontImageData = processed.data
pendingFrontFileName = processed.fileName
frontUrl = ""
case .back:
pendingBackImageData = processed.data
pendingBackFileName = processed.fileName
backUrl = ""
}
}
///
func submit(api: ProfileAPI, uploader: any OSSUploadServing, scenicId: Int) async throws {
if info?.verified == true {
statusMessage = "已完成实名认证"
return
}
if let validationMessage {
throw RealNameValidationError.message(validationMessage)
}
submitting = true
defer { submitting = false }
try await uploadPendingIdentityImages(uploader: uploader, scenicId: scenicId)
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 && pendingFrontImageData == nil { return "请选择身份证人像面图片" }
if backUrl.trimmed.isEmpty && pendingBackImageData == nil { return "请选择身份证国徽面图片" }
}
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 ?? ""
pendingFrontImageData = nil
pendingBackImageData = nil
pendingFrontFileName = nil
pendingBackFileName = nil
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)
}
/// URL
private func uploadPendingIdentityImages(uploader: any OSSUploadServing, scenicId: Int) async throws {
if let pendingFrontImageData {
frontUploadProgress = 1
defer { frontUploadProgress = nil }
frontUrl = try await uploader.uploadRealNameImage(
data: pendingFrontImageData,
fileName: pendingFrontFileName ?? "real_name_front_\(Int(Date().timeIntervalSince1970)).jpg",
scenicId: scenicId
) { [weak self] progress in
Task { @MainActor in
self?.frontUploadProgress = progress
}
}
self.pendingFrontImageData = nil
pendingFrontFileName = nil
}
if let pendingBackImageData {
backUploadProgress = 1
defer { backUploadProgress = nil }
backUrl = try await uploader.uploadRealNameImage(
data: pendingBackImageData,
fileName: pendingBackFileName ?? "real_name_back_\(Int(Date().timeIntervalSince1970)).jpg",
scenicId: scenicId
) { [weak self] progress in
Task { @MainActor in
self?.backUploadProgress = progress
}
}
self.pendingBackImageData = nil
pendingBackFileName = nil
}
}
}
///
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)
}
}