暂存一下,防止丢失
This commit is contained in:
@@ -6,12 +6,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
@@ -41,7 +43,7 @@ type Service struct {
|
||||
db *gorm.DB
|
||||
agentRechargeStore *postgres.AgentRechargeStore
|
||||
agentWalletStore *postgres.AgentWalletStore
|
||||
agentWalletTxStore *postgres.AgentWalletTransactionStore
|
||||
agentWalletPosting *walletapp.PostingService
|
||||
shopStore *postgres.ShopStore
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
auditService AuditServiceInterface
|
||||
@@ -55,7 +57,6 @@ func New(
|
||||
db *gorm.DB,
|
||||
agentRechargeStore *postgres.AgentRechargeStore,
|
||||
agentWalletStore *postgres.AgentWalletStore,
|
||||
agentWalletTxStore *postgres.AgentWalletTransactionStore,
|
||||
shopStore *postgres.ShopStore,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
auditService AuditServiceInterface,
|
||||
@@ -67,7 +68,6 @@ func New(
|
||||
db: db,
|
||||
agentRechargeStore: agentRechargeStore,
|
||||
agentWalletStore: agentWalletStore,
|
||||
agentWalletTxStore: agentWalletTxStore,
|
||||
shopStore: shopStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
auditService: auditService,
|
||||
@@ -77,6 +77,11 @@ func New(
|
||||
}
|
||||
}
|
||||
|
||||
// SetAgentWalletPostingService 注入代理主钱包统一入账用例。
|
||||
func (s *Service) SetAgentWalletPostingService(service *walletapp.PostingService) {
|
||||
s.agentWalletPosting = service
|
||||
}
|
||||
|
||||
// Create 创建代理充值订单
|
||||
// POST /api/admin/agent-recharges
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeRequest) (*dto.AgentRechargeResponse, error) {
|
||||
@@ -199,12 +204,6 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该订单状态不允许确认支付")
|
||||
}
|
||||
|
||||
// 查询钱包(事务内需要用到 version)
|
||||
wallet, err := s.agentWalletStore.GetByID(ctx, record.AgentWalletID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理钱包失败")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 条件更新充值记录状态
|
||||
@@ -222,41 +221,20 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
|
||||
return errors.New(errors.CodeInvalidParam, "充值记录状态已变更")
|
||||
}
|
||||
|
||||
// 增加钱包余额(乐观锁)
|
||||
balanceResult := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||||
Updates(map[string]interface{}{
|
||||
"balance": gorm.Expr("balance + ?", record.Amount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if balanceResult.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, balanceResult.Error, "更新钱包余额失败")
|
||||
if s.agentWalletPosting == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理主钱包入账能力未配置")
|
||||
}
|
||||
if balanceResult.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInternalError, "钱包余额更新冲突,请重试")
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
|
||||
// 创建钱包交易记录
|
||||
remark := "线下充值确认"
|
||||
refType := "topup"
|
||||
txRecord := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: record.ShopID,
|
||||
UserID: userID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge,
|
||||
Amount: record.Amount,
|
||||
BalanceBefore: wallet.Balance,
|
||||
BalanceAfter: wallet.Balance + record.Amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: &refType,
|
||||
ReferenceID: &record.ID,
|
||||
Remark: &remark,
|
||||
Creator: userID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
if err := s.agentWalletTxStore.CreateWithTx(ctx, tx, txRecord); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败")
|
||||
if _, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
|
||||
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
|
||||
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge, UserID: userID, Creator: userID,
|
||||
Remark: "线下充值确认", RequestID: requestID, CorrelationID: record.RechargeNo,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -293,7 +271,8 @@ func (s *Service) OfflinePay(ctx context.Context, id uint, req *dto.AgentOffline
|
||||
|
||||
// HandlePaymentCallback 处理支付回调
|
||||
// 幂等处理:status != 待支付则直接返回成功
|
||||
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string) error {
|
||||
func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string, paidAmount int64) error {
|
||||
paymentTransactionID = strings.TrimSpace(paymentTransactionID)
|
||||
record, err := s.agentRechargeStore.GetByRechargeNo(ctx, rechargeNo)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -302,25 +281,23 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询充值订单失败")
|
||||
}
|
||||
|
||||
// 幂等检查
|
||||
if err := validateAgentRechargePayment(record, paymentMethod, paymentTransactionID, paidAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 已完成订单仅在第三方交易号一致时按同一回调幂等成功。
|
||||
if record.Status != constants.RechargeStatusPending {
|
||||
s.logger.Info("代理充值订单已处理,跳过",
|
||||
zap.String("recharge_no", rechargeNo),
|
||||
zap.Int("status", record.Status),
|
||||
)
|
||||
return nil
|
||||
if record.Status == constants.RechargeStatusCompleted && record.PaymentTransactionID != nil && *record.PaymentTransactionID == strings.TrimSpace(paymentTransactionID) {
|
||||
s.logger.Info("代理充值支付回调幂等命中", zap.String("recharge_no", rechargeNo), zap.Int("status", record.Status))
|
||||
return nil
|
||||
}
|
||||
return errors.New(errors.CodeInvalidStatus, "充值订单状态不允许确认支付")
|
||||
}
|
||||
|
||||
wallet, err := s.agentWalletStore.GetByID(ctx, record.AgentWalletID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询代理钱包失败")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 条件更新(WHERE status = 1)
|
||||
result := tx.Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", record.ID, constants.RechargeStatusPending).
|
||||
Where("id = ? AND status = ? AND payment_method <> ? AND amount = ?", record.ID, constants.RechargeStatusPending, constants.RechargeMethodOffline, paidAmount).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constants.RechargeStatusCompleted,
|
||||
"payment_transaction_id": paymentTransactionID,
|
||||
@@ -331,44 +308,31 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新充值记录状态失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
var current model.AgentRechargeRecord
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
Where("id = ?", record.ID).First(¤t).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "复核充值记录终态失败")
|
||||
}
|
||||
if current.Status == constants.RechargeStatusCompleted && current.PaymentTransactionID != nil && *current.PaymentTransactionID == paymentTransactionID {
|
||||
return nil
|
||||
}
|
||||
return errors.New(errors.CodeConflict, "充值订单已被其他请求处理")
|
||||
}
|
||||
|
||||
// 增加钱包余额(乐观锁)
|
||||
balanceResult := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||||
Updates(map[string]interface{}{
|
||||
"balance": gorm.Expr("balance + ?", record.Amount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if balanceResult.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, balanceResult.Error, "更新钱包余额失败")
|
||||
if s.agentWalletPosting == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理主钱包入账能力未配置")
|
||||
}
|
||||
if balanceResult.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInternalError, "钱包余额更新冲突,请重试")
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
|
||||
// 创建交易记录
|
||||
remark := "在线支付充值"
|
||||
refType := "topup"
|
||||
txRecord := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: record.ShopID,
|
||||
UserID: record.UserID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge,
|
||||
Amount: record.Amount,
|
||||
BalanceBefore: wallet.Balance,
|
||||
BalanceAfter: wallet.Balance + record.Amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: &refType,
|
||||
ReferenceID: &record.ID,
|
||||
Remark: &remark,
|
||||
Creator: record.UserID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
if err := s.agentWalletTxStore.CreateWithTx(ctx, tx, txRecord); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败")
|
||||
if _, err := s.agentWalletPosting.PostInTx(ctx, tx, walletapp.PostingCommand{
|
||||
ShopID: record.ShopID, WalletID: record.AgentWalletID, Amount: record.Amount,
|
||||
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: record.ID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge, UserID: record.UserID, Creator: record.UserID,
|
||||
Remark: "在线支付充值", RequestID: requestID, CorrelationID: record.RechargeNo,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -387,6 +351,29 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, rechargeNo string,
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAgentRechargePayment(record *model.AgentRechargeRecord, paymentMethod, paymentTransactionID string, paidAmount int64) error {
|
||||
if record == nil || strings.TrimSpace(paymentTransactionID) == "" || paidAmount <= 0 || paidAmount != record.Amount {
|
||||
return errors.New(errors.CodeInvalidParam, "代理充值支付回调金额或交易号不匹配")
|
||||
}
|
||||
if record.PaymentMethod == constants.RechargeMethodOffline || record.PaymentConfigID == nil || record.PaymentChannel == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "线下充值订单不能通过支付回调确认")
|
||||
}
|
||||
channel := strings.TrimSpace(*record.PaymentChannel)
|
||||
switch strings.TrimSpace(paymentMethod) {
|
||||
case model.PaymentMethodWechat:
|
||||
if record.PaymentMethod != constants.RechargeMethodWechat || (channel != model.ProviderTypeWechat && channel != model.ProviderTypeWechatV2) {
|
||||
return errors.New(errors.CodeInvalidParam, "代理充值支付渠道不匹配")
|
||||
}
|
||||
case model.ProviderTypeFuiou:
|
||||
if record.PaymentMethod != constants.RechargeMethodWechat || channel != model.ProviderTypeFuiou {
|
||||
return errors.New(errors.CodeInvalidParam, "代理充值支付渠道不匹配")
|
||||
}
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "代理充值支付渠道不受支持")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject 驳回代理充值订单
|
||||
// 仅 status=1(待支付)的订单可驳回,驳回后状态变为 6(已驳回),为终态
|
||||
func (s *Service) Reject(ctx context.Context, id uint, rejectionReason string) error {
|
||||
|
||||
Reference in New Issue
Block a user