feat: add permission tree

This commit is contained in:
daniel.w 2024-08-17 17:32:15 +08:00
parent 070ed823a8
commit f53b3beecb
19 changed files with 1151 additions and 120 deletions

View File

@ -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...)

View File

@ -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

View File

@ -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

View File

@ -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()); -- 明碼查詢

View File

@ -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<int64,PermissionStatus> 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);

4
go.mod
View File

@ -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

View File

@ -14,4 +14,8 @@ type Config struct {
Expired time.Duration
Secret string
}
// 加上DB結構體
DB struct {
DsnString string
}
}

View File

@ -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"
)

View File

@ -0,0 +1,38 @@
package usecase
import (
"ark-permission/internal/domain"
"ark-permission/internal/entity"
)
// PermissionTreeManager 定義一組操作權限樹的接口
// 這個名稱說明它是專門負責管理和操作權限樹的管理器
type PermissionTreeManager interface {
// AddPermission 將一個新的權限節點插入到樹中
// key 是父節點的IDvalue 是要插入的 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
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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),
}
}

View File

@ -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
}

View File

@ -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)
}
}