221 lines
8.3 KiB
Swift
221 lines
8.3 KiB
Swift
//
|
||
// 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<String> = [
|
||
"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",
|
||
"travel_album",
|
||
"pm",
|
||
"location_report",
|
||
"report_photographer",
|
||
"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 个 URI,再与 menuList 白名单求交。
|
||
static func defaultCommonURIs(orderedTopLevelURIs: [String]) -> [String] {
|
||
let rawTopLevel = orderedTopLevelURIs.compactMap { uri -> String? in
|
||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return trimmed.isEmpty ? nil : trimmed
|
||
}
|
||
let candidates = rawTopLevel.count > 4 ? Array(rawTopLevel.prefix(4)) : rawTopLevel
|
||
return candidates.filter { isAndroidHomeMenuURI($0) }
|
||
}
|
||
|
||
/// 返回顶层权限中属于 Android menuList 的 URI,顺序与 API 一致。
|
||
static func availableTopLevelMenuURIs(from orderedTopLevelURIs: [String]) -> [String] {
|
||
orderedTopLevelURIs.compactMap { uri -> String? in
|
||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !trimmed.isEmpty, isAndroidHomeMenuURI(trimmed) else { return nil }
|
||
return trimmed
|
||
}
|
||
}
|
||
|
||
/// 读取常用 URI;无角色时返回空,有 saved 时精确匹配且不回退默认。
|
||
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 rawTopLevel = topLevelPermissionURIs.compactMap { uri -> String? in
|
||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return trimmed.isEmpty ? nil : trimmed
|
||
}
|
||
let availableTopLevel = Self.availableTopLevelMenuURIs(from: rawTopLevel)
|
||
let availableTopLevelSet = Set(availableTopLevel)
|
||
let key = Self.storageKey(roleCode: roleCode, accountScope: accountScope)
|
||
let saved = defaults.stringArray(forKey: key) ?? []
|
||
let defaultCommon = Self.defaultCommonURIs(orderedTopLevelURIs: rawTopLevel)
|
||
|
||
HomeCommonMenuDiagnostics.log(
|
||
step: "context",
|
||
fields: [
|
||
"roleCode": roleCode,
|
||
"accountScope": accountScope ?? "",
|
||
"storageKey": key
|
||
]
|
||
)
|
||
HomeCommonMenuDiagnostics.log(
|
||
step: "rawTopLevel",
|
||
fields: [
|
||
"count": String(rawTopLevel.count),
|
||
"uris": HomeCommonMenuDiagnostics.formatURIList(rawTopLevel)
|
||
]
|
||
)
|
||
HomeCommonMenuDiagnostics.log(
|
||
step: "availableFunctions",
|
||
fields: [
|
||
"count": String(availableTopLevel.count),
|
||
"uris": HomeCommonMenuDiagnostics.formatURIList(availableTopLevel)
|
||
]
|
||
)
|
||
HomeCommonMenuDiagnostics.log(
|
||
step: "savedCommon",
|
||
fields: [
|
||
"uris": HomeCommonMenuDiagnostics.formatURIList(saved)
|
||
]
|
||
)
|
||
HomeCommonMenuDiagnostics.log(
|
||
step: "defaultCommon",
|
||
fields: [
|
||
"uris": HomeCommonMenuDiagnostics.formatURIList(defaultCommon)
|
||
]
|
||
)
|
||
|
||
let finalCommon: [String]
|
||
if !saved.isEmpty {
|
||
finalCommon = saved.filter { availableTopLevelSet.contains($0) }
|
||
} else {
|
||
finalCommon = defaultCommon
|
||
save(finalCommon, roleCode: roleCode, accountScope: accountScope)
|
||
}
|
||
|
||
HomeCommonMenuDiagnostics.log(
|
||
step: "finalCommon",
|
||
fields: [
|
||
"count": String(finalCommon.count),
|
||
"uris": HomeCommonMenuDiagnostics.formatURIList(finalCommon)
|
||
]
|
||
)
|
||
_ = menuItems
|
||
return finalCommon
|
||
}
|
||
|
||
/// 将指定 URI 加入常用应用,使用精确 URI 匹配。
|
||
func add(
|
||
_ uri: String,
|
||
current: [String],
|
||
topLevelPermissionURIs: [String],
|
||
roleCode: String?,
|
||
accountScope: String? = nil
|
||
) -> [String] {
|
||
guard let roleCode, !roleCode.isEmpty else { return current }
|
||
let availableTopLevel = Set(Self.availableTopLevelMenuURIs(from: topLevelPermissionURIs))
|
||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard availableTopLevel.contains(trimmed), !current.contains(trimmed) else { return current }
|
||
let next = current + [trimmed]
|
||
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 trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let next = current.filter { $0 != trimmed }
|
||
save(next, roleCode: roleCode, accountScope: accountScope)
|
||
return next
|
||
}
|
||
|
||
/// 保存指定角色的常用 URI。
|
||
func save(_ uris: [String], roleCode: String, accountScope: String? = nil) {
|
||
defaults.set(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)
|
||
}
|
||
}
|