updateProductLogic.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. package product
  2. import (
  3. "context"
  4. "errors"
  5. "time"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/loaders"
  8. authHelper "perms-system-server/internal/logic/auth"
  9. productModel "perms-system-server/internal/model/product"
  10. "perms-system-server/internal/response"
  11. "perms-system-server/internal/svc"
  12. "perms-system-server/internal/types"
  13. "github.com/zeromicro/go-zero/core/logx"
  14. "github.com/zeromicro/go-zero/core/stores/sqlx"
  15. )
  16. type UpdateProductLogic struct {
  17. logx.Logger
  18. ctx context.Context
  19. svcCtx *svc.ServiceContext
  20. }
  21. func NewUpdateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateProductLogic {
  22. return &UpdateProductLogic{
  23. Logger: logx.WithContext(ctx),
  24. ctx: ctx,
  25. svcCtx: svcCtx,
  26. }
  27. }
  28. // UpdateProduct 更新产品信息。仅超管可调用,可修改产品名称、备注和启用/禁用状态。禁用产品后其成员将无法访问。
  29. //
  30. // 审计 L-R15-3(产品禁用强制吊销成员会话):
  31. // 当请求构成"Enabled → Disabled"的状态迁移时,事务内除了 UpdateWithTx 产品行之外,还会对该产品下
  32. // 所有启用成员的 sys_user.tokenVersion 做一次 +1,让旧 access token 在下一次 middleware 校验时
  33. // 因 `claims.TokenVersion != ud.TokenVersion` 被 401 拒绝(方案 A)。与 UpdateMember 的降权吊销
  34. // 口径对齐:不依赖 UserDetailsLoader.CleanByProduct 的 post-commit 成功(Redis 抖动时窗口可达
  35. // 5min TTL),而是直接在**签发层**把旧令牌打无效。批量 UPDATE 的数据量是"本产品启用成员数",
  36. // 量级 ≤ 几千,一条 SQL 即可,不触碰全局用户。
  37. func (l *UpdateProductLogic) UpdateProduct(req *types.UpdateProductReq) error {
  38. if err := authHelper.RequireSuperAdmin(l.ctx); err != nil {
  39. return err
  40. }
  41. if len(req.Name) > 64 {
  42. return response.ErrBadRequest("产品名称长度不能超过64个字符")
  43. }
  44. if len(req.Remark) > 255 {
  45. return response.ErrBadRequest("备注长度不能超过255个字符")
  46. }
  47. product, err := l.svcCtx.SysProductModel.FindOne(l.ctx, req.Id)
  48. if err != nil {
  49. return response.ErrNotFound("产品不存在")
  50. }
  51. if req.Status != 0 && req.Status != consts.StatusEnabled && req.Status != consts.StatusDisabled {
  52. return response.ErrBadRequest("状态值无效,仅支持 1(启用) 和 2(禁用)")
  53. }
  54. nextStatus := product.Status
  55. if req.Status != 0 {
  56. nextStatus = req.Status
  57. }
  58. // 审计 L-R15-3:只有"Enabled → Disabled"才触发批量吊销。Disabled → Enabled(重启用)
  59. // 不需要递增——重启用不会让任何用户获得未曾持有的权限,旧 session 继续生效属于预期行为。
  60. shouldRevokeSessions := product.Status == consts.StatusEnabled && nextStatus == consts.StatusDisabled
  61. prevUpdateTime := product.UpdateTime
  62. product.Name = req.Name
  63. product.Remark = req.Remark
  64. product.Status = nextStatus
  65. product.UpdateTime = time.Now().Unix()
  66. var revokedUserIds []int64
  67. if err := l.svcCtx.SysProductModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
  68. // 审计 L-R15-3:把产品行的乐观锁更新 + 成员 userId 读取 + 批量 tokenVersion 递增收敛到
  69. // 同一事务里——任意一步失败事务整体 rollback,不会出现"产品被禁但成员 token 没吊销"或
  70. // "成员 token 被吊销但产品状态没改"的脏中间态。UpdateWithOptLockTx 的 WHERE updateTime=?
  71. // 复现了原 UpdateWithOptLock 的 CAS 语义,rowsAffected=0 → ErrUpdateConflict。
  72. if err := l.svcCtx.SysProductModel.UpdateWithOptLockTx(ctx, session, product, prevUpdateTime); err != nil {
  73. return err
  74. }
  75. if shouldRevokeSessions {
  76. ids, err := l.svcCtx.SysProductMemberModel.FindActiveMemberUserIdsByProductCodeTx(ctx, session, product.Code)
  77. if err != nil {
  78. return err
  79. }
  80. if len(ids) > 0 {
  81. if err := l.svcCtx.SysUserModel.BatchIncrementTokenVersionWithTx(ctx, session, ids); err != nil {
  82. return err
  83. }
  84. revokedUserIds = ids
  85. }
  86. }
  87. return nil
  88. }); err != nil {
  89. if errors.Is(err, productModel.ErrUpdateConflict) {
  90. return response.ErrConflict("数据已被其他操作修改,请刷新后重试")
  91. }
  92. return err
  93. }
  94. // 审计 L-R13-5 方案 A:产品禁用直接让 loadPerms 清空 Perms,UD 失效不能随请求断连丢失。
  95. cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx)
  96. defer cancel()
  97. // 审计 L-R12-1:UpdateWithOptLockTx 不再内嵌 sqlc cache 失效,需要调用方在 tx 成功后走
  98. // InvalidateProductCache 把 sysProduct 低层缓存(id/appKey/code)三把键一并失效。
  99. l.svcCtx.SysProductModel.InvalidateProductCache(cleanCtx, product.Id, product.AppKey, product.Code)
  100. l.svcCtx.UserDetailsLoader.CleanByProduct(cleanCtx, product.Code)
  101. // 审计 L-R15-3:sys_user.tokenVersion 已在 tx 内被批量 +1;post-commit 必须把对应的
  102. // sysUser 低层缓存(cacheSysUserIdPrefix / cacheSysUserUsernamePrefix)一起失效,否则
  103. // UD loader 下次 cache-miss 重建时会从 sysUser 低层缓存里读到旧 tokenVersion,把刚递增
  104. // 过的值抹回去。FindByIds 单次 IN(...) 查询,N 量级为产品成员数,通常数千内可控。
  105. if len(revokedUserIds) > 0 {
  106. users, err := l.svcCtx.SysUserModel.FindByIds(cleanCtx, revokedUserIds)
  107. if err != nil {
  108. logx.WithContext(l.ctx).Errorf("UpdateProduct post-commit FindByIds failed, tokenVersion bump succeeded but sysUser caches may stay stale until TTL: productCode=%s count=%d err=%v", product.Code, len(revokedUserIds), err)
  109. } else {
  110. for _, u := range users {
  111. l.svcCtx.SysUserModel.InvalidateProfileCache(cleanCtx, u.Id, u.Username)
  112. }
  113. }
  114. }
  115. return nil
  116. }