rbac_service.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. // RBAC 角色-权限点矩阵配置服务(T6.5-e,RBAC 三层模型的"超管配置后台",2026-11-06 用户拍板 #2)。
  2. //
  3. // 职责(平台级,不带多租户作用域——角色权限矩阵是全局配置,与机构/校区无关):
  4. // - 查某角色当前矩阵 + 全量权限点字典(超管配置页渲染勾选树用);
  5. // - 全量保存某角色矩阵(删旧插新事务;校验 roleCode 存在于 t_role + 权限码存在于 t_permission);
  6. // - 保存成功后调 rbac.Resolver.Invalidate() 清本实例缓存(跨实例靠 TTL 30s 自然过期)。
  7. //
  8. // 与 OrgService 的分工:OrgService 面向机构/校区(随请求注入 *scope.Scope,强制 Scoping);
  9. // 本服务面向 RBAC 平台级字典(t_role / t_permission / t_role_permission),无作用域过滤——
  10. // 多租户防线不在本层(路由角色白名单 RequireRole("super_admin") 是唯一访问入口,见 handler)。
  11. //
  12. // 依赖方向:service → org/model + org/repository(PermissionRepo) + org/rbac(Resolver) +
  13. // infra/errors(全下层/同域),不依赖 handler/main。
  14. package service
  15. import (
  16. "context"
  17. "gorm.io/gorm"
  18. apperrors "yuansiwei/unified-backend/internal/infra/errors"
  19. "yuansiwei/unified-backend/internal/org/model"
  20. "yuansiwei/unified-backend/internal/org/rbac"
  21. "yuansiwei/unified-backend/internal/org/repository"
  22. )
  23. // RolePermissionView 角色-权限点矩阵查询出参(超管配置页渲染依据)。
  24. type RolePermissionView struct {
  25. RoleCode string `json:"roleCode"` // 角色码(回显,t_role 字典值)
  26. Assigned []string `json:"assigned"` // 该角色当前拥有的权限点 code(升序去重;非 nil)
  27. Dictionary []model.Permission `json:"dictionary"` // 全量 active 权限点(非 nil;前端按 parent_code 构树做勾选)
  28. }
  29. // RolePermissionService RBAC 矩阵配置服务(启动装配一次,全进程共享)。
  30. type RolePermissionService struct {
  31. db *gorm.DB
  32. permRepo *repository.PermissionRepo
  33. resolver *rbac.Resolver // 保存成功后调 Invalidate 清本实例缓存(nil 时跳过)
  34. }
  35. // NewRolePermissionService 构造 RBAC 矩阵配置服务(fail-fast:db/permRepo 必填)。
  36. //
  37. // 入参:
  38. // - db *gorm.DB:数据库句柄(必填,用于 t_role 角色存在性校验——平台级直查,无作用域);
  39. // - permRepo *repository.PermissionRepo:权限点/矩阵仓储(必填);
  40. // - resolver *rbac.Resolver:RBAC 解析器(可空;nil 时保存后不清缓存——纯单测/无缓存场景,
  41. // 生产装配必给,否则超管改配最长 30s 才生效)。
  42. func NewRolePermissionService(db *gorm.DB, permRepo *repository.PermissionRepo, resolver *rbac.Resolver) (*RolePermissionService, error) {
  43. if db == nil {
  44. return nil, apperrors.New(apperrors.CodeInternal, "rbac service: DB 未注入")
  45. }
  46. if permRepo == nil {
  47. return nil, apperrors.New(apperrors.CodeInternal, "rbac service: PermissionRepo 未注入")
  48. }
  49. return &RolePermissionService{db: db, permRepo: permRepo, resolver: resolver}, nil
  50. }
  51. // ListRolePermissions 查某角色的矩阵 + 全量权限点字典。
  52. //
  53. // 入参:ctx 请求上下文;roleCode 角色码(t_role 字典值)。
  54. // 出参:
  55. // - *RolePermissionView:角色矩阵 + 字典(非 nil 切片)。
  56. // - error:ErrBadParam(2001) roleCode 不在 t_role 字典 / ErrDBError(5002)。
  57. func (s *RolePermissionService) ListRolePermissions(ctx context.Context, roleCode string) (*RolePermissionView, error) {
  58. if err := s.validateRole(ctx, roleCode); err != nil {
  59. return nil, err
  60. }
  61. assigned, err := s.permRepo.RolePermissionCodes(ctx, roleCode)
  62. if err != nil {
  63. return nil, err
  64. }
  65. dictionary, err := s.permRepo.AllActive(ctx)
  66. if err != nil {
  67. return nil, err
  68. }
  69. return &RolePermissionView{RoleCode: roleCode, Assigned: assigned, Dictionary: dictionary}, nil
  70. }
  71. // SaveRolePermissions 全量保存某角色的权限点矩阵(删旧插新;保存后清本实例缓存)。
  72. //
  73. // 入参:
  74. // - ctx 请求上下文;
  75. // - roleCode 角色码(须存在于 t_role 字典);
  76. // - codes []string:目标全量权限点集合(可为空 = 清空该角色矩阵;每项须存在于 t_permission)。
  77. //
  78. // 出参:
  79. // - error:ErrBadParam(2001) roleCode 非法 / 含未知权限码 / ErrDBError(5002)(含事务回滚)。
  80. //
  81. // 语义:保存即全量替换(前端"取消全部勾选"合法 = 传空集);幂等(目标与原矩阵一致时等价于原样重插)。
  82. // 保存成功后调 resolver.Invalidate() 清本实例缓存,使超管改配立即对本实例生效(跨实例靠 TTL 30s)。
  83. func (s *RolePermissionService) SaveRolePermissions(ctx context.Context, roleCode string, codes []string) error {
  84. if err := s.validateRole(ctx, roleCode); err != nil {
  85. return err
  86. }
  87. if err := s.validateCodes(ctx, codes); err != nil {
  88. return err
  89. }
  90. if err := s.permRepo.SaveRolePermissions(ctx, roleCode, codes); err != nil {
  91. return err
  92. }
  93. if s.resolver != nil {
  94. s.resolver.Invalidate() // 清本实例缓存(跨实例不广播,TTL 30s 自然一致)
  95. }
  96. return nil
  97. }
  98. // validateRole 校验 roleCode 存在于 t_role 字典(平台级直查,无作用域)。
  99. //
  100. // 出参:error——ErrBadParam(2001) 角色码非法 / ErrDBError(5002)。
  101. func (s *RolePermissionService) validateRole(ctx context.Context, roleCode string) error {
  102. if roleCode == "" {
  103. return apperrors.ErrBadParam
  104. }
  105. var count int64
  106. if err := s.db.WithContext(ctx).Model(&model.RoleModel{}).
  107. Where("role_code = ?", roleCode).
  108. Count(&count).Error; err != nil {
  109. return apperrors.WithCause(apperrors.ErrDBError, err)
  110. }
  111. if count == 0 {
  112. return apperrors.New(apperrors.CodeBadParam, "角色码非法(不在 t_role 字典)")
  113. }
  114. return nil
  115. }
  116. // validateCodes 校验权限码全部存在于 t_permission(防"保存未知 code"污染矩阵)。
  117. //
  118. // 语义:空集合法(清空矩阵);非空时要求每项都在 t_permission(任意 status——
  119. // 允许预置 disabled 权限点,待其 active 后自然显现,不强制必须 active)。
  120. // 出参:error——ErrBadParam(2001) 含未知权限码 / ErrDBError(5002)。
  121. func (s *RolePermissionService) validateCodes(ctx context.Context, codes []string) error {
  122. if len(codes) == 0 {
  123. return nil // 清空语义(合法)
  124. }
  125. // 去重(避免重复 code 干扰 count 比对)
  126. seen := make(map[string]struct{}, len(codes))
  127. unique := make([]string, 0, len(codes))
  128. for _, c := range codes {
  129. if c == "" {
  130. return apperrors.New(apperrors.CodeBadParam, "权限码不能为空字符串")
  131. }
  132. if _, ok := seen[c]; !ok {
  133. seen[c] = struct{}{}
  134. unique = append(unique, c)
  135. }
  136. }
  137. var count int64
  138. if err := s.db.WithContext(ctx).Model(&model.Permission{}).
  139. Where("code IN ?", unique).
  140. Count(&count).Error; err != nil {
  141. return apperrors.WithCause(apperrors.ErrDBError, err)
  142. }
  143. if count != int64(len(unique)) {
  144. return apperrors.New(apperrors.CodeBadParam, "含未知权限码(不在 t_permission 字典)")
  145. }
  146. return nil
  147. }