From 3898aae32e4422fc4a3b749c525b7dcf613b3a3f Mon Sep 17 00:00:00 2001 From: ila Date: Wed, 26 Aug 2026 22:01:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=82=E9=85=8D=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=AB=AF=E7=B3=BB=E7=BB=9F=20API=20=E4=B8=8E=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E4=BF=9D=E6=8A=A4=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/app/admin/apis/sys_role.go | 21 +++- admin/app/admin/apis/sys_user.go | 22 ++-- admin/app/admin/models/sys_user.go | 16 +-- admin/app/admin/router/sys_api.go | 1 - admin/app/admin/router/sys_login_log.go | 1 - admin/app/admin/router/sys_menu.go | 3 - admin/app/admin/router/sys_role.go | 1 - admin/app/admin/router/sys_router_test.go | 9 ++ .../admin/service/admin_protection_test.go | 64 +++++++++++ admin/app/admin/service/dto/sys_user.go | 35 ------ admin/app/admin/service/sys_role.go | 50 ++++++++- admin/app/admin/service/sys_user.go | 104 +++++++++++------- 12 files changed, 226 insertions(+), 101 deletions(-) create mode 100644 admin/app/admin/service/admin_protection_test.go diff --git a/admin/app/admin/apis/sys_role.go b/admin/app/admin/apis/sys_role.go index 05ffdb0..aa55fee 100644 --- a/admin/app/admin/apis/sys_role.go +++ b/admin/app/admin/apis/sys_role.go @@ -1,13 +1,14 @@ package apis import ( + "errors" "fmt" "git.ilapage.cn/OPC/chorus/admin/common/global" "net/http" + "git.ilapage.cn/OPC/chorus/admin/app/admin/models" "github.com/gin-gonic/gin/binding" "github.com/go-admin-team/go-admin-core/sdk" - "git.ilapage.cn/OPC/chorus/admin/app/admin/models" "github.com/gin-gonic/gin" "github.com/go-admin-team/go-admin-core/sdk/api" @@ -168,6 +169,11 @@ func (e SysRole) Update(c *gin.Context) { err = s.Update(&req, cb) if err != nil { e.Logger.Error(err) + if errors.Is(err, service.ErrBuiltInRoleProtected) { + e.Error(http.StatusConflict, err, err.Error()) + } else { + e.Error(http.StatusInternalServerError, errors.New("角色更新失败"), "角色更新失败") + } return } @@ -207,7 +213,11 @@ func (e SysRole) Delete(c *gin.Context) { err = s.Remove(&req, cb) if err != nil { e.Logger.Error(err) - e.Error(500, err, "") + if errors.Is(err, service.ErrBuiltInRoleProtected) { + e.Error(http.StatusConflict, err, err.Error()) + } else { + e.Error(http.StatusInternalServerError, errors.New("角色删除失败"), "角色删除失败") + } return } @@ -240,7 +250,12 @@ func (e SysRole) Update2Status(c *gin.Context) { req.SetUpdateBy(user.GetUserId(c)) err = s.UpdateStatus(&req) if err != nil { - e.Error(500, err, fmt.Sprintf("更新角色状态失败,失败原因:%s ", err.Error())) + if errors.Is(err, service.ErrBuiltInRoleProtected) { + e.Error(http.StatusConflict, err, err.Error()) + } else { + e.Logger.Error(err) + e.Error(http.StatusInternalServerError, errors.New("角色状态更新失败"), "角色状态更新失败") + } return } e.OK(req.GetId(), fmt.Sprintf("更新角色 %v 状态成功!", req.GetId())) diff --git a/admin/app/admin/apis/sys_user.go b/admin/app/admin/apis/sys_user.go index 99094b5..3a27cdf 100644 --- a/admin/app/admin/apis/sys_user.go +++ b/admin/app/admin/apis/sys_user.go @@ -1,8 +1,9 @@ package apis import ( - "github.com/gin-gonic/gin/binding" + "errors" "git.ilapage.cn/OPC/chorus/admin/app/admin/models" + "github.com/gin-gonic/gin/binding" "golang.org/x/crypto/bcrypt" "net/http" @@ -190,9 +191,14 @@ func (e SysUser) Delete(c *gin.Context) { // 数据权限检查 p := actions.GetPermissionFromContext(c) - err = s.Remove(&req, p) + err = s.Remove(&req, p, user.GetUserId(c)) if err != nil { e.Logger.Error(err) + if errors.Is(err, service.ErrCurrentAdminProtected) || errors.Is(err, service.ErrLastAdminProtected) { + e.Error(http.StatusConflict, err, err.Error()) + } else { + e.Error(http.StatusInternalServerError, errors.New("管理员删除失败"), "管理员删除失败") + } return } e.OK(req.GetId(), "删除成功") @@ -275,9 +281,14 @@ func (e SysUser) UpdateStatus(c *gin.Context) { //数据权限检查 p := actions.GetPermissionFromContext(c) - err = s.UpdateStatus(&req, p) + err = s.UpdateStatus(&req, p, user.GetUserId(c)) if err != nil { e.Logger.Error(err) + if errors.Is(err, service.ErrCurrentAdminProtected) || errors.Is(err, service.ErrLastAdminProtected) { + e.Error(http.StatusConflict, err, err.Error()) + } else { + e.Error(http.StatusInternalServerError, errors.New("管理员状态更新失败"), "管理员状态更新失败") + } return } e.OK(req.GetId(), "更新成功") @@ -385,8 +396,7 @@ func (e SysUser) GetProfile(c *gin.Context) { sysUser := models.SysUser{} roles := make([]models.SysRole, 0) - posts := make([]models.SysPost, 0) - err = s.GetProfile(&req, &sysUser, &roles, &posts) + err = s.GetProfile(&req, &sysUser, &roles) if err != nil { e.Logger.Errorf("get user profile error, %s", err.Error()) e.Error(500, err, "获取用户信息失败") @@ -395,7 +405,6 @@ func (e SysUser) GetProfile(c *gin.Context) { e.OK(gin.H{ "user": sysUser, "roles": roles, - "posts": posts, }, "查询成功") } @@ -452,7 +461,6 @@ func (e SysUser) GetInfo(c *gin.Context) { } mp["userName"] = sysUser.Username mp["userId"] = sysUser.UserId - mp["deptId"] = sysUser.DeptId mp["name"] = sysUser.NickName mp["code"] = 200 e.OK(mp, "") diff --git a/admin/app/admin/models/sys_user.go b/admin/app/admin/models/sys_user.go index 261ace5..8a421b0 100644 --- a/admin/app/admin/models/sys_user.go +++ b/admin/app/admin/models/sys_user.go @@ -11,20 +11,20 @@ type SysUser struct { Username string `json:"username" gorm:"size:64;comment:用户名"` Password string `json:"-" gorm:"size:128;comment:密码"` NickName string `json:"nickName" gorm:"size:128;comment:昵称"` - Phone string `json:"phone" gorm:"size:11;comment:手机号"` + Phone string `json:"-" gorm:"size:11;comment:手机号"` RoleId int `json:"roleId" gorm:"size:20;comment:角色ID"` Salt string `json:"-" gorm:"size:255;comment:加盐"` Avatar string `json:"avatar" gorm:"size:255;comment:头像"` - Sex string `json:"sex" gorm:"size:255;comment:性别"` - Email string `json:"email" gorm:"size:128;comment:邮箱"` - DeptId int `json:"deptId" gorm:"size:20;comment:部门"` - PostId int `json:"postId" gorm:"size:20;comment:岗位"` + Sex string `json:"-" gorm:"size:255;comment:性别"` + Email string `json:"-" gorm:"size:128;comment:邮箱"` + DeptId int `json:"-" gorm:"size:20;comment:部门"` + PostId int `json:"-" gorm:"size:20;comment:岗位"` Remark string `json:"remark" gorm:"size:255;comment:备注"` Status string `json:"status" gorm:"size:4;comment:状态"` - DeptIds []int `json:"deptIds" gorm:"-"` - PostIds []int `json:"postIds" gorm:"-"` + DeptIds []int `json:"-" gorm:"-"` + PostIds []int `json:"-" gorm:"-"` RoleIds []int `json:"roleIds" gorm:"-"` - Dept *SysDept `json:"dept"` + Dept *SysDept `json:"-"` models.ControlBy models.ModelTime } diff --git a/admin/app/admin/router/sys_api.go b/admin/app/admin/router/sys_api.go index 1e7eb39..cd211d4 100644 --- a/admin/app/admin/router/sys_api.go +++ b/admin/app/admin/router/sys_api.go @@ -19,6 +19,5 @@ func registerSysApiRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddlew { r.GET("", api.GetPage) r.GET("/:id", api.Get) - r.PUT("/:id", api.Update) } } diff --git a/admin/app/admin/router/sys_login_log.go b/admin/app/admin/router/sys_login_log.go index 5691178..29429f1 100644 --- a/admin/app/admin/router/sys_login_log.go +++ b/admin/app/admin/router/sys_login_log.go @@ -19,6 +19,5 @@ func registerSysLoginLogRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi { r.GET("", api.GetPage) r.GET("/:id", api.Get) - r.DELETE("", api.Delete) } } diff --git a/admin/app/admin/router/sys_menu.go b/admin/app/admin/router/sys_menu.go index 93acc7e..39ae744 100644 --- a/admin/app/admin/router/sys_menu.go +++ b/admin/app/admin/router/sys_menu.go @@ -19,9 +19,6 @@ func registerSysMenuRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle { r.GET("", api.GetPage) r.GET("/:id", api.Get) - r.POST("", api.Insert) - r.PUT("/:id", api.Update) - r.DELETE("", api.Delete) } r1 := v1.Group("").Use(authMiddleware.MiddlewareFunc()) diff --git a/admin/app/admin/router/sys_role.go b/admin/app/admin/router/sys_role.go index 20ca574..0578ca1 100644 --- a/admin/app/admin/router/sys_role.go +++ b/admin/app/admin/router/sys_role.go @@ -26,6 +26,5 @@ func registerSysRoleRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle r1 := v1.Group("").Use(authMiddleware.MiddlewareFunc()) { r1.PUT("/role-status", api.Update2Status) - r1.PUT("/roledatascope", api.Update2DataScope) } } diff --git a/admin/app/admin/router/sys_router_test.go b/admin/app/admin/router/sys_router_test.go index ecc98be..a89343b 100644 --- a/admin/app/admin/router/sys_router_test.go +++ b/admin/app/admin/router/sys_router_test.go @@ -24,6 +24,15 @@ func TestSystemRouterExposesLoginWithoutCaptcha(t *testing.T) { t.Fatalf("system route is not registered: %s", path) } } + for _, path := range []string{ + "POST /api/v1/menu", "PUT /api/v1/menu/:id", "DELETE /api/v1/menu", + "PUT /api/v1/sys-api/:id", "DELETE /api/v1/sys-login-log", + "PUT /api/v1/roledatascope", + } { + if _, ok := routes[path]; ok { + t.Fatalf("read-only system route must not be registered: %s", path) + } + } for _, path := range []string{"GET /api/v1/captcha", "GET /api/v1/getCaptcha"} { if _, ok := routes[path]; ok { t.Fatalf("captcha route must not be registered: %s", path) diff --git a/admin/app/admin/service/admin_protection_test.go b/admin/app/admin/service/admin_protection_test.go new file mode 100644 index 0000000..3324ab7 --- /dev/null +++ b/admin/app/admin/service/admin_protection_test.go @@ -0,0 +1,64 @@ +package service + +import ( + "errors" + "testing" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "git.ilapage.cn/OPC/chorus/admin/app/admin/models" +) + +func TestProtectAdminRemovalRejectsCurrentAndLastActiveAdmin(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&models.SysUser{}); err != nil { + t.Fatal(err) + } + admin := models.SysUser{UserId: 1, Username: "admin", Status: "2"} + if err := db.Create(&admin).Error; err != nil { + t.Fatal(err) + } + + if err := protectAdminRemoval(db, []models.SysUser{admin}, 1); !errors.Is(err, ErrCurrentAdminProtected) { + t.Fatalf("current admin error = %v", err) + } + if err := protectAdminRemoval(db, []models.SysUser{admin}, 99); !errors.Is(err, ErrLastAdminProtected) { + t.Fatalf("last active admin error = %v", err) + } + + second := models.SysUser{UserId: 2, Username: "operator", Status: "2"} + if err := db.Create(&second).Error; err != nil { + t.Fatal(err) + } + if err := protectAdminRemoval(db, []models.SysUser{second}, 1); err != nil { + t.Fatalf("removing one of two active admins: %v", err) + } +} + +func TestProtectBuiltInRole(t *testing.T) { + role := models.SysRole{RoleId: 1, RoleName: "Chorus Operator", RoleKey: BuiltInOperatorRoleKey, Status: "2"} + for name, test := range map[string]struct { + roleName string + roleKey string + status string + op string + }{ + "rename display name": {"Other", BuiltInOperatorRoleKey, "2", "update"}, + "rename key": {role.RoleName, "other", "2", "update"}, + "disable": {role.RoleName, BuiltInOperatorRoleKey, "1", "status"}, + "delete": {"", "", "", "delete"}, + } { + t.Run(name, func(t *testing.T) { + if err := protectBuiltInRole(role, test.roleName, test.roleKey, test.status, test.op); !errors.Is(err, ErrBuiltInRoleProtected) { + t.Fatalf("error = %v", err) + } + }) + } + if err := protectBuiltInRole(role, role.RoleName, role.RoleKey, "2", "update"); err != nil { + t.Fatalf("menu-only update should remain allowed: %v", err) + } +} diff --git a/admin/app/admin/service/dto/sys_user.go b/admin/app/admin/service/dto/sys_user.go index 56f4565..b64babb 100644 --- a/admin/app/admin/service/dto/sys_user.go +++ b/admin/app/admin/service/dto/sys_user.go @@ -12,13 +12,8 @@ type SysUserGetPageReq struct { UserId int `form:"userId" search:"type:exact;column:user_id;table:sys_user" comment:"用户ID"` Username string `form:"username" search:"type:contains;column:username;table:sys_user" comment:"用户名"` NickName string `form:"nickName" search:"type:contains;column:nick_name;table:sys_user" comment:"昵称"` - Phone string `form:"phone" search:"type:contains;column:phone;table:sys_user" comment:"手机号"` RoleId string `form:"roleId" search:"type:exact;column:role_id;table:sys_user" comment:"角色ID"` - Sex string `form:"sex" search:"type:exact;column:sex;table:sys_user" comment:"性别"` - Email string `form:"email" search:"type:contains;column:email;table:sys_user" comment:"邮箱"` - PostId string `form:"postId" search:"type:exact;column:post_id;table:sys_user" comment:"岗位"` Status string `form:"status" search:"type:exact;column:status;table:sys_user" comment:"状态"` - DeptJoin `search:"type:left;on:dept_id:dept_id;table:sys_user;join:sys_dept"` SysUserOrder } @@ -29,10 +24,6 @@ type SysUserOrder struct { CreatedAtOrder string `search:"type:order;column:created_at;table:sys_user" form:"createdAtOrder"` } -type DeptJoin struct { - DeptId string `search:"type:contains;column:dept_path;table:sys_dept" form:"deptId"` -} - func (m *SysUserGetPageReq) GetNeedSearch() interface{} { return *m } @@ -93,13 +84,7 @@ type SysUserInsertReq struct { Username string `json:"username" comment:"用户名" vd:"len($)>0"` Password string `json:"password" comment:"密码"` NickName string `json:"nickName" comment:"昵称" vd:"len($)>0"` - Phone string `json:"phone" comment:"手机号" vd:"len($)>0"` RoleId int `json:"roleId" comment:"角色ID"` - Avatar string `json:"avatar" comment:"头像"` - Sex string `json:"sex" comment:"性别"` - Email string `json:"email" comment:"邮箱" vd:"len($)>0,email"` - DeptId int `json:"deptId" comment:"部门" vd:"$>0"` - PostId int `json:"postId" comment:"岗位"` Remark string `json:"remark" comment:"备注"` Status string `json:"status" comment:"状态" vd:"len($)>0" default:"1"` common.ControlBy @@ -112,13 +97,7 @@ func (s *SysUserInsertReq) Generate(model *models.SysUser) { model.Username = s.Username model.Password = s.Password model.NickName = s.NickName - model.Phone = s.Phone model.RoleId = s.RoleId - model.Avatar = s.Avatar - model.Sex = s.Sex - model.Email = s.Email - model.DeptId = s.DeptId - model.PostId = s.PostId model.Remark = s.Remark model.Status = s.Status model.CreateBy = s.CreateBy @@ -132,15 +111,8 @@ type SysUserUpdateReq struct { UserId int `json:"userId" comment:"用户ID"` // 用户ID Username string `json:"username" comment:"用户名" vd:"len($)>0"` NickName string `json:"nickName" comment:"昵称" vd:"len($)>0"` - Phone string `json:"phone" comment:"手机号" vd:"len($)>0"` RoleId int `json:"roleId" comment:"角色ID"` - Avatar string `json:"avatar" comment:"头像"` - Sex string `json:"sex" comment:"性别"` - Email string `json:"email" comment:"邮箱" vd:"len($)>0,email"` - DeptId int `json:"deptId" comment:"部门" vd:"$>0"` - PostId int `json:"postId" comment:"岗位"` Remark string `json:"remark" comment:"备注"` - Status string `json:"status" comment:"状态" default:"1"` common.ControlBy } @@ -150,15 +122,8 @@ func (s *SysUserUpdateReq) Generate(model *models.SysUser) { } model.Username = s.Username model.NickName = s.NickName - model.Phone = s.Phone model.RoleId = s.RoleId - model.Avatar = s.Avatar - model.Sex = s.Sex - model.Email = s.Email - model.DeptId = s.DeptId - model.PostId = s.PostId model.Remark = s.Remark - model.Status = s.Status } func (s *SysUserUpdateReq) GetId() interface{} { diff --git a/admin/app/admin/service/sys_role.go b/admin/app/admin/service/sys_role.go index fc28f28..4b08a29 100644 --- a/admin/app/admin/service/sys_role.go +++ b/admin/app/admin/service/sys_role.go @@ -20,6 +20,29 @@ type SysRole struct { service.Service } +const BuiltInOperatorRoleKey = "chorus_operator" + +var ErrBuiltInRoleProtected = errors.New("ADMIN_BUILT_IN_ROLE_PROTECTED: 内置角色不能改名、删除或停用") + +func protectBuiltInRole(role models.SysRole, roleName, roleKey, status, operation string) error { + if role.RoleKey != BuiltInOperatorRoleKey { + return nil + } + switch operation { + case "update": + if roleName != role.RoleName || roleKey != role.RoleKey || status != "2" { + return ErrBuiltInRoleProtected + } + case "delete": + return ErrBuiltInRoleProtected + case "status": + if status != "2" { + return ErrBuiltInRoleProtected + } + } + return nil +} + // GetPage 获取SysRole列表 func (e *SysRole) GetPage(c *dto.SysRoleGetPageReq, list *[]models.SysRole, count *int64) error { var err error @@ -143,7 +166,12 @@ func (e *SysRole) Update(c *dto.SysRoleUpdateReq, cb *casbin.SyncedEnforcer) err } var model = models.SysRole{} var mlist = make([]models.SysMenu, 0) - tx.Preload("SysMenu").First(&model, c.GetId()) + if err = tx.Preload("SysMenu").First(&model, c.GetId()).Error; err != nil { + return err + } + if err = protectBuiltInRole(model, c.RoleName, c.RoleKey, c.Status, "update"); err != nil { + return err + } tx.Preload("SysApi").Where("menu_id in ?", c.MenuIds).Find(&mlist) err = tx.Model(&model).Association("SysMenu").Delete(model.SysMenu) if err != nil { @@ -206,8 +234,19 @@ func (e *SysRole) Remove(c *dto.SysRoleDeleteReq, cb *casbin.SyncedEnforcer) err } }() } + var roles []models.SysRole + if err = tx.Where("role_id IN ?", c.Ids).Find(&roles).Error; err != nil { + return err + } + for _, role := range roles { + if err = protectBuiltInRole(role, "", "", "", "delete"); err != nil { + return err + } + } var model = models.SysRole{} - tx.Preload("SysMenu").Preload("SysDept").First(&model, c.GetId()) + if err = tx.Preload("SysMenu").Preload("SysDept").First(&model, c.GetId()).Error; err != nil { + return err + } //删除 SysRole 时,同时删除角色所有 关联其它表 记录 (SysMenu 和 SysMenu) db := tx.Select(clause.Associations).Delete(&model) @@ -295,7 +334,12 @@ func (e *SysRole) UpdateStatus(c *dto.UpdateStatusReq) error { }() } var model = models.SysRole{} - tx.First(&model, c.GetId()) + if err = tx.First(&model, c.GetId()).Error; err != nil { + return err + } + if err = protectBuiltInRole(model, model.RoleName, model.RoleKey, c.Status, "status"); err != nil { + return err + } c.Generate(&model) // 更新关联的数据,使用 FullSaveAssociations 模式 db := tx.Session(&gorm.Session{FullSaveAssociations: true}).Debug().Save(&model) diff --git a/admin/app/admin/service/sys_user.go b/admin/app/admin/service/sys_user.go index 626fa35..00c4d44 100644 --- a/admin/app/admin/service/sys_user.go +++ b/admin/app/admin/service/sys_user.go @@ -9,6 +9,7 @@ import ( "github.com/go-admin-team/go-admin-core/sdk/pkg" "github.com/go-admin-team/go-admin-core/sdk/service" "gorm.io/gorm" + "gorm.io/gorm/clause" "git.ilapage.cn/OPC/chorus/admin/common/actions" cDto "git.ilapage.cn/OPC/chorus/admin/common/dto" @@ -18,12 +19,17 @@ type SysUser struct { service.Service } +var ( + ErrCurrentAdminProtected = errors.New("ADMIN_CURRENT_ACCOUNT_PROTECTED: 不能停用或删除当前登录管理员") + ErrLastAdminProtected = errors.New("ADMIN_LAST_ACTIVE_ACCOUNT_PROTECTED: 至少保留一个有效管理员") +) + // GetPage 获取SysUser列表 func (e *SysUser) GetPage(c *dto.SysUserGetPageReq, p *actions.DataPermission, list *[]models.SysUser, count *int64) error { var err error var data models.SysUser - err = e.Orm.Debug().Preload("Dept"). + err = e.Orm.Debug(). Scopes( cDto.MakeCondition(c.GetNeedSearch()), cDto.Paginate(c.GetPageSize(), c.GetPageIndex()), @@ -136,26 +142,21 @@ func (e *SysUser) UpdateAvatar(c *dto.UpdateSysUserAvatarReq, p *actions.DataPer } // UpdateStatus 更新用户状态 -func (e *SysUser) UpdateStatus(c *dto.UpdateSysUserStatusReq, p *actions.DataPermission) error { - var err error - var model models.SysUser - db := e.Orm.Scopes( - actions.Permission(model.TableName(), p), - ).First(&model, c.GetId()) - if err = db.Error; err != nil { - e.Log.Errorf("Service UpdateSysUser error: %s", err) - return err - } - if db.RowsAffected == 0 { - return errors.New("无权更新该数据") - - } - err = e.Orm.Table(model.TableName()).Where("user_id =? ", c.UserId).Updates(c).Error - if err != nil { - e.Log.Errorf("Service UpdateSysUser error: %s", err) - return err - } - return nil +func (e *SysUser) UpdateStatus(c *dto.UpdateSysUserStatusReq, p *actions.DataPermission, currentUserID int) error { + return e.Orm.Transaction(func(tx *gorm.DB) error { + var model models.SysUser + if err := tx.Scopes(actions.Permission(model.TableName(), p)).First(&model, c.GetId()).Error; err != nil { + return err + } + if c.Status != "2" { + if err := protectAdminRemoval(tx, []models.SysUser{model}, currentUserID); err != nil { + return err + } + } + return tx.Model(&model).Select("status", "update_by").Updates(map[string]interface{}{ + "status": c.Status, "update_by": c.UpdateBy, + }).Error + }) } // ResetPwd 重置用户密码 @@ -182,20 +183,50 @@ func (e *SysUser) ResetPwd(c *dto.ResetSysUserPwdReq, p *actions.DataPermission) } // Remove 删除SysUser -func (e *SysUser) Remove(c *dto.SysUserById, p *actions.DataPermission) error { - var err error - var data models.SysUser +func (e *SysUser) Remove(c *dto.SysUserById, p *actions.DataPermission, currentUserID int) error { + return e.Orm.Transaction(func(tx *gorm.DB) error { + var targets []models.SysUser + if err := tx.Scopes(actions.Permission((&models.SysUser{}).TableName(), p)). + Where("user_id IN ?", normalizedUserIDs(c)).Find(&targets).Error; err != nil { + return err + } + if len(targets) == 0 { + return errors.New("无权删除该数据") + } + if err := protectAdminRemoval(tx, targets, currentUserID); err != nil { + return err + } + return tx.Delete(&models.SysUser{}, normalizedUserIDs(c)).Error + }) +} - db := e.Orm.Model(&data). - Scopes( - actions.Permission(data.TableName(), p), - ).Delete(&data, c.GetId()) - if err = db.Error; err != nil { - e.Log.Errorf("Error found in RemoveSysUser : %s", err) +func normalizedUserIDs(c *dto.SysUserById) []int { + ids := append([]int(nil), c.Ids...) + if c.Id > 0 { + ids = append(ids, c.Id) + } + return ids +} + +func protectAdminRemoval(tx *gorm.DB, targets []models.SysUser, currentUserID int) error { + activeTargets := 0 + for _, target := range targets { + if target.UserId == currentUserID { + return ErrCurrentAdminProtected + } + if target.Status == "2" { + activeTargets++ + } + } + if activeTargets == 0 { + return nil + } + var active []models.SysUser + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("status = ?", "2").Find(&active).Error; err != nil { return err } - if db.RowsAffected == 0 { - return errors.New("无权删除该数据") + if len(active)-activeTargets < 1 { + return ErrLastAdminProtected } return nil } @@ -248,8 +279,8 @@ func (e *SysUser) UpdatePwd(id int, oldPassword, newPassword string, p *actions. return nil } -func (e *SysUser) GetProfile(c *dto.SysUserById, user *models.SysUser, roles *[]models.SysRole, posts *[]models.SysPost) error { - err := e.Orm.Preload("Dept").First(user, c.GetId()).Error +func (e *SysUser) GetProfile(c *dto.SysUserById, user *models.SysUser, roles *[]models.SysRole) error { + err := e.Orm.First(user, c.GetId()).Error if err != nil { return err } @@ -257,10 +288,5 @@ func (e *SysUser) GetProfile(c *dto.SysUserById, user *models.SysUser, roles *[] if err != nil { return err } - err = e.Orm.Find(posts, user.PostIds).Error - if err != nil { - return err - } - return nil }