1 Commits

Author SHA1 Message Date
loveuer
62e8acf757 refactor: remove GORM FK associations, handle relations in business layer (v0.6.1)
Some checks are pending
Release Binaries / Build and Release (.exe, amd64, windows, windows-amd64) (push) Waiting to run
Release Binaries / Build and Release (amd64, darwin, darwin-amd64) (push) Waiting to run
Release Binaries / Build and Release (amd64, linux, linux-amd64) (push) Waiting to run
Release Binaries / Build and Release (arm64, darwin, darwin-arm64) (push) Waiting to run
Release Binaries / Build and Release (arm64, linux, linux-arm64) (push) Waiting to run
- 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]
2026-02-28 01:56:56 -08:00
5 changed files with 110 additions and 40 deletions

View File

@@ -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. // Verify looks up a DB API token and returns a Session if valid.
func (tm *tokenManager) Verify(rawToken string) (*model.Session, error) { func (tm *tokenManager) Verify(rawToken string) (*model.Session, error) {
var t model.Token var t model.Token
err := db.Default.Session(). if err := db.Default.Session().Where("token = ?", rawToken).First(&t).Error; err != nil {
Where("token = ?", rawToken).
Preload("User").
Preload("User.Role").
First(&t).Error
if err != nil {
return nil, errors.New("无效的 API Token") 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 已过期") 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 // Update last_used_at asynchronously
now := time.Now() now := time.Now()
go db.Default.Session().Model(&t).Update("last_used_at", now) //nolint:errcheck go db.Default.Session().Model(&t).Update("last_used_at", now) //nolint:errcheck
session := &model.Session{ session := &model.Session{
UserID: t.User.ID, UserID: user.ID,
Username: t.User.Username, Username: user.Username,
Role: t.User.Role.Name, Role: role.Name,
RoleLabel: t.User.Role.Label, RoleLabel: role.Label,
Permissions: t.User.Role.PermissionList(), Permissions: role.PermissionList(),
LoginAt: now.Unix(), LoginAt: now.Unix(),
Token: rawToken, Token: rawToken,
} }

View File

@@ -89,7 +89,6 @@ func (um *userManager) Login(username, password string) (*model.Session, error)
user := new(model.User) user := new(model.User)
if err := db.Default.Session(). if err := db.Default.Session().
Where("username = ? AND active = ?", username, true). Where("username = ? AND active = ?", username, true).
Preload("Role").
First(user).Error; err != nil { First(user).Error; err != nil {
return nil, errors.New("账号或密码错误") return nil, errors.New("账号或密码错误")
} }
@@ -98,12 +97,17 @@ func (um *userManager) Login(username, password string) (*model.Session, error)
return nil, errors.New("账号或密码错误") 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{ session := &model.Session{
UserID: user.ID, UserID: user.ID,
Username: user.Username, Username: user.Username,
Role: user.Role.Name, Role: role.Name,
RoleLabel: user.Role.Label, RoleLabel: role.Label,
Permissions: user.Role.PermissionList(), Permissions: role.PermissionList(),
LoginAt: now.Unix(), LoginAt: now.Unix(),
Token: tool.RandomString(32), Token: tool.RandomString(32),
} }

View File

@@ -3,6 +3,7 @@ package handler
import ( import (
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/loveuer/nf" "github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log" "github.com/loveuer/nf/nft/log"
@@ -12,14 +13,65 @@ import (
"github.com/spf13/cast" "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 { func AdminListUsers() nf.HandlerFunc {
return func(c *nf.Ctx) error { return func(c *nf.Ctx) error {
var users []model.User 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()) log.Error("handler.AdminListUsers: %s", err.Error())
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询失败"}) 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": "用户名已存在"}) 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{ user := &model.User{
Username: req.Username, Username: req.Username,
Password: tool.NewPassword(req.Password), Password: tool.NewPassword(req.Password),
@@ -69,11 +126,7 @@ func AdminCreateUser() nf.HandlerFunc {
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "创建用户失败"}) return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "创建用户失败"})
} }
if err := db.Default.Session().Preload("Role").First(user, user.ID).Error; err != nil { return c.Status(http.StatusOK).JSON(map[string]any{"data": toUserResp(*user, role)})
log.Error("handler.AdminCreateUser: preload role: %s", err.Error())
}
return c.Status(http.StatusOK).JSON(map[string]any{"data": user})
} }
} }
@@ -97,11 +150,16 @@ func AdminUpdateUser() nf.HandlerFunc {
session := c.Locals("user").(*model.Session) session := c.Locals("user").(*model.Session)
user := new(model.User) var user model.User
if err := db.Default.Session().Preload("Role").First(user, id).Error; err != nil { if err := db.Default.Session().First(&user, id).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"}) return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"})
} }
var currentRole model.Role
if err := db.Default.Session().First(&currentRole, user.RoleID).Error; err != nil {
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "查询角色失败"})
}
updates := map[string]any{} updates := map[string]any{}
if req.RoleID != nil && *req.RoleID != user.RoleID { 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": "无效的角色"}) return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "无效的角色"})
} }
// If demoting from admin, ensure at least one other active admin remains // 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 var adminCount int64
db.Default.Session().Model(&model.User{}). db.Default.Session().Model(&model.User{}).
Where("role_id = ? AND active = ? AND id != ?", user.RoleID, true, id). Where("role_id = ? AND active = ? AND id != ?", user.RoleID, true, id).
@@ -120,13 +178,14 @@ func AdminUpdateUser() nf.HandlerFunc {
} }
} }
updates["role_id"] = *req.RoleID updates["role_id"] = *req.RoleID
currentRole = newRole
} }
if req.Active != nil && *req.Active != user.Active { if req.Active != nil && *req.Active != user.Active {
if user.ID == session.UserID && !*req.Active { if user.ID == session.UserID && !*req.Active {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "不能禁用自己的账号"}) 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 var adminCount int64
db.Default.Session().Model(&model.User{}). db.Default.Session().Model(&model.User{}).
Where("role_id = ? AND active = ? AND id != ?", user.RoleID, true, id). 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": "没有需要更新的字段"}) 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()) log.Error("handler.AdminUpdateUser: %s", err.Error())
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "更新失败"}) return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "更新失败"})
} }
if err := db.Default.Session().Preload("Role").First(user, user.ID).Error; err != nil { return c.Status(http.StatusOK).JSON(map[string]any{"data": toUserResp(user, currentRole)})
log.Error("handler.AdminUpdateUser: preload: %s", err.Error())
}
return c.Status(http.StatusOK).JSON(map[string]any{"data": user})
} }
} }
@@ -174,13 +229,18 @@ func AdminDeleteUser() nf.HandlerFunc {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "不能删除自己的账号"}) return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "不能删除自己的账号"})
} }
user := new(model.User) var user model.User
if err := db.Default.Session().Preload("Role").First(user, id).Error; err != nil { if err := db.Default.Session().First(&user, id).Error; err != nil {
return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"}) return c.Status(http.StatusNotFound).JSON(map[string]string{"msg": "用户不存在"})
} }
// Prevent deleting the last admin // Prevent deleting the last admin: check via role name
if user.Role.Name == model.RoleAdmin { 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 var adminCount int64
db.Default.Session().Model(&model.User{}). db.Default.Session().Model(&model.User{}).
Where("role_id = ? AND id != ?", user.RoleID, id). 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()) log.Error("handler.AdminDeleteUser: %s", err.Error())
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "删除失败"}) return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "删除失败"})
} }

View File

@@ -7,7 +7,6 @@ import "time"
type Token struct { type Token struct {
ID uint `gorm:"primarykey" json:"id"` ID uint `gorm:"primarykey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"` UserID uint `gorm:"not null;index" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"-"`
Name string `gorm:"not null" json:"name"` Name string `gorm:"not null" json:"name"`
Token string `gorm:"uniqueIndex;not null" json:"-"` Token string `gorm:"uniqueIndex;not null" json:"-"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`

View File

@@ -8,7 +8,6 @@ type User struct {
Username string `gorm:"uniqueIndex;not null" json:"username"` Username string `gorm:"uniqueIndex;not null" json:"username"`
Password string `gorm:"not null" json:"-"` Password string `gorm:"not null" json:"-"`
RoleID uint `gorm:"not null" json:"role_id"` RoleID uint `gorm:"not null" json:"role_id"`
Role Role `gorm:"foreignKey:RoleID" json:"role"`
Active bool `gorm:"default:true" json:"active"` Active bool `gorm:"default:true" json:"active"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`