package role import ( "context" "fmt" "time" "perms-system-server/internal/consts" authHelper "perms-system-server/internal/logic/auth" "perms-system-server/internal/model/roleperm" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/types" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/sqlx" ) type BindRolePermsLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewBindRolePermsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindRolePermsLogic { return &BindRolePermsLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // BindRolePerms 绑定角色权限。对指定角色做权限全量覆盖(diff 后批量新增/删除),变更后自动清理该角色下所有用户的权限缓存。 func (l *BindRolePermsLogic) BindRolePerms(req *types.BindPermsReq) error { role, err := l.svcCtx.SysRoleModel.FindOne(l.ctx, req.RoleId) if err != nil { return response.ErrNotFound("角色不存在") } if err := authHelper.RequireProductAdminFor(l.ctx, role.ProductCode); err != nil { return err } permIds := req.PermIds if len(permIds) > 0 { seen := make(map[int64]bool, len(permIds)) uniqueIds := make([]int64, 0, len(permIds)) for _, id := range permIds { if !seen[id] { seen[id] = true uniqueIds = append(uniqueIds, id) } } permIds = uniqueIds } if len(permIds) > 0 { perms, err := l.svcCtx.SysPermModel.FindByIds(l.ctx, permIds) if err != nil { return err } if len(perms) != len(permIds) { return response.ErrBadRequest("包含无效的权限ID") } for _, p := range perms { if p.ProductCode != role.ProductCode { return response.ErrBadRequest("不能绑定其他产品的权限") } if p.Status != consts.StatusEnabled { return response.ErrBadRequest(fmt.Sprintf("权限 %s 已被禁用,无法绑定", p.Code)) } } } newSet := make(map[int64]bool, len(permIds)) for _, id := range permIds { newSet[id] = true } // 审计 M-R10-2:把 existing 读 + diff + delete/insert 整段收敛进事务,并以 LockByIdTx // 锁住 sys_role 行。两个并发的"完全覆盖 bind" 会在 role 行级别被串行化,"A 完成 → B 基于 // A 的最终态重新覆盖"成为唯一可能的交错,彻底消除"A/B diff 各自的 toRemove/toAdd 在时间 // 线上交织、最终态是两者都不想要的第三态"这一 RMW 类 bug。 diffCounts := struct { add int remove int }{} if err := l.svcCtx.SysRolePermModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error { if _, err := l.svcCtx.SysRoleModel.LockByIdTx(ctx, session, req.RoleId); err != nil { return err } existingPermIds, err := l.svcCtx.SysRolePermModel.FindPermIdsByRoleIdTx(ctx, session, req.RoleId) if err != nil { return err } existingSet := make(map[int64]bool, len(existingPermIds)) for _, id := range existingPermIds { existingSet[id] = true } var toAdd []int64 for _, id := range permIds { if !existingSet[id] { toAdd = append(toAdd, id) } } var toRemove []int64 for _, id := range existingPermIds { if !newSet[id] { toRemove = append(toRemove, id) } } diffCounts.add, diffCounts.remove = len(toAdd), len(toRemove) if len(toAdd) == 0 && len(toRemove) == 0 { return nil } if err := l.svcCtx.SysRolePermModel.DeleteByRoleIdAndPermIdsTx(ctx, session, req.RoleId, toRemove); err != nil { return err } if len(toAdd) > 0 { now := time.Now().Unix() data := make([]*roleperm.SysRolePerm, 0, len(toAdd)) for _, permId := range toAdd { data = append(data, &roleperm.SysRolePerm{ RoleId: req.RoleId, PermId: permId, CreateTime: now, UpdateTime: now, }) } return l.svcCtx.SysRolePermModel.BatchInsertWithTx(ctx, session, data) } return nil }); err != nil { return err } if diffCounts.add == 0 && diffCounts.remove == 0 { return nil } // 事务已提交成功,缓存清理属于尽力而为:FindUserIdsByRoleId 失败仅记录 Errorf, // 不映射为 500——否则客户端会把"数据已改但缓存未刷"的 degraded 成功状态误判为完全失败 // 而发起重试,重试时 diff 出的 toAdd/toRemove 均为空将静默 200,业务语义反而更怪 // (见审计 M-4)。旧权限缓存最多在 TTL (5 分钟) 后自然过期,不影响正确性。 if affectedUserIds, err := l.svcCtx.SysUserRoleModel.FindUserIdsByRoleId(l.ctx, req.RoleId); err == nil { l.svcCtx.UserDetailsLoader.BatchDel(l.ctx, affectedUserIds, role.ProductCode) } else { logx.WithContext(l.ctx).Errorf("BindRolePerms roleId=%d 角色权限已更新但 FindUserIdsByRoleId 失败,用户权限缓存将等待 TTL 自然过期: %v", req.RoleId, err) } return nil }