暂存一下,防止丢失

This commit is contained in:
2026-07-24 16:07:18 +08:00
parent 5d6e23f1a5
commit a18ed8bc8d
180 changed files with 13597 additions and 1986 deletions

View File

@@ -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(&current).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 {