392 lines
12 KiB
Go
392 lines
12 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/goravel/framework/contracts/database/orm"
|
|
"github.com/goravel/framework/facades"
|
|
|
|
apperrors "goravel/app/errors"
|
|
"goravel/app/models"
|
|
"goravel/app/utils"
|
|
"goravel/app/utils/errorlog"
|
|
)
|
|
|
|
type UserService interface {
|
|
// GetByID 根据ID获取用户
|
|
GetByID(id uint) (*models.User, error)
|
|
// GetList 获取用户列表
|
|
GetList(filters UserFilters, page, pageSize int) ([]models.User, int64, error)
|
|
// Create 创建用户
|
|
Create(user *models.User) error
|
|
// CreateWithValidation 创建用户(包含验证、密码加密、默认货币设置)
|
|
CreateWithValidation(username, password, nickname, email, phone string, status uint8) (*models.User, error)
|
|
// Update 更新用户
|
|
Update(id uint, user *models.User) error
|
|
// Delete 删除用户(软删除)
|
|
Delete(id uint) error
|
|
// UpdateBalance 更新用户余额(同时创建余额变动记录)
|
|
UpdateBalance(userID uint, amount float64, logType string, source string, sourceID *uint, description string, operatorID *uint, remark string) error
|
|
// ResetPassword 重置用户密码
|
|
ResetPassword(userID uint, newPassword string) error
|
|
// ValidateUserExists 验证用户是否存在(用户名、邮箱、手机号)
|
|
ValidateUserExists(username, email, phone string, excludeID uint) error
|
|
}
|
|
|
|
type UserFilters struct {
|
|
Username string
|
|
Email string
|
|
Phone string
|
|
Nickname string
|
|
Status string
|
|
}
|
|
|
|
// BuildUserQuery 构建用户查询(通用查询构建,供列表和导出复用)
|
|
func BuildUserQuery(filters UserFilters) orm.Query {
|
|
query := facades.Orm().Query().Model(&models.User{})
|
|
|
|
if filters.Username != "" {
|
|
query = query.Where("username LIKE ?", "%"+filters.Username+"%")
|
|
}
|
|
if filters.Nickname != "" {
|
|
query = query.Where("nickname LIKE ?", "%"+filters.Nickname+"%")
|
|
}
|
|
if filters.Email != "" {
|
|
query = query.Where("email LIKE ?", "%"+filters.Email+"%")
|
|
}
|
|
if filters.Phone != "" {
|
|
query = query.Where("phone LIKE ?", "%"+filters.Phone+"%")
|
|
}
|
|
if filters.Status != "" {
|
|
query = query.Where("status", filters.Status)
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
type UserServiceImpl struct {
|
|
balanceLogService UserBalanceLogService
|
|
}
|
|
|
|
func NewUserService() UserService {
|
|
return &UserServiceImpl{
|
|
balanceLogService: NewUserBalanceLogService(),
|
|
}
|
|
}
|
|
|
|
// GetByID 根据ID获取用户
|
|
func (s *UserServiceImpl) GetByID(id uint) (*models.User, error) {
|
|
var user models.User
|
|
if err := facades.Orm().Query().Where("id", id).FirstOrFail(&user); err != nil {
|
|
return nil, apperrors.ErrUserNotFound.WithError(err)
|
|
}
|
|
|
|
// 加载货币信息
|
|
if user.CurrencyID > 0 {
|
|
var currency models.Currency
|
|
if err := facades.Orm().Query().Where("id", user.CurrencyID).First(¤cy); err == nil {
|
|
user.Currency = ¤cy
|
|
}
|
|
}
|
|
|
|
// 格式化余额
|
|
user.Balance = utils.FormatBalance(user.Balance, user.Currency)
|
|
return &user, nil
|
|
}
|
|
|
|
// GetList 获取用户列表
|
|
func (s *UserServiceImpl) GetList(filters UserFilters, page, pageSize int) ([]models.User, int64, error) {
|
|
query := BuildUserQuery(filters)
|
|
|
|
// 分页查询
|
|
var users []models.User
|
|
var total int64
|
|
if err := query.Order("created_at desc").Paginate(page, pageSize, &users, &total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// 加载货币信息并格式化每个用户的余额
|
|
for i := range users {
|
|
if users[i].CurrencyID > 0 {
|
|
var currency models.Currency
|
|
if err := facades.Orm().Query().Where("id", users[i].CurrencyID).First(¤cy); err == nil {
|
|
users[i].Currency = ¤cy
|
|
}
|
|
}
|
|
users[i].Balance = utils.FormatBalance(users[i].Balance, users[i].Currency)
|
|
}
|
|
|
|
return users, total, nil
|
|
}
|
|
|
|
// Create 创建用户
|
|
func (s *UserServiceImpl) Create(user *models.User) error {
|
|
// 如果未设置货币ID,默认使用人民币
|
|
if user.CurrencyID == 0 {
|
|
var cnyCurrency models.Currency
|
|
if err := facades.Orm().Query().Where("code", "CNY").First(&cnyCurrency); err == nil {
|
|
user.CurrencyID = cnyCurrency.ID
|
|
}
|
|
}
|
|
// 使用 map 创建,确保 Status 为 0 时也能正确保存(与管理员创建方式一致)
|
|
// GORM 在处理结构体时可能会忽略零值字段,使用 map 可以确保所有字段都被保存
|
|
userData := map[string]any{
|
|
"username": user.Username,
|
|
"password": user.Password,
|
|
"nickname": user.Nickname,
|
|
"avatar": user.Avatar,
|
|
"email": user.Email,
|
|
"phone": user.Phone,
|
|
"balance": user.Balance,
|
|
"currency_id": user.CurrencyID,
|
|
"status": user.Status,
|
|
"last_login_at": user.LastLoginAt,
|
|
}
|
|
if err := facades.Orm().Query().Table("users").Create(userData); err != nil {
|
|
return err
|
|
}
|
|
// 将创建后的 ID 赋值回 user 对象(GORM 会将生成的 ID 填充到 map 中)
|
|
if id, ok := userData["id"].(uint); ok {
|
|
user.ID = id
|
|
} else if id, ok := userData["id"].(uint64); ok {
|
|
user.ID = uint(id)
|
|
} else {
|
|
// 如果 map 中没有 ID,通过用户名查询获取(与管理员创建方式一致)
|
|
var createdUser models.User
|
|
if err := facades.Orm().Query().Where("username", user.Username).First(&createdUser); err == nil {
|
|
user.ID = createdUser.ID
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Update 更新用户
|
|
func (s *UserServiceImpl) Update(id uint, user *models.User) error {
|
|
// 如果密码为空,则不更新密码字段
|
|
updateData := map[string]any{
|
|
"nickname": user.Nickname,
|
|
"avatar": user.Avatar,
|
|
"email": user.Email,
|
|
"phone": user.Phone,
|
|
"status": user.Status,
|
|
"currency_id": user.CurrencyID,
|
|
}
|
|
|
|
// 只有用户名不为空时才更新用户名(通常用户名不允许修改,但保留此逻辑以防需要)
|
|
if user.Username != "" {
|
|
updateData["username"] = user.Username
|
|
}
|
|
|
|
// 只有密码不为空时才更新密码
|
|
if user.Password != "" {
|
|
updateData["password"] = user.Password
|
|
}
|
|
|
|
_, err := facades.Orm().Query().Model(&models.User{}).Where("id", id).Update(updateData)
|
|
return err
|
|
}
|
|
|
|
// Delete 删除用户(软删除)
|
|
func (s *UserServiceImpl) Delete(id uint) error {
|
|
_, err := facades.Orm().Query().Where("id", id).Delete(&models.User{})
|
|
return err
|
|
}
|
|
|
|
// UpdateBalance 更新用户余额(同时创建余额变动记录)
|
|
func (s *UserServiceImpl) UpdateBalance(userID uint, amount float64, logType string, source string, sourceID *uint, description string, operatorID *uint, remark string) error {
|
|
// 获取当前用户
|
|
user, err := s.GetByID(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 计算新余额
|
|
var newBalance float64
|
|
switch logType {
|
|
case "income", "refund":
|
|
newBalance = user.Balance + amount
|
|
case "expense":
|
|
newBalance = user.Balance - amount
|
|
if newBalance < 0 {
|
|
return apperrors.ErrInsufficientBalance.WithParams(map[string]any{
|
|
"balance": user.Balance,
|
|
})
|
|
}
|
|
default:
|
|
return apperrors.ErrInvalidBalanceType.WithParams(map[string]any{
|
|
"type": logType,
|
|
})
|
|
}
|
|
|
|
// 1. 更新用户余额
|
|
_, err = facades.Orm().Query().Model(&models.User{}).Where("id", userID).Update(map[string]any{
|
|
"balance": newBalance,
|
|
})
|
|
if err != nil {
|
|
errorlog.Record(context.Background(), "user", "更新用户余额失败", map[string]any{
|
|
"user_id": userID,
|
|
"amount": amount,
|
|
"log_type": logType,
|
|
"new_balance": newBalance,
|
|
"error": err.Error(),
|
|
}, "更新用户余额失败: %v", err)
|
|
return apperrors.ErrUpdateFailed.WithError(err)
|
|
}
|
|
|
|
// 2. 创建余额变动记录(使用自定义分表逻辑)
|
|
_, err = s.balanceLogService.CreateLog(userID, logType, amount, newBalance, source, sourceID, description, operatorID, "success", remark)
|
|
if err != nil {
|
|
// 如果创建记录失败,尝试回滚余额更新
|
|
// 注意:由于涉及分表,无法使用跨表事务,这里手动回滚
|
|
rollbackErr := s.rollbackBalance(userID, user.Balance)
|
|
if rollbackErr != nil {
|
|
errorlog.Record(context.Background(), "user", "回滚余额失败", map[string]any{
|
|
"user_id": userID,
|
|
"old_balance": user.Balance,
|
|
"new_balance": newBalance,
|
|
"rollback_err": rollbackErr.Error(),
|
|
}, "回滚余额失败: %v", rollbackErr)
|
|
}
|
|
|
|
errorlog.Record(context.Background(), "user", "创建余额变动记录失败", map[string]any{
|
|
"user_id": userID,
|
|
"amount": amount,
|
|
"log_type": logType,
|
|
"new_balance": newBalance,
|
|
"error": err.Error(),
|
|
}, "创建余额变动记录失败: %v", err)
|
|
return apperrors.ErrCreateFailed.WithError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rollbackBalance 回滚余额到指定值
|
|
func (s *UserServiceImpl) rollbackBalance(userID uint, balance float64) error {
|
|
_, err := facades.Orm().Query().Model(&models.User{}).Where("id", userID).Update(map[string]any{
|
|
"balance": balance,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// ValidateUserExists 验证用户是否存在(用户名、邮箱、手机号)
|
|
func (s *UserServiceImpl) ValidateUserExists(username, email, phone string, excludeID uint) error {
|
|
// 检查用户名是否已存在
|
|
if username != "" {
|
|
query := facades.Orm().Query().Model(&models.User{}).Where("username", username)
|
|
if excludeID > 0 {
|
|
query = query.Where("id != ?", excludeID)
|
|
}
|
|
exists, err := query.Exists()
|
|
if err != nil {
|
|
return apperrors.ErrCreateFailed.WithError(err)
|
|
}
|
|
if exists {
|
|
return apperrors.ErrUsernameExists
|
|
}
|
|
}
|
|
|
|
// 检查邮箱是否已存在(如果提供了邮箱)
|
|
if email != "" {
|
|
query := facades.Orm().Query().Model(&models.User{}).Where("email", email)
|
|
if excludeID > 0 {
|
|
query = query.Where("id != ?", excludeID)
|
|
}
|
|
exists, err := query.Exists()
|
|
if err != nil {
|
|
return apperrors.ErrCreateFailed.WithError(err)
|
|
}
|
|
if exists {
|
|
return apperrors.NewBusinessError("email_already_exists", "邮箱已存在")
|
|
}
|
|
}
|
|
|
|
// 检查手机号是否已存在(如果提供了手机号)
|
|
if phone != "" {
|
|
query := facades.Orm().Query().Model(&models.User{}).Where("phone", phone)
|
|
if excludeID > 0 {
|
|
query = query.Where("id != ?", excludeID)
|
|
}
|
|
exists, err := query.Exists()
|
|
if err != nil {
|
|
return apperrors.ErrCreateFailed.WithError(err)
|
|
}
|
|
if exists {
|
|
return apperrors.NewBusinessError("phone_already_exists", "手机号已存在")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateWithValidation 创建用户(包含验证、密码加密、默认货币设置)
|
|
func (s *UserServiceImpl) CreateWithValidation(username, password, nickname, email, phone string, status uint8) (*models.User, error) {
|
|
// 验证用户是否存在
|
|
if err := s.ValidateUserExists(username, email, phone, 0); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 密码加密
|
|
hashedPassword, err := facades.Hash().Make(password)
|
|
if err != nil {
|
|
return nil, apperrors.NewBusinessError("password_encrypt_failed", "密码加密失败").WithError(err)
|
|
}
|
|
|
|
// 如果未设置货币ID,默认使用人民币
|
|
var currencyID uint
|
|
var cnyCurrency models.Currency
|
|
if err := facades.Orm().Query().Where("code", "CNY").First(&cnyCurrency); err == nil {
|
|
currencyID = cnyCurrency.ID
|
|
}
|
|
|
|
// 创建用户
|
|
user := &models.User{
|
|
Username: username,
|
|
Password: hashedPassword,
|
|
Nickname: nickname,
|
|
Avatar: "",
|
|
Email: email,
|
|
Phone: phone,
|
|
Balance: 0,
|
|
CurrencyID: currencyID,
|
|
Status: status,
|
|
}
|
|
|
|
if err := s.Create(user); err != nil {
|
|
return nil, apperrors.ErrCreateFailed.WithError(err)
|
|
}
|
|
|
|
// 查询创建后的用户(确保获取完整信息)
|
|
createdUser, err := s.GetByID(user.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return createdUser, nil
|
|
}
|
|
|
|
// ResetPassword 重置用户密码
|
|
func (s *UserServiceImpl) ResetPassword(userID uint, newPassword string) error {
|
|
// 检查用户是否存在
|
|
_, err := s.GetByID(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 密码加密
|
|
hashedPassword, err := facades.Hash().Make(newPassword)
|
|
if err != nil {
|
|
return apperrors.ErrPasswordEncryptFailed.WithError(err)
|
|
}
|
|
|
|
// 更新密码
|
|
_, err = facades.Orm().Query().Model(&models.User{}).Where("id", userID).Update(map[string]any{
|
|
"password": hashedPassword,
|
|
})
|
|
if err != nil {
|
|
return apperrors.ErrUpdateFailed.WithError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|