userDetailsLoader.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. package loaders
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "math"
  8. "time"
  9. "perms-system-server/internal/consts"
  10. "perms-system-server/internal/model"
  11. "perms-system-server/internal/model/productmember"
  12. "github.com/zeromicro/go-zero/core/logx"
  13. "github.com/zeromicro/go-zero/core/stores/redis"
  14. "github.com/zeromicro/go-zero/core/stores/sqlx"
  15. "golang.org/x/sync/singleflight"
  16. )
  17. const (
  18. defaultCacheTTL = 300 // 5 分钟
  19. // negativeCacheTTL 控制"用户不存在/已删除"的短期负缓存窗口;必须显著短于 defaultCacheTTL,避免
  20. // 刚刚 createUser 的合法用户被误判为不存在,但又要足够长到能吸收一波由离职用户残留 token 带来的
  21. // 无效流量(审计 M-3 所说的 DB DoS 放大路径)。
  22. negativeCacheTTL = 30 // 30s
  23. // negativeCacheMarker 是写入 Redis 的哨兵字符串;选用非合法 JSON,确保任何升级带来的 schema
  24. // 变动都不会把它误解析为真实 UserDetails。
  25. negativeCacheMarker = "_NOT_FOUND_"
  26. )
  27. // -------- UserDetails 及子结构 --------
  28. // UserDetails 用户完整信息,包含用户、部门、产品、成员、角色、权限等所有有效字段。
  29. // 由 UserDetailsLoader 一次性加载,可用于中间件 context 注入、login/userInfo 响应、Claims 构造等。
  30. type UserDetails struct {
  31. // 用户基本信息 (sys_user)
  32. UserId int64 `json:"userId"`
  33. Username string `json:"username"`
  34. Nickname string `json:"nickname"`
  35. Avatar string `json:"avatar"`
  36. Email string `json:"email"`
  37. Phone string `json:"phone"`
  38. Remark string `json:"remark"`
  39. IsSuperAdmin bool `json:"isSuperAdmin"`
  40. IsSuperAdminRaw int64 `json:"isSuperAdminRaw"`
  41. MustChangePassword bool `json:"mustChangePassword"`
  42. MustChangePwdRaw int64 `json:"mustChangePwdRaw"`
  43. Status int64 `json:"status"`
  44. TokenVersion int64 `json:"tokenVersion"`
  45. // 部门信息 (sys_dept)
  46. DeptId int64 `json:"deptId"`
  47. DeptName string `json:"deptName"`
  48. DeptPath string `json:"deptPath"`
  49. DeptType string `json:"deptType"`
  50. DeptStatus int64 `json:"deptStatus"`
  51. // 产品上下文 (sys_product)
  52. ProductCode string `json:"productCode"`
  53. ProductName string `json:"productName"`
  54. ProductStatus int64 `json:"productStatus"`
  55. // 成员信息 (sys_product_member)
  56. MemberType string `json:"memberType"`
  57. // 角色列表 (sys_role,当前产品下已启用的角色)
  58. Roles []RoleInfo `json:"roles"`
  59. // 权限列表 (计算后的权限 code 集合)
  60. Perms []string `json:"perms"`
  61. // 当前产品下最小 permsLevel(无角色时为 math.MaxInt64)
  62. MinPermsLevel int64 `json:"minPermsLevel"`
  63. }
  64. // RoleInfo 角色摘要信息。
  65. type RoleInfo struct {
  66. Id int64 `json:"id"`
  67. Name string `json:"name"`
  68. Remark string `json:"remark"`
  69. PermsLevel int64 `json:"permsLevel"`
  70. }
  71. // -------- UserDetailsLoader --------
  72. // UserDetailsLoader 负责加载、缓存、失效用户详细信息。
  73. // 优先从 Redis 读取完整 UserDetails,miss 时查 DB 并回填。
  74. type UserDetailsLoader struct {
  75. rds *redis.Redis
  76. keyPrefix string
  77. ttl int
  78. models *model.Models
  79. sf singleflight.Group
  80. }
  81. func NewUserDetailsLoader(rds *redis.Redis, keyPrefix string, models *model.Models) *UserDetailsLoader {
  82. return &UserDetailsLoader{
  83. rds: rds,
  84. keyPrefix: keyPrefix,
  85. ttl: defaultCacheTTL,
  86. models: models,
  87. }
  88. }
  89. func (l *UserDetailsLoader) cacheKey(userId int64, productCode string) string {
  90. return fmt.Sprintf("%s:ud:%d:%s", l.keyPrefix, userId, productCode)
  91. }
  92. func (l *UserDetailsLoader) userIndexKey(userId int64) string {
  93. return fmt.Sprintf("%s:ud:idx:u:%d", l.keyPrefix, userId)
  94. }
  95. func (l *UserDetailsLoader) productIndexKey(productCode string) string {
  96. return fmt.Sprintf("%s:ud:idx:p:%s", l.keyPrefix, productCode)
  97. }
  98. // Load 根据 userId 和 productCode 加载完整的 UserDetails。
  99. func (l *UserDetailsLoader) Load(ctx context.Context, userId int64, productCode string) *UserDetails {
  100. key := l.cacheKey(userId, productCode)
  101. if val, err := l.rds.GetCtx(ctx, key); err == nil && val != "" {
  102. // 命中负缓存:该 userId/productCode 最近查询确认为不存在;直接返回空 UserDetails,
  103. // 避免离职/伪造账号的残余 token 持续压垮 DB(见审计 M-3)。
  104. if val == negativeCacheMarker {
  105. return &UserDetails{UserId: userId, ProductCode: productCode}
  106. }
  107. var ud UserDetails
  108. if err := json.Unmarshal([]byte(val), &ud); err == nil {
  109. return &ud
  110. }
  111. }
  112. v, sfErr, _ := l.sf.Do(key, func() (interface{}, error) {
  113. ud, err := l.loadFromDB(ctx, userId, productCode)
  114. if err != nil {
  115. return nil, err
  116. }
  117. if ud.Username == "" {
  118. // 写短 TTL 的负缓存哨兵;不走 registerCacheKey:负缓存短窗口自然过期即可,
  119. // 也避免 Clean/CleanByProduct 路径误当成真实 UserDetails key 进去全量扫。
  120. if err := l.rds.SetexCtx(ctx, key, negativeCacheMarker, negativeCacheTTL); err != nil {
  121. logx.WithContext(ctx).Errorf("set user details negative cache failed: %v", err)
  122. }
  123. return nil, nil
  124. }
  125. if val, err := json.Marshal(ud); err == nil {
  126. if err := l.rds.SetexCtx(ctx, key, string(val), l.ttl); err != nil {
  127. logx.WithContext(ctx).Errorf("set user details cache failed: %v", err)
  128. }
  129. l.registerCacheKey(ctx, key, userId, productCode)
  130. }
  131. return ud, nil
  132. })
  133. if sfErr != nil {
  134. logx.WithContext(ctx).Errorf("load user details from DB failed: %v", sfErr)
  135. }
  136. ud, ok := v.(*UserDetails)
  137. if !ok || ud == nil {
  138. return &UserDetails{UserId: userId, ProductCode: productCode}
  139. }
  140. return ud
  141. }
  142. // Del 删除指定用户在指定产品下的缓存。
  143. func (l *UserDetailsLoader) Del(ctx context.Context, userId int64, productCode string) {
  144. key := l.cacheKey(userId, productCode)
  145. if _, err := l.rds.DelCtx(ctx, key); err != nil {
  146. logx.WithContext(ctx).Errorf("del user details cache [%s] failed: %v", key, err)
  147. }
  148. l.unregisterCacheKey(ctx, key, userId, productCode)
  149. }
  150. // Clean 清除指定用户所有产品下的缓存。
  151. func (l *UserDetailsLoader) Clean(ctx context.Context, userId int64) {
  152. idxKey := l.userIndexKey(userId)
  153. l.cleanByIndex(ctx, idxKey)
  154. }
  155. // CleanByUserIds 批量清除多个用户在所有产品下的缓存:利用 Redis SUNION 把 N 个用户索引集合合并,
  156. // 配合一次批量 DEL,RTT 从"N × 3"压到常数 2,用于部门字段批量变更后的一次性缓存失效(见审计 M-1)。
  157. // 调用方(如 UpdateDeptLogic)需要先拿到受影响的 userIds 再整体调用一次,避免在 handler 里串行清理
  158. // 几百个用户的 Clean 把请求时长推到秒级。
  159. func (l *UserDetailsLoader) CleanByUserIds(ctx context.Context, userIds []int64) {
  160. if len(userIds) == 0 {
  161. return
  162. }
  163. idxKeys := make([]string, 0, len(userIds))
  164. for _, uid := range userIds {
  165. idxKeys = append(idxKeys, l.userIndexKey(uid))
  166. }
  167. cacheKeys, err := l.rds.SunionCtx(ctx, idxKeys...)
  168. if err != nil {
  169. logx.WithContext(ctx).Errorf("CleanByUserIds sunion failed: %v", err)
  170. return
  171. }
  172. toDelete := make([]string, 0, len(cacheKeys)+len(idxKeys))
  173. toDelete = append(toDelete, cacheKeys...)
  174. toDelete = append(toDelete, idxKeys...)
  175. if len(toDelete) == 0 {
  176. return
  177. }
  178. if _, err := l.rds.DelCtx(ctx, toDelete...); err != nil {
  179. logx.WithContext(ctx).Errorf("CleanByUserIds bulk del failed: %v", err)
  180. }
  181. }
  182. // CleanByProduct 清除指定产品下所有用户的缓存。
  183. func (l *UserDetailsLoader) CleanByProduct(ctx context.Context, productCode string) {
  184. idxKey := l.productIndexKey(productCode)
  185. l.cleanByIndex(ctx, idxKey)
  186. }
  187. // BatchDel 批量删除多个用户在指定产品下的缓存。
  188. func (l *UserDetailsLoader) BatchDel(ctx context.Context, userIds []int64, productCode string) {
  189. if len(userIds) == 0 {
  190. return
  191. }
  192. keys := make([]string, 0, len(userIds))
  193. for _, uid := range userIds {
  194. keys = append(keys, l.cacheKey(uid, productCode))
  195. }
  196. if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
  197. logx.WithContext(ctx).Errorf("batch del user details cache failed: %v", err)
  198. }
  199. for i, uid := range userIds {
  200. l.unregisterCacheKey(ctx, keys[i], uid, productCode)
  201. }
  202. }
  203. func (l *UserDetailsLoader) cleanByIndex(ctx context.Context, indexKey string) {
  204. keys, err := l.rds.SmembersCtx(ctx, indexKey)
  205. if err != nil {
  206. logx.WithContext(ctx).Errorf("smembers [%s] failed: %v", indexKey, err)
  207. return
  208. }
  209. if len(keys) > 0 {
  210. if _, err := l.rds.DelCtx(ctx, keys...); err != nil {
  211. logx.WithContext(ctx).Errorf("del cached keys failed: %v", err)
  212. }
  213. }
  214. if _, err := l.rds.DelCtx(ctx, indexKey); err != nil {
  215. logx.WithContext(ctx).Errorf("del index key [%s] failed: %v", indexKey, err)
  216. }
  217. }
  218. func (l *UserDetailsLoader) registerCacheKey(ctx context.Context, cacheKey string, userId int64, productCode string) {
  219. // 索引维护使用 pipeline 把 SADD + EXPIRE(以及 productCode 维度的另一对)合并为单次 RTT,
  220. // 避免大产品启动瞬间 N 个并发 Load 各自打 4 次 RTT 把 Redis 连接队列打满(见审计 L-3)。
  221. uIdxKey := l.userIndexKey(userId)
  222. expireSec := time.Duration(l.ttl+60) * time.Second
  223. err := l.rds.PipelinedCtx(ctx, func(pipe redis.Pipeliner) error {
  224. pipe.SAdd(ctx, uIdxKey, cacheKey)
  225. pipe.Expire(ctx, uIdxKey, expireSec)
  226. if productCode != "" {
  227. pIdxKey := l.productIndexKey(productCode)
  228. pipe.SAdd(ctx, pIdxKey, cacheKey)
  229. pipe.Expire(ctx, pIdxKey, expireSec)
  230. }
  231. return nil
  232. })
  233. if err != nil {
  234. logx.WithContext(ctx).Errorf("registerCacheKey pipeline failed: %v", err)
  235. }
  236. }
  237. func (l *UserDetailsLoader) unregisterCacheKey(ctx context.Context, cacheKey string, userId int64, productCode string) {
  238. if _, err := l.rds.SremCtx(ctx, l.userIndexKey(userId), cacheKey); err != nil {
  239. logx.WithContext(ctx).Errorf("srem user index failed: %v", err)
  240. }
  241. if productCode != "" {
  242. if _, err := l.rds.SremCtx(ctx, l.productIndexKey(productCode), cacheKey); err != nil {
  243. logx.WithContext(ctx).Errorf("srem product index failed: %v", err)
  244. }
  245. }
  246. }
  247. // -------- 内部加载逻辑 --------
  248. func (l *UserDetailsLoader) loadFromDB(ctx context.Context, userId int64, productCode string) (*UserDetails, error) {
  249. ud := &UserDetails{
  250. UserId: userId,
  251. ProductCode: productCode,
  252. MinPermsLevel: math.MaxInt64,
  253. }
  254. if err := l.loadUser(ctx, ud); err != nil {
  255. return ud, err
  256. }
  257. if ud.Username == "" {
  258. return ud, nil
  259. }
  260. l.loadDept(ctx, ud)
  261. l.loadProduct(ctx, ud)
  262. l.loadMembership(ctx, ud)
  263. l.loadRoles(ctx, ud)
  264. l.loadPerms(ctx, ud)
  265. return ud, nil
  266. }
  267. func (l *UserDetailsLoader) loadUser(ctx context.Context, ud *UserDetails) error {
  268. u, err := l.models.SysUserModel.FindOne(ctx, ud.UserId)
  269. if err != nil {
  270. if errors.Is(err, sqlx.ErrNotFound) {
  271. return nil
  272. }
  273. logx.WithContext(ctx).Errorf("userDetailsLoader: query user %d failed: %v", ud.UserId, err)
  274. return err
  275. }
  276. ud.Username = u.Username
  277. ud.Nickname = u.Nickname
  278. ud.Avatar = u.Avatar.String
  279. ud.Email = u.Email
  280. ud.Phone = u.Phone
  281. ud.Remark = u.Remark
  282. ud.DeptId = u.DeptId
  283. ud.IsSuperAdminRaw = u.IsSuperAdmin
  284. ud.IsSuperAdmin = u.IsSuperAdmin == consts.IsSuperAdminYes
  285. ud.MustChangePwdRaw = u.MustChangePassword
  286. ud.MustChangePassword = u.MustChangePassword == consts.MustChangePasswordYes
  287. ud.Status = u.Status
  288. ud.TokenVersion = u.TokenVersion
  289. return nil
  290. }
  291. func (l *UserDetailsLoader) loadDept(ctx context.Context, ud *UserDetails) {
  292. if ud.DeptId == 0 {
  293. return
  294. }
  295. d, err := l.models.SysDeptModel.FindOne(ctx, ud.DeptId)
  296. if err != nil {
  297. logx.WithContext(ctx).Errorf("userDetailsLoader: query dept %d failed: %v", ud.DeptId, err)
  298. return
  299. }
  300. ud.DeptName = d.Name
  301. ud.DeptPath = d.Path
  302. ud.DeptType = d.DeptType
  303. ud.DeptStatus = d.Status
  304. }
  305. func (l *UserDetailsLoader) loadProduct(ctx context.Context, ud *UserDetails) {
  306. if ud.ProductCode == "" {
  307. return
  308. }
  309. p, err := l.models.SysProductModel.FindOneByCode(ctx, ud.ProductCode)
  310. if err != nil {
  311. logx.WithContext(ctx).Errorf("userDetailsLoader: query product %s failed: %v", ud.ProductCode, err)
  312. return
  313. }
  314. ud.ProductName = p.Name
  315. ud.ProductStatus = p.Status
  316. }
  317. func (l *UserDetailsLoader) loadMembership(ctx context.Context, ud *UserDetails) {
  318. if ud.IsSuperAdmin {
  319. ud.MemberType = consts.MemberTypeSuperAdmin
  320. }
  321. if ud.ProductCode == "" {
  322. return
  323. }
  324. if ud.IsSuperAdmin {
  325. return
  326. }
  327. member, err := l.models.SysProductMemberModel.FindOneByProductCodeUserId(ctx, ud.ProductCode, ud.UserId)
  328. if err != nil {
  329. if err != productmember.ErrNotFound {
  330. logx.WithContext(ctx).Errorf("userDetailsLoader: query member failed: %v", err)
  331. }
  332. return
  333. }
  334. if member.Status != consts.StatusEnabled {
  335. return
  336. }
  337. ud.MemberType = member.MemberType
  338. }
  339. func (l *UserDetailsLoader) loadRoles(ctx context.Context, ud *UserDetails) {
  340. if ud.ProductCode == "" {
  341. return
  342. }
  343. roleIds, err := l.models.SysUserRoleModel.FindRoleIdsByUserIdForProduct(ctx, ud.UserId, ud.ProductCode)
  344. if err != nil || len(roleIds) == 0 {
  345. return
  346. }
  347. roles, err := l.models.SysRoleModel.FindByIds(ctx, roleIds)
  348. if err != nil {
  349. logx.WithContext(ctx).Errorf("userDetailsLoader: query roles failed: %v", err)
  350. return
  351. }
  352. ud.Roles = make([]RoleInfo, 0)
  353. minLevel := int64(math.MaxInt64)
  354. for _, r := range roles {
  355. if r.Status == consts.StatusEnabled {
  356. ud.Roles = append(ud.Roles, RoleInfo{
  357. Id: r.Id,
  358. Name: r.Name,
  359. Remark: r.Remark,
  360. PermsLevel: r.PermsLevel,
  361. })
  362. if r.PermsLevel < minLevel {
  363. minLevel = r.PermsLevel
  364. }
  365. }
  366. }
  367. if minLevel < math.MaxInt64 {
  368. ud.MinPermsLevel = minLevel
  369. }
  370. }
  371. func (l *UserDetailsLoader) loadPerms(ctx context.Context, ud *UserDetails) {
  372. if ud.ProductCode == "" {
  373. return
  374. }
  375. if ud.ProductStatus != consts.StatusEnabled {
  376. ud.Perms = nil
  377. return
  378. }
  379. if !ud.IsSuperAdmin && ud.MemberType == "" {
  380. ud.Perms = nil
  381. return
  382. }
  383. // 超管 / ADMIN / DEVELOPER / 研发部门的有效成员 → 全量权限
  384. if ud.IsSuperAdmin ||
  385. ud.MemberType == consts.MemberTypeAdmin ||
  386. ud.MemberType == consts.MemberTypeDeveloper ||
  387. (ud.MemberType != "" && ud.DeptType == consts.DeptTypeDev && ud.DeptStatus == consts.StatusEnabled) {
  388. codes, err := l.models.SysPermModel.FindAllCodesByProductCode(ctx, ud.ProductCode)
  389. if err != nil {
  390. logx.WithContext(ctx).Errorf("userDetailsLoader: query all perms failed: %v", err)
  391. }
  392. ud.Perms = codes
  393. return
  394. }
  395. // 普通成员:角色权限 + 用户附加权限 - 用户拒绝权限
  396. rolePermIds := make([]int64, 0)
  397. if len(ud.Roles) > 0 {
  398. roleIds := make([]int64, 0, len(ud.Roles))
  399. for _, r := range ud.Roles {
  400. roleIds = append(roleIds, r.Id)
  401. }
  402. ids, err := l.models.SysRolePermModel.FindPermIdsByRoleIds(ctx, roleIds)
  403. if err == nil {
  404. rolePermIds = ids
  405. }
  406. }
  407. allowIds, err := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectAllow, ud.ProductCode)
  408. if err != nil {
  409. logx.WithContext(ctx).Errorf("userDetailsLoader: load allow perms failed: %v", err)
  410. return
  411. }
  412. denyIds, err := l.models.SysUserPermModel.FindPermIdsByUserIdAndEffectForProduct(ctx, ud.UserId, consts.PermEffectDeny, ud.ProductCode)
  413. if err != nil {
  414. logx.WithContext(ctx).Errorf("userDetailsLoader: load deny perms failed: %v", err)
  415. }
  416. denySet := make(map[int64]bool, len(denyIds))
  417. for _, id := range denyIds {
  418. denySet[id] = true
  419. }
  420. permIdSet := make(map[int64]bool)
  421. for _, id := range rolePermIds {
  422. if !denySet[id] {
  423. permIdSet[id] = true
  424. }
  425. }
  426. for _, id := range allowIds {
  427. if !denySet[id] {
  428. permIdSet[id] = true
  429. }
  430. }
  431. finalIds := make([]int64, 0, len(permIdSet))
  432. for id := range permIdSet {
  433. finalIds = append(finalIds, id)
  434. }
  435. if len(finalIds) > 0 {
  436. perms, err := l.models.SysPermModel.FindByIds(ctx, finalIds)
  437. if err == nil {
  438. codes := make([]string, 0, len(perms))
  439. for _, p := range perms {
  440. if p.Status == consts.StatusEnabled {
  441. codes = append(codes, p.Code)
  442. }
  443. }
  444. ud.Perms = codes
  445. }
  446. }
  447. }