userDetailsLoader.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package loaders
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "math"
  7. "perms-system-server/internal/consts"
  8. "perms-system-server/internal/model"
  9. "perms-system-server/internal/model/productmember"
  10. "github.com/zeromicro/go-zero/core/logx"
  11. "github.com/zeromicro/go-zero/core/stores/redis"
  12. "golang.org/x/sync/singleflight"
  13. )
  14. const defaultCacheTTL = 300 // 5 分钟
  15. // -------- UserDetails 及子结构 --------
  16. // UserDetails 用户完整信息,包含用户、部门、产品、成员、角色、权限等所有有效字段。
  17. // 由 UserDetailsLoader 一次性加载,可用于中间件 context 注入、login/userInfo 响应、Claims 构造等。
  18. type UserDetails struct {
  19. // 用户基本信息 (sys_user)
  20. UserId int64 `json:"userId"`
  21. Username string `json:"username"`
  22. Nickname string `json:"nickname"`
  23. Avatar string `json:"avatar"`
  24. Email string `json:"email"`
  25. Phone string `json:"phone"`
  26. Remark string `json:"remark"`
  27. IsSuperAdmin bool `json:"isSuperAdmin"`
  28. IsSuperAdminRaw int64 `json:"isSuperAdminRaw"`
  29. MustChangePassword bool `json:"mustChangePassword"`
  30. MustChangePwdRaw int64 `json:"mustChangePwdRaw"`
  31. Status int64 `json:"status"`
  32. // 部门信息 (sys_dept)
  33. DeptId int64 `json:"deptId"`
  34. DeptName string `json:"deptName"`
  35. DeptPath string `json:"deptPath"`
  36. DeptType string `json:"deptType"`
  37. DeptStatus int64 `json:"deptStatus"`
  38. // 产品上下文 (sys_product)
  39. ProductCode string `json:"productCode"`
  40. ProductName string `json:"productName"`
  41. // 成员信息 (sys_product_member)
  42. MemberType string `json:"memberType"`
  43. // 角色列表 (sys_role,当前产品下已启用的角色)
  44. Roles []RoleInfo `json:"roles"`
  45. // 权限列表 (计算后的权限 code 集合)
  46. Perms []string `json:"perms"`
  47. // 当前产品下最小 permsLevel(无角色时为 math.MaxInt64)
  48. MinPermsLevel int64 `json:"minPermsLevel"`
  49. }
  50. // RoleInfo 角色摘要信息。
  51. type RoleInfo struct {
  52. Id int64 `json:"id"`
  53. Name string `json:"name"`
  54. Remark string `json:"remark"`
  55. PermsLevel int64 `json:"permsLevel"`
  56. }
  57. // -------- UserDetailsLoader --------
  58. // UserDetailsLoader 负责加载、缓存、失效用户详细信息。
  59. // 优先从 Redis 读取完整 UserDetails,miss 时查 DB 并回填。
  60. type UserDetailsLoader struct {
  61. rds *redis.Redis
  62. keyPrefix string
  63. ttl int
  64. models *model.Models
  65. sf singleflight.Group
  66. }
  67. func NewUserDetailsLoader(rds *redis.Redis, keyPrefix string, models *model.Models) *UserDetailsLoader {
  68. return &UserDetailsLoader{
  69. rds: rds,
  70. keyPrefix: keyPrefix,
  71. ttl: defaultCacheTTL,
  72. models: models,
  73. }
  74. }
  75. func (l *UserDetailsLoader) cacheKey(userId int64, productCode string) string {
  76. return fmt.Sprintf("%s:ud:%d:%s", l.keyPrefix, userId, productCode)
  77. }
  78. // Load 根据 userId 和 productCode 加载完整的 UserDetails。
  79. func (l *UserDetailsLoader) Load(ctx context.Context, userId int64, productCode string) *UserDetails {
  80. key := l.cacheKey(userId, productCode)
  81. if val, err := l.rds.GetCtx(ctx, key); err == nil && val != "" {
  82. var ud UserDetails
  83. if err := json.Unmarshal([]byte(val), &ud); err == nil {
  84. return &ud
  85. }
  86. }
  87. v, _, _ := l.sf.Do(key, func() (interface{}, error) {
  88. ud, ok := l.loadFromDB(ctx, userId, productCode)
  89. if ok {
  90. if val, err := json.Marshal(ud); err == nil {
  91. if err := l.rds.SetexCtx(ctx, key, string(val), l.ttl); err != nil {
  92. logx.WithContext(ctx).Errorf("set user details cache failed: %v", err)
  93. }
  94. }
  95. }
  96. return ud, nil
  97. })
  98. ud, ok := v.(*UserDetails)
  99. if !ok || ud == nil {
  100. return &UserDetails{UserId: userId, ProductCode: productCode}
  101. }
  102. return ud
  103. }
  104. // Del 删除指定用户在指定产品下的缓存。
  105. func (l *UserDetailsLoader) Del(ctx context.Context, userId int64, productCode string) {
  106. key := l.cacheKey(userId, productCode)
  107. if _, err := l.rds.DelCtx(ctx, key); err != nil {
  108. logx.WithContext(ctx).Errorf("del user details cache [%s] failed: %v", key, err)
  109. }
  110. }
  111. // Clean 清除指定用户所有产品下的缓存。
  112. func (l *UserDetailsLoader) Clean(ctx context.Context, userId int64) {
  113. pattern := fmt.Sprintf("%s:ud:%d:*", l.keyPrefix, userId)
  114. l.cleanByPattern(ctx, pattern)
  115. }
  116. // CleanByProduct 清除指定产品下所有用户的缓存。
  117. func (l *UserDetailsLoader) CleanByProduct(ctx context.Context, productCode string) {
  118. pattern := fmt.Sprintf("%s:ud:*:%s", l.keyPrefix, productCode)
  119. l.cleanByPattern(ctx, pattern)
  120. }
  121. // BatchDel 批量删除多个用户在指定产品下的缓存。
  122. func (l *UserDetailsLoader) BatchDel(ctx context.Context, userIds []int64, productCode string) {
  123. if len(userIds) == 0 {
  124. return
  125. }
  126. keys := make([]string, 0, len(userIds))
  127. for _, uid := range userIds {
  128. keys = append(keys, l.cacheKey(uid, productCode))
  129. }
  130. if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
  131. logx.WithContext(ctx).Errorf("batch del user details cache failed: %v", err)
  132. }
  133. }
  134. // NOTE: SCAN only works on single-node Redis. For Redis Cluster, consider using hash tags
  135. // in key design or switching to a different cache invalidation strategy.
  136. func (l *UserDetailsLoader) cleanByPattern(ctx context.Context, pattern string) {
  137. var cursor uint64
  138. for {
  139. keys, cur, err := l.rds.ScanCtx(ctx, cursor, pattern, 100)
  140. if err != nil {
  141. logx.WithContext(ctx).Errorf("scan keys [%s] failed: %v", pattern, err)
  142. return
  143. }
  144. if len(keys) > 0 {
  145. if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
  146. logx.WithContext(ctx).Errorf("del keys failed: %v", err)
  147. }
  148. }
  149. if cur == 0 {
  150. return
  151. }
  152. cursor = cur
  153. }
  154. }
  155. // -------- 内部加载逻辑 --------
  156. func (l *UserDetailsLoader) loadFromDB(ctx context.Context, userId int64, productCode string) (*UserDetails, bool) {
  157. ud := &UserDetails{
  158. UserId: userId,
  159. ProductCode: productCode,
  160. MinPermsLevel: math.MaxInt64,
  161. }
  162. if !l.loadUser(ctx, ud) {
  163. return ud, false
  164. }
  165. l.loadDept(ctx, ud)
  166. l.loadProduct(ctx, ud)
  167. l.loadMembership(ctx, ud)
  168. l.loadRoles(ctx, ud)
  169. l.loadPerms(ctx, ud)
  170. return ud, true
  171. }
  172. func (l *UserDetailsLoader) loadUser(ctx context.Context, ud *UserDetails) bool {
  173. u, err := l.models.SysUserModel.FindOne(ctx, ud.UserId)
  174. if err != nil {
  175. logx.WithContext(ctx).Errorf("userDetailsLoader: query user %d failed: %v", ud.UserId, err)
  176. return false
  177. }
  178. ud.Username = u.Username
  179. ud.Nickname = u.Nickname
  180. ud.Avatar = u.Avatar.String
  181. ud.Email = u.Email
  182. ud.Phone = u.Phone
  183. ud.Remark = u.Remark
  184. ud.DeptId = u.DeptId
  185. ud.IsSuperAdminRaw = u.IsSuperAdmin
  186. ud.IsSuperAdmin = u.IsSuperAdmin == consts.IsSuperAdminYes
  187. ud.MustChangePwdRaw = u.MustChangePassword
  188. ud.MustChangePassword = u.MustChangePassword == consts.MustChangePasswordYes
  189. ud.Status = u.Status
  190. return true
  191. }
  192. func (l *UserDetailsLoader) loadDept(ctx context.Context, ud *UserDetails) {
  193. if ud.DeptId == 0 {
  194. return
  195. }
  196. d, err := l.models.SysDeptModel.FindOne(ctx, ud.DeptId)
  197. if err != nil {
  198. logx.WithContext(ctx).Errorf("userDetailsLoader: query dept %d failed: %v", ud.DeptId, err)
  199. return
  200. }
  201. ud.DeptName = d.Name
  202. ud.DeptPath = d.Path
  203. ud.DeptType = d.DeptType
  204. ud.DeptStatus = d.Status
  205. }
  206. func (l *UserDetailsLoader) loadProduct(ctx context.Context, ud *UserDetails) {
  207. if ud.ProductCode == "" {
  208. return
  209. }
  210. p, err := l.models.SysProductModel.FindOneByCode(ctx, ud.ProductCode)
  211. if err != nil {
  212. logx.WithContext(ctx).Errorf("userDetailsLoader: query product %s failed: %v", ud.ProductCode, err)
  213. return
  214. }
  215. ud.ProductName = p.Name
  216. }
  217. func (l *UserDetailsLoader) loadMembership(ctx context.Context, ud *UserDetails) {
  218. if ud.IsSuperAdmin {
  219. ud.MemberType = consts.MemberTypeSuperAdmin
  220. }
  221. if ud.ProductCode == "" {
  222. return
  223. }
  224. if ud.IsSuperAdmin {
  225. return
  226. }
  227. member, err := l.models.SysProductMemberModel.FindOneByProductCodeUserId(ctx, ud.ProductCode, ud.UserId)
  228. if err != nil {
  229. if err != productmember.ErrNotFound {
  230. logx.WithContext(ctx).Errorf("userDetailsLoader: query member failed: %v", err)
  231. }
  232. return
  233. }
  234. if member.Status != consts.StatusEnabled {
  235. return
  236. }
  237. ud.MemberType = member.MemberType
  238. }
  239. func (l *UserDetailsLoader) loadRoles(ctx context.Context, ud *UserDetails) {
  240. if ud.ProductCode == "" {
  241. return
  242. }
  243. roleIds, err := l.models.SysUserRoleModel.FindRoleIdsByUserId(ctx, ud.UserId)
  244. if err != nil || len(roleIds) == 0 {
  245. return
  246. }
  247. roles, err := l.models.SysRoleModel.FindByIds(ctx, roleIds)
  248. if err != nil {
  249. logx.WithContext(ctx).Errorf("userDetailsLoader: query roles failed: %v", err)
  250. return
  251. }
  252. ud.Roles = make([]RoleInfo, 0)
  253. minLevel := int64(math.MaxInt64)
  254. for _, r := range roles {
  255. if r.ProductCode == ud.ProductCode && r.Status == consts.StatusEnabled {
  256. ud.Roles = append(ud.Roles, RoleInfo{
  257. Id: r.Id,
  258. Name: r.Name,
  259. Remark: r.Remark,
  260. PermsLevel: r.PermsLevel,
  261. })
  262. if r.PermsLevel < minLevel {
  263. minLevel = r.PermsLevel
  264. }
  265. }
  266. }
  267. if minLevel < math.MaxInt64 {
  268. ud.MinPermsLevel = minLevel
  269. }
  270. }
  271. func (l *UserDetailsLoader) loadPerms(ctx context.Context, ud *UserDetails) {
  272. if ud.ProductCode == "" {
  273. return
  274. }
  275. // 超管 / ADMIN / DEVELOPER / 研发部门成员 → 全量权限
  276. if ud.IsSuperAdmin ||
  277. ud.MemberType == consts.MemberTypeAdmin ||
  278. ud.MemberType == consts.MemberTypeDeveloper ||
  279. (ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
  280. codes, err := l.models.SysPermModel.FindAllCodesByProductCode(ctx, ud.ProductCode)
  281. if err != nil {
  282. logx.WithContext(ctx).Errorf("userDetailsLoader: query all perms failed: %v", err)
  283. }
  284. ud.Perms = codes
  285. return
  286. }
  287. // 普通成员:角色权限 + 用户附加权限 - 用户拒绝权限
  288. rolePermIds := make([]int64, 0)
  289. if len(ud.Roles) > 0 {
  290. roleIds := make([]int64, 0, len(ud.Roles))
  291. for _, r := range ud.Roles {
  292. roleIds = append(roleIds, r.Id)
  293. }
  294. ids, err := l.models.SysRolePermModel.FindPermIdsByRoleIds(ctx, roleIds)
  295. if err == nil {
  296. rolePermIds = ids
  297. }
  298. }
  299. allowIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectAllow, ud.ProductCode)
  300. denyIds, _ := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectDeny, ud.ProductCode)
  301. denySet := make(map[int64]bool, len(denyIds))
  302. for _, id := range denyIds {
  303. denySet[id] = true
  304. }
  305. permIdSet := make(map[int64]bool)
  306. for _, id := range rolePermIds {
  307. if !denySet[id] {
  308. permIdSet[id] = true
  309. }
  310. }
  311. for _, id := range allowIds {
  312. if !denySet[id] {
  313. permIdSet[id] = true
  314. }
  315. }
  316. finalIds := make([]int64, 0, len(permIdSet))
  317. for id := range permIdSet {
  318. finalIds = append(finalIds, id)
  319. }
  320. if len(finalIds) > 0 {
  321. perms, err := l.models.SysPermModel.FindByIds(ctx, finalIds)
  322. if err == nil {
  323. codes := make([]string, 0, len(perms))
  324. for _, p := range perms {
  325. if p.Status == consts.StatusEnabled {
  326. codes = append(codes, p.Code)
  327. }
  328. }
  329. ud.Perms = codes
  330. }
  331. }
  332. }