199 lines
7.3 KiB
Swift
199 lines
7.3 KiB
Swift
//
|
|
// AccountDeletionMockService.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// 注销服务协议,隔离前置核验、短信验证、提交和取消能力。
|
|
protocol AccountDeletionServing: AnyObject {
|
|
/// 加载整个登录账号的注销资产快照。
|
|
func loadPrecheck(username: String) async throws -> AccountDeletionPrecheck
|
|
|
|
/// 向当前绑定手机号发送注销验证码。
|
|
func sendVerificationCode(to username: String) async throws
|
|
|
|
/// 提交注销申请并开始7天冷静期。
|
|
func submitDeletion(
|
|
username: String,
|
|
verificationCode: String,
|
|
acknowledgedAssetKinds: Set<AccountDeletionAssetSummary.Kind>,
|
|
clientRequestID: UUID
|
|
) async throws -> AccountDeletionRequest
|
|
|
|
/// 查询指定登录账号在登录时应处理的注销状态。
|
|
func loginState(for username: String) -> AccountDeletionLoginState
|
|
|
|
/// 在7天冷静期内取消注销申请。
|
|
func cancelDeletion(username: String) throws -> AccountDeletionRequest
|
|
}
|
|
|
|
/// 注销功能 Mock 服务,使用 UserDefaults 完成可重复演示的7天注销闭环。
|
|
final class AccountDeletionMockService: AccountDeletionServing {
|
|
|
|
/// 应用内共享的注销 Mock 服务。
|
|
static let shared = AccountDeletionMockService()
|
|
|
|
/// Mock 短信验证码。
|
|
static let verificationCode = "123456"
|
|
|
|
private let defaults: UserDefaults
|
|
private let now: () -> Date
|
|
private let responseDelayNanoseconds: UInt64
|
|
private let encoder = JSONEncoder()
|
|
private let decoder = JSONDecoder()
|
|
private let keyPrefix = "account_deletion_mock_v1_"
|
|
|
|
/// 创建注销 Mock 服务,测试可注入独立存储、时间和响应延迟。
|
|
init(
|
|
defaults: UserDefaults = .standard,
|
|
now: @escaping () -> Date = Date.init,
|
|
responseDelayNanoseconds: UInt64 = 180_000_000
|
|
) {
|
|
self.defaults = defaults
|
|
self.now = now
|
|
self.responseDelayNanoseconds = responseDelayNanoseconds
|
|
}
|
|
|
|
func loadPrecheck(username: String) async throws -> AccountDeletionPrecheck {
|
|
try await simulateDelay()
|
|
_ = try validateUsername(username)
|
|
return AccountDeletionPrecheck(
|
|
assets: [
|
|
AccountDeletionAssetSummary(kind: .wallet, title: "钱包余额", valueText: "¥286.50"),
|
|
AccountDeletionAssetSummary(kind: .works, title: "作品与相册", valueText: "36个"),
|
|
AccountDeletionAssetSummary(kind: .projects, title: "项目", valueText: "4个"),
|
|
AccountDeletionAssetSummary(kind: .cloudFiles, title: "云盘文件", valueText: "8.6 GB"),
|
|
],
|
|
consequences: [
|
|
"所有景区与门店账号将解除",
|
|
"个人作品和云盘文件将删除",
|
|
"提交后7天内再次登录可取消注销",
|
|
]
|
|
)
|
|
}
|
|
|
|
func sendVerificationCode(to username: String) async throws {
|
|
try await simulateDelay()
|
|
_ = try validateUsername(username)
|
|
}
|
|
|
|
func submitDeletion(
|
|
username: String,
|
|
verificationCode: String,
|
|
acknowledgedAssetKinds: Set<AccountDeletionAssetSummary.Kind>,
|
|
clientRequestID: UUID
|
|
) async throws -> AccountDeletionRequest {
|
|
try await simulateDelay()
|
|
let normalized = try validateUsername(username)
|
|
guard verificationCode == Self.verificationCode else {
|
|
throw AccountDeletionError.invalidVerificationCode
|
|
}
|
|
guard acknowledgedAssetKinds == Set(AccountDeletionAssetSummary.Kind.allCases) else {
|
|
throw AccountDeletionError.incompleteAcknowledgement
|
|
}
|
|
|
|
if let existing = storedRequest(for: normalized), existing.status == .pending {
|
|
return existing
|
|
}
|
|
if let existing = storedRequest(for: normalized), existing.status == .completed {
|
|
throw AccountDeletionError.deletionCompleted
|
|
}
|
|
|
|
let submittedAt = now()
|
|
guard let scheduledDeletionAt = Calendar(identifier: .gregorian)
|
|
.date(byAdding: .day, value: 7, to: submittedAt) else {
|
|
throw AccountDeletionError.persistenceFailed
|
|
}
|
|
let request = AccountDeletionRequest(
|
|
id: UUID(),
|
|
clientRequestID: clientRequestID,
|
|
username: normalized,
|
|
submittedAt: submittedAt,
|
|
scheduledDeletionAt: scheduledDeletionAt,
|
|
status: .pending,
|
|
acknowledgedAssetKinds: AccountDeletionAssetSummary.Kind.allCases
|
|
)
|
|
try save(request)
|
|
return request
|
|
}
|
|
|
|
func loginState(for username: String) -> AccountDeletionLoginState {
|
|
let normalized = AccountDeletionIdentity.normalizedUsername(username)
|
|
guard var request = storedRequest(for: normalized) else { return .none }
|
|
|
|
switch request.status {
|
|
case .canceled:
|
|
return .none
|
|
case .completed:
|
|
return .completed(request)
|
|
case .pending:
|
|
guard now() < request.scheduledDeletionAt else {
|
|
request.status = .completed
|
|
try? save(request)
|
|
return .completed(request)
|
|
}
|
|
return .pending(request)
|
|
}
|
|
}
|
|
|
|
func cancelDeletion(username: String) throws -> AccountDeletionRequest {
|
|
let normalized = try validateUsername(username)
|
|
guard var request = storedRequest(for: normalized) else {
|
|
throw AccountDeletionError.noPendingRequest
|
|
}
|
|
guard request.status == .pending else {
|
|
if request.status == .completed {
|
|
throw AccountDeletionError.cancellationExpired
|
|
}
|
|
throw AccountDeletionError.noPendingRequest
|
|
}
|
|
guard now() < request.scheduledDeletionAt else {
|
|
request.status = .completed
|
|
try save(request)
|
|
throw AccountDeletionError.cancellationExpired
|
|
}
|
|
|
|
request.status = .canceled
|
|
try save(request)
|
|
return request
|
|
}
|
|
|
|
/// 删除指定账号的 Mock 注销状态,仅供单元测试与调试使用。
|
|
func reset(username: String) {
|
|
let normalized = AccountDeletionIdentity.normalizedUsername(username)
|
|
defaults.removeObject(forKey: storageKey(username: normalized))
|
|
}
|
|
|
|
private func validateUsername(_ username: String) throws -> String {
|
|
let normalized = AccountDeletionIdentity.normalizedUsername(username)
|
|
guard normalized.count == 11, normalized.first == "1" else {
|
|
throw AccountDeletionError.missingIdentity
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
private func storedRequest(for username: String) -> AccountDeletionRequest? {
|
|
guard let data = defaults.data(forKey: storageKey(username: username)) else { return nil }
|
|
return try? decoder.decode(AccountDeletionRequest.self, from: data)
|
|
}
|
|
|
|
private func save(_ request: AccountDeletionRequest) throws {
|
|
do {
|
|
let data = try encoder.encode(request)
|
|
defaults.set(data, forKey: storageKey(username: request.username))
|
|
} catch {
|
|
throw AccountDeletionError.persistenceFailed
|
|
}
|
|
}
|
|
|
|
private func storageKey(username: String) -> String {
|
|
keyPrefix + username
|
|
}
|
|
|
|
private func simulateDelay() async throws {
|
|
guard responseDelayNanoseconds > 0 else { return }
|
|
try await Task.sleep(nanoseconds: responseDelayNanoseconds)
|
|
}
|
|
}
|