// Package permission 提供权限管理的业务逻辑服务 // 包含权限创建、查询、更新、删除、权限检查等功能 package permission import ( "context" "encoding/json" "regexp" "time" accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/internal/store" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) // permCodeRegex 权限编码格式验证正则(module:action) var permCodeRegex = regexp.MustCompile(`^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$`) type AccountServiceInterface interface { GetRoleIDsForAccount(ctx context.Context, accountID uint) ([]uint, error) } // Service 权限业务服务 type Service struct { db *gorm.DB accessAudit accessauditapp.Writer permissionStore *postgres.PermissionStore accountRoleStore *postgres.AccountRoleStore rolePermStore *postgres.RolePermissionStore accountService AccountServiceInterface redisClient *redis.Client } // SetAccessAudit 注入权限定义变更的事务审计接缝。 func (s *Service) SetAccessAudit(db *gorm.DB, writer accessauditapp.Writer) { s.db = db s.accessAudit = writer } // New 创建权限服务 func New( permissionStore *postgres.PermissionStore, accountRoleStore *postgres.AccountRoleStore, rolePermStore *postgres.RolePermissionStore, accountService AccountServiceInterface, redisClient *redis.Client, ) *Service { return &Service{ permissionStore: permissionStore, accountRoleStore: accountRoleStore, rolePermStore: rolePermStore, accountService: accountService, redisClient: redisClient, } } // Create 创建权限 func (s *Service) Create(ctx context.Context, req *dto.CreatePermissionRequest) (*model.Permission, error) { // 获取当前用户 ID currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } permission := &model.Permission{ PermName: req.PermName, PermCode: req.PermCode, PermType: req.PermType, Platform: req.Platform, URL: req.URL, ParentID: req.ParentID, Sort: req.Sort, Status: constants.StatusEnabled, BaseModel: model.BaseModel{ Creator: currentUserID, Updater: currentUserID, }, } if permission.Platform == "" { permission.Platform = constants.PlatformAll } // 验证权限编码格式 if !permCodeRegex.MatchString(req.PermCode) { appErr := errors.New(errors.CodeInvalidPermCode, "权限编码格式不正确(应为 module:action 格式)") s.recordFailure(ctx, constants.AuditActionPermissionCreated, "拒绝创建非法权限编码", constants.AuditResultDenied, permission, nil, appErr) return nil, appErr } // 检查权限编码唯一性 existing, err := s.permissionStore.GetByCode(ctx, req.PermCode) if err == nil && existing != nil { appErr := errors.New(errors.CodePermCodeExists, "权限编码已存在") s.recordFailure(ctx, constants.AuditActionPermissionCreated, "拒绝创建重复权限编码", constants.AuditResultDenied, permission, nil, appErr) return nil, appErr } if err != nil && err != gorm.ErrRecordNotFound { s.recordFailure(ctx, constants.AuditActionPermissionCreated, "创建权限失败", constants.AuditResultFailed, permission, nil, err) return nil, errors.Wrap(errors.CodeInternalError, err, "检查权限编码失败") } // 验证 parent_id 存在(如果提供) if req.ParentID != nil { parent, err := s.permissionStore.GetByID(ctx, *req.ParentID) if err != nil && err != gorm.ErrRecordNotFound { s.recordFailure(ctx, constants.AuditActionPermissionCreated, "创建权限失败", constants.AuditResultFailed, permission, nil, err) return nil, errors.Wrap(errors.CodeInternalError, err, "检查上级权限失败") } if err == gorm.ErrRecordNotFound || parent == nil { appErr := errors.New(errors.CodeNotFound, "上级权限不存在") s.recordFailure(ctx, constants.AuditActionPermissionCreated, "拒绝创建上级不存在的权限", constants.AuditResultDenied, permission, nil, appErr) return nil, appErr } } if err := s.runAccessTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewPermissionStore(tx).Create(ctx, permission); err != nil { return err } return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionPermissionCreated, Summary: "创建权限", Result: constants.AuditResultSuccess, OperatorID: currentUserID, Permissions: permissionChanges(permission, nil, permissionAuditData(permission)), }) }); err != nil { permission.ID = 0 s.recordFailure(ctx, constants.AuditActionPermissionCreated, "创建权限失败", constants.AuditResultFailed, permission, nil, err) return nil, errors.Wrap(errors.CodeInternalError, err, "创建权限失败") } return permission, nil } // Get 获取权限 func (s *Service) Get(ctx context.Context, id uint) (*model.Permission, error) { permission, err := s.permissionStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodePermissionNotFound, "权限不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取权限失败") } return permission, nil } // Update 更新权限 func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePermissionRequest) (*model.Permission, error) { // 获取当前用户 ID currentUserID := middleware.GetUserIDFromContext(ctx) if currentUserID == 0 { return nil, errors.New(errors.CodeUnauthorized, "未授权访问") } // 获取现有权限 permission, err := s.permissionStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodePermissionNotFound, "权限不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "获取权限失败") } beforeData := permissionAuditData(permission) // 更新字段 if req.PermName != nil { permission.PermName = *req.PermName } if req.PermCode != nil { // 验证权限编码格式 if !permCodeRegex.MatchString(*req.PermCode) { appErr := errors.New(errors.CodeInvalidPermCode, "权限编码格式不正确(应为 module:action 格式)") s.recordFailure(ctx, constants.AuditActionPermissionUpdated, "拒绝更新非法权限编码", constants.AuditResultDenied, permission, beforeData, appErr) return nil, appErr } // 检查新权限编码唯一性 existing, err := s.permissionStore.GetByCode(ctx, *req.PermCode) if err == nil && existing != nil && existing.ID != id { appErr := errors.New(errors.CodePermCodeExists, "权限编码已存在") s.recordFailure(ctx, constants.AuditActionPermissionUpdated, "拒绝更新重复权限编码", constants.AuditResultDenied, permission, beforeData, appErr) return nil, appErr } if err != nil && err != gorm.ErrRecordNotFound { s.recordFailure(ctx, constants.AuditActionPermissionUpdated, "更新权限失败", constants.AuditResultFailed, permission, beforeData, err) return nil, errors.Wrap(errors.CodeInternalError, err, "检查权限编码失败") } permission.PermCode = *req.PermCode } if req.Platform != nil { permission.Platform = *req.Platform } if req.URL != nil { permission.URL = *req.URL } if req.ParentID != nil { // 验证 parent_id 存在 parent, err := s.permissionStore.GetByID(ctx, *req.ParentID) if err != nil && err != gorm.ErrRecordNotFound { s.recordFailure(ctx, constants.AuditActionPermissionUpdated, "更新权限失败", constants.AuditResultFailed, permission, beforeData, err) return nil, errors.Wrap(errors.CodeInternalError, err, "检查上级权限失败") } if err == gorm.ErrRecordNotFound || parent == nil { appErr := errors.New(errors.CodeNotFound, "上级权限不存在") s.recordFailure(ctx, constants.AuditActionPermissionUpdated, "拒绝更新不存在的上级权限", constants.AuditResultDenied, permission, beforeData, appErr) return nil, appErr } permission.ParentID = req.ParentID } if req.Sort != nil { permission.Sort = *req.Sort } if req.Status != nil { permission.Status = *req.Status } permission.Updater = currentUserID var accountIDs []uint if err := s.runAccessTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewPermissionStore(tx).Update(ctx, permission); err != nil { return err } var err error accountIDs, err = permissionCacheAccountIDs(ctx, tx, permission.ID) if err != nil { return err } return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionPermissionUpdated, Summary: "更新权限", Result: constants.AuditResultSuccess, OperatorID: currentUserID, Permissions: permissionChanges(permission, beforeData, permissionAuditData(permission)), }) }); err != nil { s.recordFailure(ctx, constants.AuditActionPermissionUpdated, "更新权限失败", constants.AuditResultFailed, permission, beforeData, err) return nil, errors.Wrap(errors.CodeInternalError, err, "更新权限失败") } s.clearPermissionCaches(ctx, accountIDs) return permission, nil } // Delete 软删除权限 func (s *Service) Delete(ctx context.Context, id uint) error { // 检查权限存在 permission, err := s.permissionStore.GetByID(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodePermissionNotFound, "权限不存在") } return errors.Wrap(errors.CodeInternalError, err, "获取权限失败") } operatorID := middleware.GetUserIDFromContext(ctx) beforeData := permissionAuditData(permission) var accountIDs []uint if err := s.runAccessTransaction(ctx, func(tx *gorm.DB) error { if err := postgres.NewPermissionStore(tx).Delete(ctx, id); err != nil { return err } var err error accountIDs, err = permissionCacheAccountIDs(ctx, tx, permission.ID) if err != nil { return err } return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionPermissionDeleted, Summary: "删除权限", Result: constants.AuditResultSuccess, OperatorID: operatorID, Permissions: permissionChanges(permission, beforeData, map[string]any{"deleted": true}), }) }); err != nil { s.recordFailure(ctx, constants.AuditActionPermissionDeleted, "删除权限失败", constants.AuditResultFailed, permission, beforeData, err) return errors.Wrap(errors.CodeInternalError, err, "删除权限失败") } s.clearPermissionCaches(ctx, accountIDs) return nil } // List 查询权限列表 func (s *Service) List(ctx context.Context, req *dto.PermissionListRequest) ([]*model.Permission, int64, error) { opts := &store.QueryOptions{ Page: req.Page, PageSize: req.PageSize, OrderBy: "sort ASC, id ASC", } if opts.Page == 0 { opts.Page = 1 } if opts.PageSize == 0 { opts.PageSize = constants.DefaultPageSize } filters := make(map[string]interface{}) if req.PermName != "" { filters["perm_name"] = req.PermName } if req.PermCode != "" { filters["perm_code"] = req.PermCode } if req.PermType != nil { filters["perm_type"] = *req.PermType } if req.Platform != "" { filters["platform"] = req.Platform } if req.AvailableForRoleType != nil { filters["available_for_role_type"] = *req.AvailableForRoleType } if req.ParentID != nil { filters["parent_id"] = *req.ParentID } if req.Status != nil { filters["status"] = *req.Status } return s.permissionStore.List(ctx, opts, filters) } // GetTree 获取权限树 func (s *Service) GetTree(ctx context.Context, req *dto.PermissionTreeRequest) ([]*dto.PermissionTreeNode, error) { permissions, err := s.permissionStore.GetAll(ctx, req.AvailableForRoleType, req.Status) if err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "获取权限列表失败") } return buildPermissionTree(permissions), nil } // buildPermissionTree 构建权限树 func buildPermissionTree(permissions []*model.Permission) []*dto.PermissionTreeNode { nodeMap := make(map[uint]*dto.PermissionTreeNode) for _, p := range permissions { nodeMap[p.ID] = &dto.PermissionTreeNode{ ID: p.ID, PermName: p.PermName, PermCode: p.PermCode, PermType: p.PermType, Platform: p.Platform, AvailableForRoleTypes: p.AvailableForRoleTypes, URL: p.URL, Sort: p.Sort, Status: p.Status, Children: make([]*dto.PermissionTreeNode, 0), } } var roots []*dto.PermissionTreeNode for _, p := range permissions { node := nodeMap[p.ID] if p.ParentID == nil || *p.ParentID == 0 { roots = append(roots, node) } else if parent, ok := nodeMap[*p.ParentID]; ok { parent.Children = append(parent.Children, node) } else { roots = append(roots, node) } } return roots } // permissionCacheItem 权限缓存项 type permissionCacheItem struct { PermCode string `json:"perm_code"` Platform string `json:"platform"` } // CheckPermission 检查用户是否拥有指定权限(实现 PermissionChecker 接口) // userID: 用户ID // permCode: 权限编码 // platform: 端口类型 (all/web/h5) func (s *Service) CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error) { userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeSuperAdmin { return true, nil } cacheKey := constants.RedisUserPermissionsKey(userID) cachedData, err := s.redisClient.Get(ctx, cacheKey).Result() if err == nil && cachedData != "" { var permissions []permissionCacheItem if err := json.Unmarshal([]byte(cachedData), &permissions); err == nil { return s.matchPermission(permissions, permCode, platform), nil } } roleIDs, err := s.accountService.GetRoleIDsForAccount(ctx, userID) if err != nil { return false, errors.Wrap(errors.CodeInternalError, err, "查询用户角色失败") } if len(roleIDs) == 0 { return false, nil } permIDs, err := s.rolePermStore.GetPermIDsByRoleIDs(ctx, roleIDs) if err != nil { return false, errors.Wrap(errors.CodeInternalError, err, "查询角色权限失败") } if len(permIDs) == 0 { return false, nil } permissions, err := s.permissionStore.GetByIDs(ctx, permIDs) if err != nil { return false, errors.Wrap(errors.CodeInternalError, err, "查询权限详情失败") } cacheItems := make([]permissionCacheItem, 0, len(permissions)) for _, perm := range permissions { cacheItems = append(cacheItems, permissionCacheItem{ PermCode: perm.PermCode, Platform: perm.Platform, }) } if cacheData, err := json.Marshal(cacheItems); err == nil { s.redisClient.Set(ctx, cacheKey, cacheData, 30*time.Minute) } return s.matchPermission(cacheItems, permCode, platform), nil } func (s *Service) matchPermission(permissions []permissionCacheItem, permCode string, platform string) bool { for _, perm := range permissions { if perm.PermCode == permCode { if perm.Platform == constants.PlatformAll || perm.Platform == platform { return true } } } return false } func (s *Service) runAccessTransaction(ctx context.Context, fn func(tx *gorm.DB) error) error { if s.db == nil || s.accessAudit == nil { return errors.New(errors.CodeInvalidStatus, "权限审计接缝未配置") } return s.db.WithContext(ctx).Transaction(fn) } func (s *Service) recordFailure( ctx context.Context, actionCode, summary, result string, permission *model.Permission, beforeData map[string]any, originalErr error, ) { accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{ ActionCode: actionCode, Summary: summary, Result: result, OperatorID: middleware.GetUserIDFromContext(ctx), Permissions: permissionChanges(permission, beforeData, nil), }, originalErr) } func permissionChanges(permission *model.Permission, beforeData, afterData map[string]any) []accessauditapp.PermissionChange { if permission == nil { return nil } return []accessauditapp.PermissionChange{{Permission: permission, BeforeData: beforeData, AfterData: afterData}} } func permissionAuditData(permission *model.Permission) map[string]any { if permission == nil { return nil } return map[string]any{ "perm_name": permission.PermName, "perm_code": permission.PermCode, "perm_type": permission.PermType, "platform": permission.Platform, "available_for_role_types": permission.AvailableForRoleTypes, "url": permission.URL, "parent_id": permission.ParentID, "sort": permission.Sort, "status": permission.Status, } } func permissionCacheAccountIDs(ctx context.Context, tx *gorm.DB, permissionID uint) ([]uint, error) { var roleIDs []uint if err := tx.WithContext(ctx).Model(&model.RolePermission{}). Where("perm_id = ?", permissionID).Pluck("role_id", &roleIDs).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询权限关联角色失败") } if len(roleIDs) == 0 { return nil, nil } var accountIDs []uint if err := tx.WithContext(ctx).Model(&model.AccountRole{}). Where("role_id IN ?", roleIDs).Distinct().Pluck("account_id", &accountIDs).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询权限关联账号失败") } return accountIDs, nil } func (s *Service) clearPermissionCaches(ctx context.Context, accountIDs []uint) { if len(accountIDs) == 0 || s.redisClient == nil { return } pipe := s.redisClient.Pipeline() for _, accountID := range accountIDs { pipe.Del(ctx, constants.RedisUserPermissionsKey(accountID)) } _, _ = pipe.Exec(ctx) }