新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
6.0 KiB
Swift
155 lines
6.0 KiB
Swift
//
|
||
// 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<String> = []
|
||
@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] {
|
||
guard let roleCode,
|
||
let rolePermission = matchedRolePermission(roleCode: roleCode) else {
|
||
return []
|
||
}
|
||
return rolePermission.role.permission.compactMap { item in
|
||
let uri = item.uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return uri.isEmpty ? nil : uri
|
||
}
|
||
}
|
||
|
||
/// 返回当前角色关联的景区作用域。
|
||
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<Int>()
|
||
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
|
||
)
|
||
}
|
||
}
|
||
}
|