From 62e8acf7577fe697224f224947c96ce2c0ca7d18 Mon Sep 17 00:00:00 2001 From: loveuer Date: Sat, 28 Feb 2026 01:56:56 -0800 Subject: [PATCH] refactor: remove GORM FK associations, handle relations in business layer (v0.6.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Role association field from User model - Remove User association field from Token model - controller/user.go: query Role separately after loading User - controller/token.go: query User and Role with separate DB calls - handler/admin.go: introduce userResp type, build role info manually; batch-load roles in AdminListUsers to avoid N+1 🤖 Generated with [Qoder][https://qoder.com] --- internal/controller/token.go | 32 +++++++---- internal/controller/user.go | 12 ++-- internal/handler/admin.go | 104 +++++++++++++++++++++++++++-------- internal/model/token.go | 1 - internal/model/user.go | 1 - 5 files changed, 110 insertions(+), 40 deletions(-) diff --git a/internal/controller/token.go b/internal/controller/token.go index e200ffe..4d6d4ba 100644 --- a/internal/controller/token.go +++ b/internal/controller/token.go @@ -65,13 +65,7 @@ func (tm *tokenManager) Delete(userID uint, tokenID uint) error { // Verify looks up a DB API token and returns a Session if valid. func (tm *tokenManager) Verify(rawToken string) (*model.Session, error) { var t model.Token - err := db.Default.Session(). - Where("token = ?", rawToken). - Preload("User"). - Preload("User.Role"). - First(&t).Error - - if err != nil { + if err := db.Default.Session().Where("token = ?", rawToken).First(&t).Error; err != nil { return nil, errors.New("无效的 API Token") } @@ -79,16 +73,30 @@ func (tm *tokenManager) Verify(rawToken string) (*model.Session, error) { return nil, errors.New("API Token 已过期") } + var user model.User + if err := db.Default.Session().First(&user, t.UserID).Error; err != nil { + return nil, errors.New("Token 关联用户不存在") + } + + if !user.Active { + return nil, errors.New("账号已被禁用") + } + + var role model.Role + if err := db.Default.Session().First(&role, user.RoleID).Error; err != nil { + return nil, errors.New("账号角色异常") + } + // Update last_used_at asynchronously now := time.Now() go db.Default.Session().Model(&t).Update("last_used_at", now) //nolint:errcheck session := &model.Session{ - UserID: t.User.ID, - Username: t.User.Username, - Role: t.User.Role.Name, - RoleLabel: t.User.Role.Label, - Permissions: t.User.Role.PermissionList(), + UserID: user.ID, + Username: user.Username, + Role: role.Name, + RoleLabel: role.Label, + Permissions: role.PermissionList(), LoginAt: now.Unix(), Token: rawToken, } diff --git a/internal/controller/user.go b/internal/controller/user.go index d56d970..de25645 100644 --- a/internal/controller/user.go +++ b/internal/controller/user.go @@ -89,7 +89,6 @@ func (um *userManager) Login(username, password string) (*model.Session, error) user := new(model.User) if err := db.Default.Session(). Where("username = ? AND active = ?", username, true). - Preload("Role"). First(user).Error; err != nil { return nil, errors.New("账号或密码错误") } @@ -98,12 +97,17 @@ func (um *userManager) Login(username, password string) (*model.Session, error) return nil, errors.New("账号或密码错误") } + var role model.Role + if err := db.Default.Session().First(&role, user.RoleID).Error; err != nil { + return nil, errors.New("账号角色异常,请联系管理员") + } + session := &model.Session{ UserID: user.ID, Username: user.Username, - Role: user.Role.Name, - RoleLabel: user.Role.Label, - Permissions: user.Role.PermissionList(), + Role: role.Name, + RoleLabel: role.Label, + Permissions: role.PermissionList(), LoginAt: now.Unix(), Token: tool.RandomString(32), } diff --git a/internal/handler/admin.go b/internal/handler/admin.go index 81a508b..76a3a07 100644 --- a/internal/handler/admin.go +++ b/internal/handler/admin.go @@ -3,6 +3,7 @@ package handler import ( "net/http" "strings" + "time" "github.com/loveuer/nf" "github.com/loveuer/nf/nft/log" @@ -12,14 +13,65 @@ import ( "github.com/spf13/cast" ) +// userResp is the JSON response shape for a user including role info, +// built manually at the business layer instead of relying on GORM associations. +type userResp struct { + ID uint `json:"id"` + Username string `json:"username"` + RoleID uint `json:"role_id"` + Role model.Role `json:"role"` + Active bool `json:"active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func toUserResp(u model.User, r model.Role) userResp { + return userResp{ + ID: u.ID, + Username: u.Username, + RoleID: u.RoleID, + Role: r, + Active: u.Active, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + } +} + func AdminListUsers() nf.HandlerFunc { return func(c *nf.Ctx) error { var users []model.User - if err := db.Default.Session().Preload("Role").Find(&users).Error; err != nil { + if err := db.Default.Session().Find(&users).Error; err != nil { log.Error("handler.AdminListUsers: %s", err.Error()) return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询失败"}) } - return c.Status(http.StatusOK).JSON(map[string]any{"data": users}) + + // Collect unique role IDs and query them in one shot + roleIDSet := make(map[uint]struct{}) + for _, u := range users { + roleIDSet[u.RoleID] = struct{}{} + } + roleIDs := make([]uint, 0, len(roleIDSet)) + for id := range roleIDSet { + roleIDs = append(roleIDs, id) + } + + var roles []model.Role + if err := db.Default.Session().Where("id IN ?", roleIDs).Find(&roles).Error; err != nil { + log.Error("handler.AdminListUsers: query roles: %s", err.Error()) + return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询失败"}) + } + + roleMap := make(map[uint]model.Role, len(roles)) + for _, r := range roles { + roleMap[r.ID] = r + } + + resp := make([]userResp, 0, len(users)) + for _, u := range users { + resp = append(resp, toUserResp(u, roleMap[u.RoleID])) + } + + return c.Status(http.StatusOK).JSON(map[string]any{"data": resp}) } } @@ -57,6 +109,11 @@ func AdminCreateUser() nf.HandlerFunc { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "用户名已存在"}) } + var role model.Role + if err := db.Default.Session().First(&role, req.RoleID).Error; err != nil { + return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无效的角色"}) + } + user := &model.User{ Username: req.Username, Password: tool.NewPassword(req.Password), @@ -69,11 +126,7 @@ func AdminCreateUser() nf.HandlerFunc { return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "创建用户失败"}) } - if err := db.Default.Session().Preload("Role").First(user, user.ID).Error; err != nil { - log.Error("handler.AdminCreateUser: preload role: %s", err.Error()) - } - - return c.Status(http.StatusOK).JSON(map[string]any{"data": user}) + return c.Status(http.StatusOK).JSON(map[string]any{"data": toUserResp(*user, role)}) } } @@ -97,11 +150,16 @@ func AdminUpdateUser() nf.HandlerFunc { session := c.Locals("user").(*model.Session) - user := new(model.User) - if err := db.Default.Session().Preload("Role").First(user, id).Error; err != nil { + var user model.User + if err := db.Default.Session().First(&user, id).Error; err != nil { return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"}) } + var currentRole model.Role + if err := db.Default.Session().First(¤tRole, user.RoleID).Error; err != nil { + return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询角色失败"}) + } + updates := map[string]any{} if req.RoleID != nil && *req.RoleID != user.RoleID { @@ -110,7 +168,7 @@ func AdminUpdateUser() nf.HandlerFunc { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无效的角色"}) } // If demoting from admin, ensure at least one other active admin remains - if user.Role.Name == model.RoleAdmin && newRole.Name != model.RoleAdmin { + if currentRole.Name == model.RoleAdmin && newRole.Name != model.RoleAdmin { var adminCount int64 db.Default.Session().Model(&model.User{}). Where("role_id = ? AND active = ? AND id != ?", user.RoleID, true, id). @@ -120,13 +178,14 @@ func AdminUpdateUser() nf.HandlerFunc { } } updates["role_id"] = *req.RoleID + currentRole = newRole } if req.Active != nil && *req.Active != user.Active { if user.ID == session.UserID && !*req.Active { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "不能禁用自己的账号"}) } - if user.Role.Name == model.RoleAdmin && !*req.Active { + if currentRole.Name == model.RoleAdmin && !*req.Active { var adminCount int64 db.Default.Session().Model(&model.User{}). Where("role_id = ? AND active = ? AND id != ?", user.RoleID, true, id). @@ -149,16 +208,12 @@ func AdminUpdateUser() nf.HandlerFunc { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "没有需要更新的字段"}) } - if err := db.Default.Session().Model(user).Updates(updates).Error; err != nil { + if err := db.Default.Session().Model(&user).Updates(updates).Error; err != nil { log.Error("handler.AdminUpdateUser: %s", err.Error()) return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "更新失败"}) } - if err := db.Default.Session().Preload("Role").First(user, user.ID).Error; err != nil { - log.Error("handler.AdminUpdateUser: preload: %s", err.Error()) - } - - return c.Status(http.StatusOK).JSON(map[string]any{"data": user}) + return c.Status(http.StatusOK).JSON(map[string]any{"data": toUserResp(user, currentRole)}) } } @@ -174,13 +229,18 @@ func AdminDeleteUser() nf.HandlerFunc { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "不能删除自己的账号"}) } - user := new(model.User) - if err := db.Default.Session().Preload("Role").First(user, id).Error; err != nil { + var user model.User + if err := db.Default.Session().First(&user, id).Error; err != nil { return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"}) } - // Prevent deleting the last admin - if user.Role.Name == model.RoleAdmin { + // Prevent deleting the last admin: check via role name + var userRole model.Role + if err := db.Default.Session().First(&userRole, user.RoleID).Error; err != nil { + return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询角色失败"}) + } + + if userRole.Name == model.RoleAdmin { var adminCount int64 db.Default.Session().Model(&model.User{}). Where("role_id = ? AND id != ?", user.RoleID, id). @@ -190,7 +250,7 @@ func AdminDeleteUser() nf.HandlerFunc { } } - if err := db.Default.Session().Delete(user).Error; err != nil { + if err := db.Default.Session().Delete(&user).Error; err != nil { log.Error("handler.AdminDeleteUser: %s", err.Error()) return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "删除失败"}) } diff --git a/internal/model/token.go b/internal/model/token.go index 74a1477..8db43c3 100644 --- a/internal/model/token.go +++ b/internal/model/token.go @@ -7,7 +7,6 @@ import "time" type Token struct { ID uint `gorm:"primarykey" json:"id"` UserID uint `gorm:"not null;index" json:"user_id"` - User User `gorm:"foreignKey:UserID" json:"-"` Name string `gorm:"not null" json:"name"` Token string `gorm:"uniqueIndex;not null" json:"-"` CreatedAt time.Time `json:"created_at"` diff --git a/internal/model/user.go b/internal/model/user.go index 0eb2fa6..c52f8d7 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -8,7 +8,6 @@ type User struct { Username string `gorm:"uniqueIndex;not null" json:"username"` Password string `gorm:"not null" json:"-"` RoleID uint `gorm:"not null" json:"role_id"` - Role Role `gorm:"foreignKey:RoleID" json:"role"` Active bool `gorm:"default:true" json:"active"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"`