deleteDeptLogic.go 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. package dept
  2. import (
  3. "context"
  4. "fmt"
  5. "perms-system-server/internal/loaders"
  6. authHelper "perms-system-server/internal/logic/auth"
  7. "perms-system-server/internal/response"
  8. "perms-system-server/internal/svc"
  9. "perms-system-server/internal/types"
  10. "github.com/zeromicro/go-zero/core/logx"
  11. "github.com/zeromicro/go-zero/core/stores/sqlx"
  12. )
  13. type DeleteDeptLogic struct {
  14. logx.Logger
  15. ctx context.Context
  16. svcCtx *svc.ServiceContext
  17. }
  18. func NewDeleteDeptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteDeptLogic {
  19. return &DeleteDeptLogic{
  20. Logger: logx.WithContext(ctx),
  21. ctx: ctx,
  22. svcCtx: svcCtx,
  23. }
  24. }
  25. // DeleteDept 删除部门。在事务内加行锁后检查是否存在子部门或关联用户,均无则删除。仅超管可调用。
  26. //
  27. // 审计 H-R17-2:DeleteWithTx 内部走 sqlc.CachedConn.ExecCtx,其"exec → DelCache"钩子会在外层
  28. // `TransactCtx` **commit 之前**把 Redis 里的 `sysDeptIdKey` 清掉;此时 DB 对其他事务的可见性
  29. // 还没切换(RR 下其他事务 FindOne 会按非锁读路径读到**旧行**并把它回填进 Redis),commit 之后
  30. // DB 已经没有该行,但 Redis 里留着一张"幽灵快照"直到 TTL 到期。叠加 H-R17-1 的 orphan user,
  31. // 该用户可被按"DEV 部门 + Path"继续授权最长 5min。修复口径:tx 成功后走 detached cleanCtx
  32. // 再手动 `InvalidateDeptCache`,与 RemoveMember / UpdateMember 的 post-commit 失效套路对齐。
  33. func (l *DeleteDeptLogic) DeleteDept(req *types.DeleteDeptReq) error {
  34. if err := authHelper.RequireSuperAdmin(l.ctx); err != nil {
  35. return err
  36. }
  37. if err := l.svcCtx.SysDeptModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
  38. // 锁序协议:先锁本部门行(X 锁),再对子部门 / 关联用户用 FOR SHARE 做"存在性检查"。
  39. // 存在性检查不修改目标行,用 S 锁即可,不会与 UpdateUser 改 DeptId(持有 sys_user 行的 X 锁)
  40. // 或 CreateDept 插子部门(持有 sys_dept 新行的 X 锁)构成 AB-BA 冲突:
  41. // DeleteDept: sys_dept.X(self) → sys_dept.S(children range) → sys_user.S(dept range)
  42. // CreateDept: sys_dept.X(newRow) ——独立范围
  43. // UpdateUser: sys_user.X(self) ± sys_dept.S(newDept existence) ——独立方向
  44. // 相比之前"全链路 FOR UPDATE"减少一种理论 AB-BA 死锁路径(见审计 L-1)。
  45. var deptId int64
  46. lockQuery := fmt.Sprintf("SELECT `id` FROM %s WHERE `id` = ? FOR UPDATE", l.svcCtx.SysDeptModel.TableName())
  47. if err := session.QueryRowCtx(ctx, &deptId, lockQuery, req.Id); err != nil {
  48. return response.ErrNotFound("部门不存在")
  49. }
  50. var childIds []int64
  51. childQuery := fmt.Sprintf("SELECT `id` FROM %s WHERE `parentId` = ? FOR SHARE", l.svcCtx.SysDeptModel.TableName())
  52. if err := session.QueryRowsCtx(ctx, &childIds, childQuery, req.Id); err != nil {
  53. return err
  54. }
  55. if len(childIds) > 0 {
  56. return response.ErrBadRequest("该部门下存在子部门,无法删除")
  57. }
  58. var userIds []int64
  59. userQuery := fmt.Sprintf("SELECT `id` FROM %s WHERE `deptId` = ? FOR SHARE", l.svcCtx.SysUserModel.TableName())
  60. if err := session.QueryRowsCtx(ctx, &userIds, userQuery, req.Id); err != nil {
  61. return err
  62. }
  63. if len(userIds) > 0 {
  64. return response.ErrBadRequest("该部门下仍有关联用户,无法删除")
  65. }
  66. return l.svcCtx.SysDeptModel.DeleteWithTx(ctx, session, req.Id)
  67. }); err != nil {
  68. return err
  69. }
  70. // 审计 H-R17-2:post-commit 再补一次 DelCache 兜底,把 tx 体内 sqlc ExecCtx 过早 DelCache
  71. // 之后被并发 FindOne 回填的"幽灵快照"显式清掉。detached ctx 与请求生命周期解耦,避免
  72. // client 断连时跳过失效。
  73. cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx)
  74. defer cancel()
  75. l.svcCtx.SysDeptModel.InvalidateDeptCache(cleanCtx, req.Id)
  76. return nil
  77. }