// // AuthModels.swift // suixinkan // import Foundation /// App 配置响应,对齐 Android `AppConfigResponse`。 struct AppConfigResponse: Codable, Equatable { let enableRegister: Bool enum CodingKeys: String, CodingKey { case enableRegister = "enable_register" } init(enableRegister: Bool = false) { self.enableRegister = enableRegister } } /// 登录输入焦点实体,标识当前需要聚焦的输入框。 enum LoginField: Hashable { case username case password } /// 登录表单校验错误实体,负责提供用户提示和焦点位置。 enum LoginValidationError: Equatable { case invalidPhone case emptyPassword case privacyUnchecked var message: String { switch self { case .invalidPhone: "请输入有效的手机号码" case .emptyPassword: "请输入密码" case .privacyUnchecked: "请先阅读并同意用户协议与隐私政策" } } var focusField: LoginField? { switch self { case .invalidPhone: .username case .emptyPassword: .password case .privacyUnchecked: nil } } } /// 登录结果实体,区分已完成登录和需要用户选择账号两种情况。 enum LoginResolution { case completed(V9AuthResponse, AccountSwitchAccount) case needsAccountSelection(AccountSelectionPayload) } /// 登录流程错误实体,表示 token、账号列表或账号 ID 异常。 enum LoginFlowError: LocalizedError { case missingToken case noAvailableAccount case invalidAccount var errorDescription: String? { switch self { case .missingToken: "登录响应缺少 token" case .noAvailableAccount: "当前账号没有可用的景区账号或门店账号,请联系管理员" case .invalidAccount: "账号信息异常,请重新选择账号" } } } /// 登录请求实体,表示手机号密码登录接口需要的参数。 struct LoginRequest: Encodable { let username: String let type: Int let password: String let mobile: String let code: String /// 创建手机号密码登录请求,并填充后端要求的默认字段。 init(username: String, password: String) { self.username = username self.type = 1 self.password = password self.mobile = "" self.code = "" } } /// 账号选择请求实体,表示 set-user 接口需要的景区账号或门店账号 ID。 struct SetUserRequest: Encodable, Equatable { let storeUserId: Int? let ssUserId: Int? enum CodingKeys: String, CodingKey { case storeUserId = "store_user_id" case ssUserId = "ss_user_id" } init(storeUserId: Int? = nil, ssUserId: Int? = nil) { self.storeUserId = storeUserId self.ssUserId = ssUserId } } /// 可切换账号实体,统一表示登录返回的景区账号和门店账号。 struct AccountSwitchAccount: Identifiable, Hashable { let accountType: String let businessUserId: Int let title: String let subtitle: String let phone: String let avatar: String let scenicName: String let storeId: Int? let storeName: String let scenicId: Int? let isCurrent: Bool var id: String { "\(accountType)_\(businessUserId)" } var isStoreUser: Bool { accountType == V9StoreUser.accountTypeValue } var accountTypeLabel: String { isStoreUser ? "门店账号" : "景区账号" } func toSetUserRequest() -> SetUserRequest { if isStoreUser { return SetUserRequest(storeUserId: businessUserId) } return SetUserRequest(ssUserId: businessUserId) } } /// 登录账号选择载荷,保存临时 token 和待用户选择的账号列表。 struct AccountSelectionPayload: Equatable, Identifiable { let id = UUID() let tempToken: String let accounts: [AccountSwitchAccount] var hasTempToken: Bool { !tempToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } /// v9 登录响应实体,包含 token、景区账号列表和门店账号列表。 struct V9AuthResponse: Decodable, Equatable { let token: String let scenicUsers: [V9ScenicUser] let storeUsers: [V9StoreUser] var accounts: [AccountSwitchAccount] { scenicUsers.map { $0.toAccountSwitchAccount() } + storeUsers.map { $0.toAccountSwitchAccount() } } enum CodingKeys: String, CodingKey { case token case scenicUsers = "scenic_users" case storeUsers = "store_users" } init(token: String = "", scenicUsers: [V9ScenicUser] = [], storeUsers: [V9StoreUser] = []) { self.token = token self.scenicUsers = scenicUsers self.storeUsers = storeUsers } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) token = try container.decodeLossyString(forKey: .token) scenicUsers = (try? container.decodeIfPresent([V9ScenicUser].self, forKey: .scenicUsers)) ?? [] storeUsers = (try? container.decodeIfPresent([V9StoreUser].self, forKey: .storeUsers)) ?? [] } } /// v9 景区账号实体,表示用户可进入的某个景区身份。 struct V9ScenicUser: Decodable, Equatable { static let accountTypeValue = "scenic_user" let accountType: String let id: Int let userId: Int let scenicUserId: Int let ssUserId: Int let username: String let realName: String let nickname: String let phone: String let scenicId: Int let scenicName: String let appRoleCode: String let appRoleName: String let isCurrent: Bool var businessUserId: Int { [ssUserId, scenicUserId, userId, id].first { $0 > 0 } ?? 0 } var displayName: String { scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? username } func toAccountSwitchAccount() -> AccountSwitchAccount { AccountSwitchAccount( accountType: accountType.nonEmpty ?? Self.accountTypeValue, businessUserId: businessUserId, title: scenicName.nonEmpty ?? nickname.nonEmpty ?? realName.nonEmpty ?? "景区账号", subtitle: username, phone: phone, avatar: "", scenicName: scenicName, storeId: nil, storeName: "", scenicId: scenicId > 0 ? scenicId : nil, isCurrent: isCurrent ) } enum CodingKeys: String, CodingKey { case accountType = "account_type" case id case userId = "user_id" case scenicUserId = "scenic_user_id" case ssUserId = "ss_user_id" case username = "user_name" case legacyUsername = "username" case realName = "real_name" case nickname case phone case scenicId = "scenic_id" case scenicName = "scenic_name" case appRoleCode = "app_role_code" case appRoleName = "app_role_name" case isCurrent = "is_current" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) accountType = try container.decodeLossyString(forKey: .accountType).nonEmpty ?? Self.accountTypeValue id = try container.decodeLossyInt(forKey: .id) ?? 0 userId = try container.decodeLossyInt(forKey: .userId) ?? 0 scenicUserId = try container.decodeLossyInt(forKey: .scenicUserId) ?? 0 ssUserId = try container.decodeLossyInt(forKey: .ssUserId) ?? 0 let preferredUsername = try container.decodeLossyString(forKey: .username) let legacyUsername = try container.decodeLossyString(forKey: .legacyUsername) username = preferredUsername.nonEmpty ?? legacyUsername realName = try container.decodeLossyString(forKey: .realName) nickname = try container.decodeLossyString(forKey: .nickname) phone = try container.decodeLossyString(forKey: .phone) scenicId = try container.decodeLossyInt(forKey: .scenicId) ?? 0 scenicName = try container.decodeLossyString(forKey: .scenicName) appRoleCode = try container.decodeLossyString(forKey: .appRoleCode) appRoleName = try container.decodeLossyString(forKey: .appRoleName) isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false } } /// v9 门店账号实体,表示用户可进入的某个门店身份。 struct V9StoreUser: Decodable, Equatable { static let accountTypeValue = "store_user" let accountType: String let id: Int let userId: Int let storeUserId: Int let username: String let userName: String let realName: String let phone: String let avatar: String let scenicId: Int let scenicName: String let storeId: Int let storeName: String let appRoleCode: String let appRoleName: String let isCurrent: Bool var businessUserId: Int { [storeUserId, userId, id].first { $0 > 0 } ?? 0 } var displayName: String { storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? username } func toAccountSwitchAccount() -> AccountSwitchAccount { AccountSwitchAccount( accountType: accountType.nonEmpty ?? Self.accountTypeValue, businessUserId: businessUserId, title: storeName.nonEmpty ?? scenicName.nonEmpty ?? realName.nonEmpty ?? userName.nonEmpty ?? "门店账号", subtitle: userName.nonEmpty ?? username, phone: phone, avatar: avatar, scenicName: scenicName, storeId: storeId > 0 ? storeId : nil, storeName: storeName, scenicId: scenicId > 0 ? scenicId : nil, isCurrent: isCurrent ) } enum CodingKeys: String, CodingKey { case accountType = "account_type" case id case userId = "user_id" case storeUserId = "store_user_id" case username case userName = "user_name" case realName = "real_name" case phone case avatar case scenicId = "scenic_id" case scenicName = "scenic_name" case storeId = "store_id" case storeName = "store_name" case appRoleCode = "app_role_code" case appRoleName = "app_role_name" case isCurrent = "is_current" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) accountType = try container.decodeLossyString(forKey: .accountType).nonEmpty ?? Self.accountTypeValue id = try container.decodeLossyInt(forKey: .id) ?? 0 userId = try container.decodeLossyInt(forKey: .userId) ?? 0 storeUserId = try container.decodeLossyInt(forKey: .storeUserId) ?? 0 username = try container.decodeLossyString(forKey: .username) userName = try container.decodeLossyString(forKey: .userName) realName = try container.decodeLossyString(forKey: .realName) phone = try container.decodeLossyString(forKey: .phone) avatar = try container.decodeLossyString(forKey: .avatar) scenicId = try container.decodeLossyInt(forKey: .scenicId) ?? 0 scenicName = try container.decodeLossyString(forKey: .scenicName) storeId = try container.decodeLossyInt(forKey: .storeId) ?? 0 storeName = try container.decodeLossyString(forKey: .storeName) appRoleCode = try container.decodeLossyString(forKey: .appRoleCode) appRoleName = try container.decodeLossyString(forKey: .appRoleName) isCurrent = try container.decodeLossyBool(forKey: .isCurrent) ?? false } } private extension KeyedDecodingContainer { func decodeLossyString(forKey key: Key) throws -> String { if let value = try? decodeIfPresent(String.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Double.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value ? "true" : "false" } return "" } func decodeLossyInt(forKey key: Key) throws -> Int? { if let value = try? decodeIfPresent(Int.self, forKey: key) { return value } if let value = try? decodeIfPresent(String.self, forKey: key) { let text = value.trimmingCharacters(in: .whitespacesAndNewlines) if let intValue = Int(text) { return intValue } if let doubleValue = Double(text) { return Int(doubleValue) } } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) } return nil } func decodeLossyBool(forKey key: Key) throws -> Bool? { if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return value != 0 } if let value = try? decodeIfPresent(String.self, forKey: key) { let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() if ["1", "true", "yes"].contains(normalized) { return true } if ["0", "false", "no"].contains(normalized) { return false } } return nil } } private extension String { var nonEmpty: String? { let text = trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text } }