guard/internal/usecase/permission_tree.go

301 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"ark-permission/internal/domain"
"ark-permission/internal/domain/usecase"
"ark-permission/internal/entity"
ers "code.30cm.net/wanderland/library-go/errors"
"fmt"
"sort"
)
// NewPermissionTree 創建新的權限樹
func NewPermissionTree() *PermissionTree {
// 插入虛擬跟節點,讓其他人都放在這個底下,行為一致,無需處理邊界
root := &usecase.Permission{
ID: 0, // 根節點 ID 通常設為 0 或其他標示符號
Name: "root", // 虛擬根節點名稱
Children: []*usecase.Permission{},
}
return &PermissionTree{
root: root,
nodes: map[int64]*usecase.Permission{0: root}, // 根節點也加入 nodes 記錄
paths: make(map[int64][]int),
names: make(map[string][]int64),
ids: make(map[int64]string),
}
}
// PermissionTree 將初始化與方法分開
// 不考慮使用其樹型結構原因是,在我們的的場景下,
// 因為深度不超過 100 層,且資料量在 300 到 500 筆之間,至多不超過一萬筆
// (考慮到一萬條權限已很複雜) 讀取操作是主要需求,而寫入與刪除操作相對較少,
// 這樣的場景適合空間換時間的策略。
// 優點:
// 1. 空間換時間:
// • 使用 map 來保存節點和它們的路徑、名稱等,可以在 O(1) 的時間複雜度內查找節點、名稱、路徑等資料,非常適合頻繁讀取的場景。
// • 雖然消耗了更多的空間儲存多個映射但這樣的空間開銷在你的資料量規模300-500 筆)下是可以接受的。
// 2. 高效查找:
// • 查找節點nodes map、路徑paths map和名稱names map都是透過直接查找 map 實現的,這會極大提升查找效率,尤其是在讀取大量資料的情況下。
// • map 在 Golang 中的查找操作具有平均 O(1) 的時間複雜度,非常適合大量查找的場景。
// 3. 樹結構較小,寫入與刪除頻率較低:
// • 由於你的資料量不大,且寫入和刪除操作較少,這樣的設計不會過度影響性能。即使有少量的寫入或刪除操作,更新 map 的開銷也是可以接受的。
// 為何設計 names map[string][]int64
// 雖然目前sql 層面來說 name 是唯一的
// 這是因為在某些情況下,權限名稱並不是唯一的,可能會有多個權限使用同一個名稱。例如:
// • 你可能在多個模組中使用相同的權限名稱,例如 “Read” 或 “Write”但它們的實際權限 ID 是不同的。
// • 使用 map[string][]int64 結構來儲存這些同名權限,可以快速查找所有對應的權限 ID並有效管理不同模組的相同名稱權限。
type PermissionTree struct {
root *usecase.Permission // 根節點,表示權限樹的最頂層,所有其他權限都從這裡派生
nodes map[int64]*usecase.Permission // 透過 permission ID 快速查找權限節點,允許 O(1) 查找
paths map[int64][]int // 每個權限節點的完整路徑,儲存節點 ID 對應的索引路徑,方便快速查找()
names map[string][]int64 // 權限名稱對應權限 ID支持多個同名權限允許快速根據名稱查找所有相關 ID
ids map[int64]string // 權限 ID 對應權限名稱,允許根據權限 ID 反向查找權限名稱
}
// AddPermission 插入新的權限節點
func (tree *PermissionTree) AddPermission(parentID int64, value entity.Permission) error {
node := &usecase.Permission{
ID: value.ID,
Name: value.Name,
HTTPPath: value.HTTPPath,
HTTPMethod: value.HTTPMethod,
Children: []*usecase.Permission{},
}
// 查找父節點,找不到回傳錯誤。
parentNode, err := tree.FindPermissionByID(parentID)
if err != nil {
return ers.ResourceNotFound(err.Error())
}
parentNode.Children = append(parentNode.Children, node)
node.Parent = parentNode
// 設置路徑 ID
if node.Parent.ID >= 0 {
node.PathIDs = append(node.Parent.PathIDs, node.Parent.ID)
}
// 更新樹結構中的數據
tree.nodes[value.ID] = node // 節點加入紀錄
tree.names[value.Name] = append(tree.names[value.Name], value.ID)
tree.ids[value.ID] = value.Name
// 計算完整路徑
tree.buildNodePath(node, value.ID)
return nil
}
// buildNodePath 構建節點的完整路徑
// paths 是一個 map 結構,儲存每個權限節點的完整路徑
// key 是節點的權限 ID (int64)
// value 是從根節點到該節點的索引路徑 ([]int)
// 每個 int 值代表該節點在其父節點的 Children 列表中的索引位置。
//
// 例如:
// 假設有以下的權限樹結構:
// root (ID: 1)
// ├── child_permission_1 (ID: 2)
// │ └── grandchild_permission_1 (ID: 4)
// └── child_permission_2 (ID: 3)
//
// 那麼:
// paths[4] = []int{0, 0}
// 表示 grandchild_permission_1 的完整路徑:
// - 它是根節點的第一個子節點(索引 0
// - 它的父節點child_permission_1也是根節點的第一個子節點索引 0
//
// 類似的:
// paths[2] = []int{0}
// 表示 child_permission_1 是根節點的第一個子節點。
func (tree *PermissionTree) buildNodePath(node *usecase.Permission, insertID int64) {
// 找出該node完整的path路徑
var path []int
for {
if node.Parent == nil {
sort.SliceStable(path, func(_, _ int) bool {
return true
})
tree.paths[insertID] = path
break
}
for i, v := range node.Parent.Children {
if node.ID == v.ID {
path = append(path, i)
node = node.Parent
}
}
}
}
// FindPermissionByID 根據 ID 查找權限節點
// 如果權限節點存在,返回對應的 Permission 資料
func (tree *PermissionTree) FindPermissionByID(permissionID int64) (*usecase.Permission, error) {
// 直接從 nodes map 中查找對應的節點O(1) 時間複雜度
node, ok := tree.nodes[permissionID]
if !ok {
return nil, fmt.Errorf("failed to find ID %d", permissionID)
}
// 返回找到的節點
return node, nil
}
// GetAllParentPermissionIDs 返回所有父節點權限 ID
func (tree *PermissionTree) GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error) {
exist := make(map[int64]bool) // 用於記錄已處理的權限 ID
var ids []int64
for name, status := range permissions {
if status != domain.PermissionStatusOpenCode {
continue // 只處理開啟狀態的權限
}
// 根據名稱查找對應的權限 ID 列表
pIDs, ok := tree.names[name]
if !ok {
return nil, ers.ResourceNotFound(fmt.Sprintf("permission with name %s not found", name))
}
for _, pID := range pIDs {
// 檢查是否已經處理過這個權限 ID
if exist[pID] {
continue
}
node, err := tree.FindPermissionByID(pID)
if err != nil {
return nil, err
}
// 如果該節點有子節點且父節點未開啟,則跳過該節點
if len(node.Children) > 0 && !tree.isParentPermissionOpen(node, permissions) {
continue
}
// 加入該節點的父節點路徑和自身 ID
ids = append(ids, node.PathIDs...)
ids = append(ids, pID)
// 將所有相關的 ID 記錄為已處理
for _, id := range append(node.PathIDs, pID) {
exist[id] = true
}
}
}
return ids, nil
}
// GetAllParentPermissionStatuses 返回所有父節點的權限狀態
// 如果一個權限開啟,返回所有相關父節點的狀態
func (tree *PermissionTree) GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error) {
// 用來存儲已經處理過的節點,避免重複處理
exist := make(map[int64]bool)
// 用來存儲返回的所有權限狀態
resultStatuses := make(domain.Permissions)
// 遍歷每個權限
for name, status := range permissions {
// 只處理已開啟的權限
if status != domain.PermissionStatusOpenCode {
continue
}
// 根據權限名稱查找對應的權限 ID 列表
pIDs, ok := tree.names[name]
if !ok {
return nil, ers.ResourceNotFound(fmt.Sprintf("permission with name %s not found", name))
}
// 遍歷所有權限 ID
for _, pID := range pIDs {
// 檢查是否已處理過這個 ID
if exist[pID] {
continue
}
node, err := tree.FindPermissionByID(pID)
if err != nil {
return nil, err
}
// 處理該節點的父節點路徑和自身 ID
allIDs := append(node.PathIDs, pID)
for _, id := range allIDs {
// 避免重複處理
if exist[id] {
continue
}
if permissionName, exists := tree.ids[id]; exists {
// 設置權限狀態為已開啟
resultStatuses[permissionName] = domain.PermissionStatusOpenCode
exist[id] = true
}
}
}
}
return resultStatuses, nil
}
// isParentPermissionOpen 檢查父節點的權限是否已開啟
func (tree *PermissionTree) isParentPermissionOpen(node *usecase.Permission, permissions domain.Permissions) bool {
for _, child := range node.Children {
// 檢查子節點是否有開啟的權限
if status, ok := permissions[child.Name]; ok && status == domain.PermissionStatusOpenCode {
return true
}
}
return false
}
// GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態
// 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態
func (tree *PermissionTree) GetRolePermissionTree(rolePermissions []entity.RolePermission) (domain.Permissions, error) {
// 初始化用來存儲所有權限狀態的 map
resultStatuses := make(domain.Permissions)
// 初始化用來記錄已處理的權限 ID避免重複處理
exist := make(map[int64]bool)
// 遍歷所有角色的權限
for _, rolePermission := range rolePermissions {
// 根據權限 ID 找到對應的節點
node, err := tree.FindPermissionByID(rolePermission.PermissionID)
if err != nil {
return nil, fmt.Errorf("permission with ID %d not found: %w", rolePermission.PermissionID, err)
}
// 將該節點及其父節點的權限狀態加入 resultStatuses
allIDs := append(node.PathIDs, rolePermission.PermissionID) // 包含所有父節點和當前節點
for _, id := range allIDs {
if !exist[id] { // 檢查是否已經處理過
if permissionName, ok := tree.ids[id]; ok {
// 設置權限狀態為已開啟(或者根據 rolePermission 狀態設置)
resultStatuses[permissionName] = domain.PermissionStatusOpenCode
exist[id] = true // 記錄該 ID 已處理過
}
}
}
// 遍歷該節點的所有子節點,並設置權限狀態
for _, child := range node.Children {
if !exist[child.ID] {
if permissionName, ok := tree.ids[child.ID]; ok {
resultStatuses[permissionName] = domain.PermissionStatusOpenCode
exist[child.ID] = true
}
}
}
}
return resultStatuses, nil
}