301 lines
10 KiB
Go
301 lines
10 KiB
Go
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
|
||
}
|