Files
goalfylearning-admin/internal/services/goalfymax_user_service.go

513 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package services
import (
"context"
"errors"
"fmt"
"goalfymax-admin/internal/models"
"goalfymax-admin/internal/storage"
"goalfymax-admin/pkg/redis"
"goalfymax-admin/pkg/utils"
"strconv"
goredis "github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
type GoalfyMaxUserService interface {
List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error)
GetByID(id uint) (*models.GoalfyMaxUser, error)
Create(req *models.GoalfyMaxUserCreateRequest) (*models.GoalfyMaxUser, error)
Update(id uint, req *models.GoalfyMaxUserUpdateRequest) (*models.GoalfyMaxUser, error)
Delete(id uint) error
Ban(id uint, req *models.GoalfyMaxUserBanRequest, adminID int) error
Unban(id uint) error
AddBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error
DeductBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error
}
type goalfyMaxUserService struct {
storage storage.GoalfyMaxUserStorage
messagePushService MessagePushService
ssoAdminService SSOAdminService
redisClient *redis.Client
balanceOperationLogStorage storage.BalanceOperationLogStorage
auditLogService AuditLogService
logger *utils.Logger
}
func NewGoalfyMaxUserService(s storage.GoalfyMaxUserStorage, messagePushService MessagePushService, ssoAdminService SSOAdminService, redisClient *redis.Client, balanceOperationLogStorage storage.BalanceOperationLogStorage, auditLogService AuditLogService, logger *utils.Logger) GoalfyMaxUserService {
return &goalfyMaxUserService{
storage: s,
messagePushService: messagePushService,
ssoAdminService: ssoAdminService,
redisClient: redisClient,
balanceOperationLogStorage: balanceOperationLogStorage,
auditLogService: auditLogService,
logger: logger,
}
}
func (s *goalfyMaxUserService) List(req *models.GoalfyMaxUserListRequest) ([]models.GoalfyMaxUser, int64, error) {
users, total, err := s.storage.List(req)
if err != nil {
return nil, 0, err
}
// 如果Redis客户端可用查询每个用户的余额
if s.redisClient != nil {
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
for i := range users {
// 默认余额为0
balanceUSD := 0.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", users[i].UserID)
// 从Redis获取余额
balanceStr, err := s.redisClient.Rdb.Get(ctx, redisKey).Result()
if err == nil {
// 解析余额值
balanceValue, err := strconv.ParseInt(balanceStr, 10, 64)
if err == nil {
// 转换为美元除以100000000
balanceUSD = float64(balanceValue) / balanceMultiplier
}
}
// 无论是否查询成功都设置余额查询失败则为0
users[i].Balance = &balanceUSD
}
} else {
// 如果Redis客户端不可用设置所有用户余额为0
zeroBalance := 0.0
for i := range users {
users[i].Balance = &zeroBalance
}
}
return users, total, nil
}
func (s *goalfyMaxUserService) GetByID(id uint) (*models.GoalfyMaxUser, error) {
return s.storage.GetByID(id)
}
func (s *goalfyMaxUserService) Create(req *models.GoalfyMaxUserCreateRequest) (*models.GoalfyMaxUser, error) {
// 检查用户名是否已存在
_, err := s.storage.GetByUsername(req.Username)
if err == nil {
return nil, errors.New("用户名已存在")
}
// 检查邮箱是否已存在
_, err = s.storage.GetByEmail(req.Email)
if err == nil {
return nil, errors.New("邮箱已存在")
}
// 1. 先调用SSO创建用户
ctx := context.Background()
ssoReq := &SSOAdminUserCreateRequest{
Username: req.Username,
Email: req.Email,
Phone: "", // 默认空手机号,可以根据需要修改
Password: req.Password, // 使用用户输入的密码
}
ssoUser, err := s.ssoAdminService.CreateUser(ctx, ssoReq)
if err != nil {
s.logger.Error("create goalfymax user failed", zap.Error(err))
return nil, fmt.Errorf("SSO创建用户失败: %w", err)
}
// 2. 设置系统角色:默认 custom而非固定角色ID
if err := s.ssoAdminService.SetSystemRole(ctx, ssoUser.ID, "custom"); err != nil {
// 如果设置失败,记录错误但不阻止用户创建
if s.logger != nil {
s.logger.Error("设置系统角色失败", zap.Int("user_id", ssoUser.ID), zap.Error(err))
}
}
// 3. 在本地数据库创建用户记录使用SSO返回的用户ID
// 如果未指定用户等级,默认为 normal
userLevelCode := req.UserLevelCode
if userLevelCode == "" {
userLevelCode = "normal"
}
// 如果未指定版本,默认为 1用户版
version := req.Version
if version == 0 {
version = 1
}
user := &models.GoalfyMaxUser{
UserID: ssoUser.ID, // 使用SSO返回的用户ID
Username: req.Username,
Email: req.Email,
Nickname: req.Nickname,
Avatar: req.Avatar,
UserLevelCode: userLevelCode,
Version: version,
IsBanned: false,
}
if err := s.storage.Create(user); err != nil {
// 如果本地创建失败需要清理SSO用户这里简化处理实际应该调用SSO删除接口
if s.logger != nil {
s.logger.Warn("本地用户创建失败但SSO用户已创建", zap.Int("sso_user_id", ssoUser.ID), zap.Error(err))
}
return nil, fmt.Errorf("创建用户失败: %w", err)
}
return user, nil
}
func (s *goalfyMaxUserService) Update(id uint, req *models.GoalfyMaxUserUpdateRequest) (*models.GoalfyMaxUser, error) {
user, err := s.storage.GetByID(id)
if err != nil {
return nil, errors.New("用户不存在")
}
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Email != "" {
user.Email = req.Email
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.UserLevelCode != "" {
user.UserLevelCode = req.UserLevelCode
}
if req.Version != nil {
user.Version = *req.Version
}
if req.GoalfyHubPermission != nil {
user.GoalfyHubPermission = *req.GoalfyHubPermission
}
if err := s.storage.Update(user); err != nil {
return nil, err
}
return user, nil
}
func (s *goalfyMaxUserService) Delete(id uint) error {
_, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
return s.storage.Delete(id)
}
func (s *goalfyMaxUserService) Ban(id uint, req *models.GoalfyMaxUserBanRequest, adminID int) error {
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 执行封禁操作
if err := s.storage.SetBanned(id, req.Reason, adminID); err != nil {
return err
}
// 发送封禁通知
ctx := context.Background()
banMessage := &models.MessagePushRequest{
Title: "封禁通知",
Content: fmt.Sprintf("您的账户已被封禁。封禁原因:%s。如有疑问请联系客服。", req.Reason),
UserIDs: []int{user.UserID},
}
// 异步发送通知,不阻塞封禁操作
go func() {
if _, err := s.messagePushService.SendMessage(ctx, banMessage, adminID, "系统管理员"); err != nil {
// 记录错误日志,但不影响封禁操作
if s.logger != nil {
s.logger.Error("发送封禁通知失败", zap.Int("user_id", user.UserID), zap.Int("admin_id", adminID), zap.Error(err))
}
}
}()
return nil
}
func (s *goalfyMaxUserService) Unban(id uint) error {
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 执行解封操作
if err := s.storage.Unban(id); err != nil {
return err
}
// 发送解封通知
ctx := context.Background()
unbanMessage := &models.MessagePushRequest{
Title: "解封通知",
Content: "您的账户已被解封,现在可以正常使用所有功能。感谢您的理解与配合。",
UserIDs: []int{user.UserID},
}
// 异步发送通知,不阻塞解封操作
go func() {
if _, err := s.messagePushService.SendMessage(ctx, unbanMessage, 0, "系统管理员"); err != nil {
// 记录错误日志,但不影响解封操作
if s.logger != nil {
s.logger.Error("发送解封通知失败", zap.Int("user_id", user.UserID), zap.Error(err))
}
}
}()
return nil
}
func (s *goalfyMaxUserService) AddBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error {
// 检查用户是否存在
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 检查Redis客户端是否可用
if s.redisClient == nil {
return errors.New("Redis客户端不可用无法增加余额")
}
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", user.UserID)
// 获取操作前余额
balanceBeforeUSD := 0.0
balanceBeforeStr, err := s.redisClient.Rdb.Get(ctx, redisKey).Result()
if err == nil {
balanceBeforeValue, err := strconv.ParseInt(balanceBeforeStr, 10, 64)
if err == nil {
balanceBeforeUSD = float64(balanceBeforeValue) / balanceMultiplier
}
}
// 将美元金额转换为Redis存储的数值
amountToAdd := int64(req.Amount * balanceMultiplier)
// 使用Redis的INCRBY命令增加余额如果key不存在会自动创建并设置为0然后加上amountToAdd
newBalance, err := s.redisClient.Rdb.IncrBy(ctx, redisKey, amountToAdd).Result()
if err != nil {
return fmt.Errorf("增加余额失败: %w", err)
}
// 计算操作后余额
balanceAfterUSD := float64(newBalance) / balanceMultiplier
// 异步记录操作日志(保留原有的余额操作日志)
go func() {
logEntry := &models.BalanceOperationLog{
UserID: user.UserID,
OperationType: models.OperationTypeAdd,
Amount: req.Amount,
BalanceBefore: balanceBeforeUSD,
BalanceAfter: balanceAfterUSD,
OperatorID: operatorID,
OperatorName: operatorEmail,
Remark: "", // 可以后续扩展
}
if err := s.balanceOperationLogStorage.Create(logEntry); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录余额操作日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeAdd),
zap.Float64("amount", req.Amount),
zap.Error(err))
}
}
}()
// 异步记录审计日志
if s.auditLogService != nil {
go func() {
operationDetails := models.OperationDetails{
"action": "recharge",
"amount": req.Amount,
"currency": "USD",
"before_balance": balanceBeforeUSD,
"after_balance": balanceAfterUSD,
"remark": "",
}
auditLogReq := &models.CreateAuditLogRequest{
OperationType: models.OperationTypeBalanceAdjustment,
OperatorID: operatorID,
OperatorEmail: operatorEmail,
TargetType: models.TargetTypeUser,
TargetID: &user.UserID,
TargetEmail: user.Email,
OperationDetails: operationDetails,
IPAddress: ipAddress,
UserAgent: userAgent,
Status: models.AuditLogStatusSuccess,
}
if err := s.auditLogService.Create(auditLogReq); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录审计日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeBalanceAdjustment),
zap.Error(err))
}
}
}()
}
// 记录操作日志
if s.logger != nil {
s.logger.Info("用户余额增加成功",
zap.Int("user_id", user.UserID),
zap.String("username", user.Username),
zap.Float64("amount", req.Amount),
zap.Float64("balance_before", balanceBeforeUSD),
zap.Float64("balance_after", balanceAfterUSD),
zap.Int64("redis_balance_raw", newBalance),
zap.Int("operator_id", operatorID),
zap.String("operator_email", operatorEmail))
}
return nil
}
func (s *goalfyMaxUserService) DeductBalance(id uint, req *models.GoalfyMaxUserAddBalanceRequest, operatorID int, operatorEmail string, ipAddress string, userAgent string) error {
// 检查用户是否存在
user, err := s.storage.GetByID(id)
if err != nil {
return errors.New("用户不存在")
}
// 检查Redis客户端是否可用
if s.redisClient == nil {
return errors.New("Redis客户端不可用无法减少余额")
}
ctx := context.Background()
// 余额换算比例1美元 = 100,000000 (即100000000)
const balanceMultiplier = 100000000.0
// Redis key格式: GW:QU_{用户ID}
redisKey := fmt.Sprintf("GW:QU_%d", user.UserID)
// 将美元金额转换为Redis存储的数值
amountToDeduct := int64(req.Amount * balanceMultiplier)
// 先获取当前余额,检查是否足够
currentBalance, err := s.redisClient.Rdb.Get(ctx, redisKey).Int64()
if err != nil {
// 如果key不存在表示余额为0不能减少
if err == goredis.Nil {
return errors.New("余额不足,无法减少")
}
return fmt.Errorf("查询余额失败: %w", err)
}
// 计算操作前余额
balanceBeforeUSD := float64(currentBalance) / balanceMultiplier
// 检查余额是否足够
if currentBalance < amountToDeduct {
return errors.New("余额不足,无法减少")
}
// 使用Redis的DECRBY命令减少余额
newBalance, err := s.redisClient.Rdb.DecrBy(ctx, redisKey, amountToDeduct).Result()
if err != nil {
return fmt.Errorf("减少余额失败: %w", err)
}
// 计算操作后余额
balanceAfterUSD := float64(newBalance) / balanceMultiplier
// 异步记录操作日志(保留原有的余额操作日志)
go func() {
logEntry := &models.BalanceOperationLog{
UserID: user.UserID,
OperationType: models.OperationTypeDeduct,
Amount: req.Amount,
BalanceBefore: balanceBeforeUSD,
BalanceAfter: balanceAfterUSD,
OperatorID: operatorID,
OperatorName: operatorEmail,
Remark: "", // 可以后续扩展
}
if err := s.balanceOperationLogStorage.Create(logEntry); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录余额操作日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeDeduct),
zap.Float64("amount", req.Amount),
zap.Error(err))
}
}
}()
// 异步记录审计日志
if s.auditLogService != nil {
go func() {
operationDetails := models.OperationDetails{
"action": "deduct",
"amount": req.Amount,
"currency": "USD",
"before_balance": balanceBeforeUSD,
"after_balance": balanceAfterUSD,
"remark": "",
}
auditLogReq := &models.CreateAuditLogRequest{
OperationType: models.OperationTypeBalanceAdjustment,
OperatorID: operatorID,
OperatorEmail: operatorEmail,
TargetType: models.TargetTypeUser,
TargetID: &user.UserID,
TargetEmail: user.Email,
OperationDetails: operationDetails,
IPAddress: ipAddress,
UserAgent: userAgent,
Status: models.AuditLogStatusSuccess,
}
if err := s.auditLogService.Create(auditLogReq); err != nil {
// 记录错误但不影响主流程
if s.logger != nil {
s.logger.Error("记录审计日志失败",
zap.Int("user_id", user.UserID),
zap.String("operation_type", models.OperationTypeBalanceAdjustment),
zap.Error(err))
}
}
}()
}
// 记录操作日志
if s.logger != nil {
s.logger.Info("用户余额减少成功",
zap.Int("user_id", user.UserID),
zap.String("username", user.Username),
zap.Float64("amount", req.Amount),
zap.Float64("balance_before", balanceBeforeUSD),
zap.Float64("balance_after", balanceAfterUSD),
zap.Int64("redis_balance_raw", newBalance),
zap.Int("operator_id", operatorID),
zap.String("operator_email", operatorEmail))
}
return nil
}