Files
junhong_cmp_fiber/internal/service/permission/service.go
break b3499adfca 固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。

Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。

Confidence: medium

Scope-risk: broad

Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。

Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。

Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
2026-08-03 09:47:22 +08:00

504 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package 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)
}