513 lines
16 KiB
Go
513 lines
16 KiB
Go
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
|
||
}
|