package loaders import ( "context" "encoding/json" "fmt" "math" "perms-system-server/internal/consts" "perms-system-server/internal/model" "perms-system-server/internal/model/productmember" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/redis" "golang.org/x/sync/singleflight" ) const defaultCacheTTL = 300 // 5 分钟 // -------- UserDetails 及子结构 -------- // UserDetails 用户完整信息,包含用户、部门、产品、成员、角色、权限等所有有效字段。 // 由 UserDetailsLoader 一次性加载,可用于中间件 context 注入、login/userInfo 响应、Claims 构造等。 type UserDetails struct { // 用户基本信息 (sys_user) UserId int64 `json:"userId"` Username string `json:"username"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` Email string `json:"email"` Phone string `json:"phone"` Remark string `json:"remark"` IsSuperAdmin bool `json:"isSuperAdmin"` IsSuperAdminRaw int64 `json:"isSuperAdminRaw"` MustChangePassword bool `json:"mustChangePassword"` MustChangePwdRaw int64 `json:"mustChangePwdRaw"` Status int64 `json:"status"` // 部门信息 (sys_dept) DeptId int64 `json:"deptId"` DeptName string `json:"deptName"` DeptPath string `json:"deptPath"` DeptType string `json:"deptType"` DeptStatus int64 `json:"deptStatus"` // 产品上下文 (sys_product) ProductCode string `json:"productCode"` ProductName string `json:"productName"` // 成员信息 (sys_product_member) MemberType string `json:"memberType"` // 角色列表 (sys_role,当前产品下已启用的角色) Roles []RoleInfo `json:"roles"` // 权限列表 (计算后的权限 code 集合) Perms []string `json:"perms"` // 当前产品下最小 permsLevel(无角色时为 math.MaxInt64) MinPermsLevel int64 `json:"minPermsLevel"` } // RoleInfo 角色摘要信息。 type RoleInfo struct { Id int64 `json:"id"` Name string `json:"name"` Remark string `json:"remark"` PermsLevel int64 `json:"permsLevel"` } // -------- UserDetailsLoader -------- // UserDetailsLoader 负责加载、缓存、失效用户详细信息。 // 优先从 Redis 读取完整 UserDetails,miss 时查 DB 并回填。 type UserDetailsLoader struct { rds *redis.Redis keyPrefix string ttl int models *model.Models sf singleflight.Group } func NewUserDetailsLoader(rds *redis.Redis, keyPrefix string, models *model.Models) *UserDetailsLoader { return &UserDetailsLoader{ rds: rds, keyPrefix: keyPrefix, ttl: defaultCacheTTL, models: models, } } func (l *UserDetailsLoader) cacheKey(userId int64, productCode string) string { return fmt.Sprintf("%s:ud:%d:%s", l.keyPrefix, userId, productCode) } // Load 根据 userId 和 productCode 加载完整的 UserDetails。 func (l *UserDetailsLoader) Load(ctx context.Context, userId int64, productCode string) *UserDetails { key := l.cacheKey(userId, productCode) if val, err := l.rds.GetCtx(ctx, key); err == nil && val != "" { var ud UserDetails if err := json.Unmarshal([]byte(val), &ud); err == nil { return &ud } } v, _, _ := l.sf.Do(key, func() (interface{}, error) { ud, ok := l.loadFromDB(ctx, userId, productCode) if ok { if val, err := json.Marshal(ud); err == nil { if err := l.rds.SetexCtx(ctx, key, string(val), l.ttl); err != nil { logx.WithContext(ctx).Errorf("set user details cache failed: %v", err) } } } return ud, nil }) ud, ok := v.(*UserDetails) if !ok || ud == nil { return &UserDetails{UserId: userId, ProductCode: productCode} } return ud } // Del 删除指定用户在指定产品下的缓存。 func (l *UserDetailsLoader) Del(ctx context.Context, userId int64, productCode string) { key := l.cacheKey(userId, productCode) if _, err := l.rds.DelCtx(ctx, key); err != nil { logx.WithContext(ctx).Errorf("del user details cache [%s] failed: %v", key, err) } } // Clean 清除指定用户所有产品下的缓存。 func (l *UserDetailsLoader) Clean(ctx context.Context, userId int64) { pattern := fmt.Sprintf("%s:ud:%d:*", l.keyPrefix, userId) l.cleanByPattern(ctx, pattern) } // CleanByProduct 清除指定产品下所有用户的缓存。 func (l *UserDetailsLoader) CleanByProduct(ctx context.Context, productCode string) { pattern := fmt.Sprintf("%s:ud:*:%s", l.keyPrefix, productCode) l.cleanByPattern(ctx, pattern) } // BatchDel 批量删除多个用户在指定产品下的缓存。 func (l *UserDetailsLoader) BatchDel(ctx context.Context, userIds []int64, productCode string) { if len(userIds) == 0 { return } keys := make([]string, 0, len(userIds)) for _, uid := range userIds { keys = append(keys, l.cacheKey(uid, productCode)) } if _, err := l.rds.DelCtx(ctx, keys...); err != nil { logx.WithContext(ctx).Errorf("batch del user details cache failed: %v", err) } } // NOTE: SCAN only works on single-node Redis. For Redis Cluster, consider using hash tags // in key design or switching to a different cache invalidation strategy. func (l *UserDetailsLoader) cleanByPattern(ctx context.Context, pattern string) { var cursor uint64 for { keys, cur, err := l.rds.ScanCtx(ctx, cursor, pattern, 100) if err != nil { logx.WithContext(ctx).Errorf("scan keys [%s] failed: %v", pattern, err) return } if len(keys) > 0 { if _, err := l.rds.DelCtx(ctx, keys...); err != nil { logx.WithContext(ctx).Errorf("del keys failed: %v", err) } } if cur == 0 { return } cursor = cur } } // -------- 内部加载逻辑 -------- func (l *UserDetailsLoader) loadFromDB(ctx context.Context, userId int64, productCode string) (*UserDetails, bool) { ud := &UserDetails{ UserId: userId, ProductCode: productCode, MinPermsLevel: math.MaxInt64, } if !l.loadUser(ctx, ud) { return ud, false } l.loadDept(ctx, ud) l.loadProduct(ctx, ud) l.loadMembership(ctx, ud) l.loadRoles(ctx, ud) l.loadPerms(ctx, ud) return ud, true } func (l *UserDetailsLoader) loadUser(ctx context.Context, ud *UserDetails) bool { u, err := l.models.SysUserModel.FindOne(ctx, ud.UserId) if err != nil { logx.WithContext(ctx).Errorf("userDetailsLoader: query user %d failed: %v", ud.UserId, err) return false } ud.Username = u.Username ud.Nickname = u.Nickname ud.Avatar = u.Avatar.String ud.Email = u.Email ud.Phone = u.Phone ud.Remark = u.Remark ud.DeptId = u.DeptId ud.IsSuperAdminRaw = u.IsSuperAdmin ud.IsSuperAdmin = u.IsSuperAdmin == consts.IsSuperAdminYes ud.MustChangePwdRaw = u.MustChangePassword ud.MustChangePassword = u.MustChangePassword == consts.MustChangePasswordYes ud.Status = u.Status return true } func (l *UserDetailsLoader) loadDept(ctx context.Context, ud *UserDetails) { if ud.DeptId == 0 { return } d, err := l.models.SysDeptModel.FindOne(ctx, ud.DeptId) if err != nil { logx.WithContext(ctx).Errorf("userDetailsLoader: query dept %d failed: %v", ud.DeptId, err) return } ud.DeptName = d.Name ud.DeptPath = d.Path ud.DeptType = d.DeptType ud.DeptStatus = d.Status } func (l *UserDetailsLoader) loadProduct(ctx context.Context, ud *UserDetails) { if ud.ProductCode == "" { return } p, err := l.models.SysProductModel.FindOneByCode(ctx, ud.ProductCode) if err != nil { logx.WithContext(ctx).Errorf("userDetailsLoader: query product %s failed: %v", ud.ProductCode, err) return } ud.ProductName = p.Name } func (l *UserDetailsLoader) loadMembership(ctx context.Context, ud *UserDetails) { if ud.IsSuperAdmin { ud.MemberType = consts.MemberTypeSuperAdmin } if ud.ProductCode == "" { return } if ud.IsSuperAdmin { return } member, err := l.models.SysProductMemberModel.FindOneByProductCodeUserId(ctx, ud.ProductCode, ud.UserId) if err != nil { if err != productmember.ErrNotFound { logx.WithContext(ctx).Errorf("userDetailsLoader: query member failed: %v", err) } return } if member.Status != consts.StatusEnabled { return } ud.MemberType = member.MemberType } func (l *UserDetailsLoader) loadRoles(ctx context.Context, ud *UserDetails) { if ud.ProductCode == "" { return } roleIds, err := l.models.SysUserRoleModel.FindRoleIdsByUserId(ctx, ud.UserId) if err != nil || len(roleIds) == 0 { return } roles, err := l.models.SysRoleModel.FindByIds(ctx, roleIds) if err != nil { logx.WithContext(ctx).Errorf("userDetailsLoader: query roles failed: %v", err) return } ud.Roles = make([]RoleInfo, 0) minLevel := int64(math.MaxInt64) for _, r := range roles { if r.ProductCode == ud.ProductCode && r.Status == consts.StatusEnabled { ud.Roles = append(ud.Roles, RoleInfo{ Id: r.Id, Name: r.Name, Remark: r.Remark, PermsLevel: r.PermsLevel, }) if r.PermsLevel < minLevel { minLevel = r.PermsLevel } } } if minLevel < math.MaxInt64 { ud.MinPermsLevel = minLevel } } func (l *UserDetailsLoader) loadPerms(ctx context.Context, ud *UserDetails) { if ud.ProductCode == "" { return } // 超管 / ADMIN / DEVELOPER / 研发部门成员 → 全量权限 if ud.IsSuperAdmin || ud.MemberType == consts.MemberTypeAdmin || ud.MemberType == consts.MemberTypeDeveloper || (ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) { codes, err := l.models.SysPermModel.FindAllCodesByProductCode(ctx, ud.ProductCode) if err != nil { logx.WithContext(ctx).Errorf("userDetailsLoader: query all perms failed: %v", err) } ud.Perms = codes return } // 普通成员:角色权限 + 用户附加权限 - 用户拒绝权限 rolePermIds := make([]int64, 0) if len(ud.Roles) > 0 { roleIds := make([]int64, 0, len(ud.Roles)) for _, r := range ud.Roles { roleIds = append(roleIds, r.Id) } ids, err := l.models.SysRolePermModel.FindPermIdsByRoleIds(ctx, roleIds) if err == nil { rolePermIds = ids } } allowIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectAllow, ud.ProductCode) denyIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectDeny, ud.ProductCode) denySet := make(map[int64]bool, len(denyIds)) for _, id := range denyIds { denySet[id] = true } permIdSet := make(map[int64]bool) for _, id := range rolePermIds { if !denySet[id] { permIdSet[id] = true } } for _, id := range allowIds { if !denySet[id] { permIdSet[id] = true } } finalIds := make([]int64, 0, len(permIdSet)) for id := range permIdSet { finalIds = append(finalIds, id) } if len(finalIds) > 0 { perms, err := l.models.SysPermModel.FindByIds(ctx, finalIds) if err == nil { codes := make([]string, 0, len(perms)) for _, p := range perms { if p.Status == consts.StatusEnabled { codes = append(codes, p.Code) } } ud.Perms = codes } } }