// // 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, 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, 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) } }