固化七月迭代审计治理进展以隔离线上热修
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 参数类型错误未通过。
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"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"
|
||||
@@ -28,6 +29,8 @@ type AccountServiceInterface interface {
|
||||
|
||||
// Service 权限业务服务
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
accessAudit accessauditapp.Writer
|
||||
permissionStore *postgres.PermissionStore
|
||||
accountRoleStore *postgres.AccountRoleStore
|
||||
rolePermStore *postgres.RolePermissionStore
|
||||
@@ -35,6 +38,12 @@ type Service struct {
|
||||
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,
|
||||
@@ -60,26 +69,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePermissionRequest)
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
// 验证权限编码格式
|
||||
if !permCodeRegex.MatchString(req.PermCode) {
|
||||
return nil, errors.New(errors.CodeInvalidPermCode, "权限编码格式不正确(应为 module:action 格式)")
|
||||
}
|
||||
|
||||
// 检查权限编码唯一性
|
||||
existing, err := s.permissionStore.GetByCode(ctx, req.PermCode)
|
||||
if err == nil && existing != nil {
|
||||
return nil, errors.New(errors.CodePermCodeExists, "权限编码已存在")
|
||||
}
|
||||
|
||||
// 验证 parent_id 存在(如果提供)
|
||||
if req.ParentID != nil {
|
||||
parent, err := s.permissionStore.GetByID(ctx, *req.ParentID)
|
||||
if err != nil || parent == nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "上级权限不存在")
|
||||
}
|
||||
}
|
||||
|
||||
// 创建权限
|
||||
permission := &model.Permission{
|
||||
PermName: req.PermName,
|
||||
PermCode: req.PermCode,
|
||||
@@ -89,14 +78,59 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePermissionRequest)
|
||||
ParentID: req.ParentID,
|
||||
Sort: req.Sort,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: currentUserID,
|
||||
Updater: currentUserID,
|
||||
},
|
||||
}
|
||||
|
||||
// 如果未指定 platform,默认为 all
|
||||
if permission.Platform == "" {
|
||||
permission.Platform = constants.PlatformAll
|
||||
}
|
||||
|
||||
if err := s.permissionStore.Create(ctx, permission); err != nil {
|
||||
// 验证权限编码格式
|
||||
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, "创建权限失败")
|
||||
}
|
||||
|
||||
@@ -131,6 +165,7 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePermission
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取权限失败")
|
||||
}
|
||||
beforeData := permissionAuditData(permission)
|
||||
|
||||
// 更新字段
|
||||
if req.PermName != nil {
|
||||
@@ -139,12 +174,20 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePermission
|
||||
if req.PermCode != nil {
|
||||
// 验证权限编码格式
|
||||
if !permCodeRegex.MatchString(*req.PermCode) {
|
||||
return nil, errors.New(errors.CodeInvalidPermCode, "权限编码格式不正确(应为 module:action 格式)")
|
||||
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 {
|
||||
return nil, errors.New(errors.CodePermCodeExists, "权限编码已存在")
|
||||
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
|
||||
}
|
||||
@@ -157,8 +200,14 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePermission
|
||||
if req.ParentID != nil {
|
||||
// 验证 parent_id 存在
|
||||
parent, err := s.permissionStore.GetByID(ctx, *req.ParentID)
|
||||
if err != nil || parent == nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "上级权限不存在")
|
||||
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
|
||||
}
|
||||
@@ -171,9 +220,25 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePermission
|
||||
|
||||
permission.Updater = currentUserID
|
||||
|
||||
if err := s.permissionStore.Update(ctx, permission); err != nil {
|
||||
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
|
||||
}
|
||||
@@ -181,7 +246,7 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePermission
|
||||
// Delete 软删除权限
|
||||
func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
// 检查权限存在
|
||||
_, err := s.permissionStore.GetByID(ctx, id)
|
||||
permission, err := s.permissionStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodePermissionNotFound, "权限不存在")
|
||||
@@ -189,9 +254,27 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取权限失败")
|
||||
}
|
||||
|
||||
if err := s.permissionStore.Delete(ctx, id); err != nil {
|
||||
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
|
||||
}
|
||||
@@ -351,3 +434,70 @@ func (s *Service) matchPermission(permissions []permissionCacheItem, permCode st
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user