updateUserLogic.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. package user
  2. import (
  3. "context"
  4. "errors"
  5. "strings"
  6. "perms-system-server/internal/consts"
  7. authHelper "perms-system-server/internal/logic/auth"
  8. "perms-system-server/internal/middleware"
  9. userModel "perms-system-server/internal/model/user"
  10. "perms-system-server/internal/response"
  11. "perms-system-server/internal/svc"
  12. "perms-system-server/internal/types"
  13. "perms-system-server/internal/util"
  14. "github.com/zeromicro/go-zero/core/logx"
  15. "github.com/zeromicro/go-zero/core/stores/sqlx"
  16. )
  17. type UpdateUserLogic struct {
  18. logx.Logger
  19. ctx context.Context
  20. svcCtx *svc.ServiceContext
  21. }
  22. func NewUpdateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserLogic {
  23. return &UpdateUserLogic{
  24. Logger: logx.WithContext(ctx),
  25. ctx: ctx,
  26. svcCtx: svcCtx,
  27. }
  28. }
  29. // UpdateUser 更新用户信息。修改用户昵称、邮箱、手机、备注、部门归属等。用户可修改自身非敏感字段,管理者可修改下属用户信息。
  30. func (l *UpdateUserLogic) UpdateUser(req *types.UpdateUserReq) error {
  31. caller := middleware.GetUserDetails(l.ctx)
  32. if caller == nil {
  33. return response.ErrUnauthorized("未登录")
  34. }
  35. if caller.UserId == req.Id {
  36. if req.DeptId != nil || req.Status != 0 {
  37. return response.ErrForbidden("不允许修改自己的部门和状态")
  38. }
  39. }
  40. // 前置 FindOne,后续 CheckManageAccess / ValidateStatusChange 都复用此对象,避免一次请求内
  41. // 对 target 做 2~3 次重复查询(见审计 M-5)。
  42. user, err := l.svcCtx.SysUserModel.FindOne(l.ctx, req.Id)
  43. if err != nil {
  44. return response.ErrNotFound("用户不存在")
  45. }
  46. if caller.UserId != req.Id {
  47. productCode := middleware.GetProductCode(l.ctx)
  48. if err := authHelper.CheckManageAccess(l.ctx, l.svcCtx, req.Id, productCode, authHelper.WithPrefetchedTarget(user)); err != nil {
  49. return err
  50. }
  51. }
  52. // req.Status != 0 仅会落在 caller.UserId != req.Id 分支(上方 caller==target 的请求已经拦截),
  53. // 此处沿用 ValidateStatusChange 的超管保护语义,避免再次 FindOne。
  54. if req.Status != 0 && user.IsSuperAdmin == consts.IsSuperAdminYes {
  55. return response.ErrForbidden("不能修改超级管理员的状态")
  56. }
  57. if caller.UserId != req.Id && user.IsSuperAdmin == consts.IsSuperAdminYes {
  58. if req.DeptId != nil {
  59. return response.ErrForbidden("不能通过此接口修改其他超级管理员的部门")
  60. }
  61. }
  62. if req.Nickname != nil && len(*req.Nickname) > 64 {
  63. return response.ErrBadRequest("昵称长度不能超过64个字符")
  64. }
  65. if req.Email != nil && len(*req.Email) > 64 {
  66. return response.ErrBadRequest("邮箱长度不能超过64个字符")
  67. }
  68. if req.Phone != nil && len(*req.Phone) > 32 {
  69. return response.ErrBadRequest("手机号长度不能超过32个字符")
  70. }
  71. if req.Remark != nil && len(*req.Remark) > 255 {
  72. return response.ErrBadRequest("备注长度不能超过255个字符")
  73. }
  74. nickname := user.Nickname
  75. email := user.Email
  76. phone := user.Phone
  77. remark := user.Remark
  78. deptId := user.DeptId
  79. if req.Nickname != nil {
  80. nickname = *req.Nickname
  81. }
  82. if req.Email != nil {
  83. if *req.Email != "" && !util.IsValidEmail(*req.Email) {
  84. return response.ErrBadRequest("邮箱格式不正确")
  85. }
  86. email = *req.Email
  87. }
  88. if req.Phone != nil {
  89. if *req.Phone != "" && !util.IsValidPhone(*req.Phone) {
  90. return response.ErrBadRequest("手机号格式不正确")
  91. }
  92. phone = *req.Phone
  93. }
  94. if req.Remark != nil {
  95. remark = *req.Remark
  96. }
  97. if req.DeptId != nil {
  98. if *req.DeptId > 0 {
  99. newDept, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, *req.DeptId)
  100. if err != nil {
  101. return response.ErrBadRequest("部门不存在")
  102. }
  103. // 审计 L-N2:与 UpdateDept 禁用语义闭环 —— 已禁用的部门代表"冻结该部门所有活动",
  104. // 再往该部门调入新成员会破坏不变量(新成员会因 DeptStatus!=Enabled 被撤销 DEV 全权
  105. // 特权),且无法被 AddMember / CheckAddMemberAccess 的校验感知。此处统一拦截。
  106. if newDept.Status != consts.StatusEnabled {
  107. return response.ErrBadRequest("目标部门已停用")
  108. }
  109. if !caller.IsSuperAdmin &&
  110. caller.MemberType != consts.MemberTypeAdmin &&
  111. caller.DeptPath != "" &&
  112. !strings.HasPrefix(newDept.Path, caller.DeptPath) {
  113. return response.ErrForbidden("无权将用户调入非自己管辖的部门")
  114. }
  115. } else {
  116. // deptId=0 意味着"把用户移出部门树";一旦生效目标将失去 DeptPath,此后 MEMBER / DEVELOPER
  117. // 级别的调用者都通不过 checkDeptHierarchy 对"目标必须归属部门"的强校验,无法再被管辖。
  118. // 因此仅超管和产品 ADMIN 有权执行该破坏组织结构语义的操作(见审计 H-4)。
  119. if !caller.IsSuperAdmin && caller.MemberType != consts.MemberTypeAdmin {
  120. return response.ErrForbidden("仅超级管理员或产品管理员可将用户移出部门")
  121. }
  122. }
  123. deptId = *req.DeptId
  124. }
  125. statusChanged := false
  126. if req.Status != 0 {
  127. if req.Status != consts.StatusEnabled && req.Status != consts.StatusDisabled {
  128. return response.ErrBadRequest("状态值无效,仅支持 1(启用) 和 2(冻结)")
  129. }
  130. if user.Status != req.Status {
  131. statusChanged = true
  132. }
  133. }
  134. newStatus := user.Status
  135. if statusChanged {
  136. newStatus = req.Status
  137. }
  138. // 审计 M-R11-3:改 deptId 到 `newDeptId>0` 时必须把 UPDATE 收敛进事务,并在同事务内对目标
  139. // sys_dept[newDeptId] 加 S 锁——这样并发 DeleteDept 持有 sys_dept[X] 的 X 锁,会被 S 锁阻塞,
  140. // 等本事务提交后 DeleteDept 重读 `sys_user WHERE deptId=X FOR SHARE` 就能看到新行并拒绝删除,
  141. // 闭合"两侧都读不到对方提交 → 各自提交 → orphan deptId"的 write skew。
  142. // 其余分支(只改其它字段 / 移出部门 deptId=0)无 write skew 风险,沿用非事务的 UpdateProfile。
  143. needDeptShareLock := req.DeptId != nil && *req.DeptId > 0 && *req.DeptId != user.DeptId
  144. if !needDeptShareLock {
  145. if err := l.svcCtx.SysUserModel.UpdateProfile(
  146. l.ctx, req.Id, user.Username,
  147. nickname, email, phone, remark, deptId,
  148. newStatus, statusChanged, user.UpdateTime,
  149. ); err != nil {
  150. if errors.Is(err, userModel.ErrUpdateConflict) {
  151. return response.ErrConflict("数据已被其他操作修改,请刷新后重试")
  152. }
  153. return err
  154. }
  155. l.svcCtx.UserDetailsLoader.Clean(l.ctx, req.Id)
  156. return nil
  157. }
  158. targetDeptId := *req.DeptId
  159. if err := l.svcCtx.SysUserModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
  160. // 事务内 S 锁目标 dept,保证 DeleteDept 的 X 锁被阻塞;顺带在事务内复核 Status。
  161. // 上面非事务的 FindOne 已经校过一遍,这里是"在锁生效后的一致性视图"下的最终校验。
  162. lockedDept, err := l.svcCtx.SysDeptModel.FindOneForShareTx(ctx, session, targetDeptId)
  163. if err != nil {
  164. if errors.Is(err, sqlx.ErrNotFound) {
  165. return response.ErrBadRequest("部门不存在")
  166. }
  167. return err
  168. }
  169. if lockedDept.Status != consts.StatusEnabled {
  170. return response.ErrBadRequest("目标部门已停用")
  171. }
  172. return l.svcCtx.SysUserModel.UpdateProfileWithTx(
  173. ctx, session, req.Id, user.Username,
  174. nickname, email, phone, remark, deptId,
  175. newStatus, statusChanged, user.UpdateTime,
  176. )
  177. }); err != nil {
  178. if errors.Is(err, userModel.ErrUpdateConflict) {
  179. return response.ErrConflict("数据已被其他操作修改,请刷新后重试")
  180. }
  181. return err
  182. }
  183. // 审计 L-R12-1:UpdateProfileWithTx 不再自己 DelCache(避免 pre-commit 窗口里并发 FindOne
  184. // 把未提交旧值灌回缓存);这里在 commit 成功后显式失效 sysUser 低层 id/username 键,再叠加
  185. // UserDetails 聚合缓存的 Clean,整条"两级缓存 → DB 权威"读链回到 cache-miss → loadFromDB。
  186. l.svcCtx.SysUserModel.InvalidateProfileCache(l.ctx, req.Id, user.Username)
  187. l.svcCtx.UserDetailsLoader.Clean(l.ctx, req.Id)
  188. return nil
  189. }