Implement permission-driven home tab aligned with Android.

Load role-permission on first visit to drive menus, role-based layout, store card, and location reporting with account-scoped persistence and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:27:54 +08:00
co-authored by Cursor
parent 75d0cb1f9a
commit 3b17b7f7f0
27 changed files with 2573 additions and 10 deletions
+119
View File
@@ -26,6 +26,13 @@ final class AppStore {
static let currentScenicId = "key_in_current_scenic_id"
static let currentScenicName = "key_in_current_scenic_name"
static let currentStoreId = "key_in_current_store_id"
static let legacyRoleId = "key_in_role_id"
static let rolePermissionList = "key_role_permission_list"
static let permissionItems = "key_in_permission"
static let roleScenicList = "key_current_role_scenic_list"
static let onlineStatus = "key_online_status"
static let lastLocationReportTime = "key_last_location_report_time"
static let locationReminderMinutes = "key_location_reminder_minutes"
}
private let defaults: UserDefaults
@@ -123,6 +130,39 @@ final class AppStore {
set { defaults.set(newValue, forKey: Key.currentStoreId) }
}
/// 旧版 role_id,用于权限匹配兜底。
var legacyRoleId: Int {
get { defaults.integer(forKey: accountScopedKey(Key.legacyRoleId)) }
set { defaults.set(newValue, forKey: accountScopedKey(Key.legacyRoleId)) }
}
/// 是否在线(位置上报会话)。
var onlineStatus: Bool {
get { defaults.bool(forKey: accountScopedKey(Key.onlineStatus)) }
set { defaults.set(newValue, forKey: accountScopedKey(Key.onlineStatus)) }
}
/// 上次位置上报时间戳(毫秒)。
var lastLocationReportTime: Int64 {
get { Int64(defaults.integer(forKey: accountScopedKey(Key.lastLocationReportTime))) }
set { defaults.set(Int(newValue), forKey: accountScopedKey(Key.lastLocationReportTime)) }
}
/// 位置超时提前提醒分钟数,0 表示不提醒。
var locationReminderMinutes: Int {
get { defaults.integer(forKey: accountScopedKey(Key.locationReminderMinutes)) }
set { defaults.set(newValue, forKey: accountScopedKey(Key.locationReminderMinutes)) }
}
/// 当前账号缓存前缀,对齐 Android `getAccountCachePrefix`。
var accountCachePrefix: String {
let uid = userId.trimmingCharacters(in: .whitespacesAndNewlines)
let type = accountType.trimmingCharacters(in: .whitespacesAndNewlines)
if uid.isEmpty { return "guest" }
if type.isEmpty { return uid }
return "\(uid)_\(type)"
}
/// 当前解析后的业务角色。
var currentAppRole: AppRoleCode? {
AppRoleCode.fromCode(roleCode) ?? AppRoleCode.fromRoleName(roleName)
@@ -183,6 +223,7 @@ final class AppStore {
/// 清除登录态与会话快照。
func logout() {
clearPermissionSnapshot()
token = ""
userId = ""
userName = ""
@@ -197,4 +238,82 @@ final class AppStore {
currentScenicName = ""
currentStoreId = 0
}
/// 保存完整 role-permission 列表。
func saveRolePermissionList(_ list: [RolePermissionResponse]) {
guard let data = try? JSONEncoder().encode(list) else { return }
defaults.set(data, forKey: accountScopedKey(Key.rolePermissionList))
}
/// 读取完整 role-permission 列表。
func rolePermissionList() -> [RolePermissionResponse] {
guard let data = defaults.data(forKey: accountScopedKey(Key.rolePermissionList)),
let list = try? JSONDecoder().decode([RolePermissionResponse].self, from: data) else {
return []
}
return list
}
/// 保存当前角色扁平权限。
func savePermissionItems(_ items: [HomePermissionItem]) {
guard let data = try? JSONEncoder().encode(items) else { return }
defaults.set(data, forKey: accountScopedKey(Key.permissionItems))
}
/// 读取当前角色扁平权限。
func permissionItems() -> [HomePermissionItem] {
guard let data = defaults.data(forKey: accountScopedKey(Key.permissionItems)),
let items = try? JSONDecoder().decode([HomePermissionItem].self, from: data) else {
return []
}
return items
}
/// 保存当前角色可选景区列表。
func saveRoleScenicList(_ scenicList: [ScenicInfo]) {
guard let data = try? JSONEncoder().encode(scenicList) else { return }
defaults.set(data, forKey: accountScopedKey(Key.roleScenicList))
}
/// 读取当前角色可选景区列表。
func roleScenicList() -> [ScenicInfo] {
guard let data = defaults.data(forKey: accountScopedKey(Key.roleScenicList)),
let list = try? JSONDecoder().decode([ScenicInfo].self, from: data) else {
return []
}
return list
}
/// 在 role-permission 列表中匹配当前账号角色。
func matchRolePermissionItem(in list: [RolePermissionResponse]) -> RolePermissionResponse? {
RolePermissionMatcher.match(
in: list,
roleCode: roleCode,
roleName: roleName,
legacyRoleId: legacyRoleId
)
}
/// 清除上次位置上报时间。
func clearLastLocationReportTime() {
defaults.removeObject(forKey: accountScopedKey(Key.lastLocationReportTime))
}
/// 清除权限与位置会话快照。
func clearPermissionSnapshot() {
let prefix = accountScopedKey("")
let keys = [
Key.rolePermissionList,
Key.permissionItems,
Key.roleScenicList,
Key.onlineStatus,
Key.lastLocationReportTime,
Key.locationReminderMinutes,
]
keys.forEach { defaults.removeObject(forKey: prefix + $0) }
}
private func accountScopedKey(_ key: String) -> String {
"\(accountCachePrefix)_\(key)"
}
}