new
This commit is contained in:
186
internal/domains/finance/services/balance_alert_service.go
Normal file
186
internal/domains/finance/services/balance_alert_service.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
)
|
||||
|
||||
// BalanceAlertService 余额预警服务接口
|
||||
type BalanceAlertService interface {
|
||||
CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error
|
||||
}
|
||||
|
||||
// BalanceAlertServiceImpl 余额预警服务实现
|
||||
type BalanceAlertServiceImpl struct {
|
||||
apiUserRepo api_repositories.ApiUserRepository
|
||||
userRepo user_repositories.UserRepository
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository
|
||||
smsService *sms.AliSMSService
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewBalanceAlertService 创建余额预警服务
|
||||
func NewBalanceAlertService(
|
||||
apiUserRepo api_repositories.ApiUserRepository,
|
||||
userRepo user_repositories.UserRepository,
|
||||
enterpriseInfoRepo user_repositories.EnterpriseInfoRepository,
|
||||
smsService *sms.AliSMSService,
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
) BalanceAlertService {
|
||||
return &BalanceAlertServiceImpl{
|
||||
apiUserRepo: apiUserRepo,
|
||||
userRepo: userRepo,
|
||||
enterpriseInfoRepo: enterpriseInfoRepo,
|
||||
smsService: smsService,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAndSendAlert 检查余额并发送预警
|
||||
func (s *BalanceAlertServiceImpl) CheckAndSendAlert(ctx context.Context, userID string, balance decimal.Decimal) error {
|
||||
// 1. 获取API用户信息
|
||||
apiUser, err := s.apiUserRepo.FindByUserId(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取API用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取API用户信息失败: %w", err)
|
||||
}
|
||||
|
||||
if apiUser == nil {
|
||||
s.logger.Debug("API用户不存在,跳过余额预警检查", zap.String("user_id", userID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. 兼容性处理:如果API用户没有配置预警信息,从用户表获取并更新
|
||||
needUpdate := false
|
||||
if apiUser.AlertPhone == "" {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取用户信息失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("获取用户信息失败: %w", err)
|
||||
}
|
||||
if user.Phone != "" {
|
||||
apiUser.AlertPhone = user.Phone
|
||||
needUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 兼容性处理:如果API用户没有配置预警阈值,使用默认值
|
||||
if apiUser.BalanceAlertThreshold == 0 {
|
||||
apiUser.BalanceAlertThreshold = s.config.Wallet.BalanceAlert.DefaultThreshold
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
// 4. 如果需要更新API用户信息,保存到数据库
|
||||
if needUpdate {
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警配置失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
// 不返回错误,继续执行预警检查
|
||||
}
|
||||
}
|
||||
|
||||
balanceFloat, _ := balance.Float64()
|
||||
|
||||
// 5. 检查是否需要发送欠费预警(不受冷却期限制)
|
||||
if apiUser.ShouldSendArrearsAlert(balanceFloat) {
|
||||
if err := s.sendArrearsAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送欠费预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 欠费预警不受冷却期限制,不需要更新LastArrearsAlert时间
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. 检查是否需要发送低余额预警
|
||||
if apiUser.ShouldSendLowBalanceAlert(balanceFloat) {
|
||||
if err := s.sendLowBalanceAlert(ctx, apiUser, balanceFloat); err != nil {
|
||||
s.logger.Error("发送低余额预警失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
// 标记预警已发送
|
||||
apiUser.MarkLowBalanceAlertSent()
|
||||
if err := s.apiUserRepo.Update(ctx, apiUser); err != nil {
|
||||
s.logger.Error("更新API用户预警时间失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendArrearsAlert 发送欠费预警
|
||||
func (s *BalanceAlertServiceImpl) sendArrearsAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", "天远数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "天远数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送欠费预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", enterpriseName)
|
||||
}
|
||||
|
||||
// sendLowBalanceAlert 发送低余额预警
|
||||
func (s *BalanceAlertServiceImpl) sendLowBalanceAlert(ctx context.Context, apiUser *entities.ApiUser, balance float64) error {
|
||||
// 直接从企业信息表获取企业名称
|
||||
enterpriseInfo, err := s.enterpriseInfoRepo.GetByUserID(ctx, apiUser.UserId)
|
||||
if err != nil {
|
||||
s.logger.Error("获取企业信息失败",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.Error(err))
|
||||
// 如果获取企业信息失败,使用默认名称
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", "天远数据用户")
|
||||
}
|
||||
|
||||
// 获取企业名称,如果没有则使用默认名称
|
||||
enterpriseName := "天远数据用户"
|
||||
if enterpriseInfo != nil && enterpriseInfo.CompanyName != "" {
|
||||
enterpriseName = enterpriseInfo.CompanyName
|
||||
}
|
||||
|
||||
s.logger.Info("发送低余额预警短信",
|
||||
zap.String("user_id", apiUser.UserId),
|
||||
zap.String("phone", apiUser.AlertPhone),
|
||||
zap.Float64("balance", balance),
|
||||
zap.Float64("threshold", apiUser.BalanceAlertThreshold),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", enterpriseName)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
@@ -25,21 +26,27 @@ type WalletAggregateService interface {
|
||||
|
||||
// WalletAggregateServiceImpl 钱包聚合服务实现
|
||||
type WalletAggregateServiceImpl struct {
|
||||
db *gorm.DB
|
||||
walletRepo repositories.WalletRepository
|
||||
transactionRepo repositories.WalletTransactionRepository
|
||||
balanceAlertSvc BalanceAlertService
|
||||
logger *zap.Logger
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewWalletAggregateService(
|
||||
walletRepo repositories.WalletRepository,
|
||||
transactionRepo repositories.WalletTransactionRepository,
|
||||
logger *zap.Logger,
|
||||
db *gorm.DB,
|
||||
walletRepo repositories.WalletRepository,
|
||||
transactionRepo repositories.WalletTransactionRepository,
|
||||
balanceAlertSvc BalanceAlertService,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) WalletAggregateService {
|
||||
return &WalletAggregateServiceImpl{
|
||||
db: db,
|
||||
walletRepo: walletRepo,
|
||||
transactionRepo: transactionRepo,
|
||||
balanceAlertSvc: balanceAlertSvc,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
}
|
||||
@@ -62,72 +69,59 @@ func (s *WalletAggregateServiceImpl) CreateWallet(ctx context.Context, userID st
|
||||
return &created, nil
|
||||
}
|
||||
|
||||
// Recharge 充值
|
||||
// Recharge 充值 - 使用事务确保一致性
|
||||
func (s *WalletAggregateServiceImpl) Recharge(ctx context.Context, userID string, amount decimal.Decimal) error {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
// 使用数据库事务确保一致性
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(ctx, userID, amount, "add")
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新钱包余额失败: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下充值失败,请重试")
|
||||
}
|
||||
|
||||
// 更新钱包余额
|
||||
w.AddBalance(amount)
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下充值失败,请重试")
|
||||
}
|
||||
s.logger.Info("钱包充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()))
|
||||
|
||||
s.logger.Info("钱包充值成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("wallet_id", w.ID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("balance_after", w.Balance.String()))
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
// Deduct 扣款,含欠费规则
|
||||
|
||||
// Deduct 扣款,含欠费规则 - 使用事务确保一致性
|
||||
func (s *WalletAggregateServiceImpl) Deduct(ctx context.Context, userID string, amount decimal.Decimal, apiCallID, transactionID, productID string) error {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("钱包不存在")
|
||||
}
|
||||
// 使用数据库事务确保一致性
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 使用乐观锁更新余额(通过用户ID直接更新,避免重复查询)
|
||||
ok, err := s.walletRepo.UpdateBalanceByUserID(ctx, userID, amount, "subtract")
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新钱包余额失败: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下扣款失败,请重试")
|
||||
}
|
||||
|
||||
// 扣减余额
|
||||
if err := w.SubtractBalance(amount); err != nil {
|
||||
return err
|
||||
}
|
||||
// 2. 创建扣款记录(检查是否已存在)
|
||||
transaction := entities.NewWalletTransaction(userID, apiCallID, transactionID, productID, amount)
|
||||
|
||||
// 更新钱包余额
|
||||
ok, err := s.walletRepo.UpdateBalanceWithVersion(ctx, w.ID, w.Balance.String(), w.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("高并发下扣款失败,请重试")
|
||||
}
|
||||
if err := tx.Create(transaction).Error; err != nil {
|
||||
return fmt.Errorf("创建扣款记录失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建扣款记录
|
||||
transaction := entities.NewWalletTransaction(userID, apiCallID, transactionID, productID, amount)
|
||||
_, err = s.transactionRepo.Create(ctx, *transaction)
|
||||
if err != nil {
|
||||
s.logger.Error("创建扣款记录失败", zap.Error(err))
|
||||
// 不返回错误,因为钱包余额已经更新成功
|
||||
}
|
||||
s.logger.Info("钱包扣款成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("api_call_id", apiCallID),
|
||||
zap.String("transaction_id", transactionID))
|
||||
|
||||
s.logger.Info("钱包扣款成功",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("wallet_id", w.ID),
|
||||
zap.String("amount", amount.String()),
|
||||
zap.String("balance_after", w.Balance.String()),
|
||||
zap.String("api_call_id", apiCallID))
|
||||
// 3. 扣费成功后异步检查余额预警
|
||||
go s.checkBalanceAlertAsync(context.Background(), userID)
|
||||
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
// GetBalance 查询余额
|
||||
func (s *WalletAggregateServiceImpl) GetBalance(ctx context.Context, userID string) (decimal.Decimal, error) {
|
||||
w, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
@@ -140,3 +134,22 @@ func (s *WalletAggregateServiceImpl) GetBalance(ctx context.Context, userID stri
|
||||
func (s *WalletAggregateServiceImpl) LoadWalletByUserId(ctx context.Context, userID string) (*entities.Wallet, error) {
|
||||
return s.walletRepo.GetByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
// checkBalanceAlertAsync 异步检查余额预警
|
||||
func (s *WalletAggregateServiceImpl) checkBalanceAlertAsync(ctx context.Context, userID string) {
|
||||
// 获取最新余额
|
||||
wallet, err := s.walletRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("获取钱包余额失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查并发送预警
|
||||
if err := s.balanceAlertSvc.CheckAndSendAlert(ctx, userID, wallet.Balance); err != nil {
|
||||
s.logger.Error("余额预警检查失败",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user