feat():learning后台管理项目初始化

This commit is contained in:
yuj
2025-12-04 16:23:46 +08:00
parent 39886d50d2
commit 88e048f4d1
154 changed files with 28966 additions and 6 deletions

View 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
}