| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- package product
- import (
- "context"
- "errors"
- "time"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/loaders"
- authHelper "perms-system-server/internal/logic/auth"
- productModel "perms-system-server/internal/model/product"
- "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 UpdateProductLogic struct {
- logx.Logger
- ctx context.Context
- svcCtx *svc.ServiceContext
- }
- func NewUpdateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateProductLogic {
- return &UpdateProductLogic{
- Logger: logx.WithContext(ctx),
- ctx: ctx,
- svcCtx: svcCtx,
- }
- }
- // UpdateProduct 更新产品信息。仅超管可调用,可修改产品名称、备注和启用/禁用状态。禁用产品后其成员将无法访问。
- //
- // 审计 L-R15-3(产品禁用强制吊销成员会话):
- // 当请求构成"Enabled → Disabled"的状态迁移时,事务内除了 UpdateWithTx 产品行之外,还会对该产品下
- // 所有启用成员的 sys_user.tokenVersion 做一次 +1,让旧 access token 在下一次 middleware 校验时
- // 因 `claims.TokenVersion != ud.TokenVersion` 被 401 拒绝(方案 A)。与 UpdateMember 的降权吊销
- // 口径对齐:不依赖 UserDetailsLoader.CleanByProduct 的 post-commit 成功(Redis 抖动时窗口可达
- // 5min TTL),而是直接在**签发层**把旧令牌打无效。批量 UPDATE 的数据量是"本产品启用成员数",
- // 量级 ≤ 几千,一条 SQL 即可,不触碰全局用户。
- func (l *UpdateProductLogic) UpdateProduct(req *types.UpdateProductReq) error {
- if err := authHelper.RequireSuperAdmin(l.ctx); err != nil {
- return err
- }
- if len(req.Name) > 64 {
- return response.ErrBadRequest("产品名称长度不能超过64个字符")
- }
- if len(req.Remark) > 255 {
- return response.ErrBadRequest("备注长度不能超过255个字符")
- }
- product, err := l.svcCtx.SysProductModel.FindOne(l.ctx, req.Id)
- if err != nil {
- return response.ErrNotFound("产品不存在")
- }
- if req.Status != 0 && req.Status != consts.StatusEnabled && req.Status != consts.StatusDisabled {
- return response.ErrBadRequest("状态值无效,仅支持 1(启用) 和 2(禁用)")
- }
- nextStatus := product.Status
- if req.Status != 0 {
- nextStatus = req.Status
- }
- // 审计 L-R15-3:只有"Enabled → Disabled"才触发批量吊销。Disabled → Enabled(重启用)
- // 不需要递增——重启用不会让任何用户获得未曾持有的权限,旧 session 继续生效属于预期行为。
- shouldRevokeSessions := product.Status == consts.StatusEnabled && nextStatus == consts.StatusDisabled
- prevUpdateTime := product.UpdateTime
- product.Name = req.Name
- product.Remark = req.Remark
- product.Status = nextStatus
- product.UpdateTime = time.Now().Unix()
- var revokedUserIds []int64
- if err := l.svcCtx.SysProductModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
- // 审计 L-R15-3:把产品行的乐观锁更新 + 成员 userId 读取 + 批量 tokenVersion 递增收敛到
- // 同一事务里——任意一步失败事务整体 rollback,不会出现"产品被禁但成员 token 没吊销"或
- // "成员 token 被吊销但产品状态没改"的脏中间态。UpdateWithOptLockTx 的 WHERE updateTime=?
- // 复现了原 UpdateWithOptLock 的 CAS 语义,rowsAffected=0 → ErrUpdateConflict。
- if err := l.svcCtx.SysProductModel.UpdateWithOptLockTx(ctx, session, product, prevUpdateTime); err != nil {
- return err
- }
- if shouldRevokeSessions {
- ids, err := l.svcCtx.SysProductMemberModel.FindActiveMemberUserIdsByProductCodeTx(ctx, session, product.Code)
- if err != nil {
- return err
- }
- if len(ids) > 0 {
- if err := l.svcCtx.SysUserModel.BatchIncrementTokenVersionWithTx(ctx, session, ids); err != nil {
- return err
- }
- revokedUserIds = ids
- }
- }
- return nil
- }); err != nil {
- if errors.Is(err, productModel.ErrUpdateConflict) {
- return response.ErrConflict("数据已被其他操作修改,请刷新后重试")
- }
- return err
- }
- // 审计 L-R13-5 方案 A:产品禁用直接让 loadPerms 清空 Perms,UD 失效不能随请求断连丢失。
- cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx)
- defer cancel()
- // 审计 L-R12-1:UpdateWithOptLockTx 不再内嵌 sqlc cache 失效,需要调用方在 tx 成功后走
- // InvalidateProductCache 把 sysProduct 低层缓存(id/appKey/code)三把键一并失效。
- l.svcCtx.SysProductModel.InvalidateProductCache(cleanCtx, product.Id, product.AppKey, product.Code)
- l.svcCtx.UserDetailsLoader.CleanByProduct(cleanCtx, product.Code)
- // 审计 L-R15-3:sys_user.tokenVersion 已在 tx 内被批量 +1;post-commit 必须把对应的
- // sysUser 低层缓存(cacheSysUserIdPrefix / cacheSysUserUsernamePrefix)一起失效,否则
- // UD loader 下次 cache-miss 重建时会从 sysUser 低层缓存里读到旧 tokenVersion,把刚递增
- // 过的值抹回去。FindByIds 单次 IN(...) 查询,N 量级为产品成员数,通常数千内可控。
- if len(revokedUserIds) > 0 {
- users, err := l.svcCtx.SysUserModel.FindByIds(cleanCtx, revokedUserIds)
- if err != nil {
- 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)
- } else {
- for _, u := range users {
- l.svcCtx.SysUserModel.InvalidateProfileCache(cleanCtx, u.Id, u.Username)
- }
- }
- }
- return nil
- }
|