feat: 优化账号注销流程

This commit is contained in:
2026-08-31 15:56:16 +08:00
parent e529bb5942
commit 4444d328df
16 changed files with 982 additions and 208 deletions
@@ -23,6 +23,8 @@ final class StoreAccountDeregistrationViewModel {
private(set) var submissionAttempted = false
/// 成功响应仅代表申请已提交,不代表账号注销完成。
private(set) var submissionAccepted = false
/// 申请明确受理后,以当前凭证只读查询到的冷静期截止时间;查询失败不改变受理结果。
private(set) var submittedCoolingUntil: String?
private let storeUserID: Int
private let submissionStore: (any StoreAccountDeregistrationSubmissionTracking)?
private var previousCancellationFingerprint: String?
@@ -179,20 +181,42 @@ final class StoreAccountDeregistrationViewModel {
} catch { invalidate(error); return false }
}
/// 校验输入后申请;服务端明确接受即交由页面退出,不再查询状态或推断正式完成。
/// 同步校验验证码与必填原因;失败时不启动查询、持久化或申请请求。
@discardableResult
func validateSubmissionInput(smsCode: String, reason: String) -> Bool {
let code = smsCode.trimmingCharacters(in: .whitespacesAndNewlines)
let reason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
if code.isEmpty {
errorMessage = "请输入收到的短信验证码"
return false
}
if reason.isEmpty {
errorMessage = "请输入注销原因"
return false
}
if reason.count > 50 {
errorMessage = "注销原因不能超过 50 个字"
return false
}
errorMessage = nil
return true
}
/// 校验输入后申请;服务端明确接受后只读查询一次冷静期截止时间,再交由页面退出。
func submit(smsCode: String, reason: String, api: any StoreAccountDeregistrationServing) async {
guard canContinue, step == .verification else { return }
let code = smsCode.trimmingCharacters(in: .whitespacesAndNewlines)
let reason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
guard !code.isEmpty, !reason.isEmpty else {
errorMessage = "请输入收到的短信验证码和注销原因"
return
}
guard validateSubmissionInput(smsCode: code, reason: reason) else { return }
var verifiedEligibility: StoreAccountDeregistrationEligibility?
var verifiedStatus: StoreAccountDeregistrationStatus?
isBusy = true
errorMessage = nil
defer { isBusy = false }
do {
try await checkReady(api: api)
verifiedEligibility = eligibility
verifiedStatus = status
previousCancellationFingerprint = status?.cancellationFingerprint
try submissionStore?.recordSubmissionIntent(previousStatus: status)
submissionAttempted = true
@@ -200,7 +224,21 @@ final class StoreAccountDeregistrationViewModel {
eligibility = nil
try await api.apply(smsCode: code, reason: reason)
submissionAccepted = true
// 申请成功已经成立;状态查询只用于结果页展示,失败或尚未进入冷静期都不能推翻成功响应。
if let acceptedStatus = try? await api.status(), acceptedStatus.isCooling {
status = acceptedStatus
submittedCoolingUntil = acceptedStatus.coolingUntil
}
} catch {
if !submissionAccepted, isVerificationCodeRejection(error) {
submissionStore?.clearRejectedSubmission()
submissionAttempted = false
eligibility = verifiedEligibility
status = verifiedStatus
step = .verification
errorMessage = error.localizedDescription
return
}
// 业务码明确拒绝申请时可重新核验;网络/解码错误不能证明服务端没有收到申请。
if !submissionAccepted, case APIError.serverCode = error {
submissionStore?.clearRejectedSubmission()
@@ -211,6 +249,14 @@ final class StoreAccountDeregistrationViewModel {
}
}
/// 旧接口没有独立错误码,验证码拒绝只能按服务端业务提示识别。
private func isVerificationCodeRejection(_ error: Error) -> Bool {
guard case let APIError.serverCode(_, message) = error else { return false }
return message.localizedCaseInsensitiveContains("验证码")
|| message.localizedCaseInsensitiveContains("verification code")
|| message.localizedCaseInsensitiveContains("sms code")
}
private func checkReady(api: any StoreAccountDeregistrationServing) async throws {
try await reload(api: api)
guard status?.permitsPreparation == true, eligibility?.permitsApplication == true else {