package dept import ( "context" "database/sql" "errors" "fmt" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/sqlx" ) var ErrUpdateConflict = errors.New("update conflict: data has been modified by another operation") var _ SysDeptModel = (*customSysDeptModel)(nil) type ( SysDeptModel interface { sysDeptModel FindAll(ctx context.Context) ([]*SysDept, error) UpdateWithOptLock(ctx context.Context, data *SysDept, expectedUpdateTime int64) error // UpdateWithOptLockTx 与 UpdateWithOptLock 语义等价,但 UPDATE 在调用方提供的事务里执行。 // 用于 UpdateDept 需要在同一事务里做"乐观锁更新 sys_dept + 批量递增 sys_user.tokenVersion" // 的场景(审计 L-R16-2):任一步失败整体回滚,不会出现"部门已改但成员 tokenVersion 未递增" // 或"tokenVersion 已递增但部门未改"的脏中间态。 // // 契约(与 UpdateProfileWithTx 的 L-R12-1 对齐): // - 不在方法内做缓存失效;commit 成功后由调用方显式调用 InvalidateDeptCache(id); // - session==nil 返回错误; // - affected=0 → ErrUpdateConflict,交由上层映射 409。 UpdateWithOptLockTx(ctx context.Context, session sqlx.Session, data *SysDept, expectedUpdateTime int64) error // InvalidateDeptCache 失效 sysDept 的 id 键缓存,仅应在事务 commit 成功后调用。 // best-effort:失效失败只留日志,最终由 TTL 兜底(审计 L-R16-2,与 InvalidateProfileCache // 相同口径)。 InvalidateDeptCache(ctx context.Context, id int64) // FindOneForShareTx 在当前事务里对 sys_dept 目标行加 S 锁(SELECT ... LOCK IN SHARE MODE), // 用于"UpdateUser 改 deptId 到 X"与"DeleteDept 删除 X"之间的 write skew 闭环(审计 M-R11-3)。 // DeleteDept 会先对 sys_dept[X] 取 X 锁——被本 S 锁阻塞;等 UpdateUser 提交后 DeleteDept 再 // 去 FOR SHARE sys_user WHERE deptId=X 时能看到新行,改删除为 400,整链路不产生 orphan deptId。 // 本方法不走缓存,必须在 TransactCtx / Session 下调用。 FindOneForShareTx(ctx context.Context, session sqlx.Session, id int64) (*SysDept, error) // UpdatePathWithTx 只更新 sys_dept 某行的 `path` 与 `updateTime`。专用于 CreateDept 新建 // 行之后用 LastInsertId 拼接 path 的"第二步"(审计 L-R17-5):原实现要先 FindOneWithTx // 把刚插入的整行读回来再走 UpdateWithTx,DB 往返 3 次(INSERT + SELECT + UPDATE);新 // 方法把它压到 2 次(INSERT + UPDATE),tx 持锁时间对应缩短。 // 不走缓存失效(本事务是 sys_dept 新行,无既有低层缓存需要清);必须在 TransactCtx / // Session 下调用,session==nil 直接返错。 UpdatePathWithTx(ctx context.Context, session sqlx.Session, id int64, path string, updateTime int64) error } customSysDeptModel struct { *defaultSysDeptModel } ) func NewSysDeptModel(conn sqlx.SqlConn, c cache.CacheConf, cachePrefix string, opts ...cache.Option) SysDeptModel { return &customSysDeptModel{ defaultSysDeptModel: newSysDeptModel(conn, c, cachePrefix, opts...), } } func (m *customSysDeptModel) FindAll(ctx context.Context) ([]*SysDept, error) { var list []*SysDept query := fmt.Sprintf("SELECT %s FROM %s ORDER BY `sort` ASC, `id` ASC", sysDeptRows, m.table) if err := m.QueryRowsNoCacheCtx(ctx, &list, query); err != nil { return nil, err } return list, nil } func (m *customSysDeptModel) FindOneForShareTx(ctx context.Context, session sqlx.Session, id int64) (*SysDept, error) { var data SysDept query := fmt.Sprintf("SELECT %s FROM %s WHERE `id` = ? LIMIT 1 LOCK IN SHARE MODE", sysDeptRows, m.table) if err := session.QueryRowCtx(ctx, &data, query, id); err != nil { return nil, err } return &data, nil } func (m *customSysDeptModel) UpdateWithOptLock(ctx context.Context, data *SysDept, expectedUpdateTime int64) error { sysDeptIdKey := fmt.Sprintf("%s%v", cacheSysDeptIdPrefix, data.Id) res, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) { query := fmt.Sprintf("UPDATE %s SET `name`=?, `sort`=?, `deptType`=?, `remark`=?, `status`=?, `updateTime`=? WHERE `id`=? AND `updateTime`=?", m.table) return conn.ExecCtx(ctx, query, data.Name, data.Sort, data.DeptType, data.Remark, data.Status, data.UpdateTime, data.Id, expectedUpdateTime) }, sysDeptIdKey) if err != nil { return err } affected, _ := res.RowsAffected() if affected == 0 { return ErrUpdateConflict } return nil } // UpdateWithOptLockTx 见接口注释(审计 L-R16-2)。 // 实现上**绕过** m.ExecCtx 的 pre-commit DelCache 语义——仅调用 session.ExecCtx,缓存失效由 // 调用方在事务 commit 成功后显式走 InvalidateDeptCache。 func (m *customSysDeptModel) UpdateWithOptLockTx(ctx context.Context, session sqlx.Session, data *SysDept, expectedUpdateTime int64) error { if session == nil { return errors.New("UpdateWithOptLockTx requires a non-nil session") } query := fmt.Sprintf("UPDATE %s SET `name`=?, `sort`=?, `deptType`=?, `remark`=?, `status`=?, `updateTime`=? WHERE `id`=? AND `updateTime`=?", m.table) res, err := session.ExecCtx(ctx, query, data.Name, data.Sort, data.DeptType, data.Remark, data.Status, data.UpdateTime, data.Id, expectedUpdateTime) if err != nil { return err } affected, _ := res.RowsAffected() if affected == 0 { return ErrUpdateConflict } return nil } // UpdatePathWithTx 见接口注释(审计 L-R17-5)。与 UpdateWithOptLockTx 一样**绕过** m.ExecCtx // 的 pre-commit DelCache 钩子——CreateDept 的新行目前也不存在低层缓存条目,无需事务内失效。 // 仅更新 `path` / `updateTime` 两列,其余字段保持 InsertWithTx 落盘时的值不变。 func (m *customSysDeptModel) UpdatePathWithTx(ctx context.Context, session sqlx.Session, id int64, path string, updateTime int64) error { if session == nil { return errors.New("UpdatePathWithTx requires a non-nil session") } query := fmt.Sprintf("UPDATE %s SET `path`=?, `updateTime`=? WHERE `id`=?", m.table) _, err := session.ExecCtx(ctx, query, path, updateTime, id) return err } // InvalidateDeptCache 见接口注释(审计 L-R16-2)。与 sysUserModel.InvalidateProfileCache 对齐: // post-commit best-effort 失效,ctx 取消与其它错误分档日志,方便 Redis 抖动与主动取消区分告警。 func (m *customSysDeptModel) InvalidateDeptCache(ctx context.Context, id int64) { sysDeptIdKey := fmt.Sprintf("%s%v", cacheSysDeptIdPrefix, id) if err := m.DelCacheCtx(ctx, sysDeptIdKey); err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { logx.WithContext(ctx).Errorw("cache invalidation skipped: ctx canceled", logx.Field("audit", "cache_invalidation_skipped_due_to_ctx_cancel"), logx.Field("scope", "sysDeptModel.InvalidateDeptCache"), logx.Field("id", id), logx.Field("err", err.Error()), ) } else { logx.WithContext(ctx).Errorf("sysDeptModel.InvalidateDeptCache failed: id=%d err=%v", id, err) } } }