// // HomeCommonMenuStore.swift // suixinkan // // Created by Codex on 2026/6/22. // import Foundation /// 首页常用应用存储服务,负责按当前角色权限过滤和持久化常用 URI。 struct HomeCommonMenuStore { static let storageKeyPrefix = "home.common.menu.uris.role." static let accountScopedStorageKeyPrefix = "home.common.menu.uris.account." /// 与 Android `Constants.menuList` 对齐的首页菜单 URI 白名单。 static let androidHomeMenuURIs: Set = [ "space_settings", "wallet", "cloud_management", "asset_management", "task_management", "schedule_management", "system_settings", "message_center", "checkin_points", "sample_management", "live_stream_management", "verification_order", "live_album", "pm", "location_report", "registration_invitation", "store", "fly", "pilot_cert", "task_management_editor", "pm_manager", "pilot_controller", "/scenic-queue", "operating-area", "/scenic-order-manage", "cooperation_order" ] private let defaults: UserDefaults /// 初始化常用应用存储服务,并允许测试注入独立 UserDefaults。 init(defaults: UserDefaults = .standard) { self.defaults = defaults } /// 按 Android 规则生成默认常用 URI:顶层权限顺序、可用权限过滤、别名去重,超过 4 个取前 4 个。 static func defaultCommonURIs( orderedTopLevelURIs: [String], availableURIs: Set ) -> [String] { let resolved = orderedTopLevelURIs.compactMap { uri -> String? in let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } let canonical = HomeMenuRouter.canonicalURI(for: trimmed, availableURIs: availableURIs) return Self.isAndroidHomeMenuURI(canonical) && availableURIs.contains(canonical) ? canonical : nil } let deduplicated = unique(resolved) if deduplicated.count > 4 { return Array(deduplicated.prefix(4)) } return deduplicated } /// 读取常用 URI;无角色时返回空,无保存或过滤后为空时写入 Android 对齐默认值。 func load( menuItems: [HomeMenuItem], roleCode: String?, accountScope: String? = nil, legacyRoleId: Int? = nil, topLevelPermissionURIs: [String] ) -> [String] { guard let roleCode, !roleCode.isEmpty else { return [] } migrateLegacyStorageIfNeeded(roleCode: roleCode, accountScope: accountScope, legacyRoleId: legacyRoleId) let availableURIs = Set(menuItems.map(\.uri)) let key = Self.storageKey(roleCode: roleCode, accountScope: accountScope) let saved = defaults.stringArray(forKey: key) if let saved, !saved.isEmpty { let filtered = Self.unique(saved.compactMap { uri -> String? in let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs) return Self.isAndroidHomeMenuURI(resolved) && availableURIs.contains(resolved) ? resolved : nil }) if !filtered.isEmpty { save(filtered, roleCode: roleCode, accountScope: accountScope) return filtered } } let defaultURIs = Self.defaultCommonURIs( orderedTopLevelURIs: topLevelPermissionURIs, availableURIs: availableURIs ) save(defaultURIs, roleCode: roleCode, accountScope: accountScope) return defaultURIs } /// 将指定 URI 加入常用应用,按别名避免重复添加。 func add(_ uri: String, current: [String], menuItems: [HomeMenuItem], roleCode: String?, accountScope: String? = nil) -> [String] { guard let roleCode, !roleCode.isEmpty else { return current } let availableURIs = Set(menuItems.map(\.uri)) let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs) guard Self.isAndroidHomeMenuURI(resolved), availableURIs.contains(resolved) else { return current } guard !current.contains(where: { HomeMenuRouter.menuAliasKey(for: $0) == HomeMenuRouter.menuAliasKey(for: resolved) }) else { return current } let next = current + [resolved] save(next, roleCode: roleCode, accountScope: accountScope) return next } /// 从常用应用中移除指定 URI,同义 URI 会一起移除。 func remove(_ uri: String, current: [String], roleCode: String?, accountScope: String? = nil) -> [String] { guard let roleCode, !roleCode.isEmpty else { return current } let aliasKey = HomeMenuRouter.menuAliasKey(for: uri) let next = current.filter { HomeMenuRouter.menuAliasKey(for: $0) != aliasKey } save(next, roleCode: roleCode, accountScope: accountScope) return next } /// 保存指定角色的常用 URI。 func save(_ uris: [String], roleCode: String, accountScope: String? = nil) { defaults.set(Self.unique(uris), forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope)) } /// 若账号级 key 无数据,则从旧 roleCode 或 legacy role id key 一次性迁移常用菜单。 func migrateLegacyStorageIfNeeded(roleCode: String, accountScope: String? = nil, legacyRoleId: Int?) { let newKey = Self.storageKey(roleCode: roleCode, accountScope: accountScope) guard defaults.stringArray(forKey: newKey) == nil else { return } let legacyKeys = [ legacyStorageKey(for: roleCode), legacyRoleId.map { legacyStorageKey(for: $0) } ].compactMap { $0 } for legacyKey in legacyKeys { guard let legacyData = defaults.stringArray(forKey: legacyKey), !legacyData.isEmpty else { continue } defaults.set(legacyData, forKey: newKey) defaults.removeObject(forKey: legacyKey) return } } /// 生成按账号和 role_code 隔离的 UserDefaults key。 static func storageKey(roleCode: String, accountScope: String?) -> String { let normalizedScope = accountScope?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if normalizedScope.isEmpty { return storageKeyPrefix + roleCode } return accountScopedStorageKeyPrefix + normalizedScope + ".role." + roleCode } /// 旧版按 numeric role id 存储的 key。 private func legacyStorageKey(for roleId: Int) -> String { Self.storageKeyPrefix + String(roleId) } /// 旧版按 role_code 存储的 key。 private func legacyStorageKey(for roleCode: String) -> String { Self.storageKeyPrefix + roleCode } /// 判断 URI 是否属于 Android 首页菜单白名单。 private static func isAndroidHomeMenuURI(_ uri: String) -> Bool { androidHomeMenuURIs.contains(uri) } /// 按 URI 别名去重,保留首次出现的 URI。 private static func unique(_ uris: [String]) -> [String] { var seen = Set() return uris.filter { uri in seen.insert(HomeMenuRouter.menuAliasKey(for: uri)).inserted } } }