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 }