// // PermissionContext.swift // suixinkan // // Created by Codex on 2026/6/22. // import Foundation import Combine @MainActor /// 权限上下文状态中心,保存角色权限、当前角色和扁平化权限 URI。 final class PermissionContext: ObservableObject { @Published private(set) var rolePermissions: [RolePermissionResponse] = [] @Published private(set) var permissionURIs: Set = [] @Published var currentRole: RoleInfo? /// 当前 App 业务角色,优先 role_code,兼容 legacy id 与角色名。 var currentAppRole: AppRoleCode? { if let code = AppRoleCode.fromCode(currentRole?.roleCode) { return code } if let roleId = currentRole?.id, let code = AppRoleCode.fromLegacyRoleId(roleId) { return code } return AppRoleCode.fromRoleName(currentRole?.name) } /// 替换角色权限列表,并按 role_code 优先恢复当前角色。 func replaceRolePermissions( _ rolePermissions: [RolePermissionResponse], currentRoleCode: String? = nil, cachedLegacyRoleId: Int? = nil, roleName: String? = nil ) { self.rolePermissions = rolePermissions let resolvedCode = RolePermissionMatching.resolveRoleCode( roleCode: currentRoleCode, roleId: cachedLegacyRoleId, roleName: roleName, permissions: rolePermissions )?.rawValue ?? currentRoleCode if let matched = RolePermissionMatching.match( in: rolePermissions, roleCode: resolvedCode, roleId: cachedLegacyRoleId, roleName: roleName ?? currentRole?.name ) { currentRole = matched.role } else if let currentRole, let matched = rolePermissions.first(where: { $0.role.id == currentRole.id }) { self.currentRole = matched.role } else { currentRole = rolePermissions.first?.role } permissionURIs = Set(Self.flattenPermissions(currentRole?.permission ?? [])) } /// 切换当前角色,并同步账号上下文中的景区和门店选择。 func selectRole(code: AppRoleCode, accountContext: AccountContext) { guard let rolePermission = rolePermissions.first(where: { AppRoleCode.fromCode($0.role.roleCode) == code }) else { return } applyRoleSelection(rolePermission, accountContext: accountContext) } /// 切换当前角色(legacy id),供权限申请等仍使用 API role id 的场景。 func selectRole(id roleId: Int, accountContext: AccountContext) { guard let rolePermission = rolePermissions.first(where: { $0.role.id == roleId }) else { return } applyRoleSelection(rolePermission, accountContext: accountContext) } /// 判断当前角色是否拥有指定 URI 权限。 func canAccess(_ uri: String) -> Bool { permissionURIs.contains(uri.trimmingCharacters(in: .whitespacesAndNewlines)) } /// 返回指定角色的顶层权限 URI 列表,顺序与 API 返回一致。 func topLevelPermissionURIs(for roleCode: String?) -> [String] { topLevelPermissions(for: roleCode).map(\.uri) } /// 返回指定角色的顶层权限节点,顺序与 API 返回一致。 func topLevelPermissions(for roleCode: String?) -> [PermissionItem] { guard let roleCode, let rolePermission = matchedRolePermission(roleCode: roleCode) else { return [] } return rolePermission.role.permission.filter { item in !item.uri.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } /// 返回当前角色关联的景区作用域。 func currentRoleScenicScopes() -> [BusinessScope] { guard let currentRole else { return [] } guard let rolePermission = matchedRolePermission(roleCode: currentRole.roleCode) ?? rolePermissions.first(where: { $0.role.id == currentRole.id }) else { return [] } return Self.uniqueScenicScopes(from: rolePermission.scenic) } /// 清空权限上下文,通常在退出登录时调用。 func reset() { rolePermissions = [] permissionURIs = [] currentRole = nil } /// 应用角色切换并同步景区作用域。 private func applyRoleSelection(_ rolePermission: RolePermissionResponse, accountContext: AccountContext) { currentRole = rolePermission.role permissionURIs = Set(Self.flattenPermissions(rolePermission.role.permission)) let scenicScopes = Self.uniqueScenicScopes(from: rolePermission.scenic) accountContext.replaceScopes( scenic: scenicScopes, stores: accountContext.storeScopes, currentScenicId: accountContext.currentScenic?.id, currentStoreId: accountContext.currentStore?.id ) } /// 按 role_code 在权限列表中查找角色权限项。 private func matchedRolePermission(roleCode: String) -> RolePermissionResponse? { RolePermissionMatching.match( in: rolePermissions, roleCode: roleCode, roleId: nil, roleName: nil ) } /// 递归提取权限树里的非空 URI。 private static func flattenPermissions(_ permissions: [PermissionItem]) -> [String] { permissions.flatMap { item -> [String] in let current = item.uri.trimmingCharacters(in: .whitespacesAndNewlines) let children = flattenPermissions(item.children) return current.isEmpty ? children : [current] + children } } /// 将角色关联景区去重并转换为业务作用域。 private static func uniqueScenicScopes(from scenics: [ScenicInfo]) -> [BusinessScope] { var seen = Set() return scenics.compactMap { scenic in guard seen.insert(scenic.id).inserted else { return nil } return BusinessScope( id: scenic.id, name: scenic.name, kind: .scenic, status: scenic.status, address: scenic.location?.address, latitude: scenic.location?.lat, longitude: scenic.location?.lng, coverURLString: scenic.coverImg ) } } }