feat():learning后台管理项目初始化
This commit is contained in:
512
internal/services/goalfymax_user_service.go
Normal file
512
internal/services/goalfymax_user_service.go
Normal file
@@ -0,0 +1,512 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user