import Foundation /// 旧注销接口的门店身份上下文;冻结身份与凭证,防止切换账号后误操作另一身份。 struct StoreAccountDeregistrationIdentity: Equatable, Sendable { /// 当前门店用户身份 ID,对应 ss_store_user.id,不是门店实体 ID。 let userID: String /// 进入流程时的门店身份 Token,仅用于接口鉴权。 let token: String /// 用于确认页展示当前正在注销的门店身份。 let displayName: String /// 从当前业务会话构造上下文;景区身份、未知身份和未登录状态均不可发起注销。 init(session: AppSessionStore) throws { guard session.accountType == .storeUser else { throw StoreAccountDeregistrationError.unsupportedIdentity } let id = session.userId.trimmingCharacters(in: .whitespacesAndNewlines) let token = session.token.trimmingCharacters(in: .whitespacesAndNewlines) guard let numericID = Int(id), numericID > 0, !token.isEmpty else { throw StoreAccountDeregistrationError.invalidSession } userID = id self.token = token let name = session.accountDisplayName.trimmingCharacters(in: .whitespacesAndNewlines) displayName = name.isEmpty ? "当前门店身份" : name } /// 校验操作前后仍为同一个身份和同一份登录凭证。 func matches(session: AppSessionStore) -> Bool { session.accountType == .storeUser && session.userId.trimmingCharacters(in: .whitespacesAndNewlines) == userID && session.token.trimmingCharacters(in: .whitespacesAndNewlines) == token } } /// 门店身份注销的本地安全校验错误,不替代后端业务错误码。 enum StoreAccountDeregistrationError: LocalizedError, Equatable { case unsupportedIdentity case invalidSession case sessionChanged /// 可直接展示给用户的错误说明。 var errorDescription: String? { switch self { case .unsupportedIdentity: "当前身份暂不支持注销" case .invalidSession: "当前门店身份信息不完整,请重新登录" case .sessionChanged: "登录状态或当前身份已变化,请重新进入注销页面" } } } /// 旧接口的余额或积分放弃确认请求;两种资产必须分别调用对应接口。 struct StoreAccountDeregistrationWaiverRequest: Encodable, Sendable { /// 用户明确接受后才创建该请求,固定提交 true。 let accepted = true } /// 旧接口提交注销申请的请求体,不附加新协议中的主账号 ID 或核验参数。 struct StoreAccountDeregistrationApplyRequest: Encodable, Sendable { /// 用户实际收到的短信验证码,不使用本地固定验证码。 let smsCode: String /// 用户确认的注销原因。 let reason: String /// 对齐旧文档的请求字段。 enum CodingKeys: String, CodingKey { case smsCode = "sms_code" case reason } } /// 未定义完整响应字段的旧接口原始 JSON;保留信息,待实际契约确认后映射为业务模型。 /// 此类型不推断状态、余额或确认结果,也不应直接用于决定是否允许注销。 indirect enum StoreAccountDeregistrationJSON: Decodable, Equatable, Sendable { case object([String: StoreAccountDeregistrationJSON]) case array([StoreAccountDeregistrationJSON]) case string(String) case number(Decimal) case bool(Bool) case null /// 无损保留 JSON 结构,金额数值使用 Decimal,避免浮点误差。 init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null } else if let value = try? container.decode(Bool.self) { self = .bool(value) } else if let value = try? container.decode(String.self) { self = .string(value) } else if let value = try? container.decode(Decimal.self) { self = .number(value) } else if let value = try? container.decode([String: Self].self) { self = .object(value) } else { self = .array(try container.decode([Self].self)) } } }