diff --git a/client/permissionservice/permission_service.go b/client/permissionservice/permission_service.go index c16de6d..223681e 100644 --- a/client/permissionservice/permission_service.go +++ b/client/permissionservice/permission_service.go @@ -24,6 +24,7 @@ type ( GetPermissionStatusByPathReq = permission.GetPermissionStatusByPathReq ListPermissionResp = permission.ListPermissionResp ListPermissionStatusResp = permission.ListPermissionStatusResp + MapPermissionStatusResp = permission.MapPermissionStatusResp NoneReq = permission.NoneReq OKResp = permission.OKResp PermissionItem = permission.PermissionItem @@ -39,11 +40,11 @@ type ( ValidationTokenResp = permission.ValidationTokenResp PermissionService interface { - // ListPermissionStatus 取得狀態 + // ListPermissionStatus 取得所有權限狀態列表,給前端表演用 ListPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*ListPermissionStatusResp, error) - // ListPermission 取得完整權限 - ListPermission(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*ListPermissionResp, error) - // CheckPermissionByRole 透過角色 ID 來檢視權限 + // MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用 + MapPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*MapPermissionStatusResp, error) + // CheckPermissionByRole 透過角色 ID 來檢視權限,後台要通過時真的看這個 CheckPermissionByRole(ctx context.Context, in *CheckPermissionByRoleReq, opts ...grpc.CallOption) (*PermissionResp, error) // GetPermissionStatusByPath 透過資源拿取角色的狀態 GetPermissionStatusByPath(ctx context.Context, in *GetPermissionStatusByPathReq, opts ...grpc.CallOption) (*PermissionStatusItem, error) @@ -60,19 +61,19 @@ func NewPermissionService(cli zrpc.Client) PermissionService { } } -// ListPermissionStatus 取得狀態 +// ListPermissionStatus 取得所有權限狀態列表,給前端表演用 func (m *defaultPermissionService) ListPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*ListPermissionStatusResp, error) { client := permission.NewPermissionServiceClient(m.cli.Conn()) return client.ListPermissionStatus(ctx, in, opts...) } -// ListPermission 取得完整權限 -func (m *defaultPermissionService) ListPermission(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*ListPermissionResp, error) { +// MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用 +func (m *defaultPermissionService) MapPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*MapPermissionStatusResp, error) { client := permission.NewPermissionServiceClient(m.cli.Conn()) - return client.ListPermission(ctx, in, opts...) + return client.MapPermissionStatus(ctx, in, opts...) } -// CheckPermissionByRole 透過角色 ID 來檢視權限 +// CheckPermissionByRole 透過角色 ID 來檢視權限,後台要通過時真的看這個 func (m *defaultPermissionService) CheckPermissionByRole(ctx context.Context, in *CheckPermissionByRoleReq, opts ...grpc.CallOption) (*PermissionResp, error) { client := permission.NewPermissionServiceClient(m.cli.Conn()) return client.CheckPermissionByRole(ctx, in, opts...) diff --git a/client/roleservice/role_service.go b/client/roleservice/role_service.go index 9c4ea68..c2bbf81 100644 --- a/client/roleservice/role_service.go +++ b/client/roleservice/role_service.go @@ -24,6 +24,7 @@ type ( GetPermissionStatusByPathReq = permission.GetPermissionStatusByPathReq ListPermissionResp = permission.ListPermissionResp ListPermissionStatusResp = permission.ListPermissionStatusResp + MapPermissionStatusResp = permission.MapPermissionStatusResp NoneReq = permission.NoneReq OKResp = permission.OKResp PermissionItem = permission.PermissionItem diff --git a/client/tokenservice/token_service.go b/client/tokenservice/token_service.go index f4d2b44..78dc0ac 100644 --- a/client/tokenservice/token_service.go +++ b/client/tokenservice/token_service.go @@ -24,6 +24,7 @@ type ( GetPermissionStatusByPathReq = permission.GetPermissionStatusByPathReq ListPermissionResp = permission.ListPermissionResp ListPermissionStatusResp = permission.ListPermissionStatusResp + MapPermissionStatusResp = permission.MapPermissionStatusResp NoneReq = permission.NoneReq OKResp = permission.OKResp PermissionItem = permission.PermissionItem diff --git a/generate/database/mysql/seeder/20240816014305_init_permission.up.sql b/generate/database/mysql/seeder/20240816014305_init_permission.up.sql new file mode 100644 index 0000000..1293885 --- /dev/null +++ b/generate/database/mysql/seeder/20240816014305_init_permission.up.sql @@ -0,0 +1,15 @@ +-- 一級分類 +INSERT INTO `permission` (`parent`, `name`, `http_method`, `http_path`, `create_time`, `update_time`) +VALUES (0, 'user.info.management', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); -- 用戶資訊管理 + +# -- 二級分類 用戶資訊管理 +SET @id := (SELECT id FROM `permission` where name = 'user.info.management'); +INSERT INTO `permission` (`parent`, `name`, `http_method`, `http_path`, `create_time`, `update_time`) +VALUES (@id, 'user.basic.info', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); -- 用戶基礎資訊查詢 + + +# -- 三級分類 用戶基礎資訊管理-基礎資訊查詢表 +SET @id := (SELECT id FROM `permission` where name = 'user.basic.info'); +INSERT INTO `permission` (`parent`, `name`, `http_method`, `http_path`, `create_time`, `update_time`) +VALUES (@id, 'user.info.select', 'GET', '/v1/user', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), -- 查詢 + (@id, 'user.info.select.plain_code', 'GET', '/v1/user', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); -- 明碼查詢 diff --git a/generate/protobuf/permission.proto b/generate/protobuf/permission.proto index e010a23..4063650 100644 --- a/generate/protobuf/permission.proto +++ b/generate/protobuf/permission.proto @@ -171,7 +171,8 @@ message PermissionStatusItem { int64 parent_id =2; string name =3; PermissionStatus status = 4; - bool approval = 5; + string type =5; + bool approval = 6; } message ListPermissionStatusResp { @@ -210,12 +211,16 @@ message GetPermissionStatusByPathReq { string method = 3; } +message MapPermissionStatusResp { + map data = 1; // permission id : open close +} + service PermissionService { - // ListPermissionStatus 取得狀態 + // ListPermissionStatus 取得所有權限狀態列表,給前端表演用 rpc ListPermissionStatus(NoneReq)returns(ListPermissionStatusResp); - // ListPermission 取得完整權限 - rpc ListPermission(NoneReq)returns(ListPermissionResp); - // CheckPermissionByRole 透過角色 ID 來檢視權限 + // ListPermission 一次性取得所有權限表 + rpc ListPermission(NoneReq)returns(MapPermissionStatusResp); + // CheckPermissionByRole 透過角色 ID 來檢視權限,後台要通過時真的看這個 rpc CheckPermissionByRole(CheckPermissionByRoleReq)returns(PermissionResp); // GetPermissionStatusByPath 透過資源拿取角色的狀態 rpc GetPermissionStatusByPath(GetPermissionStatusByPathReq)returns(PermissionStatusItem); diff --git a/go.mod b/go.mod index 09ac137..3d0f05e 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,7 @@ require ( github.com/go-playground/validator/v10 v10.22.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 - github.com/mitchellh/mapstructure v1.5.0 github.com/open-policy-agent/opa v0.67.1 - github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 github.com/zeromicro/go-zero v1.7.0 go.uber.org/mock v0.4.0 @@ -18,6 +16,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -38,6 +37,7 @@ require ( github.com/go-openapi/swag v0.22.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index 6cd58e1..4b3c20b 100755 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,4 +14,8 @@ type Config struct { Expired time.Duration Secret string } + // 加上DB結構體 + DB struct { + DsnString string + } } diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 02f1eb4..56e152d 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -6,3 +6,36 @@ const ( PermissionTypeBackendUser PermissionType = iota + 1 PermissionTypeFrontendUser ) + +type PermissionTypeCode string + +const ( + PermissionTypeBackCode PermissionTypeCode = "back" + PermissionTypeFrontCode PermissionTypeCode = "front" +) + +var permissionMap = map[int64]PermissionTypeCode{ + 1: PermissionTypeFrontCode, + 2: PermissionTypeBackCode, +} + +func ToPermissionTypeCode(code int64) (PermissionTypeCode, bool) { + result, ok := permissionMap[code] + if !ok { + return "", false + } + + return result, true +} + +func (t *PermissionTypeCode) ToString() string { + return string(*t) +} + +type PermissionStatus string +type Permissions map[string]PermissionStatus + +const ( + PermissionStatusOpenCode PermissionStatus = "open" + PermissionStatusCloseCode PermissionStatus = "close" +) diff --git a/internal/domain/usecase/permission_tree.go b/internal/domain/usecase/permission_tree.go new file mode 100644 index 0000000..f67f436 --- /dev/null +++ b/internal/domain/usecase/permission_tree.go @@ -0,0 +1,38 @@ +package usecase + +import ( + "ark-permission/internal/domain" + "ark-permission/internal/entity" +) + +// PermissionTreeManager 定義一組操作權限樹的接口 +// 這個名稱說明它是專門負責管理和操作權限樹的管理器 +type PermissionTreeManager interface { + // AddPermission 將一個新的權限節點插入到樹中 + // key 是父節點的ID,value 是要插入的 Permission 資料 + // 此方法應該能處理節點是否存在於父節點下的情況 + AddPermission(parentID int64, permission entity.Permission) error + // FindPermissionByID 根據權限 ID 查詢樹中的某個節點 + // 如果節點存在,返回對應的 Permission 資料,否則返回 nil + FindPermissionByID(permissionID int64) (*Permission, error) + // GetAllParentPermissionIDs 根據傳入的 permissions 列表 + // 找出每個權限的完整父節點權限 ID 路徑 + // 例如,如果 B 的父權限是 A,並且給了 B 權限,則返回 A 和 B 的權限 ID + GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error) + // GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態 + // 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態 + GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error) + // GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態 + // 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態 + GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions +} + +type Permission struct { + ID int64 `json:"-"` + Name string `json:"name"` + HTTPMethod string `json:"http_method"` + HTTPPath string `json:"http_path"` + Parent *Permission `json:"-"` + Children []*Permission `json:"children"` + PathIDs []int64 `json:"-"` // full path id +} diff --git a/internal/entity/role_permission.go b/internal/entity/role_permission.go new file mode 100644 index 0000000..3268aae --- /dev/null +++ b/internal/entity/role_permission.go @@ -0,0 +1,14 @@ +package entity + +type RolePermission struct { + ID int64 `gorm:"column:id"` + RoleID int64 `gorm:"column:role_id"` + PermissionID int64 `gorm:"column:permission_id"` + + CreateTime int64 `gorm:"column:create_time;autoCreateTime"` + UpdateTime int64 `gorm:"column:update_time;autoUpdateTime"` +} + +func (c *RolePermission) TableName() string { + return "role_permission" +} diff --git a/internal/logic/permissionservice/list_permission_logic.go b/internal/logic/permissionservice/list_permission_logic.go deleted file mode 100644 index a5011da..0000000 --- a/internal/logic/permissionservice/list_permission_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package permissionservicelogic - -import ( - "context" - - "ark-permission/gen_result/pb/permission" - "ark-permission/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ListPermissionLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewListPermissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListPermissionLogic { - return &ListPermissionLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// ListPermission 取得完整權限 -func (l *ListPermissionLogic) ListPermission(in *permission.NoneReq) (*permission.ListPermissionResp, error) { - // todo: add your logic here and delete this line - - return &permission.ListPermissionResp{}, nil -} diff --git a/internal/logic/permissionservice/list_permission_status_logic.go b/internal/logic/permissionservice/list_permission_status_logic.go index ff1e1b6..f63dd12 100644 --- a/internal/logic/permissionservice/list_permission_status_logic.go +++ b/internal/logic/permissionservice/list_permission_status_logic.go @@ -1,6 +1,8 @@ package permissionservicelogic import ( + "ark-permission/internal/domain" + ers "code.30cm.net/wanderland/library-go/errors" "context" "ark-permission/gen_result/pb/permission" @@ -23,9 +25,31 @@ func NewListPermissionStatusLogic(ctx context.Context, svcCtx *svc.ServiceContex } } -// ListPermissionStatus 取得狀態 +// ListPermissionStatus 取得 func (l *ListPermissionStatusLogic) ListPermissionStatus(in *permission.NoneReq) (*permission.ListPermissionStatusResp, error) { - // todo: add your logic here and delete this line + // 搜尋所有權限 + permissions, err := l.svcCtx.Permission.FindAllOpenPermission(l.ctx) + if err != nil { + return nil, ers.DBError(err.Error()) + } - return &permission.ListPermissionStatusResp{}, nil + exist := make(map[string]struct{}) + status := make([]*permission.PermissionStatusItem, 0, len(permissions)) + for _, v := range permissions { + if _, ok := exist[v.Name]; !ok { + t, _ := domain.ToPermissionTypeCode(v.Type) + status = append(status, &permission.PermissionStatusItem{ + Id: v.Id, // 權限 ID + ParentId: v.Parent.Int64, // 上級權限的ID + Name: v.Name, // 權限名稱 + Status: permission.PermissionStatus(v.Status), // 權限開啟或關閉,判斷時上級權限如果關閉,下級也應該關閉對此人關閉 + Type: t.ToString(), // 前台權限,還是後台權限,還是其他中台之類的 + }) + exist[v.Name] = struct{}{} + } + } + + return &permission.ListPermissionStatusResp{ + Data: status, + }, nil } diff --git a/internal/logic/permissionservice/map_permission_status_logic.go b/internal/logic/permissionservice/map_permission_status_logic.go new file mode 100644 index 0000000..93e148c --- /dev/null +++ b/internal/logic/permissionservice/map_permission_status_logic.go @@ -0,0 +1,31 @@ +package permissionservicelogic + +import ( + "context" + + "ark-permission/gen_result/pb/permission" + "ark-permission/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MapPermissionStatusLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewMapPermissionStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MapPermissionStatusLogic { + return &MapPermissionStatusLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +// MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用 +func (l *MapPermissionStatusLogic) MapPermissionStatus(in *permission.NoneReq) (*permission.MapPermissionStatusResp, error) { + // todo: add your logic here and delete this line + + return &permission.MapPermissionStatusResp{}, nil +} diff --git a/internal/model/permission_model.go b/internal/model/permission_model.go index 719486a..61361e4 100755 --- a/internal/model/permission_model.go +++ b/internal/model/permission_model.go @@ -1,7 +1,10 @@ package model import ( - "github.com/zeromicro/go-zero/core/stores/cache" + "context" + "errors" + "fmt" + "github.com/zeromicro/go-zero/core/stores/sqlc" "github.com/zeromicro/go-zero/core/stores/sqlx" ) @@ -12,6 +15,7 @@ type ( // and implement the added methods in customPermissionModel. PermissionModel interface { permissionModel + FindAllOpenPermission(ctx context.Context) ([]*Permission, error) } customPermissionModel struct { @@ -19,9 +23,25 @@ type ( } ) -// NewPermissionModel returns a model for the database table. -func NewPermissionModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) PermissionModel { +// NewPermissionModel 者裡不用快取版本,因為快取我想要自己控制,也就是 local cache 不想上升至 redis 的層級 +// 因為我 permission 設計是由 sql 新增,重啟服務就可以重啟,或者是未來可以放一個mq 來同步 +func NewPermissionModel(conn sqlx.SqlConn) PermissionModel { return &customPermissionModel{ - defaultPermissionModel: newPermissionModel(conn, c, opts...), + defaultPermissionModel: newPermissionModel(conn), + } +} + +func (m *customPermissionModel) FindAllOpenPermission(ctx context.Context) ([]*Permission, error) { + query := fmt.Sprintf("select %s from %s where `status` = ? order by `create_time` asc", permissionRows, m.table) + var resp []*Permission + err := m.conn.QueryRowsCtx(ctx, &resp, query, 1) + + switch { + case err == nil: + return resp, nil + case errors.Is(err, sqlc.ErrNotFound): + return nil, ErrNotFound + default: + return nil, err } } diff --git a/internal/model/permission_model_gen.go b/internal/model/permission_model_gen.go index 74bd248..ce455e6 100755 --- a/internal/model/permission_model_gen.go +++ b/internal/model/permission_model_gen.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/zeromicro/go-zero/core/stores/builder" - "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/sqlc" "github.com/zeromicro/go-zero/core/stores/sqlx" "github.com/zeromicro/go-zero/core/stringx" @@ -20,9 +19,6 @@ var ( permissionRows = strings.Join(permissionFieldNames, ",") permissionRowsExpectAutoSet = strings.Join(stringx.Remove(permissionFieldNames, "`id`"), ",") permissionRowsWithPlaceHolder = strings.Join(stringx.Remove(permissionFieldNames, "`id`"), "=?,") + "=?" - - cachePermissionIdPrefix = "cache:permission:id:" - cachePermissionNamePrefix = "cache:permission:name:" ) type ( @@ -35,7 +31,7 @@ type ( } defaultPermissionModel struct { - sqlc.CachedConn + conn sqlx.SqlConn table string } @@ -52,42 +48,30 @@ type ( } ) -func newPermissionModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultPermissionModel { +func newPermissionModel(conn sqlx.SqlConn) *defaultPermissionModel { return &defaultPermissionModel{ - CachedConn: sqlc.NewConn(conn, c, opts...), - table: "`permission`", + conn: conn, + table: "`permission`", } } func (m *defaultPermissionModel) withSession(session sqlx.Session) *defaultPermissionModel { return &defaultPermissionModel{ - CachedConn: m.CachedConn.WithSession(session), - table: "`permission`", + conn: sqlx.NewSqlConnFromSession(session), + table: "`permission`", } } func (m *defaultPermissionModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - return err - } - - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, id) - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, data.Name) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("delete from %s where `id` = ?", m.table) - return conn.ExecCtx(ctx, query, id) - }, permissionIdKey, permissionNameKey) + query := fmt.Sprintf("delete from %s where `id` = ?", m.table) + _, err := m.conn.ExecCtx(ctx, query, id) return err } func (m *defaultPermissionModel) FindOne(ctx context.Context, id int64) (*Permission, error) { - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, id) + query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", permissionRows, m.table) var resp Permission - err := m.QueryRowCtx(ctx, &resp, permissionIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", permissionRows, m.table) - return conn.QueryRowCtx(ctx, v, query, id) - }) + err := m.conn.QueryRowCtx(ctx, &resp, query, id) switch err { case nil: return &resp, nil @@ -99,15 +83,9 @@ func (m *defaultPermissionModel) FindOne(ctx context.Context, id int64) (*Permis } func (m *defaultPermissionModel) FindOneByName(ctx context.Context, name string) (*Permission, error) { - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, name) var resp Permission - err := m.QueryRowIndexCtx(ctx, &resp, permissionNameKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) { - query := fmt.Sprintf("select %s from %s where `name` = ? limit 1", permissionRows, m.table) - if err := conn.QueryRowCtx(ctx, &resp, query, name); err != nil { - return nil, err - } - return resp.Id, nil - }, m.queryPrimary) + query := fmt.Sprintf("select %s from %s where `name` = ? limit 1", permissionRows, m.table) + err := m.conn.QueryRowCtx(ctx, &resp, query, name) switch err { case nil: return &resp, nil @@ -119,39 +97,17 @@ func (m *defaultPermissionModel) FindOneByName(ctx context.Context, name string) } func (m *defaultPermissionModel) Insert(ctx context.Context, data *Permission) (sql.Result, error) { - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, data.Id) - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, data.Name) - ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?)", m.table, permissionRowsExpectAutoSet) - return conn.ExecCtx(ctx, query, data.Parent, data.Name, data.HttpMethod, data.HttpPath, data.Status, data.Type, data.CreateTime, data.UpdateTime) - }, permissionIdKey, permissionNameKey) + query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?)", m.table, permissionRowsExpectAutoSet) + ret, err := m.conn.ExecCtx(ctx, query, data.Parent, data.Name, data.HttpMethod, data.HttpPath, data.Status, data.Type, data.CreateTime, data.UpdateTime) return ret, err } func (m *defaultPermissionModel) Update(ctx context.Context, newData *Permission) error { - data, err := m.FindOne(ctx, newData.Id) - if err != nil { - return err - } - - permissionIdKey := fmt.Sprintf("%s%v", cachePermissionIdPrefix, data.Id) - permissionNameKey := fmt.Sprintf("%s%v", cachePermissionNamePrefix, data.Name) - _, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) { - query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, permissionRowsWithPlaceHolder) - return conn.ExecCtx(ctx, query, newData.Parent, newData.Name, newData.HttpMethod, newData.HttpPath, newData.Status, newData.Type, newData.CreateTime, newData.UpdateTime, newData.Id) - }, permissionIdKey, permissionNameKey) + query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, permissionRowsWithPlaceHolder) + _, err := m.conn.ExecCtx(ctx, query, newData.Parent, newData.Name, newData.HttpMethod, newData.HttpPath, newData.Status, newData.Type, newData.CreateTime, newData.UpdateTime, newData.Id) return err } -func (m *defaultPermissionModel) formatPrimary(primary any) string { - return fmt.Sprintf("%s%v", cachePermissionIdPrefix, primary) -} - -func (m *defaultPermissionModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error { - query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", permissionRows, m.table) - return conn.QueryRowCtx(ctx, v, query, primary) -} - func (m *defaultPermissionModel) tableName() string { return m.table } diff --git a/internal/server/permissionservice/permission_service_server.go b/internal/server/permissionservice/permission_service_server.go index fb31be3..8f89f8f 100644 --- a/internal/server/permissionservice/permission_service_server.go +++ b/internal/server/permissionservice/permission_service_server.go @@ -22,19 +22,19 @@ func NewPermissionServiceServer(svcCtx *svc.ServiceContext) *PermissionServiceSe } } -// ListPermissionStatus 取得狀態 +// ListPermissionStatus 取得所有權限狀態列表,給前端表演用 func (s *PermissionServiceServer) ListPermissionStatus(ctx context.Context, in *permission.NoneReq) (*permission.ListPermissionStatusResp, error) { l := permissionservicelogic.NewListPermissionStatusLogic(ctx, s.svcCtx) return l.ListPermissionStatus(in) } -// ListPermission 取得完整權限 -func (s *PermissionServiceServer) ListPermission(ctx context.Context, in *permission.NoneReq) (*permission.ListPermissionResp, error) { - l := permissionservicelogic.NewListPermissionLogic(ctx, s.svcCtx) - return l.ListPermission(in) +// MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用 +func (s *PermissionServiceServer) MapPermissionStatus(ctx context.Context, in *permission.NoneReq) (*permission.MapPermissionStatusResp, error) { + l := permissionservicelogic.NewMapPermissionStatusLogic(ctx, s.svcCtx) + return l.MapPermissionStatus(in) } -// CheckPermissionByRole 透過角色 ID 來檢視權限 +// CheckPermissionByRole 透過角色 ID 來檢視權限,後台要通過時真的看這個 func (s *PermissionServiceServer) CheckPermissionByRole(ctx context.Context, in *permission.CheckPermissionByRoleReq) (*permission.PermissionResp, error) { l := permissionservicelogic.NewCheckPermissionByRoleLogic(ctx, s.svcCtx) return l.CheckPermissionByRole(in) diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 2784204..99e615f 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -4,10 +4,12 @@ import ( "ark-permission/internal/config" "ark-permission/internal/domain/repository" "ark-permission/internal/lib/required" + "ark-permission/internal/model" repo "ark-permission/internal/repository" ers "code.30cm.net/wanderland/library-go/errors" "code.30cm.net/wanderland/library-go/errors/code" "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/core/stores/sqlx" ) type ServiceContext struct { @@ -16,6 +18,8 @@ type ServiceContext struct { Validate required.Validate Redis redis.Redis TokenRedisRepo repository.TokenRepository + + Permission model.PermissionModel } func NewServiceContext(c config.Config) *ServiceContext { @@ -23,8 +27,11 @@ func NewServiceContext(c config.Config) *ServiceContext { if err != nil { panic(err) } + ers.Scope = code.CloudEPPermission + sqlConn := sqlx.NewMysql(c.DB.DsnString) + return &ServiceContext{ Config: c, Validate: required.MustValidator(), @@ -32,5 +39,6 @@ func NewServiceContext(c config.Config) *ServiceContext { TokenRedisRepo: repo.NewTokenRepository(repo.TokenRepositoryParam{ Store: newRedis, }), + Permission: model.NewPermissionModel(sqlConn), } } diff --git a/internal/usecase/permission_tree.go b/internal/usecase/permission_tree.go new file mode 100644 index 0000000..6b13315 --- /dev/null +++ b/internal/usecase/permission_tree.go @@ -0,0 +1,300 @@ +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 +} diff --git a/internal/usecase/permission_tree_test.go b/internal/usecase/permission_tree_test.go new file mode 100644 index 0000000..732b7f5 --- /dev/null +++ b/internal/usecase/permission_tree_test.go @@ -0,0 +1,611 @@ +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" + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +func TestNewPermissionTree(t *testing.T) { + tests := []struct { + name string + want *PermissionTree + }{ + { + name: "ok", + want: &PermissionTree{ + root: &usecase.Permission{ + ID: 0, + Name: "root", + Children: []*usecase.Permission{}, + }, + nodes: map[int64]*usecase.Permission{0: { + ID: 0, + Name: "root", + Children: []*usecase.Permission{}, + }}, // 根節點也加入 nodes 記錄 + paths: make(map[int64][]int), + names: make(map[string][]int64), + ids: make(map[int64]string), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tree := NewPermissionTree() + // 驗證 Root 的值 + assert.Equal(t, tree.root.ID, tt.want.root.ID) + assert.Equal(t, tree.root.Name, tt.want.root.Name) + assert.Equal(t, len(tree.root.Children), len(tt.want.root.Children)) + + assert.Equal(t, len(tree.nodes), len(tt.want.nodes)) + assert.Equal(t, len(tree.paths), len(tt.want.paths)) + assert.Equal(t, len(tree.ids), len(tt.want.ids)) + + }) + } +} + +// 測試 AddPermission 函數 +func TestAddPermission(t *testing.T) { + tree := NewPermissionTree() + + tests := []struct { + name string + parentID int64 + permission entity.Permission + expectedError error + }{ + { + name: "ok", + parentID: 0, + permission: entity.Permission{ID: 2, Name: "new_permission", HTTPPath: "/new", HTTPMethod: "POST"}, + }, + { + name: "Invalid Parent ID", + parentID: 99, // 無效的 parentID + permission: entity.Permission{ID: 3, Name: "invalid_parent", HTTPPath: "/invalid", HTTPMethod: "GET"}, + expectedError: ers.ResourceNotFound("failed to find ID 99"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tree.AddPermission(tt.parentID, tt.permission) + + // 錯誤檢查 + if !reflect.DeepEqual(err, tt.expectedError) { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } + + if tt.expectedError == nil { + // 檢查是否已正確插入到 nodes 中 + node, ok := tree.nodes[tt.permission.ID] + if !ok { + t.Errorf("expected permission with ID %d to be in nodes map", tt.permission.ID) + } + + if node.Name != tt.permission.Name { + t.Errorf("expected permission name %s, got %s", tt.permission.Name, node.Name) + } + + // 檢查父節點的子節點是否正確加入 + parentNode, _ := tree.FindPermissionByID(tt.parentID) + found := false + for _, child := range parentNode.Children { + if child.ID == tt.permission.ID { + found = true + break + } + } + if !found { + t.Errorf("expected permission ID %d to be child of parent ID %d", tt.permission.ID, tt.parentID) + } + } + }) + } +} + +// 測試 buildNodePath 函數 +func TestBuildNodePath(t *testing.T) { + // ======== 準備測試 ======== + tree := NewPermissionTree() + // 插入一些節點 + err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + assert.NoError(t, err) + err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission"}) + assert.NoError(t, err) + // ======== 準備測試 ======== + + tests := []struct { + name string + nodeID int64 + expectedPath []int + }{ + { + name: "Grandchild Permission Path", + nodeID: 4, + expectedPath: []int{0, 0, 0}, // 根 -> 子 -> 孫節點的索引 + }, + { + name: "Child Permission 1 Path", + nodeID: 2, + expectedPath: []int{0, 0}, // 根 -> 子節點的索引 + }, + { + name: "Child Permission 2 Path", + nodeID: 3, + expectedPath: []int{0, 1}, // 根 -> 子節點的索引 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 查找要測試的節點 + node, err := tree.FindPermissionByID(tt.nodeID) + if err != nil { + t.Fatalf("failed to find node with ID %d: %v", tt.nodeID, err) + } + // 構建節點的完整路徑 + // Grandchild Permission Path 測試孫節點的完整路徑,這應該包含根節點和子節點的索引,結果為 [0, 0],表示它是根節點的第一個子節點的第一個子節點。 + // Child Permission 1 Path 測試子節點 1 的路徑,應該是 [0],表示它是根節點的第一個子節點。 + // Child Permission 2 Path 測試子節點 2 的路徑,應該是 [1],表示它是根節點的第二個子節點。 + tree.buildNodePath(node, tt.nodeID) + + // 從 paths 中獲取構建好的路徑 + resultPath, ok := tree.paths[tt.nodeID] + if !ok { + t.Fatalf("path not found for node ID %d", tt.nodeID) + } + + // 比較結果路徑與預期路徑 + if !reflect.DeepEqual(resultPath, tt.expectedPath) { + t.Errorf("expected path %v, got %v", tt.expectedPath, resultPath) + } + }) + } +} + +// 測試 GetAllParentPermissionIDs 函數 +func TestGetAllParentPermissionIDs(t *testing.T) { + tree := NewPermissionTree() + // ======== 準備測試 ======== + // 添加節點 + err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + assert.NoError(t, err) + err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + assert.NoError(t, err) + err = tree.AddPermission(0, entity.Permission{ID: 6, Name: "root_permission_2"}) + assert.NoError(t, err) + err = tree.AddPermission(6, entity.Permission{ID: 6, Name: "child_permission_3"}) + assert.NoError(t, err) + // ======== 準備測試 ======== + + tests := []struct { + name string + permissions domain.Permissions + expectedResult []int64 + expectedError error + }{ + { + name: "Valid permissions with open status", + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + }, + expectedResult: []int64{0, 1, 2, 4}, // 根 -> 子 -> 孫節點 + expectedError: nil, + }, + { + name: "Valid multiple permissions with open status", + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_2": domain.PermissionStatusOpenCode, + }, + expectedResult: []int64{0, 1, 2, 4, 0, 1, 3, 5}, // 多個權限 + expectedError: nil, + }, + { + name: "Permission name not found", + permissions: domain.Permissions{ + "unknown_permission": domain.PermissionStatusOpenCode, + }, + expectedResult: nil, + expectedError: fmt.Errorf("permission with name unknown_permission not found"), + }, + { + name: "Permission close by parent node", + permissions: domain.Permissions{ + "root_permission_2": domain.PermissionStatusCloseCode, + }, + expectedResult: nil, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tree.GetAllParentPermissionIDs(tt.permissions) + + // 檢查返回結果 + if !reflect.DeepEqual(result, tt.expectedResult) { + t.Errorf("expected %v, got %v", tt.expectedResult, result) + } + + // 檢查錯誤 + if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } + }) + } +} + +// 測試 GetAllParentPermissionStatuses 函數 +func TestGetAllParentPermissionStatuses(t *testing.T) { + tree := NewPermissionTree() + + // 添加節點 + err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + assert.NoError(t, err) + err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + assert.NoError(t, err) + + tests := []struct { + name string + permissions domain.Permissions + expectedResult domain.Permissions + expectedError error + }{ + { + name: "Valid permissions with open status", + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + }, + expectedResult: domain.Permissions{ + "root_permission": domain.PermissionStatusOpenCode, + "child_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_1": domain.PermissionStatusOpenCode, + }, + expectedError: nil, + }, + { + name: "Multiple permissions with open status", + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_2": domain.PermissionStatusOpenCode, + }, + expectedResult: domain.Permissions{ + "root_permission": domain.PermissionStatusOpenCode, + "child_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_1": domain.PermissionStatusOpenCode, + "child_permission_2": domain.PermissionStatusOpenCode, + "grandchild_permission_2": domain.PermissionStatusOpenCode, + }, + expectedError: nil, + }, + { + name: "Permission name not found", + permissions: domain.Permissions{ + "unknown_permission": domain.PermissionStatusOpenCode, + }, + expectedResult: nil, + expectedError: fmt.Errorf("permission with name unknown_permission not found"), + }, + { + name: "Closed permissions are ignored", + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusCloseCode, + }, + expectedResult: domain.Permissions{}, + expectedError: nil, + }, + { + name: "Multiple permissions with close status", + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusCloseCode, + }, + expectedResult: domain.Permissions{}, + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tree.GetAllParentPermissionStatuses(tt.permissions) + + // 檢查返回結果 + if !reflect.DeepEqual(result, tt.expectedResult) { + t.Errorf("expected %v, got %v", tt.expectedResult, result) + } + + // 檢查錯誤 + if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } + }) + } +} + +// 測試 isParentPermissionOpen 函數 +func TestIsParentPermissionOpen(t *testing.T) { + tree := NewPermissionTree() + + // 添加節點 + err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + assert.NoError(t, err) + err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(2, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + assert.NoError(t, err) + + tests := []struct { + name string + nodeID int64 + permissions domain.Permissions + expectedOpen bool + }{ + { + name: "Parent has open child permission", + nodeID: 2, + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + }, + expectedOpen: true, + }, + { + name: "Parent has no open child permissions", + nodeID: 2, + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusCloseCode, + }, + expectedOpen: false, + }, + { + name: "Parent with multiple children, one open", + nodeID: 2, + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusCloseCode, + "grandchild_permission_2": domain.PermissionStatusOpenCode, + }, + expectedOpen: true, + }, + { + name: "Parent with no child permissions in list", + nodeID: 3, + permissions: domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + }, + expectedOpen: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node, err := tree.FindPermissionByID(tt.nodeID) + if err != nil { + t.Fatalf("failed to find node with ID %d: %v", tt.nodeID, err) + } + + result := tree.isParentPermissionOpen(node, tt.permissions) + if result != tt.expectedOpen { + t.Errorf("expected %v, got %v", tt.expectedOpen, result) + } + }) + } +} + +// 測試 GetRolePermissionTree 函數 +func TestGetRolePermissionTree(t *testing.T) { + tree := NewPermissionTree() + + // 添加節點 + err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + assert.NoError(t, err) + err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + assert.NoError(t, err) + err = tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + assert.NoError(t, err) + + // 測試數據 + tests := []struct { + name string + rolePermissions []entity.RolePermission + expectedResult domain.Permissions + expectedError error + }{ + { + name: "Single role permission with parent and child", + rolePermissions: []entity.RolePermission{ + {PermissionID: 4}, // grandchild_permission_1 + }, + expectedResult: domain.Permissions{ + "root_permission": domain.PermissionStatusOpenCode, + "child_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_1": domain.PermissionStatusOpenCode, + }, + expectedError: nil, + }, + { + name: "Multiple role permissions with parents and children", + rolePermissions: []entity.RolePermission{ + {PermissionID: 4}, // grandchild_permission_1 + {PermissionID: 5}, // grandchild_permission_2 + }, + expectedResult: domain.Permissions{ + "root_permission": domain.PermissionStatusOpenCode, + "child_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_1": domain.PermissionStatusOpenCode, + "child_permission_2": domain.PermissionStatusOpenCode, + "grandchild_permission_2": domain.PermissionStatusOpenCode, + }, + expectedError: nil, + }, + { + name: "Role permission with no children", + rolePermissions: []entity.RolePermission{ + {PermissionID: 2}, // child_permission_1 + }, + expectedResult: domain.Permissions{ + "root_permission": domain.PermissionStatusOpenCode, + "child_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_1": domain.PermissionStatusOpenCode, + }, + expectedError: nil, + }, + { + name: "Role permission not found", + rolePermissions: []entity.RolePermission{ + {PermissionID: 99}, // non-existent permission + }, + expectedResult: nil, + expectedError: fmt.Errorf("permission with ID %d not found", 99), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tree.GetRolePermissionTree(tt.rolePermissions) + + // 檢查返回結果 + if !reflect.DeepEqual(result, tt.expectedResult) { + t.Errorf("expected %v, got %v", tt.expectedResult, result) + } + + // 檢查錯誤 + if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) { + t.Errorf("expected error %v, got %v", tt.expectedError, err) + } + }) + } +} + +func BenchmarkGetAllParentPermissionIDs(b *testing.B) { + tree := NewPermissionTree() + + // 添加節點 + tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + + permissions := domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_2": domain.PermissionStatusOpenCode, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = tree.GetAllParentPermissionIDs(permissions) + } +} + +func BenchmarkGetAllParentPermissionStatuses(b *testing.B) { + tree := NewPermissionTree() + + // 添加節點 + tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + + permissions := domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_2": domain.PermissionStatusOpenCode, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = tree.GetAllParentPermissionStatuses(permissions) + } +} + +func BenchmarkIsParentPermissionOpen(b *testing.B) { + tree := NewPermissionTree() + + // 添加節點 + tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + tree.AddPermission(2, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + + permissions := domain.Permissions{ + "grandchild_permission_1": domain.PermissionStatusOpenCode, + "grandchild_permission_2": domain.PermissionStatusCloseCode, + } + + node, _ := tree.FindPermissionByID(2) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = tree.isParentPermissionOpen(node, permissions) + } +} + +func BenchmarkGetRolePermissionTree(b *testing.B) { + tree := NewPermissionTree() + + // 添加節點 + tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"}) + + rolePermissions := []entity.RolePermission{ + {PermissionID: 4}, // grandchild_permission_1 + {PermissionID: 5}, // grandchild_permission_2 + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = tree.GetRolePermissionTree(rolePermissions) + } +} + +func BenchmarkBuildNodePath(b *testing.B) { + tree := NewPermissionTree() + + // 添加節點 + tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"}) + tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"}) + tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"}) + tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"}) + + node, _ := tree.FindPermissionByID(4) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tree.buildNodePath(node, 4) + } +}