mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
refactor(plugins): restructure admin and message_gateway into standard layered sub-packages
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
package user
|
||||
|
||||
// HTTP 响应错误文案
|
||||
const (
|
||||
errInvalidParams = "无效的请求参数"
|
||||
errUserNotFound = "用户不存在"
|
||||
@@ -11,5 +12,32 @@ const (
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errOldPasswordIncorrect = "原密码不正确"
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errTokenNotFound = "访问令牌不存在"
|
||||
errTokenNotFound = "访问令牌不存在"
|
||||
errCreateUserFailed = "创建用户失败: "
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errCreateTokenFailed = "创建令牌失败"
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errPasswordEncryptFailed = "密码加密失败"
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errPasswordUpdateFailed = "密码更新失败"
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errPasswordEmpty = "password cannot be empty"
|
||||
)
|
||||
|
||||
// Service 层业务校验错误文案,取值被上游插件按字符串精确匹配消费,禁止改写
|
||||
const (
|
||||
errUsernameEmpty = "用户名不能为空"
|
||||
errEmailEmpty = "邮箱不能为空"
|
||||
errUsernameTaken = "用户名已被使用"
|
||||
errEmailTaken = "邮箱已被使用"
|
||||
errCannotRevokeSelf = "不能取消自己的管理员权限"
|
||||
errAdminCannotDisable = "管理员账号无法被禁用"
|
||||
errAdminCannotDelete = "管理员账号无法被删除"
|
||||
errCannotDeleteSelf = "不能删除当前登录用户"
|
||||
errServiceUsernameEmpty = "user: username cannot be empty"
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errServiceOldPasswordIncorrect = "user: incorrect old password"
|
||||
//nolint:gosec // error message, not hardcoded credentials
|
||||
errServicePasswordTooShort = "密码长度至少为 8 位"
|
||||
errUniqueUsernameFailed = "failed to generate unique username"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ package user
|
||||
|
||||
import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/response"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
@@ -12,44 +13,11 @@ import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
|
||||
type updateProfileRequest struct {
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Bio string `json:"bio"`
|
||||
Phone string `json:"phone"`
|
||||
Gender string `json:"gender"`
|
||||
Website string `json:"website"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
type createAccessTokenRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
||||
func getUserIDFromSession(c *gin.Context) uint64 {
|
||||
defer func() { _ = recover() }()
|
||||
session := sessions.Default(c)
|
||||
@@ -130,13 +98,12 @@ func Register(c *gin.Context) {
|
||||
IsActive: true,
|
||||
}
|
||||
if err := newUser.SetEncryptedPassword(req.Password); err != nil {
|
||||
response.AbortInternal(c, "密码加密失败")
|
||||
response.AbortInternal(c, errPasswordEncryptFailed)
|
||||
return
|
||||
}
|
||||
|
||||
gormDB := getDB(c.Request.Context())
|
||||
if err := gormDB.Create(newUser).Error; err != nil {
|
||||
response.AbortBadRequest(c, "创建用户失败: "+err.Error())
|
||||
if err := CreateUser(c.Request.Context(), newUser); err != nil {
|
||||
response.AbortBadRequest(c, errCreateUserFailed+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,12 +144,13 @@ func ChangePassword(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := user.SetEncryptedPassword(req.NewPassword); err != nil {
|
||||
response.AbortInternal(c, "密码更新失败")
|
||||
response.AbortInternal(c, errPasswordUpdateFailed)
|
||||
return
|
||||
}
|
||||
|
||||
gormDB := getDB(c.Request.Context())
|
||||
_ = gormDB.Save(&user)
|
||||
if err := UpdateUser(c.Request.Context(), user); err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "persist changed password failed: %v", err)
|
||||
}
|
||||
invalidateUserCache(c.Request.Context(), user.ID)
|
||||
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
@@ -211,8 +179,9 @@ func UpdateProfile(c *gin.Context) {
|
||||
user.Website = req.Website
|
||||
user.Location = req.Location
|
||||
|
||||
gormDB := getDB(c.Request.Context())
|
||||
_ = gormDB.Save(&user)
|
||||
if err := UpdateUser(c.Request.Context(), user); err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "persist updated profile failed: %v", err)
|
||||
}
|
||||
invalidateUserCache(c.Request.Context(), user.ID)
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(user))
|
||||
@@ -221,9 +190,10 @@ func UpdateProfile(c *gin.Context) {
|
||||
// ListAccessTokens lists access tokens for the current user.
|
||||
func ListAccessTokens(c *gin.Context) {
|
||||
userID := getUserIDFromSession(c)
|
||||
var tokens []AccessToken
|
||||
gormDB := getDB(c.Request.Context())
|
||||
_ = gormDB.Where("user_id = ?", userID).Find(&tokens).Error
|
||||
tokens, err := listAccessTokensByUser(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "list access tokens failed: %v", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, response.OK(tokens))
|
||||
}
|
||||
|
||||
@@ -260,9 +230,8 @@ func CreateAccessToken(c *gin.Context) {
|
||||
IsAdmin: req.IsAdmin,
|
||||
}
|
||||
|
||||
gormDB := getDB(c.Request.Context())
|
||||
if err := gormDB.Create(&token).Error; err != nil {
|
||||
response.AbortInternal(c, "创建令牌失败")
|
||||
if err := createAccessTokenRow(c.Request.Context(), &token); err != nil {
|
||||
response.AbortInternal(c, errCreateTokenFailed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -282,14 +251,15 @@ func DeleteAccessToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
userID := getUserIDFromSession(c)
|
||||
var token AccessToken
|
||||
gormDB := getDB(c.Request.Context())
|
||||
if err := gormDB.Where("id = ? AND user_id = ?", id, userID).First(&token).Error; err != nil {
|
||||
token, err := getAccessTokenOfUser(c.Request.Context(), id, userID)
|
||||
if err != nil {
|
||||
response.AbortNotFound(c, errTokenNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
_ = gormDB.Delete(&token)
|
||||
if err := deleteAccessTokenRow(c.Request.Context(), token); err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "delete access token failed: %v", err)
|
||||
}
|
||||
invalidateTokenCache(c.Request.Context(), token.TokenHash)
|
||||
c.JSON(http.StatusOK, response.OKNil())
|
||||
}
|
||||
@@ -304,9 +274,8 @@ func RotateAccessToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
userID := getUserIDFromSession(c)
|
||||
var token AccessToken
|
||||
gormDB := getDB(c.Request.Context())
|
||||
if err := gormDB.Where("id = ? AND user_id = ?", id, userID).First(&token).Error; err != nil {
|
||||
token, err := getAccessTokenOfUser(c.Request.Context(), id, userID)
|
||||
if err != nil {
|
||||
response.AbortNotFound(c, errTokenNotFound)
|
||||
return
|
||||
}
|
||||
@@ -325,7 +294,9 @@ func RotateAccessToken(c *gin.Context) {
|
||||
}
|
||||
token.MaskedToken = masked
|
||||
|
||||
_ = gormDB.Save(&token)
|
||||
if err := saveAccessTokenRow(c.Request.Context(), token); err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "rotate access token failed: %v", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(gin.H{
|
||||
"token": token,
|
||||
|
||||
@@ -56,7 +56,7 @@ func (User) TableName() string {
|
||||
func (u *User) SetEncryptedPassword(password string) error {
|
||||
trimmed := strings.TrimSpace(password)
|
||||
if trimmed == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
return errors.New(errPasswordEmpty)
|
||||
}
|
||||
hash, err := util.HashPassword(trimmed)
|
||||
if err != nil {
|
||||
@@ -74,3 +74,46 @@ func (u *User) CheckPassword(password string) bool {
|
||||
}
|
||||
return util.CheckPasswordHash(u.Password, password)
|
||||
}
|
||||
|
||||
// loginRequest 登录请求参数
|
||||
type loginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// registerRequest 注册请求参数
|
||||
type registerRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// changePasswordRequest 修改密码请求参数
|
||||
type changePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
|
||||
// updateProfileRequest 资料更新请求参数
|
||||
type updateProfileRequest struct {
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Bio string `json:"bio"`
|
||||
Phone string `json:"phone"`
|
||||
Gender string `json:"gender"`
|
||||
Website string `json:"website"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
// createAccessTokenRequest 创建访问令牌请求参数
|
||||
type createAccessTokenRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
||||
// userAdminFlags 用户写操作前置校验所需的最小列投影
|
||||
type userAdminFlags struct {
|
||||
ID uint64
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
@@ -188,3 +188,167 @@ func ListUsernamesMatchingBase(ctx context.Context, base string) ([]string, erro
|
||||
}
|
||||
return usernames, nil
|
||||
}
|
||||
|
||||
// adminUserColumns 后台用户查询显式列清单
|
||||
const adminUserColumns = "id, username, nickname, email, avatar_url, is_active, is_admin, bio, phone, gender, website, location, last_login_at, created_at, updated_at"
|
||||
|
||||
// updateUserColumns 按列局部更新指定用户行
|
||||
func updateUserColumns(ctx context.Context, id uint64, updates map[string]any) error {
|
||||
return getDB(ctx).Model(&User{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
// setUserAdminFlag 更新指定用户的管理员标记列
|
||||
func setUserAdminFlag(ctx context.Context, id uint64, admin bool) error {
|
||||
return getDB(ctx).Model(&User{}).Where("id = ?", id).Update("is_admin", admin).Error
|
||||
}
|
||||
|
||||
// setUserActiveColumn 后台启用/禁用用户,沿用无 schema 的按表直写语义
|
||||
// (与导出函数 UpdateUserActive 不同,后者经由 Model 会额外刷新 updated_at)
|
||||
func setUserActiveColumn(ctx context.Context, id uint64, active bool) error {
|
||||
return getDB(ctx).Table("w_users").Where("id = ?", id).Update("is_active", active).Error
|
||||
}
|
||||
|
||||
// countAllUsers 统计用户总数
|
||||
func countAllUsers(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := getDB(ctx).Model(&User{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// countActiveUsers 统计激活状态用户数
|
||||
func countActiveUsers(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := getDB(ctx).Model(&User{}).Where("is_active = ?", true).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// countUsersByUsername 统计同名用户数量
|
||||
func countUsersByUsername(ctx context.Context, username string) (int64, error) {
|
||||
var count int64
|
||||
err := getDB(ctx).Table("w_users").Where("username = ?", username).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// countUsersByEmail 统计同邮箱用户数量
|
||||
func countUsersByEmail(ctx context.Context, email string) (int64, error) {
|
||||
var count int64
|
||||
err := getDB(ctx).Table("w_users").Where("email = ?", email).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// countOtherUsersByEmail 统计除指定用户外的同邮箱数量
|
||||
func countOtherUsersByEmail(ctx context.Context, email string, id uint64) (int64, error) {
|
||||
var count int64
|
||||
err := getDB(ctx).Table("w_users").Where("email = ? AND id != ?", email, id).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// adminListUserRows 后台条件分页查询用户
|
||||
func adminListUserRows(ctx context.Context, filter contracts.AdminListUsersFilter) (int64, []*contracts.UserDTO, error) {
|
||||
query := getDB(ctx).Table("w_users")
|
||||
if filter.UserID != nil {
|
||||
query = query.Where("id = ?", *filter.UserID)
|
||||
}
|
||||
if filter.Username != "" {
|
||||
query = query.Where("username LIKE ? ESCAPE '\\'", util.EscapeLike(filter.Username)+"%")
|
||||
}
|
||||
if filter.Email != "" {
|
||||
query = query.Where("email LIKE ? ESCAPE '\\'", util.EscapeLike(filter.Email)+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
var users []*contracts.UserDTO
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
if err := query.
|
||||
Select(adminUserColumns).
|
||||
Order("id ASC").
|
||||
Offset(offset).
|
||||
Limit(filter.PageSize).
|
||||
Find(&users).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, users, nil
|
||||
}
|
||||
|
||||
// adminGetUserRow 后台按 ID 读取用户视图
|
||||
func adminGetUserRow(ctx context.Context, id uint64) (*contracts.UserDTO, error) {
|
||||
var user contracts.UserDTO
|
||||
if err := getDB(ctx).Table("w_users").
|
||||
Select(adminUserColumns).
|
||||
Where("id = ?", id).
|
||||
First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// getUserRow 按 ID 整行读取用户视图
|
||||
func getUserRow(ctx context.Context, id uint64) (*contracts.UserDTO, error) {
|
||||
var user contracts.UserDTO
|
||||
if err := getDB(ctx).Table("w_users").Where("id = ?", id).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// insertUserRow 以显式列映射写入用户行
|
||||
func insertUserRow(ctx context.Context, row map[string]any) error {
|
||||
return getDB(ctx).Table("w_users").Create(row).Error
|
||||
}
|
||||
|
||||
// getUserAdminFlags 读取指定用户的管理员标记
|
||||
func getUserAdminFlags(ctx context.Context, id uint64) (userAdminFlags, error) {
|
||||
var flags userAdminFlags
|
||||
if err := getDB(ctx).Table("w_users").Select("id, is_admin").Where("id = ?", id).First(&flags).Error; err != nil {
|
||||
return userAdminFlags{}, err
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
// deleteUserCascadeAdmin 在事务中删除用户的访问令牌、外部账号绑定与用户行
|
||||
func deleteUserCascadeAdmin(ctx context.Context, id uint64) error {
|
||||
return getDB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Table("w_access_tokens").Where("user_id = ?", id).Delete(map[string]any{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Table("w_external_accounts").Where("user_id = ?", id).Delete(map[string]any{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Table("w_users").Where("id = ?", id).Delete(map[string]any{}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// listAccessTokensByUser 列出指定用户的访问令牌
|
||||
func listAccessTokensByUser(ctx context.Context, userID uint64) ([]AccessToken, error) {
|
||||
var tokens []AccessToken
|
||||
err := getDB(ctx).Where("user_id = ?", userID).Find(&tokens).Error
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
// getAccessTokenOfUser 按 ID 与所属用户读取访问令牌
|
||||
func getAccessTokenOfUser(ctx context.Context, id, userID uint64) (*AccessToken, error) {
|
||||
var token AccessToken
|
||||
if err := getDB(ctx).Where("id = ? AND user_id = ?", id, userID).First(&token).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// createAccessTokenRow 写入访问令牌记录
|
||||
func createAccessTokenRow(ctx context.Context, token *AccessToken) error {
|
||||
return getDB(ctx).Create(token).Error
|
||||
}
|
||||
|
||||
// saveAccessTokenRow 全量保存访问令牌记录
|
||||
func saveAccessTokenRow(ctx context.Context, token *AccessToken) error {
|
||||
return getDB(ctx).Save(token).Error
|
||||
}
|
||||
|
||||
// deleteAccessTokenRow 删除访问令牌记录
|
||||
func deleteAccessTokenRow(ctx context.Context, token *AccessToken) error {
|
||||
return getDB(ctx).Delete(token).Error
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
pkgu "Wavelet/pkg/util"
|
||||
)
|
||||
|
||||
@@ -73,16 +71,16 @@ func (s *userServiceImpl) GetUserByUsername(ctx context.Context, username string
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) GetUserByEmail(ctx context.Context, email string) (*contracts.UserDTO, error) {
|
||||
var u User
|
||||
if err := getDB(ctx).Where("email = ?", email).First(&u).Error; err != nil {
|
||||
u, err := GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toUserDTO(&u), nil
|
||||
return toUserDTO(u), nil
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) CreateUser(ctx context.Context, req contracts.CreateUserRequest) (*contracts.UserDTO, error) {
|
||||
if req.Username == "" {
|
||||
return nil, errors.New("user: username cannot be empty")
|
||||
return nil, errors.New(errServiceUsernameEmpty)
|
||||
}
|
||||
|
||||
user := User{
|
||||
@@ -142,7 +140,7 @@ func (s *userServiceImpl) UpdateProfile(ctx context.Context, id uint64, req cont
|
||||
}
|
||||
updates[columnUpdatedAt] = time.Now()
|
||||
|
||||
if err := getDB(ctx).Model(&User{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
if err := updateUserColumns(ctx, id, updates); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -150,29 +148,28 @@ func (s *userServiceImpl) UpdateProfile(ctx context.Context, id uint64, req cont
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) UpdatePassword(ctx context.Context, id uint64, oldPassword, newPassword string) error {
|
||||
var user User
|
||||
if err := getDB(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
||||
user, err := GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !user.CheckPassword(oldPassword) {
|
||||
return errors.New("user: incorrect old password")
|
||||
return errors.New(errServiceOldPasswordIncorrect)
|
||||
}
|
||||
|
||||
if err := user.SetEncryptedPassword(newPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return getDB(ctx).Model(&User{}).Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"password": user.Password,
|
||||
columnUpdatedAt: time.Now(),
|
||||
}).Error
|
||||
return updateUserColumns(ctx, id, map[string]any{
|
||||
"password": user.Password,
|
||||
columnUpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) VerifyPassword(ctx context.Context, id uint64, password string) bool {
|
||||
var user User
|
||||
if err := getDB(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
||||
user, err := GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
pkgu.DummyCheckPassword(password)
|
||||
return false
|
||||
}
|
||||
@@ -180,11 +177,10 @@ func (s *userServiceImpl) VerifyPassword(ctx context.Context, id uint64, passwor
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) UpdateLastLogin(ctx context.Context, id uint64, _ string) error {
|
||||
return getDB(ctx).Model(&User{}).Where("id = ?", id).
|
||||
Updates(map[string]any{
|
||||
"last_login_at": time.Now(),
|
||||
columnUpdatedAt: time.Now(),
|
||||
}).Error
|
||||
return updateUserColumns(ctx, id, map[string]any{
|
||||
"last_login_at": time.Now(),
|
||||
columnUpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) ListUsers(ctx context.Context, page, pageSize int, keyword string) ([]*contracts.UserDTO, int64, error) {
|
||||
@@ -219,7 +215,7 @@ func (s *userServiceImpl) SetUserActive(ctx context.Context, id uint64, active b
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) SetUserAdmin(ctx context.Context, id uint64, admin bool) error {
|
||||
return getDB(ctx).Model(&User{}).Where("id = ?", id).Update("is_admin", admin).Error
|
||||
return setUserAdminFlag(ctx, id, admin)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) VerifyAccessToken(ctx context.Context, tokenHash string) (*contracts.UserDTO, bool, error) {
|
||||
@@ -241,15 +237,11 @@ func (s *userServiceImpl) DeleteUser(ctx context.Context, id uint64) error {
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) CountUsers(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := getDB(ctx).Model(&User{}).Count(&count).Error
|
||||
return count, err
|
||||
return countAllUsers(ctx)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) CountActiveUsers(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := getDB(ctx).Model(&User{}).Where("is_active = ?", true).Count(&count).Error
|
||||
return count, err
|
||||
return countActiveUsers(ctx)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) GetFirstAdminUser(ctx context.Context) (*contracts.UserDTO, error) {
|
||||
@@ -287,55 +279,21 @@ func (s *userServiceImpl) UniqueUsername(ctx context.Context, base string) (stri
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("failed to generate unique username")
|
||||
return "", errors.New(errUniqueUsernameFailed)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) AdminListUsers(ctx context.Context, filter contracts.AdminListUsersFilter) (int64, []*contracts.UserDTO, error) {
|
||||
query := getDB(ctx).Table("w_users")
|
||||
if filter.UserID != nil {
|
||||
query = query.Where("id = ?", *filter.UserID)
|
||||
}
|
||||
if filter.Username != "" {
|
||||
query = query.Where("username LIKE ? ESCAPE '\\'", pkgu.EscapeLike(filter.Username)+"%")
|
||||
}
|
||||
if filter.Email != "" {
|
||||
query = query.Where("email LIKE ? ESCAPE '\\'", pkgu.EscapeLike(filter.Email)+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if filter.Page <= 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
if filter.PageSize <= 0 {
|
||||
filter.PageSize = 20
|
||||
}
|
||||
|
||||
var users []*contracts.UserDTO
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
if err := query.
|
||||
Select("id, username, nickname, email, avatar_url, is_active, is_admin, bio, phone, gender, website, location, last_login_at, created_at, updated_at").
|
||||
Order("id ASC").
|
||||
Offset(offset).
|
||||
Limit(filter.PageSize).
|
||||
Find(&users).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, users, nil
|
||||
return adminListUserRows(ctx, filter)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) AdminGetUser(ctx context.Context, id uint64) (*contracts.UserDTO, error) {
|
||||
var user contracts.UserDTO
|
||||
if err := getDB(ctx).Table("w_users").
|
||||
Select("id, username, nickname, email, avatar_url, is_active, is_admin, bio, phone, gender, website, location, last_login_at, created_at, updated_at").
|
||||
Where("id = ?", id).
|
||||
First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
return adminGetUserRow(ctx, id)
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) AdminCreateUser(ctx context.Context, req contracts.AdminCreateUserRequest) (*contracts.UserDTO, error) {
|
||||
@@ -345,30 +303,30 @@ func (s *userServiceImpl) AdminCreateUser(ctx context.Context, req contracts.Adm
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
|
||||
if req.Username == "" {
|
||||
return nil, errors.New("用户名不能为空")
|
||||
return nil, errors.New(errUsernameEmpty)
|
||||
}
|
||||
if req.Email == "" {
|
||||
return nil, errors.New("邮箱不能为空")
|
||||
return nil, errors.New(errEmailEmpty)
|
||||
}
|
||||
const minPasswordLen = 8
|
||||
if len(req.Password) < minPasswordLen {
|
||||
return nil, errors.New("密码长度至少为 8 位")
|
||||
return nil, errors.New(errServicePasswordTooShort)
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := getDB(ctx).Table("w_users").Where("username = ?", req.Username).Count(&count).Error; err != nil {
|
||||
count, err := countUsersByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, errors.New("用户名已被使用")
|
||||
return nil, errors.New(errUsernameTaken)
|
||||
}
|
||||
|
||||
var emailCount int64
|
||||
if err := getDB(ctx).Table("w_users").Where("email = ?", req.Email).Count(&emailCount).Error; err != nil {
|
||||
emailCount, err := countUsersByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if emailCount > 0 {
|
||||
return nil, errors.New("邮箱已被使用")
|
||||
return nil, errors.New(errEmailTaken)
|
||||
}
|
||||
|
||||
hash, err := pkgu.HashPassword(req.Password)
|
||||
@@ -403,7 +361,7 @@ func (s *userServiceImpl) AdminCreateUser(ctx context.Context, req contracts.Adm
|
||||
"created_at": now,
|
||||
columnUpdatedAt: now,
|
||||
}
|
||||
if err := getDB(ctx).Table("w_users").Create(row).Error; err != nil {
|
||||
if err := insertUserRow(ctx, row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -423,31 +381,31 @@ func (s *userServiceImpl) AdminUpdateUser(ctx context.Context, currentUserID uin
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
|
||||
if req.Email == "" {
|
||||
return errors.New("邮箱不能为空")
|
||||
return errors.New(errEmailEmpty)
|
||||
}
|
||||
|
||||
var targetUser contracts.UserDTO
|
||||
if err := getDB(ctx).Table("w_users").Where("id = ?", req.ID).First(&targetUser).Error; err != nil {
|
||||
targetUser, err := getUserRow(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if currentUserID == req.ID && !req.IsAdmin && targetUser.IsAdmin {
|
||||
return errors.New("不能取消自己的管理员权限")
|
||||
return errors.New(errCannotRevokeSelf)
|
||||
}
|
||||
|
||||
if targetUser.Email != req.Email {
|
||||
var count int64
|
||||
if err := getDB(ctx).Table("w_users").Where("email = ? AND id != ?", req.Email, req.ID).Count(&count).Error; err != nil {
|
||||
count, err := countOtherUsersByEmail(ctx, req.Email, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("邮箱已被使用")
|
||||
return errors.New(errEmailTaken)
|
||||
}
|
||||
}
|
||||
|
||||
const minPasswordLen = 8
|
||||
if req.Password != "" && len(req.Password) < minPasswordLen {
|
||||
return errors.New("密码长度至少为 8 位")
|
||||
return errors.New(errServicePasswordTooShort)
|
||||
}
|
||||
|
||||
if req.Nickname == "" {
|
||||
@@ -468,26 +426,23 @@ func (s *userServiceImpl) AdminUpdateUser(ctx context.Context, currentUserID uin
|
||||
updates["password"] = hash
|
||||
}
|
||||
|
||||
err := getDB(ctx).Table("w_users").Where("id = ?", req.ID).Updates(updates).Error
|
||||
err = updateUserColumns(ctx, req.ID, updates)
|
||||
if err == nil && s.events != nil {
|
||||
_ = s.events.Emit(ctx, contracts.EventTopicUserUpdated, &targetUser)
|
||||
_ = s.events.Emit(ctx, contracts.EventTopicUserUpdated, targetUser)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *userServiceImpl) AdminUpdateUserStatus(ctx context.Context, id uint64, active bool) error {
|
||||
var flags struct {
|
||||
ID uint64
|
||||
IsAdmin bool
|
||||
}
|
||||
if err := getDB(ctx).Table("w_users").Select("id, is_admin").Where("id = ?", id).First(&flags).Error; err != nil {
|
||||
flags, err := getUserAdminFlags(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !active && flags.IsAdmin {
|
||||
return errors.New("管理员账号无法被禁用")
|
||||
return errors.New(errAdminCannotDisable)
|
||||
}
|
||||
|
||||
err := getDB(ctx).Table("w_users").Where("id = ?", id).Update("is_active", active).Error
|
||||
err = setUserActiveColumn(ctx, id, active)
|
||||
if err == nil && s.events != nil {
|
||||
_ = s.events.Emit(ctx, contracts.EventTopicUserStatusChanged, contracts.UserStatusChangedEvent{
|
||||
UserID: id,
|
||||
@@ -499,33 +454,24 @@ func (s *userServiceImpl) AdminUpdateUserStatus(ctx context.Context, id uint64,
|
||||
|
||||
func (s *userServiceImpl) AdminDeleteUser(ctx context.Context, currentUserID, targetID uint64) error {
|
||||
if currentUserID == targetID {
|
||||
return errors.New("不能删除当前登录用户")
|
||||
return errors.New(errCannotDeleteSelf)
|
||||
}
|
||||
var flags struct {
|
||||
ID uint64
|
||||
IsAdmin bool
|
||||
}
|
||||
if err := getDB(ctx).Table("w_users").Select("id, is_admin").Where("id = ?", targetID).First(&flags).Error; err != nil {
|
||||
flags, err := getUserAdminFlags(ctx, targetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.IsAdmin {
|
||||
return errors.New("管理员账号无法被删除")
|
||||
return errors.New(errAdminCannotDelete)
|
||||
}
|
||||
|
||||
err := getDB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Table("w_access_tokens").Where("user_id = ?", targetID).Delete(map[string]any{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Table("w_external_accounts").Where("user_id = ?", targetID).Delete(map[string]any{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Table("w_users").Where("id = ?", targetID).Delete(map[string]any{}).Error
|
||||
})
|
||||
if err == nil && s.events != nil {
|
||||
if err := deleteUserCascadeAdmin(ctx, targetID); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.events != nil {
|
||||
_ = s.events.Emit(ctx, contracts.EventTopicUserDeleted, contracts.UserDeletedEvent{
|
||||
CurrentUserID: currentUserID,
|
||||
TargetUserID: targetID,
|
||||
})
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user