This commit is contained in:
190
internal/application/agentrecharge/confirm_online_payment.go
Normal file
190
internal/application/agentrecharge/confirm_online_payment.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
domain "github.com/break/junhong_cmp_fiber/internal/domain/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ConfirmOnlinePaymentCommand 描述验签或主动查单后得到的第三方收款事实。
|
||||
type ConfirmOnlinePaymentCommand struct {
|
||||
PaymentNo string
|
||||
PaymentMethod string
|
||||
ConfigID uint
|
||||
MerchantIdentity string
|
||||
ThirdPartyTradeNo string
|
||||
Amount int64
|
||||
PaidAt time.Time
|
||||
RequestID string
|
||||
CorrelationID string
|
||||
}
|
||||
|
||||
// PaymentConfirmedEvent 是第三方收款事实提交后的代理充值入账事件。
|
||||
type PaymentConfirmedEvent struct {
|
||||
EventID string `json:"event_id"`
|
||||
RechargeID uint `json:"recharge_id"`
|
||||
RechargeNo string `json:"recharge_no"`
|
||||
PaymentID uint `json:"payment_id"`
|
||||
PaymentNo string `json:"payment_no"`
|
||||
ShopID uint `json:"shop_id"`
|
||||
WalletID uint `json:"wallet_id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
ThirdPartyTradeNo string `json:"third_party_trade_no"`
|
||||
PaidAt time.Time `json:"paid_at"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
}
|
||||
|
||||
// PaymentConfirmedEventWriter 在支付确认事务内追加可靠入账事件。
|
||||
type PaymentConfirmedEventWriter interface {
|
||||
Append(ctx context.Context, tx *gorm.DB, event PaymentConfirmedEvent) error
|
||||
}
|
||||
|
||||
// ConfirmOnlinePaymentResult 返回支付确认是否属于幂等重放。
|
||||
type ConfirmOnlinePaymentResult struct {
|
||||
RechargeID uint
|
||||
AlreadyConfirmed bool
|
||||
}
|
||||
|
||||
// ConfirmOnlinePaymentService 统一处理回调和主动查单得到的代理充值支付事实。
|
||||
type ConfirmOnlinePaymentService struct {
|
||||
db *gorm.DB
|
||||
eventWriter PaymentConfirmedEventWriter
|
||||
}
|
||||
|
||||
// NewConfirmOnlinePaymentService 创建代理充值支付确认用例。
|
||||
func NewConfirmOnlinePaymentService(db *gorm.DB, eventWriter PaymentConfirmedEventWriter) *ConfirmOnlinePaymentService {
|
||||
return &ConfirmOnlinePaymentService{db: db, eventWriter: eventWriter}
|
||||
}
|
||||
|
||||
// Execute 在一个短事务中校验并固化支付事实和可靠入账事件。
|
||||
func (s *ConfirmOnlinePaymentService) Execute(ctx context.Context, command ConfirmOnlinePaymentCommand) (*ConfirmOnlinePaymentResult, error) {
|
||||
if s == nil || s.db == nil || s.eventWriter == nil {
|
||||
return nil, errors.New(errors.CodeServiceUnavailable, "代理充值支付确认能力未配置")
|
||||
}
|
||||
command.PaymentNo = strings.TrimSpace(command.PaymentNo)
|
||||
command.PaymentMethod = strings.TrimSpace(command.PaymentMethod)
|
||||
command.MerchantIdentity = strings.TrimSpace(command.MerchantIdentity)
|
||||
command.ThirdPartyTradeNo = strings.TrimSpace(command.ThirdPartyTradeNo)
|
||||
if command.PaymentNo == "" || command.ConfigID == 0 || command.PaidAt.IsZero() {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "代理充值支付确认参数不完整")
|
||||
}
|
||||
|
||||
result := &ConfirmOnlinePaymentResult{}
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
payment, recharge, err := lockPaymentConfirmationFacts(ctx, tx, command.PaymentNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alreadyConfirmed, err := domain.ValidatePaymentConfirmation(toDomainConfirmationFacts(payment, recharge, command))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.RechargeID = recharge.ID
|
||||
if alreadyConfirmed {
|
||||
result.AlreadyConfirmed = true
|
||||
return nil
|
||||
}
|
||||
if err := ensureTradeNoAvailable(ctx, tx, payment, command); err != nil {
|
||||
return err
|
||||
}
|
||||
paidAt := command.PaidAt.UTC()
|
||||
paymentUpdate := tx.WithContext(ctx).Model(&model.Payment{}).
|
||||
Where("id = ? AND status IN ?", payment.ID, []int{model.PaymentRecordStatusPending, model.PaymentRecordStatusFailed}).
|
||||
Updates(map[string]any{"status": model.PaymentRecordStatusPaid, "third_party_trade_no": command.ThirdPartyTradeNo, "paid_at": paidAt})
|
||||
if paymentUpdate.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, paymentUpdate.Error, "更新代理充值支付单失败")
|
||||
}
|
||||
if paymentUpdate.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "代理充值支付单状态已变化")
|
||||
}
|
||||
rechargeUpdate := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status IN ?", recharge.ID, []int{constants.RechargeStatusPending, constants.RechargeStatusClosed}).
|
||||
Updates(map[string]any{"status": constants.RechargeStatusPaid, "payment_transaction_id": command.ThirdPartyTradeNo, "paid_at": paidAt})
|
||||
if rechargeUpdate.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, rechargeUpdate.Error, "更新代理充值单支付状态失败")
|
||||
}
|
||||
if rechargeUpdate.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "代理充值单状态已变化")
|
||||
}
|
||||
event := PaymentConfirmedEvent{
|
||||
EventID: "agent-recharge:" + strconv.FormatUint(uint64(recharge.ID), 10) + ":payment-confirmed",
|
||||
RechargeID: recharge.ID, RechargeNo: recharge.RechargeNo, PaymentID: payment.ID, PaymentNo: payment.PaymentNo,
|
||||
ShopID: recharge.ShopID, WalletID: recharge.AgentWalletID, UserID: recharge.UserID, Amount: recharge.Amount,
|
||||
PaymentMethod: command.PaymentMethod, ThirdPartyTradeNo: command.ThirdPartyTradeNo,
|
||||
PaidAt: paidAt, RequestID: command.RequestID, CorrelationID: command.CorrelationID,
|
||||
}
|
||||
if err := s.eventWriter.Append(ctx, tx, event); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "写入代理充值支付确认事件失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func lockPaymentConfirmationFacts(ctx context.Context, tx *gorm.DB, paymentNo string) (*model.Payment, *model.AgentRechargeRecord, error) {
|
||||
var payment model.Payment
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("payment_no = ?", paymentNo).First(&payment).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, errors.New(errors.CodeNotFound, "代理充值支付单不存在")
|
||||
}
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理充值支付单失败")
|
||||
}
|
||||
var recharge model.AgentRechargeRecord
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", payment.OrderID).First(&recharge).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, errors.New(errors.CodeConflict, "支付单关联的代理充值单不存在")
|
||||
}
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理充值单失败")
|
||||
}
|
||||
return &payment, &recharge, nil
|
||||
}
|
||||
|
||||
func toDomainConfirmationFacts(payment *model.Payment, recharge *model.AgentRechargeRecord, command ConfirmOnlinePaymentCommand) domain.PaymentConfirmationFacts {
|
||||
paymentConfigID, rechargeConfigID, rechargeChannel := uint(0), uint(0), ""
|
||||
if payment.PaymentConfigID != nil {
|
||||
paymentConfigID = *payment.PaymentConfigID
|
||||
}
|
||||
if recharge.PaymentConfigID != nil {
|
||||
rechargeConfigID = *recharge.PaymentConfigID
|
||||
}
|
||||
if recharge.PaymentChannel != nil {
|
||||
rechargeChannel = *recharge.PaymentChannel
|
||||
}
|
||||
return domain.PaymentConfirmationFacts{
|
||||
OrderType: payment.OrderType, ExpectedOrderType: model.PaymentOrderTypeAgentRecharge,
|
||||
PaymentMethod: payment.PaymentMethod, RechargePaymentMethod: recharge.PaymentMethod, RechargePaymentChannel: rechargeChannel,
|
||||
PaymentConfigID: paymentConfigID, RechargePaymentConfigID: rechargeConfigID, ConfirmedConfigID: command.ConfigID,
|
||||
MerchantIdentity: payment.MerchantIdentity, ConfirmedMerchantIdentity: command.MerchantIdentity,
|
||||
PaymentAmount: payment.Amount, RechargeAmount: recharge.Amount, ConfirmedAmount: command.Amount,
|
||||
PaymentOrderID: payment.OrderID, RechargeID: recharge.ID, PaymentState: domain.PaymentState(payment.Status),
|
||||
RechargeStatus: recharge.Status, StoredTradeNo: payment.ThirdPartyTradeNo, ConfirmedTradeNo: command.ThirdPartyTradeNo,
|
||||
}
|
||||
}
|
||||
|
||||
func ensureTradeNoAvailable(ctx context.Context, tx *gorm.DB, payment *model.Payment, command ConfirmOnlinePaymentCommand) error {
|
||||
var count int64
|
||||
if err := tx.WithContext(ctx).Unscoped().Model(&model.Payment{}).
|
||||
Where("id <> ? AND payment_method = ? AND third_party_trade_no = ?", payment.ID, command.PaymentMethod, command.ThirdPartyTradeNo).
|
||||
Count(&count).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "校验第三方交易号唯一性失败")
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New(errors.CodeConflict, "第三方交易号已被其他支付单使用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
331
internal/application/agentrecharge/online_creation.go
Normal file
331
internal/application/agentrecharge/online_creation.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
domain "github.com/break/junhong_cmp_fiber/internal/domain/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/idempotency"
|
||||
)
|
||||
|
||||
const onlineCreationOperation = "agent-recharge.create-online"
|
||||
|
||||
// CreateOnlineCommand 描述从认证上下文构造的在线充值命令。
|
||||
type CreateOnlineCommand struct {
|
||||
AccountID uint
|
||||
UserType int
|
||||
CurrentShopID uint
|
||||
Amount int64
|
||||
PaymentMethod string
|
||||
RequestID string
|
||||
}
|
||||
|
||||
// CreateOnlineResult 返回在线充值单、支付单及原始付款内容。
|
||||
type CreateOnlineResult struct {
|
||||
Recharge *model.AgentRechargeRecord
|
||||
Payment *model.Payment
|
||||
}
|
||||
|
||||
// AvailablePaymentMethodsResult 返回当前可用渠道及在线金额边界。
|
||||
type AvailablePaymentMethodsResult struct {
|
||||
Methods []string
|
||||
MinAmount int64
|
||||
MaxAmount int64
|
||||
}
|
||||
|
||||
// OnlineCreationService 创建代理在线扫码充值单。
|
||||
type OnlineCreationService struct {
|
||||
db *gorm.DB
|
||||
wechat OnlinePaymentPort
|
||||
alipay OnlinePaymentPort
|
||||
}
|
||||
|
||||
// NewOnlineCreationService 创建代理在线充值用例并以结构体字段注入两个渠道 Adapter。
|
||||
func NewOnlineCreationService(db *gorm.DB, wechat, alipay OnlinePaymentPort) *OnlineCreationService {
|
||||
return &OnlineCreationService{db: db, wechat: wechat, alipay: alipay}
|
||||
}
|
||||
|
||||
// Execute 以短事务建单,事务外预下单,再条件保存付款内容或关闭失败订单。
|
||||
func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlineCommand) (*CreateOnlineResult, error) {
|
||||
if s == nil || s.db == nil || s.wechat == nil || s.alipay == nil {
|
||||
return nil, apperrors.New(apperrors.CodeServiceUnavailable, "代理在线充值能力未配置")
|
||||
}
|
||||
command.PaymentMethod = strings.TrimSpace(command.PaymentMethod)
|
||||
command.RequestID = strings.TrimSpace(command.RequestID)
|
||||
if err := domain.ValidateOnlineCreation(command.UserType, command.Amount, command.PaymentMethod); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if command.AccountID == 0 || command.CurrentShopID == 0 || len(command.RequestID) > 64 ||
|
||||
!idempotency.ValidateScope(onlineCreationScope(command.AccountID), command.RequestID) {
|
||||
return nil, apperrors.New(apperrors.CodeInvalidParam)
|
||||
}
|
||||
fingerprint, err := idempotency.Fingerprint(struct {
|
||||
Amount int64 `json:"amount"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
}{command.Amount, command.PaymentMethod})
|
||||
if err != nil {
|
||||
return nil, apperrors.Wrap(apperrors.CodeInternalError, err, "生成在线充值请求指纹失败")
|
||||
}
|
||||
if replay, found, err := s.loadReplay(ctx, command, fingerprint); err != nil || found {
|
||||
return replay, err
|
||||
}
|
||||
account, shop, wallet, config, adapter, err := s.loadCreationFacts(ctx, command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := s.createLocalFacts(ctx, command, fingerprint.Value, account, shop, wallet, config)
|
||||
if err != nil {
|
||||
if replay, found, replayErr := s.loadReplay(ctx, command, fingerprint); replayErr != nil || found {
|
||||
return replay, replayErr
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
paymentResult, err := adapter.PreCreate(ctx, OnlinePaymentRequest{
|
||||
PaymentNo: result.Payment.PaymentNo, Description: "代理主钱包充值", Amount: command.Amount,
|
||||
ExpireAt: *result.Payment.ExpireAt, Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
if !isUnknownPaymentResult(err) {
|
||||
if closeErr := s.closeFailedCreation(ctx, result); closeErr != nil {
|
||||
return nil, apperrors.Wrap(apperrors.CodeDatabaseError, closeErr, "支付预下单失败且关闭本地订单失败")
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(paymentResult.QRContent) == "" {
|
||||
if closeErr := s.closeFailedCreation(ctx, result); closeErr != nil {
|
||||
return nil, closeErr
|
||||
}
|
||||
return nil, apperrors.New(apperrors.CodeServiceUnavailable, "支付渠道未返回付款内容")
|
||||
}
|
||||
update := s.db.WithContext(ctx).Model(&model.Payment{}).
|
||||
Where("id = ? AND status = ? AND qr_content = ''", result.Payment.ID, model.PaymentRecordStatusPending).
|
||||
Update("qr_content", paymentResult.QRContent)
|
||||
if update.Error != nil {
|
||||
return nil, apperrors.Wrap(apperrors.CodeDatabaseError, update.Error, "保存扫码付款内容失败")
|
||||
}
|
||||
if update.RowsAffected != 1 {
|
||||
return nil, apperrors.New(apperrors.CodeConflict, "在线充值付款内容已变化")
|
||||
}
|
||||
result.Payment.QRContent = paymentResult.QRContent
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AvailablePaymentMethods 按固定顺序返回配置完整的扫码支付方式。
|
||||
func (s *OnlineCreationService) AvailablePaymentMethods(ctx context.Context, userType int) (AvailablePaymentMethodsResult, error) {
|
||||
result := AvailablePaymentMethodsResult{
|
||||
Methods: []string{}, MinAmount: constants.AgentOnlineRechargeMinAmount, MaxAmount: constants.AgentRechargeMaxAmount,
|
||||
}
|
||||
if userType != constants.UserTypeAgent {
|
||||
return result, apperrors.New(apperrors.CodeForbidden, "仅代理账号可以查询在线支付方式")
|
||||
}
|
||||
var config model.WechatConfig
|
||||
if err := s.db.WithContext(ctx).Where("is_active = ?", true).First(&config).Error; err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return result, nil
|
||||
}
|
||||
return result, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询生效支付配置失败")
|
||||
}
|
||||
if s.wechat.Available(&config) {
|
||||
result.Methods = append(result.Methods, constants.RechargeMethodWechat)
|
||||
}
|
||||
if s.alipay.Available(&config) {
|
||||
result.Methods = append(result.Methods, constants.RechargeMethodAlipay)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) loadCreationFacts(
|
||||
ctx context.Context,
|
||||
command CreateOnlineCommand,
|
||||
) (*model.Account, *model.Shop, *model.AgentWallet, *model.WechatConfig, OnlinePaymentPort, error) {
|
||||
var account model.Account
|
||||
if err := s.db.WithContext(ctx).Where("id = ? AND user_type = ? AND status = ?", command.AccountID, constants.UserTypeAgent, constants.StatusEnabled).First(&account).Error; err != nil || account.ShopID == nil || *account.ShopID != command.CurrentShopID {
|
||||
if err != nil && !stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询在线充值账号失败")
|
||||
}
|
||||
return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeForbidden, "当前代理账号不可为该店铺充值")
|
||||
}
|
||||
var shop model.Shop
|
||||
if err := s.db.WithContext(ctx).Where("id = ? AND status = ?", command.CurrentShopID, constants.StatusEnabled).First(&shop).Error; err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询当前店铺失败")
|
||||
}
|
||||
var wallet model.AgentWallet
|
||||
if err := s.db.WithContext(ctx).Where("shop_id = ? AND wallet_type = ? AND status = ?", command.CurrentShopID, constants.AgentWalletTypeMain, constants.AgentWalletStatusNormal).First(&wallet).Error; err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeWalletNotFound, "当前店铺主钱包不存在或不可用")
|
||||
}
|
||||
return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询当前店铺主钱包失败")
|
||||
}
|
||||
var config model.WechatConfig
|
||||
if err := s.db.WithContext(ctx).Where("is_active = ?", true).First(&config).Error; err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeNoPaymentConfig)
|
||||
}
|
||||
return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询生效支付配置失败")
|
||||
}
|
||||
adapter := s.adapter(command.PaymentMethod)
|
||||
if adapter == nil || !adapter.Available(&config) {
|
||||
return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeNoPaymentConfig)
|
||||
}
|
||||
return &account, &shop, &wallet, &config, adapter, nil
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) createLocalFacts(
|
||||
ctx context.Context,
|
||||
command CreateOnlineCommand,
|
||||
fingerprint string,
|
||||
account *model.Account,
|
||||
shop *model.Shop,
|
||||
wallet *model.AgentWallet,
|
||||
config *model.WechatConfig,
|
||||
) (*CreateOnlineResult, error) {
|
||||
rechargeNo, err := newBusinessNo(constants.AgentRechargeOrderPrefix, time.Now().Format("20060102150405"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paymentNo, err := newBusinessNo("PAY", fmt.Sprintf("%d", time.Now().UnixMilli()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expireMinutes := config.AliPayExpireMinutes
|
||||
if expireMinutes <= 0 {
|
||||
expireMinutes = model.DefaultAliPayExpireMinutes
|
||||
}
|
||||
expireAt := time.Now().Add(time.Duration(expireMinutes) * time.Minute)
|
||||
channel, requestID := command.PaymentMethod, command.RequestID
|
||||
record := &model.AgentRechargeRecord{
|
||||
UserID: account.ID, AgentWalletID: wallet.ID, ShopID: shop.ID, RechargeNo: rechargeNo,
|
||||
Amount: command.Amount, PaymentMethod: command.PaymentMethod, PaymentChannel: &channel,
|
||||
PaymentConfigID: &config.ID, Status: constants.RechargeStatusPending,
|
||||
RequestID: &requestID, RequestFingerprint: &fingerprint,
|
||||
ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo, OrderType: model.PaymentOrderTypeAgentRecharge,
|
||||
PaymentMethod: command.PaymentMethod, MerchantIdentity: paymentMerchantIdentity(command.PaymentMethod, config),
|
||||
Amount: command.Amount, Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &config.ID, ExpireAt: &expireAt,
|
||||
}
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
payment.OrderID = record.ID
|
||||
if err := tx.Create(payment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "创建在线充值本地订单失败")
|
||||
}
|
||||
return &CreateOnlineResult{Recharge: record, Payment: payment}, nil
|
||||
}
|
||||
|
||||
func paymentMerchantIdentity(paymentMethod string, config *model.WechatConfig) string {
|
||||
if config == nil {
|
||||
return ""
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodWechat {
|
||||
return config.WxMchID
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodAlipay {
|
||||
return config.AliAppID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) loadReplay(
|
||||
ctx context.Context,
|
||||
command CreateOnlineCommand,
|
||||
fingerprint idempotency.FingerprintValue,
|
||||
) (*CreateOnlineResult, bool, error) {
|
||||
var record model.AgentRechargeRecord
|
||||
err := s.db.WithContext(ctx).Where("user_id = ? AND request_id = ?", command.AccountID, command.RequestID).First(&record).Error
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, apperrors.Wrap(apperrors.CodeDatabaseError, err, "读取在线充值幂等记录失败")
|
||||
}
|
||||
existingFingerprint := ""
|
||||
if record.RequestFingerprint != nil {
|
||||
existingFingerprint = *record.RequestFingerprint
|
||||
}
|
||||
if existingFingerprint != fingerprint.Value {
|
||||
return nil, true, apperrors.New(apperrors.CodeConflict, "同一请求标识对应的充值内容不一致")
|
||||
}
|
||||
var payment model.Payment
|
||||
if err := s.db.WithContext(ctx).Where("order_id = ? AND order_type = ?", record.ID, model.PaymentOrderTypeAgentRecharge).First(&payment).Error; err != nil {
|
||||
return nil, true, apperrors.Wrap(apperrors.CodeDatabaseError, err, "读取在线充值支付单失败")
|
||||
}
|
||||
if payment.QRContent == "" {
|
||||
if record.Status == constants.RechargeStatusClosed || payment.Status == model.PaymentRecordStatusFailed {
|
||||
return nil, true, apperrors.New(apperrors.CodeInvalidStatus, "原在线充值请求预下单失败")
|
||||
}
|
||||
return nil, true, apperrors.New(apperrors.CodeConflict, "在线充值请求正在处理中,请稍后重试")
|
||||
}
|
||||
return &CreateOnlineResult{Recharge: &record, Payment: &payment}, true, nil
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) closeFailedCreation(ctx context.Context, result *CreateOnlineResult) error {
|
||||
if result == nil || result.Recharge == nil || result.Payment == nil || !domain.CanCloseAfterPreCreateFailure(result.Recharge.Status) {
|
||||
return nil
|
||||
}
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
paymentUpdate := tx.Model(&model.Payment{}).
|
||||
Where("id = ? AND status = ?", result.Payment.ID, model.PaymentRecordStatusPending).
|
||||
Update("status", model.PaymentRecordStatusFailed)
|
||||
if paymentUpdate.Error != nil {
|
||||
return apperrors.Wrap(apperrors.CodeDatabaseError, paymentUpdate.Error, "关闭失败支付单失败")
|
||||
}
|
||||
rechargeUpdate := tx.Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", result.Recharge.ID, constants.RechargeStatusPending).
|
||||
Update("status", constants.RechargeStatusClosed)
|
||||
if rechargeUpdate.Error != nil {
|
||||
return apperrors.Wrap(apperrors.CodeDatabaseError, rechargeUpdate.Error, "关闭失败充值单失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) adapter(paymentMethod string) OnlinePaymentPort {
|
||||
if paymentMethod == constants.RechargeMethodWechat {
|
||||
return s.wechat
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodAlipay {
|
||||
return s.alipay
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func onlineCreationScope(accountID uint) idempotency.Scope {
|
||||
return idempotency.Scope{Subject: fmt.Sprintf("account:%d", accountID), Operation: onlineCreationOperation}
|
||||
}
|
||||
|
||||
func newBusinessNo(prefix, timestamp string) (string, error) {
|
||||
random, err := cryptorand.Int(cryptorand.Reader, big.NewInt(1000000))
|
||||
if err != nil {
|
||||
return "", apperrors.Wrap(apperrors.CodeInternalError, err, "生成业务单号失败")
|
||||
}
|
||||
return fmt.Sprintf("%s%s%06d", prefix, timestamp, random.Int64()), nil
|
||||
}
|
||||
|
||||
func isUnknownPaymentResult(err error) bool {
|
||||
var appErr *apperrors.AppError
|
||||
return stderrors.As(err, &appErr) && appErr.Code == apperrors.CodeTimeout
|
||||
}
|
||||
48
internal/application/agentrecharge/online_payment.go
Normal file
48
internal/application/agentrecharge/online_payment.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
// OnlinePaymentStatePending 表示渠道仍在等待付款。
|
||||
OnlinePaymentStatePending = "pending"
|
||||
// OnlinePaymentStatePaid 表示渠道已确认收款。
|
||||
OnlinePaymentStatePaid = "paid"
|
||||
// OnlinePaymentStateClosed 表示渠道订单已明确关闭。
|
||||
OnlinePaymentStateClosed = "closed"
|
||||
// OnlinePaymentStateUnknown 表示渠道状态暂时无法确定。
|
||||
OnlinePaymentStateUnknown = "unknown"
|
||||
)
|
||||
|
||||
// OnlinePaymentRequest 描述扫码预下单所需的最小事实。
|
||||
type OnlinePaymentRequest struct {
|
||||
PaymentNo string
|
||||
Description string
|
||||
Amount int64
|
||||
ExpireAt time.Time
|
||||
Config *model.WechatConfig
|
||||
}
|
||||
|
||||
// OnlinePaymentResult 描述渠道返回的扫码付款内容。
|
||||
type OnlinePaymentResult struct {
|
||||
QRContent string
|
||||
}
|
||||
|
||||
// OnlinePaymentQueryResult 描述统一后的渠道支付状态。
|
||||
type OnlinePaymentQueryResult struct {
|
||||
State string
|
||||
ThirdPartyTradeNo string
|
||||
Amount int64
|
||||
PaidAt *time.Time
|
||||
}
|
||||
|
||||
// OnlinePaymentPort 定义代理扫码充值需要的最小渠道能力。
|
||||
type OnlinePaymentPort interface {
|
||||
Available(config *model.WechatConfig) bool
|
||||
PreCreate(ctx context.Context, request OnlinePaymentRequest) (OnlinePaymentResult, error)
|
||||
Query(ctx context.Context, paymentNo string, config *model.WechatConfig) (OnlinePaymentQueryResult, error)
|
||||
}
|
||||
189
internal/application/agentrecharge/recover_online_payment.go
Normal file
189
internal/application/agentrecharge/recover_online_payment.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// RecoverOnlinePaymentService 批量收敛长期待预下单或待支付的代理在线充值。
|
||||
type RecoverOnlinePaymentService struct {
|
||||
db *gorm.DB
|
||||
wechat OnlinePaymentPort
|
||||
alipay OnlinePaymentPort
|
||||
confirm *ConfirmOnlinePaymentService
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewRecoverOnlinePaymentService 创建代理在线充值支付恢复用例。
|
||||
func NewRecoverOnlinePaymentService(db *gorm.DB, wechat, alipay OnlinePaymentPort, confirm *ConfirmOnlinePaymentService) *RecoverOnlinePaymentService {
|
||||
return &RecoverOnlinePaymentService{db: db, wechat: wechat, alipay: alipay, confirm: confirm, now: time.Now}
|
||||
}
|
||||
|
||||
// ProcessBatch 按固定批次读取本地待处理事实并调用对应渠道收敛状态。
|
||||
func (s *RecoverOnlinePaymentService) ProcessBatch(ctx context.Context) (int, error) {
|
||||
if s == nil || s.db == nil || s.wechat == nil || s.alipay == nil || s.confirm == nil {
|
||||
return 0, errors.New(errors.CodeServiceUnavailable, "代理在线充值支付恢复能力未配置")
|
||||
}
|
||||
now := s.now().UTC()
|
||||
var payments []model.Payment
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("order_type = ? AND status = ? AND created_at <= ?", model.PaymentOrderTypeAgentRecharge, model.PaymentRecordStatusPending, now.Add(-constants.AgentRechargeRecoveryMinimumAge)).
|
||||
Order("created_at ASC, id ASC").Limit(constants.AgentRechargeRecoveryBatchSize).
|
||||
Find(&payments).Error; err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "扫描待恢复代理充值支付单失败")
|
||||
}
|
||||
if len(payments) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
recharges, configs, err := s.loadRecoveryFacts(ctx, payments)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
processed := 0
|
||||
var firstErr error
|
||||
for index := range payments {
|
||||
payment := &payments[index]
|
||||
recharge := recharges[payment.OrderID]
|
||||
config := recoveryConfig(payment, configs)
|
||||
if recharge == nil || config == nil {
|
||||
if firstErr == nil {
|
||||
firstErr = errors.New(errors.CodeConflict, "待恢复支付单缺少充值单或创建配置")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := s.recoverOne(ctx, payment, recharge, config, now); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
processed++
|
||||
}
|
||||
return processed, firstErr
|
||||
}
|
||||
|
||||
func (s *RecoverOnlinePaymentService) recoverOne(ctx context.Context, payment *model.Payment, recharge *model.AgentRechargeRecord, config *model.WechatConfig, now time.Time) error {
|
||||
adapter := s.adapter(payment.PaymentMethod)
|
||||
if adapter == nil {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "代理充值创建时支付配置不可用")
|
||||
}
|
||||
if payment.QRContent == "" {
|
||||
if !adapter.Available(config) {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "代理充值预下单配置不可用")
|
||||
}
|
||||
expireAt := now.Add(30 * time.Minute)
|
||||
if payment.ExpireAt != nil && payment.ExpireAt.After(now) {
|
||||
expireAt = *payment.ExpireAt
|
||||
}
|
||||
result, err := adapter.PreCreate(ctx, OnlinePaymentRequest{
|
||||
PaymentNo: payment.PaymentNo, Description: "代理主钱包充值", Amount: payment.Amount,
|
||||
ExpireAt: expireAt, Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
// 恢复阶段不能仅凭预下单错误推断未收款,保留本地状态等待下次查单。
|
||||
return nil
|
||||
}
|
||||
if result.QRContent == "" {
|
||||
return nil
|
||||
}
|
||||
update := s.db.WithContext(ctx).Model(&model.Payment{}).
|
||||
Where("id = ? AND status = ? AND qr_content = ''", payment.ID, model.PaymentRecordStatusPending).
|
||||
Update("qr_content", result.QRContent)
|
||||
if update.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, update.Error, "恢复代理充值扫码付款内容失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
queryResult, err := adapter.Query(ctx, payment.PaymentNo, config)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
switch queryResult.State {
|
||||
case OnlinePaymentStatePaid:
|
||||
if queryResult.PaidAt == nil {
|
||||
return errors.New(errors.CodeConflict, "支付渠道成功结果缺少支付时间")
|
||||
}
|
||||
_, err = s.confirm.Execute(ctx, ConfirmOnlinePaymentCommand{
|
||||
PaymentNo: payment.PaymentNo, PaymentMethod: payment.PaymentMethod, ConfigID: config.ID,
|
||||
MerchantIdentity: paymentMerchantIdentity(payment.PaymentMethod, config),
|
||||
ThirdPartyTradeNo: queryResult.ThirdPartyTradeNo, Amount: queryResult.Amount, PaidAt: *queryResult.PaidAt,
|
||||
CorrelationID: payment.PaymentNo,
|
||||
})
|
||||
return err
|
||||
case OnlinePaymentStateClosed:
|
||||
return s.closePending(ctx, payment.ID, recharge.ID)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RecoverOnlinePaymentService) loadRecoveryFacts(ctx context.Context, payments []model.Payment) (map[uint]*model.AgentRechargeRecord, map[uint]*model.WechatConfig, error) {
|
||||
rechargeIDs := make([]uint, 0, len(payments))
|
||||
configIDs := make([]uint, 0, len(payments))
|
||||
for index := range payments {
|
||||
rechargeIDs = append(rechargeIDs, payments[index].OrderID)
|
||||
if payments[index].PaymentConfigID != nil {
|
||||
configIDs = append(configIDs, *payments[index].PaymentConfigID)
|
||||
}
|
||||
}
|
||||
var rechargeRows []model.AgentRechargeRecord
|
||||
if err := s.db.WithContext(ctx).Where("id IN ?", rechargeIDs).Find(&rechargeRows).Error; err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询待恢复代理充值单失败")
|
||||
}
|
||||
var configRows []model.WechatConfig
|
||||
if len(configIDs) > 0 {
|
||||
if err := s.db.WithContext(ctx).Where("id IN ?", configIDs).Find(&configRows).Error; err != nil {
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询代理充值创建配置失败")
|
||||
}
|
||||
}
|
||||
recharges := make(map[uint]*model.AgentRechargeRecord, len(rechargeRows))
|
||||
for index := range rechargeRows {
|
||||
recharges[rechargeRows[index].ID] = &rechargeRows[index]
|
||||
}
|
||||
configs := make(map[uint]*model.WechatConfig, len(configRows))
|
||||
for index := range configRows {
|
||||
configs[configRows[index].ID] = &configRows[index]
|
||||
}
|
||||
return recharges, configs, nil
|
||||
}
|
||||
|
||||
func recoveryConfig(payment *model.Payment, configs map[uint]*model.WechatConfig) *model.WechatConfig {
|
||||
if payment.PaymentConfigID == nil {
|
||||
return nil
|
||||
}
|
||||
return configs[*payment.PaymentConfigID]
|
||||
}
|
||||
|
||||
func (s *RecoverOnlinePaymentService) closePending(ctx context.Context, paymentID, rechargeID uint) error {
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
paymentUpdate := tx.Model(&model.Payment{}).
|
||||
Where("id = ? AND status = ?", paymentID, model.PaymentRecordStatusPending).
|
||||
Update("status", model.PaymentRecordStatusFailed)
|
||||
if paymentUpdate.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, paymentUpdate.Error, "关闭失效代理充值支付单失败")
|
||||
}
|
||||
rechargeUpdate := tx.Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", rechargeID, constants.RechargeStatusPending).
|
||||
Update("status", constants.RechargeStatusClosed)
|
||||
if rechargeUpdate.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, rechargeUpdate.Error, "关闭失效代理充值单失败")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RecoverOnlinePaymentService) adapter(paymentMethod string) OnlinePaymentPort {
|
||||
if paymentMethod == constants.RechargeMethodWechat {
|
||||
return s.wechat
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodAlipay {
|
||||
return s.alipay
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
systemConfigInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/systemconfig"
|
||||
wecomInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/wecom"
|
||||
pollingPkg "github.com/break/junhong_cmp_fiber/internal/polling"
|
||||
agentRechargeQuery "github.com/break/junhong_cmp_fiber/internal/query/agentrecharge"
|
||||
assetQuery "github.com/break/junhong_cmp_fiber/internal/query/asset"
|
||||
exchangeQuery "github.com/break/junhong_cmp_fiber/internal/query/exchange"
|
||||
notificationQuery "github.com/break/junhong_cmp_fiber/internal/query/notification"
|
||||
@@ -215,7 +216,11 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
ShopSeriesGrant: admin.NewShopSeriesGrantHandler(svc.ShopSeriesGrant),
|
||||
AdminOrder: admin.NewOrderHandler(svc.Order, validate),
|
||||
AdminExchange: admin.NewExchangeHandler(svc.Exchange, exchangeQuery.NewListQuery(deps.DB), validate),
|
||||
PaymentCallback: callback.NewPaymentHandler(svc.Order, svc.Recharge, rechargeOrderService, svc.AgentRecharge, deps.WechatPayment, svc.WechatConfig, paymentStore, deps.Logger),
|
||||
PaymentCallback: callback.NewPaymentHandler(
|
||||
svc.Order, svc.Recharge, rechargeOrderService, svc.AgentRecharge,
|
||||
deps.WechatPayment, svc.WechatConfig, paymentStore,
|
||||
svc.AgentRechargePaymentConfirm, integrationlog.NewRepository(deps.DB), deps.Logger,
|
||||
),
|
||||
CTCCRealnameCallback: callback.NewCTCCRealnameHandler(
|
||||
carriercallback.NewCTCCRealnameTranslator(), carriercallback.NewCTCCCardResolver(deps.DB),
|
||||
integrationlog.NewRepository(deps.DB), svc.CardObservation, svc.CardObservationSeries, systemConfigReader, deps.Logger,
|
||||
@@ -261,8 +266,13 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
h.SetAssetService(svc.Asset)
|
||||
return h
|
||||
}(),
|
||||
WechatConfig: admin.NewWechatConfigHandler(svc.WechatConfig),
|
||||
AgentRecharge: admin.NewAgentRechargeHandler(svc.AgentRecharge, validate),
|
||||
WechatConfig: admin.NewWechatConfigHandler(svc.WechatConfig),
|
||||
AgentRecharge: func() *admin.AgentRechargeHandler {
|
||||
handler := admin.NewAgentRechargeHandler(svc.AgentRecharge, validate)
|
||||
handler.SetOnlineCreationService(svc.AgentRechargeOnline)
|
||||
handler.SetPaymentStatusQuery(agentRechargeQuery.NewPaymentStatusQuery(deps.DB))
|
||||
return handler
|
||||
}(),
|
||||
Refund: admin.NewRefundHandler(svc.Refund),
|
||||
OrderPackageInvalidate: admin.NewOrderPackageInvalidateHandler(svc.OrderPackageInvalidate),
|
||||
AssetPackageBatchOrder: admin.NewAssetPackageBatchOrderHandler(svc.AssetPackageBatchOrder, validate),
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
exchangeInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/exchange"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
||||
paymentInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/payment"
|
||||
walletinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/wallet"
|
||||
wecomInfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/wecom"
|
||||
"github.com/break/junhong_cmp_fiber/internal/polling"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/payment"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
|
||||
assetSvc "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||||
assetPackageBatchOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_package_batch_order"
|
||||
@@ -123,6 +125,8 @@ type services struct {
|
||||
StopResumeService *iotCardSvc.StopResumeService
|
||||
WechatConfig *wechatConfigSvc.Service
|
||||
AgentRecharge *agentRechargeSvc.Service
|
||||
AgentRechargeOnline *agentrechargeApp.OnlineCreationService
|
||||
AgentRechargePaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService
|
||||
PackageActivation *packageSvc.ActivationService
|
||||
Refund *refundSvc.Service
|
||||
TrafficQuery *trafficSvc.QueryService
|
||||
@@ -265,6 +269,16 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
)
|
||||
agentWalletPosting := walletapp.NewPostingService(walletinfra.NewCreditEventWriter(walletOutbox), nil)
|
||||
agentRechargeService.SetAgentWalletPostingService(agentWalletPosting)
|
||||
paymentIntegration := integrationlog.NewRepository(deps.DB)
|
||||
agentRechargeOnline := agentrechargeApp.NewOnlineCreationService(
|
||||
deps.DB,
|
||||
paymentInfra.NewWechatNativeAdapter(wechat.NewRedisCache(deps.Redis), paymentIntegration, deps.Logger),
|
||||
paymentInfra.NewAlipayPreCreateAdapter(paymentIntegration),
|
||||
)
|
||||
agentRechargePaymentConfirm := agentrechargeApp.NewConfirmOnlinePaymentService(
|
||||
deps.DB,
|
||||
paymentInfra.NewAgentRechargePaymentEventWriter(outbox.NewRepository()),
|
||||
)
|
||||
refundService := refundSvc.New(
|
||||
deps.DB,
|
||||
s.RefundRequest,
|
||||
@@ -361,51 +375,53 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
deps.Logger,
|
||||
),
|
||||
Enterprise: enterpriseSvc.New(deps.DB, s.Enterprise, s.Shop, s.Account),
|
||||
EnterpriseCard: enterpriseCardSvc.New(deps.DB, s.Enterprise, s.EnterpriseCardAuthorization, s.IotCard),
|
||||
EnterpriseDevice: enterpriseDeviceSvc.New(deps.DB, s.Enterprise, s.Device, s.DeviceSimBinding, s.EnterpriseDeviceAuthorization, s.EnterpriseCardAuthorization, deps.Logger),
|
||||
Authorization: enterpriseCardSvc.NewAuthorizationService(s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger),
|
||||
IotCard: iotCard,
|
||||
IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient, assetAudit),
|
||||
ExportTask: exportTaskSvc.New(deps.DB, s.ExportTask, deps.QueueClient, deps.StorageService),
|
||||
Device: device,
|
||||
DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient, assetAudit),
|
||||
AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account),
|
||||
Carrier: carrierSvc.New(s.Carrier),
|
||||
PackageSeries: packageSeriesSvc.New(s.PackageSeries, s.ShopSeriesAllocation, s.Package),
|
||||
Package: packageService,
|
||||
PackageDailyRecord: packageSvc.NewDailyRecordService(deps.DB, deps.Redis, s.PackageUsageDailyRecord, deps.Logger),
|
||||
PackageCustomerView: packageSvc.NewCustomerViewService(deps.DB, deps.Redis, s.PackageUsage, deps.Logger),
|
||||
ShopPackageBatchAllocation: shopPackageBatchAllocationSvc.New(deps.DB, s.Package, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.Shop, accountAudit),
|
||||
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
|
||||
ShopSeriesGrant: shopSeriesGrantSvc.New(deps.DB, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package, s.PackageSeries, deps.Logger),
|
||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
PurchaseValidation: purchaseValidation,
|
||||
Order: orderService,
|
||||
Exchange: exchangeService,
|
||||
Recharge: rechargeSvc.New(deps.DB, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger),
|
||||
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig, deps.Redis, deps.Logger),
|
||||
PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis),
|
||||
PollingMonitoring: pollingSvc.NewMonitoringServiceWithQueueMgr(deps.Redis, pollingQueueMgr, deps.Logger),
|
||||
PollingAlert: pollingSvc.NewAlertService(s.PollingAlertRule, s.PollingAlertHistory, deps.Redis, deps.Logger),
|
||||
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
|
||||
PollingManualTrigger: pollingSvc.NewManualTriggerService(s.PollingManualTriggerLog, s.IotCard, deps.Redis, deps.Logger),
|
||||
Asset: assetService,
|
||||
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device, assetAudit),
|
||||
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
||||
StopResumeService: stopResumeService,
|
||||
WechatConfig: wechatConfig,
|
||||
AgentRecharge: agentRechargeService,
|
||||
PackageActivation: packageActivation,
|
||||
TrafficQuery: trafficSvc.NewQueryService(deps.Redis, s.CardDailyUsage),
|
||||
OperationPassword: operationPassword,
|
||||
AgentOpenAPI: agentOpenAPI,
|
||||
Refund: refundService,
|
||||
CustomerBinding: customerBinding,
|
||||
OrderPackageInvalidate: orderPackageInvalidateSvc.New(s.OrderPackageInvalidateTask, deps.QueueClient),
|
||||
AssetPackageBatchOrder: assetPackageBatchOrderSvc.New(s.AssetPackageBatchOrderTask, s.Package, deps.QueueClient),
|
||||
ObservationSeries: observationSeries,
|
||||
CardObservation: cardObservationService,
|
||||
CardObservationSeries: cardObservationSeries,
|
||||
Enterprise: enterpriseSvc.New(deps.DB, s.Enterprise, s.Shop, s.Account),
|
||||
EnterpriseCard: enterpriseCardSvc.New(deps.DB, s.Enterprise, s.EnterpriseCardAuthorization, s.IotCard),
|
||||
EnterpriseDevice: enterpriseDeviceSvc.New(deps.DB, s.Enterprise, s.Device, s.DeviceSimBinding, s.EnterpriseDeviceAuthorization, s.EnterpriseCardAuthorization, deps.Logger),
|
||||
Authorization: enterpriseCardSvc.NewAuthorizationService(s.Enterprise, s.IotCard, s.EnterpriseCardAuthorization, deps.Logger),
|
||||
IotCard: iotCard,
|
||||
IotCardImport: iotCardImportSvc.New(deps.DB, s.IotCardImportTask, deps.QueueClient, assetAudit),
|
||||
ExportTask: exportTaskSvc.New(deps.DB, s.ExportTask, deps.QueueClient, deps.StorageService),
|
||||
Device: device,
|
||||
DeviceImport: deviceImportSvc.New(deps.DB, s.DeviceImportTask, deps.QueueClient, assetAudit),
|
||||
AssetAllocationRecord: assetAllocationRecordSvc.New(deps.DB, s.AssetAllocationRecord, s.Shop, s.Account),
|
||||
Carrier: carrierSvc.New(s.Carrier),
|
||||
PackageSeries: packageSeriesSvc.New(s.PackageSeries, s.ShopSeriesAllocation, s.Package),
|
||||
Package: packageService,
|
||||
PackageDailyRecord: packageSvc.NewDailyRecordService(deps.DB, deps.Redis, s.PackageUsageDailyRecord, deps.Logger),
|
||||
PackageCustomerView: packageSvc.NewCustomerViewService(deps.DB, deps.Redis, s.PackageUsage, deps.Logger),
|
||||
ShopPackageBatchAllocation: shopPackageBatchAllocationSvc.New(deps.DB, s.Package, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.Shop, accountAudit),
|
||||
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
|
||||
ShopSeriesGrant: shopSeriesGrantSvc.New(deps.DB, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package, s.PackageSeries, deps.Logger),
|
||||
CommissionStats: commissionStatsSvc.New(s.ShopSeriesCommissionStats),
|
||||
PurchaseValidation: purchaseValidation,
|
||||
Order: orderService,
|
||||
Exchange: exchangeService,
|
||||
Recharge: rechargeSvc.New(deps.DB, s.AssetWallet, s.AssetWalletTransaction, s.IotCard, s.Device, s.ShopSeriesAllocation, s.PackageSeries, s.CommissionRecord, wechatConfig, paymentLoader, deps.Logger),
|
||||
PollingConfig: pollingSvc.NewConfigService(s.PollingConfig, deps.Redis, deps.Logger),
|
||||
PollingConcurrency: pollingSvc.NewConcurrencyService(s.PollingConcurrencyConfig, deps.Redis),
|
||||
PollingMonitoring: pollingSvc.NewMonitoringServiceWithQueueMgr(deps.Redis, pollingQueueMgr, deps.Logger),
|
||||
PollingAlert: pollingSvc.NewAlertService(s.PollingAlertRule, s.PollingAlertHistory, deps.Redis, deps.Logger),
|
||||
PollingCleanup: pollingSvc.NewCleanupService(s.DataCleanupConfig, s.DataCleanupLog, deps.Logger),
|
||||
PollingManualTrigger: pollingSvc.NewManualTriggerService(s.PollingManualTriggerLog, s.IotCard, deps.Redis, deps.Logger),
|
||||
Asset: assetService,
|
||||
AssetLifecycle: assetSvc.NewLifecycleService(deps.DB, s.IotCard, s.Device, assetAudit),
|
||||
AssetWallet: assetWalletSvc.New(s.AssetWallet, s.AssetWalletTransaction),
|
||||
StopResumeService: stopResumeService,
|
||||
WechatConfig: wechatConfig,
|
||||
AgentRecharge: agentRechargeService,
|
||||
AgentRechargeOnline: agentRechargeOnline,
|
||||
AgentRechargePaymentConfirm: agentRechargePaymentConfirm,
|
||||
PackageActivation: packageActivation,
|
||||
TrafficQuery: trafficSvc.NewQueryService(deps.Redis, s.CardDailyUsage),
|
||||
OperationPassword: operationPassword,
|
||||
AgentOpenAPI: agentOpenAPI,
|
||||
Refund: refundService,
|
||||
CustomerBinding: customerBinding,
|
||||
OrderPackageInvalidate: orderPackageInvalidateSvc.New(s.OrderPackageInvalidateTask, deps.QueueClient),
|
||||
AssetPackageBatchOrder: assetPackageBatchOrderSvc.New(s.AssetPackageBatchOrderTask, s.Package, deps.QueueClient),
|
||||
ObservationSeries: observationSeries,
|
||||
CardObservation: cardObservationService,
|
||||
CardObservationSeries: cardObservationSeries,
|
||||
}
|
||||
}
|
||||
|
||||
30
internal/domain/agentrecharge/online.go
Normal file
30
internal/domain/agentrecharge/online.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Package agentrecharge 定义代理在线充值的纯业务规则。
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// ValidateOnlineCreation 校验代理在线充值的角色、金额和支付方式不变量。
|
||||
func ValidateOnlineCreation(userType int, amount int64, paymentMethod string) error {
|
||||
if userType != constants.UserTypeAgent {
|
||||
return errors.New(errors.CodeForbidden, "仅代理账号可以创建在线扫码充值")
|
||||
}
|
||||
if amount < constants.AgentOnlineRechargeMinAmount || amount > constants.AgentRechargeMaxAmount {
|
||||
return errors.New(errors.CodeInvalidParam, "在线充值金额必须在100元至100万元之间")
|
||||
}
|
||||
switch strings.TrimSpace(paymentMethod) {
|
||||
case constants.RechargeMethodWechat, constants.RechargeMethodAlipay:
|
||||
return nil
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidParam, "在线充值支付方式无效")
|
||||
}
|
||||
}
|
||||
|
||||
// CanCloseAfterPreCreateFailure 判断预下单明确失败后能否关闭充值单。
|
||||
func CanCloseAfterPreCreateFailure(status int) bool {
|
||||
return status == constants.RechargeStatusPending
|
||||
}
|
||||
82
internal/domain/agentrecharge/payment_confirmation.go
Normal file
82
internal/domain/agentrecharge/payment_confirmation.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// PaymentState 是支付单在确认用例中的领域状态。
|
||||
type PaymentState int
|
||||
|
||||
const (
|
||||
// PaymentStatePending 表示支付单仍待确认。
|
||||
PaymentStatePending PaymentState = iota
|
||||
// PaymentStatePaid 表示支付单已确认收款。
|
||||
PaymentStatePaid
|
||||
// PaymentStateFailed 表示支付单曾被明确关闭。
|
||||
PaymentStateFailed
|
||||
// PaymentStateRefunded 表示支付单已经退款。
|
||||
PaymentStateRefunded
|
||||
)
|
||||
|
||||
// PaymentConfirmationFacts 是支付确认所需的渠道与本地权威事实。
|
||||
type PaymentConfirmationFacts struct {
|
||||
OrderType string
|
||||
ExpectedOrderType string
|
||||
PaymentMethod string
|
||||
RechargePaymentMethod string
|
||||
RechargePaymentChannel string
|
||||
PaymentConfigID uint
|
||||
RechargePaymentConfigID uint
|
||||
ConfirmedConfigID uint
|
||||
MerchantIdentity string
|
||||
ConfirmedMerchantIdentity string
|
||||
PaymentAmount int64
|
||||
RechargeAmount int64
|
||||
ConfirmedAmount int64
|
||||
PaymentOrderID uint
|
||||
RechargeID uint
|
||||
PaymentState PaymentState
|
||||
RechargeStatus int
|
||||
StoredTradeNo string
|
||||
ConfirmedTradeNo string
|
||||
}
|
||||
|
||||
// ValidatePaymentConfirmation 校验支付确认不变量,并返回是否属于完全一致的重复确认。
|
||||
func ValidatePaymentConfirmation(facts PaymentConfirmationFacts) (bool, error) {
|
||||
if facts.ExpectedOrderType == "" || facts.OrderType != facts.ExpectedOrderType ||
|
||||
facts.RechargeID == 0 || facts.PaymentOrderID != facts.RechargeID {
|
||||
return false, errors.New(errors.CodeConflict, "支付单与代理充值单关联不一致")
|
||||
}
|
||||
method := strings.TrimSpace(facts.PaymentMethod)
|
||||
if method == "" || method != strings.TrimSpace(facts.RechargePaymentMethod) ||
|
||||
method != strings.TrimSpace(facts.RechargePaymentChannel) {
|
||||
return false, errors.New(errors.CodeConflict, "支付渠道与代理充值单不一致")
|
||||
}
|
||||
identity := strings.TrimSpace(facts.MerchantIdentity)
|
||||
if facts.PaymentConfigID == 0 || facts.PaymentConfigID != facts.RechargePaymentConfigID ||
|
||||
facts.PaymentConfigID != facts.ConfirmedConfigID || identity == "" ||
|
||||
identity != strings.TrimSpace(facts.ConfirmedMerchantIdentity) {
|
||||
return false, errors.New(errors.CodeConflict, "支付配置身份与创建记录不一致")
|
||||
}
|
||||
tradeNo := strings.TrimSpace(facts.ConfirmedTradeNo)
|
||||
if tradeNo == "" || facts.ConfirmedAmount <= 0 || facts.PaymentAmount != facts.RechargeAmount ||
|
||||
facts.PaymentAmount != facts.ConfirmedAmount {
|
||||
return false, errors.New(errors.CodeConflict, "支付金额或第三方交易号无效")
|
||||
}
|
||||
if facts.PaymentState == PaymentStatePaid {
|
||||
if strings.TrimSpace(facts.StoredTradeNo) == tradeNo &&
|
||||
(facts.RechargeStatus == constants.RechargeStatusPaid || facts.RechargeStatus == constants.RechargeStatusCompleted) {
|
||||
return true, nil
|
||||
}
|
||||
return false, errors.New(errors.CodeConflict, "支付单已存在不一致的确认事实")
|
||||
}
|
||||
validPending := facts.PaymentState == PaymentStatePending && facts.RechargeStatus == constants.RechargeStatusPending
|
||||
validLateSuccess := facts.PaymentState == PaymentStateFailed && facts.RechargeStatus == constants.RechargeStatusClosed
|
||||
if !validPending && !validLateSuccess {
|
||||
return false, errors.New(errors.CodeInvalidStatus, "代理充值单当前状态不可确认支付")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -1,23 +1,42 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
agentrechargeapp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
agentrechargequery "github.com/break/junhong_cmp_fiber/internal/query/agentrecharge"
|
||||
agentRechargeSvc "github.com/break/junhong_cmp_fiber/internal/service/agent_recharge"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// AgentRechargeHandler 代理预充值 Handler
|
||||
type AgentRechargeHandler struct {
|
||||
service *agentRechargeSvc.Service
|
||||
online *agentrechargeapp.OnlineCreationService
|
||||
status *agentrechargequery.PaymentStatusQuery
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// SetOnlineCreationService 注入代理在线扫码充值用例。
|
||||
func (h *AgentRechargeHandler) SetOnlineCreationService(service *agentrechargeapp.OnlineCreationService) {
|
||||
h.online = service
|
||||
}
|
||||
|
||||
// SetPaymentStatusQuery 注入代理充值本地支付状态 Query。
|
||||
func (h *AgentRechargeHandler) SetPaymentStatusQuery(query *agentrechargequery.PaymentStatusQuery) {
|
||||
h.status = query
|
||||
}
|
||||
|
||||
// NewAgentRechargeHandler 创建代理预充值 Handler
|
||||
func NewAgentRechargeHandler(service *agentRechargeSvc.Service, validator *validator.Validate) *AgentRechargeHandler {
|
||||
return &AgentRechargeHandler{service: service, validator: validator}
|
||||
@@ -27,13 +46,21 @@ func NewAgentRechargeHandler(service *agentRechargeSvc.Service, validator *valid
|
||||
// POST /api/admin/agent-recharges
|
||||
func (h *AgentRechargeHandler) Create(c *fiber.Ctx) error {
|
||||
var req dto.CreateAgentRechargeRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
decoder := sonic.ConfigStd.NewDecoder(bytes.NewReader(c.Body()))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
if req.PaymentMethod == constants.RechargeMethodWechat || req.PaymentMethod == constants.RechargeMethodAlipay {
|
||||
return h.createOnline(c, req)
|
||||
}
|
||||
if strings.TrimSpace(req.RequestID) != "" {
|
||||
return errors.New(errors.CodeInvalidParam, "线下充值不能传入在线请求标识")
|
||||
}
|
||||
result, err := h.service.Create(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -42,6 +69,45 @@ func (h *AgentRechargeHandler) Create(c *fiber.Ctx) error {
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
func (h *AgentRechargeHandler) createOnline(c *fiber.Ctx, req dto.CreateAgentRechargeRequest) error {
|
||||
if h.online == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "代理在线充值能力未配置")
|
||||
}
|
||||
if req.ShopID != nil || len(req.PaymentVoucherKey) > 0 || strings.TrimSpace(req.Remark) != "" {
|
||||
return errors.New(errors.CodeInvalidParam, "在线充值不能指定店铺、支付凭证或运营备注")
|
||||
}
|
||||
result, err := h.online.Execute(c.UserContext(), agentrechargeapp.CreateOnlineCommand{
|
||||
AccountID: middleware.GetUserIDFromContext(c.UserContext()), UserType: middleware.GetUserTypeFromContext(c.UserContext()),
|
||||
CurrentShopID: middleware.GetShopIDFromContext(c.UserContext()), Amount: req.Amount,
|
||||
PaymentMethod: req.PaymentMethod, RequestID: req.RequestID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rechargeSource, rechargeSourceName := constants.GetAgentRechargeSource(result.Payment.PaymentMethod)
|
||||
return response.Success(c, &dto.AgentRechargeOnlineResponse{
|
||||
RechargeID: result.Recharge.ID, RechargeNo: result.Recharge.RechargeNo, PaymentNo: result.Payment.PaymentNo,
|
||||
PaymentMethod: result.Payment.PaymentMethod, Amount: result.Payment.Amount, QRContent: result.Payment.QRContent,
|
||||
RechargeSource: rechargeSource, RechargeSourceName: rechargeSourceName,
|
||||
Status: result.Recharge.Status, StatusName: constants.GetRechargeStatusName(result.Recharge.Status),
|
||||
})
|
||||
}
|
||||
|
||||
// PaymentMethods 查询代理在线充值可用支付方式。
|
||||
// GET /api/admin/agent-recharges/payment-methods
|
||||
func (h *AgentRechargeHandler) PaymentMethods(c *fiber.Ctx) error {
|
||||
if h.online == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "代理在线充值能力未配置")
|
||||
}
|
||||
result, err := h.online.AvailablePaymentMethods(c.UserContext(), middleware.GetUserTypeFromContext(c.UserContext()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, &dto.AgentRechargePaymentMethodsResponse{
|
||||
Methods: result.Methods, MinAmount: result.MinAmount, MaxAmount: result.MaxAmount,
|
||||
})
|
||||
}
|
||||
|
||||
// List 查询代理充值订单列表
|
||||
// GET /api/admin/agent-recharges
|
||||
func (h *AgentRechargeHandler) List(c *fiber.Ctx) error {
|
||||
@@ -49,6 +115,9 @@ func (h *AgentRechargeHandler) List(c *fiber.Ctx) error {
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
list, total, err := h.service.List(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
@@ -74,6 +143,23 @@ func (h *AgentRechargeHandler) Get(c *fiber.Ctx) error {
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// PaymentStatus 查询代理充值本地支付与到账状态。
|
||||
// GET /api/admin/agent-recharges/:id/payment-status
|
||||
func (h *AgentRechargeHandler) PaymentStatus(c *fiber.Ctx) error {
|
||||
if h.status == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "代理充值支付状态查询未配置")
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的充值记录ID")
|
||||
}
|
||||
result, err := h.status.Get(c.UserContext(), uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return response.Success(c, result)
|
||||
}
|
||||
|
||||
// Reject 驳回代理充值订单
|
||||
// POST /api/admin/agent-recharges/:id/reject
|
||||
func (h *AgentRechargeHandler) Reject(c *fiber.Ctx) error {
|
||||
|
||||
@@ -2,16 +2,18 @@ package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp/fasthttpadaptor"
|
||||
"go.uber.org/zap"
|
||||
|
||||
agentrechargeApp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
orderService "github.com/break/junhong_cmp_fiber/internal/service/order"
|
||||
rechargeService "github.com/break/junhong_cmp_fiber/internal/service/recharge"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
|
||||
pkgmiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -44,6 +47,8 @@ type PaymentHandler struct {
|
||||
wechatPayment wechat.PaymentServiceInterface
|
||||
wechatConfigService WechatConfigServiceInterface
|
||||
paymentStore *postgres.PaymentStore
|
||||
agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService
|
||||
integration *integrationlog.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -55,6 +60,8 @@ func NewPaymentHandler(
|
||||
wechatPayment wechat.PaymentServiceInterface,
|
||||
wechatConfigService WechatConfigServiceInterface,
|
||||
paymentStore *postgres.PaymentStore,
|
||||
agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService,
|
||||
integration *integrationlog.Repository,
|
||||
logger *zap.Logger,
|
||||
) *PaymentHandler {
|
||||
return &PaymentHandler{
|
||||
@@ -65,15 +72,30 @@ func NewPaymentHandler(
|
||||
wechatPayment: wechatPayment,
|
||||
wechatConfigService: wechatConfigService,
|
||||
paymentStore: paymentStore,
|
||||
agentPaymentConfirm: agentPaymentConfirm,
|
||||
integration: integration,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type verifiedPaymentCallback struct {
|
||||
PaymentNo string
|
||||
PaymentMethod string
|
||||
TransactionID string
|
||||
Amount int64
|
||||
ConfigID uint
|
||||
MerchantIdentity string
|
||||
PaidAt time.Time
|
||||
Provider string
|
||||
RawPayload []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
// WechatPayCallback 微信支付回调(带签名验证)
|
||||
// POST /api/callback/wechat-pay
|
||||
func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
body := c.Body()
|
||||
ctx := context.Background()
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置
|
||||
orderNo, err := wechat.PeekOrderNo(body)
|
||||
@@ -103,7 +125,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
}
|
||||
// TotalFee 为字符串格式的分,解析失败则降级为 0(后续 handlePaymentCallback 会记录日志)
|
||||
totalFee, _ := strconv.ParseInt(result.TotalFee, 10, 64)
|
||||
if err := h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, totalFee); err != nil {
|
||||
paidAt, _ := time.ParseInLocation("20060102150405", result.SuccessTime, time.Local)
|
||||
if err := h.dispatchWechatCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat,
|
||||
TransactionID: result.TransactionID, Amount: totalFee, ConfigID: cfg.ID,
|
||||
MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay,
|
||||
RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
|
||||
}
|
||||
|
||||
@@ -117,7 +145,13 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
if result.TradeState != "SUCCESS" {
|
||||
return nil
|
||||
}
|
||||
return h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, result.TotalAmount)
|
||||
paidAt, _ := time.Parse(time.RFC3339, result.SuccessTime)
|
||||
return h.dispatchWechatCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat,
|
||||
TransactionID: result.TransactionID, Amount: result.TotalAmount, ConfigID: cfg.ID,
|
||||
MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay,
|
||||
RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败")
|
||||
@@ -130,20 +164,22 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
return h.wechatV2SuccessResponse(c)
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paymentNo, paymentMethod, transactionID string, paidAmount int64) (bool, error) {
|
||||
func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, callback verifiedPaymentCallback) (bool, error) {
|
||||
if h.paymentStore != nil {
|
||||
payment, err := h.paymentStore.GetByPaymentNo(ctx, paymentNo)
|
||||
payment, err := h.paymentStore.GetByPaymentNo(ctx, callback.PaymentNo)
|
||||
if err == nil {
|
||||
switch payment.OrderType {
|
||||
case model.PaymentOrderTypePackage:
|
||||
return true, h.orderService.HandlePaymentRecordCallback(ctx, paymentNo, paymentMethod, transactionID, paidAmount)
|
||||
return true, h.orderService.HandlePaymentRecordCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount)
|
||||
case model.PaymentOrderTypeRecharge:
|
||||
if h.rechargeOrderService != nil {
|
||||
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, paymentNo, paymentMethod, transactionID)
|
||||
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID)
|
||||
}
|
||||
return true, fmt.Errorf("充值订单服务未配置,无法处理支付单: %s", paymentNo)
|
||||
return true, errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
case model.PaymentOrderTypeAgentRecharge:
|
||||
return true, h.confirmAgentRechargePayment(ctx, callback)
|
||||
default:
|
||||
return true, fmt.Errorf("未知支付记录类型: %s", payment.OrderType)
|
||||
return true, errors.New(errors.CodeInvalidStatus, "未知支付记录类型")
|
||||
}
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
@@ -154,27 +190,83 @@ func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, paym
|
||||
}
|
||||
|
||||
// dispatchWechatCallback 优先按支付单分发,旧单号继续按前缀兼容处理。
|
||||
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error {
|
||||
handled, err := h.dispatchPaymentRecordCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount)
|
||||
func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, callback verifiedPaymentCallback) error {
|
||||
handled, err := h.dispatchPaymentRecordCallback(ctx, callback)
|
||||
if handled || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(outTradeNo, "ORD"):
|
||||
return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, paidAmount)
|
||||
case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix):
|
||||
case strings.HasPrefix(callback.PaymentNo, "ORD"):
|
||||
return h.orderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.Amount)
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AssetRechargeOrderPrefix):
|
||||
if h.rechargeOrderService != nil {
|
||||
return h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByWechat, transactionID)
|
||||
return h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentByWechat, callback.TransactionID)
|
||||
}
|
||||
return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", outTradeNo)
|
||||
case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix):
|
||||
return errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AgentRechargeOrderPrefix):
|
||||
if h.agentRechargeService != nil {
|
||||
return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount)
|
||||
return h.agentRechargeService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.TransactionID, callback.Amount)
|
||||
}
|
||||
return fmt.Errorf("代理充值服务未配置,无法处理订单: %s", outTradeNo)
|
||||
return errors.New(errors.CodeInternalError, "代理充值服务未配置")
|
||||
default:
|
||||
return fmt.Errorf("未知订单号前缀: %s", outTradeNo)
|
||||
return errors.New(errors.CodeInvalidStatus, "未知订单号前缀")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callback verifiedPaymentCallback) error {
|
||||
if h.agentPaymentConfirm == nil || h.integration == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理充值支付回调能力未配置")
|
||||
}
|
||||
resourceID, correlationID := callback.PaymentNo, callback.PaymentNo
|
||||
idempotencyKey := callback.TransactionID
|
||||
if idempotencyKey == "" {
|
||||
idempotencyKey = callback.PaymentNo
|
||||
}
|
||||
log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IdempotencyKey: idempotencyKey, Provider: callback.Provider,
|
||||
Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID,
|
||||
ResourceType: constants.IntegrationResourceTypeAgentRechargePayment, ResourceID: &resourceID,
|
||||
RawPayload: callback.RawPayload, ContentType: callback.ContentType,
|
||||
RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, confirmErr := h.agentPaymentConfirm.Execute(ctx, agentrechargeApp.ConfirmOnlinePaymentCommand{
|
||||
PaymentNo: callback.PaymentNo, PaymentMethod: callback.PaymentMethod, ConfigID: callback.ConfigID,
|
||||
MerchantIdentity: callback.MerchantIdentity, ThirdPartyTradeNo: callback.TransactionID,
|
||||
Amount: callback.Amount, PaidAt: callback.PaidAt, CorrelationID: correlationID,
|
||||
})
|
||||
if confirmErr != nil {
|
||||
h.completePaymentCallbackLog(ctx, log, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, ProviderMessage: confirmErr.Error(),
|
||||
ResponseSummary: map[string]any{"confirmed": false},
|
||||
})
|
||||
return confirmErr
|
||||
}
|
||||
if log.Result == constants.IntegrationResultPending {
|
||||
_, err = h.integration.Complete(ctx, log.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, ProviderCode: "SUCCESS",
|
||||
ResponseSummary: map[string]any{"confirmed": true, "already_confirmed": result.AlreadyConfirmed},
|
||||
StateChanged: !result.AlreadyConfirmed,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) completePaymentCallbackLog(ctx context.Context, log *model.IntegrationLog, completion integrationlog.Completion) {
|
||||
if log == nil || log.Result != constants.IntegrationResultPending {
|
||||
return
|
||||
}
|
||||
if _, err := h.integration.Complete(ctx, log.IntegrationID, completion); err != nil {
|
||||
h.logger.Error("支付回调 Integration Log 终结失败",
|
||||
zap.String("integration_id", log.IntegrationID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,14 +397,16 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "支付金额校验失败")
|
||||
}
|
||||
|
||||
// 写入支付宝交易流水号
|
||||
if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil {
|
||||
h.logger.Error("支付宝回调:写入 third_party_trade_no 失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不中断业务,继续分发(幂等业务层会处理状态)
|
||||
// 新代理充值由统一确认事务原子写入交易号,旧业务保持原处理方式。
|
||||
if payment.OrderType != model.PaymentOrderTypeAgentRecharge {
|
||||
if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil {
|
||||
h.logger.Error("支付宝回调:写入 third_party_trade_no 失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不中断存量业务,继续由原幂等业务层处理状态。
|
||||
}
|
||||
}
|
||||
|
||||
// 按支付单 order_type 分发业务
|
||||
@@ -353,6 +447,28 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
zap.String("order_type", payment.OrderType),
|
||||
)
|
||||
|
||||
case model.PaymentOrderTypeAgentRecharge:
|
||||
paidAt, parseErr := time.ParseInLocation("2006-01-02 15:04:05", notification.GmtPayment, time.Local)
|
||||
if parseErr != nil {
|
||||
h.logger.Error("支付宝回调:付款时间格式无效",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(parseErr),
|
||||
)
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "支付宝付款时间格式错误")
|
||||
}
|
||||
if err := h.confirmAgentRechargePayment(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: outTradeNo, PaymentMethod: model.PaymentByAlipay,
|
||||
TransactionID: notification.TradeNo, Amount: notifyAmountFen, ConfigID: cfg.ID,
|
||||
MerchantIdentity: notification.AppId, PaidAt: paidAt, Provider: constants.IntegrationProviderAlipay,
|
||||
RawPayload: c.Body(), ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
h.logger.Error("支付宝回调:确认代理充值支付失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝代理充值回调失败")
|
||||
}
|
||||
|
||||
default:
|
||||
h.logger.Error("支付宝回调:未知支付记录类型",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
@@ -452,7 +568,11 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
|
||||
orderNo := notify.MchntOrderNo
|
||||
// OrderAmt 为字符串格式的分,解析失败则降级为 0
|
||||
orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64)
|
||||
if handled, err := h.dispatchPaymentRecordCallback(ctx, orderNo, "fuiou", notify.TransactionId, orderAmt); err != nil {
|
||||
if handled, err := h.dispatchPaymentRecordCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: orderNo, PaymentMethod: "fuiou", TransactionID: notify.TransactionId, Amount: orderAmt,
|
||||
ConfigID: cfg.ID, MerchantIdentity: cfg.FyMchntCd, Provider: model.ProviderTypeFuiou,
|
||||
RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
|
||||
} else if handled {
|
||||
return c.Send(fuiou.BuildNotifySuccessResponse())
|
||||
|
||||
110
internal/infrastructure/payment/agent_recharge_consumer.go
Normal file
110
internal/infrastructure/payment/agent_recharge_consumer.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// AgentRechargePaymentConsumer 将已确认收款的代理在线充值幂等入账主钱包。
|
||||
type AgentRechargePaymentConsumer struct {
|
||||
db *gorm.DB
|
||||
posting *walletapp.PostingService
|
||||
}
|
||||
|
||||
// NewAgentRechargePaymentConsumer 创建代理在线充值入账消费者。
|
||||
func NewAgentRechargePaymentConsumer(db *gorm.DB, posting *walletapp.PostingService) *AgentRechargePaymentConsumer {
|
||||
return &AgentRechargePaymentConsumer{db: db, posting: posting}
|
||||
}
|
||||
|
||||
// Consume 校验支付与充值权威事实后,在独立事务中完成唯一入账和充值终态。
|
||||
func (c *AgentRechargePaymentConsumer) Consume(ctx context.Context, envelope outbox.DeliveryEnvelope) error {
|
||||
if c == nil || c.db == nil || c.posting == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理在线充值入账消费者未配置")
|
||||
}
|
||||
if envelope.EventType != constants.OutboxEventTypeAgentRechargePaymentConfirmed ||
|
||||
envelope.PayloadVersion != constants.AgentRechargePaymentConfirmedPayloadVersionV1 {
|
||||
return errors.New(errors.CodeInvalidParam, "代理充值支付确认事件类型或版本不受支持")
|
||||
}
|
||||
var event agentrecharge.PaymentConfirmedEvent
|
||||
if err := sonic.Unmarshal(envelope.Payload, &event); err != nil {
|
||||
return errors.Wrap(errors.CodeInvalidParam, err, "代理充值支付确认事件载荷无法解析")
|
||||
}
|
||||
if event.EventID == "" || event.EventID != envelope.EventID || event.RechargeID == 0 || event.PaymentID == 0 ||
|
||||
event.ShopID == 0 || event.WalletID == 0 || event.Amount <= 0 || event.ThirdPartyTradeNo == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "代理充值支付确认事件载荷不完整")
|
||||
}
|
||||
return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
recharge, _, err := lockCreditingFacts(ctx, tx, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if recharge.Status != constants.RechargeStatusPaid && recharge.Status != constants.RechargeStatusCompleted {
|
||||
return errors.New(errors.CodeInvalidStatus, "代理在线充值当前状态不可入账")
|
||||
}
|
||||
if _, err := c.posting.PostInTx(ctx, tx, walletapp.PostingCommand{
|
||||
ShopID: recharge.ShopID, WalletID: recharge.AgentWalletID, Amount: recharge.Amount,
|
||||
ReferenceType: constants.ReferenceTypeTopup, ReferenceID: recharge.ID,
|
||||
TransactionType: constants.AgentTransactionTypeRecharge,
|
||||
UserID: recharge.UserID, Creator: recharge.UserID, Remark: "代理在线扫码充值",
|
||||
RequestID: envelope.RequestID, CorrelationID: envelope.CorrelationID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if recharge.Status == constants.RechargeStatusCompleted {
|
||||
return nil
|
||||
}
|
||||
completedAt := time.Now().UTC()
|
||||
update := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}).
|
||||
Where("id = ? AND status = ?", recharge.ID, constants.RechargeStatusPaid).
|
||||
Updates(map[string]any{"status": constants.RechargeStatusCompleted, "completed_at": completedAt})
|
||||
if update.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, update.Error, "完成代理在线充值单失败")
|
||||
}
|
||||
if update.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "代理在线充值状态已变化")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func lockCreditingFacts(ctx context.Context, tx *gorm.DB, event agentrecharge.PaymentConfirmedEvent) (*model.AgentRechargeRecord, *model.Payment, error) {
|
||||
var recharge model.AgentRechargeRecord
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", event.RechargeID).First(&recharge).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, errors.New(errors.CodeNotFound, "代理在线充值单不存在")
|
||||
}
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理在线充值单失败")
|
||||
}
|
||||
var payment model.Payment
|
||||
if err := tx.WithContext(ctx).Where("id = ?", event.PaymentID).First(&payment).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil, errors.New(errors.CodeConflict, "代理在线充值支付单不存在")
|
||||
}
|
||||
return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理在线充值支付单失败")
|
||||
}
|
||||
if recharge.ID != event.RechargeID || recharge.RechargeNo != event.RechargeNo || recharge.ShopID != event.ShopID ||
|
||||
recharge.AgentWalletID != event.WalletID || recharge.UserID != event.UserID || recharge.Amount != event.Amount ||
|
||||
recharge.PaymentTransactionID == nil || *recharge.PaymentTransactionID != event.ThirdPartyTradeNo ||
|
||||
(recharge.PaymentMethod != constants.RechargeMethodWechat && recharge.PaymentMethod != constants.RechargeMethodAlipay) {
|
||||
return nil, nil, errors.New(errors.CodeConflict, "代理充值事件与充值权威事实不一致")
|
||||
}
|
||||
if payment.OrderType != model.PaymentOrderTypeAgentRecharge || payment.OrderID != recharge.ID ||
|
||||
payment.PaymentNo != event.PaymentNo || payment.PaymentMethod != event.PaymentMethod || payment.Amount != event.Amount ||
|
||||
payment.Status != model.PaymentRecordStatusPaid || payment.ThirdPartyTradeNo != event.ThirdPartyTradeNo {
|
||||
return nil, nil, errors.New(errors.CodeConflict, "代理充值事件与支付权威事实不一致")
|
||||
}
|
||||
return &recharge, &payment, nil
|
||||
}
|
||||
|
||||
var _ outbox.EventConsumer = (*AgentRechargePaymentConsumer)(nil)
|
||||
38
internal/infrastructure/payment/agent_recharge_event.go
Normal file
38
internal/infrastructure/payment/agent_recharge_event.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/messaging/outbox"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// AgentRechargePaymentEventWriter 将支付确认事实写入公共 Outbox。
|
||||
type AgentRechargePaymentEventWriter struct {
|
||||
outbox *outbox.Repository
|
||||
}
|
||||
|
||||
// NewAgentRechargePaymentEventWriter 创建代理充值支付确认事件 Writer。
|
||||
func NewAgentRechargePaymentEventWriter(repository *outbox.Repository) *AgentRechargePaymentEventWriter {
|
||||
return &AgentRechargePaymentEventWriter{outbox: repository}
|
||||
}
|
||||
|
||||
// Append 在支付确认事务内追加稳定的代理充值入账事件。
|
||||
func (w *AgentRechargePaymentEventWriter) Append(ctx context.Context, tx *gorm.DB, event agentrecharge.PaymentConfirmedEvent) error {
|
||||
if w == nil || w.outbox == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理充值支付确认 Outbox Writer 未配置")
|
||||
}
|
||||
_, err := w.outbox.Append(ctx, tx, outbox.Envelope{
|
||||
EventID: event.EventID, EventType: constants.OutboxEventTypeAgentRechargePaymentConfirmed,
|
||||
PayloadVersion: constants.AgentRechargePaymentConfirmedPayloadVersionV1,
|
||||
AggregateType: "agent_recharge", AggregateID: strconv.FormatUint(uint64(event.RechargeID), 10),
|
||||
ResourceType: "payment", ResourceID: strconv.FormatUint(uint64(event.PaymentID), 10),
|
||||
BusinessKey: event.EventID, RequestID: event.RequestID, CorrelationID: event.CorrelationID, Payload: event,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
|
||||
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// AgentRechargeRecoveryTaskHandler 执行代理在线充值支付恢复任务。
|
||||
type AgentRechargeRecoveryTaskHandler struct {
|
||||
service *agentrecharge.RecoverOnlinePaymentService
|
||||
}
|
||||
|
||||
// NewAgentRechargeRecoveryTaskHandler 创建代理在线充值支付恢复任务 Handler。
|
||||
func NewAgentRechargeRecoveryTaskHandler(service *agentrecharge.RecoverOnlinePaymentService) *AgentRechargeRecoveryTaskHandler {
|
||||
return &AgentRechargeRecoveryTaskHandler{service: service}
|
||||
}
|
||||
|
||||
// Handle 扫描长期待支付记录并复用原支付单号收敛渠道状态。
|
||||
func (h *AgentRechargeRecoveryTaskHandler) Handle(ctx context.Context, _ *asynq.Task) error {
|
||||
if h == nil || h.service == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "代理在线充值支付恢复任务未配置")
|
||||
}
|
||||
_, err := h.service.ProcessBatch(ctx)
|
||||
return err
|
||||
}
|
||||
178
internal/infrastructure/payment/alipay_precreate.go
Normal file
178
internal/infrastructure/payment/alipay_precreate.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdkalipay "github.com/smartwalle/alipay/v3"
|
||||
|
||||
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
alipaypkg "github.com/break/junhong_cmp_fiber/pkg/alipay"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// AlipayPreCreateAdapter 使用现有 smartwalle/alipay 实现当面付预下单与查单。
|
||||
type AlipayPreCreateAdapter struct {
|
||||
integration *integrationlog.Repository
|
||||
}
|
||||
|
||||
// NewAlipayPreCreateAdapter 创建支付宝当面付适配器。
|
||||
func NewAlipayPreCreateAdapter(integration *integrationlog.Repository) *AlipayPreCreateAdapter {
|
||||
return &AlipayPreCreateAdapter{integration: integration}
|
||||
}
|
||||
|
||||
// Available 判断配置是否完整支持支付宝预下单、验签与查单。
|
||||
func (a *AlipayPreCreateAdapter) Available(config *model.WechatConfig) bool {
|
||||
return alipayConfigComplete(config, true)
|
||||
}
|
||||
|
||||
// PreCreate 创建支付宝当面付扫码订单。
|
||||
func (a *AlipayPreCreateAdapter) PreCreate(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentResult, error) {
|
||||
client, err := a.client(request.Config)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
attempt, err := a.startAttempt(ctx, request.PaymentNo, constants.IntegrationOperationPaymentPreCreate, request.Config.ID, request.Amount)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
startedAt := time.Now()
|
||||
response, callErr := client.TradePreCreate(ctx, sdkalipay.TradePreCreate{Trade: sdkalipay.Trade{
|
||||
NotifyURL: request.Config.AliNotifyURL, Subject: request.Description, OutTradeNo: request.PaymentNo,
|
||||
TotalAmount: alipaypkg.FenToYuan(request.Amount), ProductCode: "FACE_TO_FACE_PAYMENT",
|
||||
TimeExpire: request.ExpireAt.Format("2006-01-02 15:04:05"),
|
||||
}})
|
||||
if callErr != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
|
||||
}
|
||||
if response == nil || response.IsFailure() || strings.TrimSpace(response.QRCode) == "" {
|
||||
providerCode, providerMessage := "empty_qr_code", "支付宝预下单未返回付款内容"
|
||||
responseSummary := map[string]any{"success": false}
|
||||
if response != nil && response.IsFailure() {
|
||||
providerCode, providerMessage = string(response.Code), response.SubMsg
|
||||
if response.SubCode != "" {
|
||||
providerCode += ":" + response.SubCode
|
||||
responseSummary["sub_code"] = response.SubCode
|
||||
}
|
||||
}
|
||||
_, completeErr := a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, ProviderCode: providerCode, ProviderMessage: providerMessage,
|
||||
ResponseSummary: responseSummary, DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
})
|
||||
if completeErr != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, completeErr
|
||||
}
|
||||
return agentrecharge.OnlinePaymentResult{}, apperrors.New(apperrors.CodeServiceUnavailable, "支付宝预下单失败")
|
||||
}
|
||||
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, ProviderCode: string(response.Code),
|
||||
ResponseSummary: map[string]any{"success": true}, DurationMS: time.Since(startedAt).Milliseconds(), StateChanged: true,
|
||||
}); err != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
return agentrecharge.OnlinePaymentResult{QRContent: response.QRCode}, nil
|
||||
}
|
||||
|
||||
// Query 查询支付宝当面付订单状态。
|
||||
func (a *AlipayPreCreateAdapter) Query(ctx context.Context, paymentNo string, config *model.WechatConfig) (agentrecharge.OnlinePaymentQueryResult, error) {
|
||||
client, err := a.queryClient(config)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, err
|
||||
}
|
||||
attempt, err := a.startAttempt(ctx, paymentNo, constants.IntegrationOperationPaymentQuery, config.ID, 0)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, err
|
||||
}
|
||||
startedAt := time.Now()
|
||||
response, callErr := client.TradeQuery(ctx, sdkalipay.TradeQuery{OutTradeNo: paymentNo})
|
||||
if callErr != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
|
||||
}
|
||||
if response == nil || response.IsFailure() {
|
||||
providerCode, providerMessage := "empty_response", "支付宝查单未返回有效响应"
|
||||
if response != nil {
|
||||
providerCode, providerMessage = string(response.Code), response.SubMsg
|
||||
}
|
||||
_, completeErr := a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, ProviderCode: providerCode, ProviderMessage: providerMessage,
|
||||
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
})
|
||||
if completeErr != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, completeErr
|
||||
}
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, apperrors.New(apperrors.CodeServiceUnavailable, "支付宝查单失败")
|
||||
}
|
||||
result := agentrecharge.OnlinePaymentQueryResult{
|
||||
State: mapAlipayTradeState(response.TradeStatus), ThirdPartyTradeNo: response.TradeNo,
|
||||
}
|
||||
result.Amount, _ = alipaypkg.YuanToFen(response.TotalAmount)
|
||||
if paidAt, parseErr := time.ParseInLocation("2006-01-02 15:04:05", response.SendPayDate, time.Local); parseErr == nil {
|
||||
result.PaidAt = &paidAt
|
||||
}
|
||||
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, ProviderCode: string(response.TradeStatus),
|
||||
ResponseSummary: map[string]any{"state": result.State, "has_trade_no": result.ThirdPartyTradeNo != ""},
|
||||
DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
}); err != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *AlipayPreCreateAdapter) client(config *model.WechatConfig) (*sdkalipay.Client, error) {
|
||||
if !a.Available(config) {
|
||||
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "支付宝扫码支付配置不可用")
|
||||
}
|
||||
return alipaypkg.NewClientFromConfig(config)
|
||||
}
|
||||
|
||||
func (a *AlipayPreCreateAdapter) queryClient(config *model.WechatConfig) (*sdkalipay.Client, error) {
|
||||
if !alipayConfigComplete(config, false) {
|
||||
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "支付宝查单配置不可用")
|
||||
}
|
||||
return alipaypkg.NewClientFromConfig(config)
|
||||
}
|
||||
|
||||
func alipayConfigComplete(config *model.WechatConfig, requireActive bool) bool {
|
||||
return config != nil && (!requireActive || config.IsActive) && config.AliAppID != "" && config.AliPrivateKey != "" &&
|
||||
config.AliPublicKey != "" && config.AliNotifyURL != ""
|
||||
}
|
||||
|
||||
func (a *AlipayPreCreateAdapter) startAttempt(ctx context.Context, paymentNo, operation string, configID uint, amount int64) (*model.IntegrationLog, error) {
|
||||
resourceID := paymentNo
|
||||
return a.integration.Start(ctx, integrationlog.Attempt{
|
||||
Provider: constants.IntegrationProviderAlipay, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: operation, ResourceType: constants.IntegrationResourceTypeAgentRechargePayment,
|
||||
ResourceID: &resourceID, ExternalID: &resourceID,
|
||||
RequestSummary: map[string]any{"payment_config_id": configID, "amount": amount},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *AlipayPreCreateAdapter) completeUnknown(ctx context.Context, integrationID string, startedAt time.Time, cause error) error {
|
||||
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", ProviderMessage: cause.Error(),
|
||||
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
RecoveryStrategy: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return apperrors.Wrap(apperrors.CodeTimeout, cause, "支付宝支付请求结果未知")
|
||||
}
|
||||
|
||||
func mapAlipayTradeState(state sdkalipay.TradeStatus) string {
|
||||
switch state {
|
||||
case sdkalipay.TradeStatusSuccess, sdkalipay.TradeStatusFinished:
|
||||
return agentrecharge.OnlinePaymentStatePaid
|
||||
case sdkalipay.TradeStatusClosed:
|
||||
return agentrecharge.OnlinePaymentStateClosed
|
||||
case sdkalipay.TradeStatusWaitBuyerPay:
|
||||
return agentrecharge.OnlinePaymentStatePending
|
||||
default:
|
||||
return agentrecharge.OnlinePaymentStateUnknown
|
||||
}
|
||||
}
|
||||
171
internal/infrastructure/payment/wechat_native.go
Normal file
171
internal/infrastructure/payment/wechat_native.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// Package payment 提供代理在线充值使用的支付渠道薄适配器。
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
|
||||
sdkpayment "github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
|
||||
orderRequest "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/order/request"
|
||||
"go.uber.org/zap"
|
||||
|
||||
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
wechatpay "github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
)
|
||||
|
||||
// WechatNativeAdapter 使用现有 PowerWeChat 实现 Native 预下单与查单。
|
||||
type WechatNativeAdapter struct {
|
||||
cache kernel.CacheInterface
|
||||
integration *integrationlog.Repository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewWechatNativeAdapter 创建微信 Native 支付适配器。
|
||||
func NewWechatNativeAdapter(cache kernel.CacheInterface, integration *integrationlog.Repository, logger *zap.Logger) *WechatNativeAdapter {
|
||||
return &WechatNativeAdapter{cache: cache, integration: integration, logger: logger}
|
||||
}
|
||||
|
||||
// Available 判断配置是否完整支持微信 Native 预下单、验签与查单。
|
||||
func (a *WechatNativeAdapter) Available(config *model.WechatConfig) bool {
|
||||
return wechatConfigComplete(config, true)
|
||||
}
|
||||
|
||||
// PreCreate 创建微信 Native 扫码支付单。
|
||||
func (a *WechatNativeAdapter) PreCreate(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentResult, error) {
|
||||
app, err := a.paymentApp(request.Config)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
attempt, err := a.startAttempt(ctx, request.PaymentNo, constants.IntegrationOperationPaymentPreCreate, request.Config.ID, request.Amount)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
startedAt := time.Now()
|
||||
response, callErr := app.Order.TransactionNative(ctx, &orderRequest.RequestNativePrepay{
|
||||
Description: request.Description,
|
||||
OutTradeNo: request.PaymentNo,
|
||||
TimeExpire: request.ExpireAt.Format(time.RFC3339),
|
||||
Amount: &orderRequest.NativeAmount{Total: int(request.Amount), Currency: "CNY"},
|
||||
})
|
||||
if callErr != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
|
||||
}
|
||||
if response == nil || strings.TrimSpace(response.CodeURL) == "" {
|
||||
err = apperrors.New(apperrors.CodeWechatPayFailed, "微信 Native 预下单未返回付款内容")
|
||||
_, completeErr := a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, ProviderCode: "empty_code_url", ProviderMessage: err.Error(),
|
||||
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
})
|
||||
if completeErr != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, completeErr
|
||||
}
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, ResponseSummary: map[string]any{"success": true},
|
||||
DurationMS: time.Since(startedAt).Milliseconds(), StateChanged: true,
|
||||
}); err != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
return agentrecharge.OnlinePaymentResult{QRContent: response.CodeURL}, nil
|
||||
}
|
||||
|
||||
// Query 查询微信 Native 支付单状态。
|
||||
func (a *WechatNativeAdapter) Query(ctx context.Context, paymentNo string, config *model.WechatConfig) (agentrecharge.OnlinePaymentQueryResult, error) {
|
||||
app, err := a.queryPaymentApp(config)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, err
|
||||
}
|
||||
attempt, err := a.startAttempt(ctx, paymentNo, constants.IntegrationOperationPaymentQuery, config.ID, 0)
|
||||
if err != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, err
|
||||
}
|
||||
startedAt := time.Now()
|
||||
info, callErr := wechatpay.NewPaymentService(app, a.logger).QueryOrder(ctx, paymentNo)
|
||||
if callErr != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
|
||||
}
|
||||
result := agentrecharge.OnlinePaymentQueryResult{
|
||||
State: mapWechatTradeState(info.TradeState), ThirdPartyTradeNo: info.TransactionID, Amount: info.TotalAmount,
|
||||
}
|
||||
if paidAt, parseErr := time.Parse(time.RFC3339, info.SuccessTime); parseErr == nil {
|
||||
result.PaidAt = &paidAt
|
||||
}
|
||||
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, ProviderCode: info.TradeState,
|
||||
ResponseSummary: map[string]any{"state": result.State, "has_trade_no": result.ThirdPartyTradeNo != ""},
|
||||
DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
}); err != nil {
|
||||
return agentrecharge.OnlinePaymentQueryResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *WechatNativeAdapter) paymentApp(config *model.WechatConfig) (*sdkpayment.Payment, error) {
|
||||
if !a.Available(config) {
|
||||
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "微信 Native 支付配置不可用")
|
||||
}
|
||||
app, err := wechatpay.NewPaymentAppFromConfig(config, config.OaAppID, a.cache, a.logger)
|
||||
if err != nil {
|
||||
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信 Native 支付配置不可用")
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (a *WechatNativeAdapter) queryPaymentApp(config *model.WechatConfig) (*sdkpayment.Payment, error) {
|
||||
if !wechatConfigComplete(config, false) {
|
||||
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "微信查单配置不可用")
|
||||
}
|
||||
app, err := wechatpay.NewPaymentAppFromConfig(config, config.OaAppID, a.cache, a.logger)
|
||||
if err != nil {
|
||||
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信查单配置不可用")
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func wechatConfigComplete(config *model.WechatConfig, requireActive bool) bool {
|
||||
return config != nil && (!requireActive || config.IsActive) && config.ProviderType == model.ProviderTypeWechat &&
|
||||
config.OaAppID != "" && config.WxMchID != "" && config.WxAPIV3Key != "" &&
|
||||
config.WxCertContent != "" && config.WxKeyContent != "" && config.WxSerialNo != "" && config.WxNotifyURL != ""
|
||||
}
|
||||
|
||||
func (a *WechatNativeAdapter) startAttempt(ctx context.Context, paymentNo, operation string, configID uint, amount int64) (*model.IntegrationLog, error) {
|
||||
resourceID := paymentNo
|
||||
return a.integration.Start(ctx, integrationlog.Attempt{
|
||||
Provider: constants.IntegrationProviderWechatPay, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: operation, ResourceType: constants.IntegrationResourceTypeAgentRechargePayment,
|
||||
ResourceID: &resourceID, ExternalID: &resourceID,
|
||||
RequestSummary: map[string]any{"payment_config_id": configID, "amount": amount},
|
||||
})
|
||||
}
|
||||
|
||||
func (a *WechatNativeAdapter) completeUnknown(ctx context.Context, integrationID string, startedAt time.Time, cause error) error {
|
||||
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", ProviderMessage: cause.Error(),
|
||||
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
RecoveryStrategy: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return apperrors.Wrap(apperrors.CodeTimeout, cause, "微信支付请求结果未知")
|
||||
}
|
||||
|
||||
func mapWechatTradeState(state string) string {
|
||||
switch state {
|
||||
case "SUCCESS":
|
||||
return agentrecharge.OnlinePaymentStatePaid
|
||||
case "CLOSED", "REVOKED", "PAYERROR":
|
||||
return agentrecharge.OnlinePaymentStateClosed
|
||||
case "NOTPAY", "USERPAYING":
|
||||
return agentrecharge.OnlinePaymentStatePending
|
||||
default:
|
||||
return agentrecharge.OnlinePaymentStateUnknown
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,8 @@ type AgentRechargeRecord struct {
|
||||
Remark string `gorm:"column:remark;type:text;comment:运营备注(创建时填写,不可修改)" json:"remark,omitempty"`
|
||||
RejectionReason *string `gorm:"column:rejection_reason;type:varchar(500);comment:驳回原因,仅驳回时写入" json:"rejection_reason,omitempty"`
|
||||
ApprovalInstanceID *uint `gorm:"column:approval_instance_id;comment:员工线下代充值关联的唯一通用审批实例ID" json:"approval_instance_id,omitempty"`
|
||||
RequestID *string `gorm:"column:request_id;type:varchar(64);comment:提交账号提供的在线充值幂等请求标识" json:"request_id,omitempty"`
|
||||
RequestFingerprint *string `gorm:"column:request_fingerprint;type:varchar(64);comment:在线充值请求业务字段指纹" json:"-"`
|
||||
PaidAt *time.Time `gorm:"column:paid_at;comment:支付时间" json:"paid_at,omitempty"`
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;comment:完成时间" json:"completed_at,omitempty"`
|
||||
ShopIDTag uint `gorm:"column:shop_id_tag;not null;index;comment:店铺ID标签(多租户过滤)" json:"shop_id_tag"`
|
||||
|
||||
@@ -2,13 +2,49 @@ package dto
|
||||
|
||||
// CreateAgentRechargeRequest 创建代理充值请求
|
||||
type CreateAgentRechargeRequest struct {
|
||||
ShopID uint `json:"shop_id" validate:"required" required:"true" description:"目标店铺ID,代理只能填自己店铺"`
|
||||
ShopID *uint `json:"shop_id,omitempty" description:"目标店铺ID,仅平台线下代充可填;代理在线充值禁止传入"`
|
||||
Amount int64 `json:"amount" validate:"required,min=1,max=100000000" required:"true" minimum:"1" maximum:"100000000" description:"充值金额(分),范围1分~100万元"`
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat offline" required:"true" description:"支付方式 (wechat:微信在线支付, offline:线下转账仅平台可用)"`
|
||||
PaymentMethod string `json:"payment_method" validate:"required,oneof=wechat alipay offline" required:"true" description:"支付方式 (wechat:微信在线支付, alipay:支付宝在线支付, offline:线下转账仅平台可用)"`
|
||||
RequestID string `json:"request_id,omitempty" validate:"omitempty,max=64" maxLength:"64" description:"在线充值幂等请求标识,微信或支付宝支付时必填"`
|
||||
PaymentVoucherKey []string `json:"payment_voucher_key" validate:"omitempty,max=5,dive,max=500" maxItems:"5" description:"支付凭证对象存储Key列表(payment_method=offline 时至少1个,最多5个,微信支付时忽略)"`
|
||||
Remark string `json:"remark" validate:"omitempty,max=1000" maxLength:"1000" description:"运营备注(可选,创建后只读)"`
|
||||
}
|
||||
|
||||
// AgentRechargeOnlineResponse 代理在线扫码充值创建响应。
|
||||
type AgentRechargeOnlineResponse struct {
|
||||
RechargeID uint `json:"recharge_id" description:"充值记录ID"`
|
||||
RechargeNo string `json:"recharge_no" description:"充值单号(ARCH前缀)"`
|
||||
PaymentNo string `json:"payment_no" description:"支付单号(PAY前缀)"`
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式 (wechat:微信, alipay:支付宝)"`
|
||||
RechargeSource string `json:"recharge_source" description:"充值来源 (agent_online:代理在线自充)"`
|
||||
RechargeSourceName string `json:"recharge_source_name" description:"充值来源名称(中文)"`
|
||||
Amount int64 `json:"amount" description:"在线充值金额(分),范围10000分~100000000分"`
|
||||
QRContent string `json:"qr_content" description:"支付渠道原始扫码付款内容,由前端渲染二维码"`
|
||||
Status int `json:"status" description:"状态 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款, 6:已驳回)"`
|
||||
StatusName string `json:"status_name" description:"状态名称(中文)"`
|
||||
}
|
||||
|
||||
// AgentRechargePaymentMethodsResponse 代理在线充值可用支付方式响应。
|
||||
type AgentRechargePaymentMethodsResponse struct {
|
||||
Methods []string `json:"methods" description:"可用支付方式,固定顺序 (wechat:微信, alipay:支付宝)"`
|
||||
MinAmount int64 `json:"min_amount" description:"在线充值最小金额(分),固定为10000"`
|
||||
MaxAmount int64 `json:"max_amount" description:"在线充值最大金额(分),固定为100000000"`
|
||||
}
|
||||
|
||||
// AgentRechargePaymentStatusResponse 代理充值本地支付与到账状态响应。
|
||||
type AgentRechargePaymentStatusResponse struct {
|
||||
RechargeID uint `json:"recharge_id" description:"充值记录ID"`
|
||||
RechargeNo string `json:"recharge_no" description:"充值单号(ARCH前缀)"`
|
||||
RechargeSource string `json:"recharge_source" description:"充值来源 (platform_offline:平台线下代充, agent_online:代理在线自充)"`
|
||||
RechargeSourceName string `json:"recharge_source_name" description:"充值来源名称(中文)"`
|
||||
Status int `json:"status" description:"充值状态 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款, 6:已驳回)"`
|
||||
StatusName string `json:"status_name" description:"充值状态名称(中文)"`
|
||||
PaymentStatus int `json:"payment_status" description:"支付状态 (0:待支付, 1:已支付, 2:已失败, 3:已退款)"`
|
||||
PaymentStatusName string `json:"payment_status_name" description:"支付状态名称(中文)"`
|
||||
PaidAt *string `json:"paid_at" description:"第三方支付确认时间"`
|
||||
CompletedAt *string `json:"completed_at" description:"钱包入账完成时间"`
|
||||
}
|
||||
|
||||
// AgentOfflinePayRequest 代理线下充值确认请求
|
||||
type AgentOfflinePayRequest struct {
|
||||
OperationPassword string `json:"operation_password" validate:"required" required:"true" description:"操作密码"`
|
||||
@@ -28,7 +64,9 @@ type AgentRechargeResponse struct {
|
||||
ShopName string `json:"shop_name" description:"店铺名称"`
|
||||
AgentWalletID uint `json:"agent_wallet_id" description:"代理钱包ID"`
|
||||
Amount int64 `json:"amount" description:"充值金额(分)"`
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式 (wechat:微信在线支付, offline:线下转账)"`
|
||||
PaymentMethod string `json:"payment_method" description:"支付方式 (wechat:微信在线支付, alipay:支付宝在线支付, offline:线下转账)"`
|
||||
RechargeSource string `json:"recharge_source" description:"充值来源 (platform_offline:平台线下代充, agent_online:代理在线自充)"`
|
||||
RechargeSourceName string `json:"recharge_source_name" description:"充值来源名称(中文)"`
|
||||
PaymentChannel string `json:"payment_channel" description:"实际支付通道 (wechat_direct:微信直连, fuyou:富友, offline:线下转账)"`
|
||||
PaymentConfigID *uint `json:"payment_config_id" description:"关联支付配置ID,线下充值为null"`
|
||||
PaymentTransactionID string `json:"payment_transaction_id" description:"第三方支付流水号"`
|
||||
@@ -62,12 +100,13 @@ type AgentRechargeRejectParams struct {
|
||||
|
||||
// AgentRechargeListRequest 代理充值记录列表请求
|
||||
type AgentRechargeListRequest struct {
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码,默认1"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页条数,默认20,最大100"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" description:"按店铺ID过滤"`
|
||||
Status *int `json:"status" query:"status" description:"按状态过滤 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款, 6:已驳回)"`
|
||||
StartDate string `json:"start_date" query:"start_date" description:"创建时间起始日期(YYYY-MM-DD)"`
|
||||
EndDate string `json:"end_date" query:"end_date" description:"创建时间截止日期(YYYY-MM-DD)"`
|
||||
Page int `json:"page" query:"page" validate:"omitempty,min=1" minimum:"1" description:"页码,默认1"`
|
||||
PageSize int `json:"page_size" query:"page_size" validate:"omitempty,min=1,max=100" minimum:"1" maximum:"100" description:"每页条数,默认20,最大100"`
|
||||
ShopID *uint `json:"shop_id" query:"shop_id" description:"按店铺ID过滤"`
|
||||
Status *int `json:"status" query:"status" description:"按状态过滤 (1:待支付, 2:已支付, 3:已完成, 4:已关闭, 5:已退款, 6:已驳回)"`
|
||||
RechargeSource string `json:"recharge_source" query:"recharge_source" validate:"omitempty,oneof=platform_offline agent_online" description:"按充值来源过滤 (platform_offline:平台线下代充, agent_online:代理在线自充)"`
|
||||
StartDate string `json:"start_date" query:"start_date" description:"创建时间起始日期(YYYY-MM-DD)"`
|
||||
EndDate string `json:"end_date" query:"end_date" description:"创建时间截止日期(YYYY-MM-DD)"`
|
||||
}
|
||||
|
||||
// AgentRechargeListResponse 代理充值记录列表响应
|
||||
|
||||
@@ -3,6 +3,8 @@ package model
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
type Payment struct {
|
||||
@@ -11,11 +13,13 @@ type Payment struct {
|
||||
OrderID uint `gorm:"column:order_id;not null" json:"order_id"`
|
||||
OrderType string `gorm:"column:order_type;type:varchar(30);not null" json:"order_type"`
|
||||
PaymentMethod string `gorm:"column:payment_method;type:varchar(20);not null" json:"payment_method"`
|
||||
MerchantIdentity string `gorm:"column:merchant_identity;type:varchar(100)" json:"-"`
|
||||
Amount int64 `gorm:"column:amount;type:bigint;not null" json:"amount"`
|
||||
Status int `gorm:"column:status;type:smallint;not null;default:0" json:"status"`
|
||||
ThirdPartyTradeNo string `gorm:"column:third_party_trade_no;type:varchar(100)" json:"third_party_trade_no,omitempty"`
|
||||
PaymentConfigID *uint `gorm:"column:payment_config_id" json:"payment_config_id,omitempty"`
|
||||
PaymentVoucherKey string `gorm:"column:payment_voucher_key;type:varchar(500)" json:"payment_voucher_key,omitempty"`
|
||||
QRContent string `gorm:"column:qr_content;type:text" json:"-"`
|
||||
PaidAt *time.Time `gorm:"column:paid_at" json:"paid_at,omitempty"`
|
||||
ExpireAt *time.Time `gorm:"column:expire_at" json:"expire_at,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||
@@ -28,15 +32,16 @@ func (Payment) TableName() string {
|
||||
}
|
||||
|
||||
const (
|
||||
PaymentRecordStatusPending = 0 // 待支付
|
||||
PaymentRecordStatusPaid = 1 // 已支付
|
||||
PaymentRecordStatusFailed = 2 // 已失败
|
||||
PaymentRecordStatusRefunded = 3 // 已退款
|
||||
PaymentRecordStatusPending = constants.PaymentRecordStatusPending // 待支付
|
||||
PaymentRecordStatusPaid = constants.PaymentRecordStatusPaid // 已支付
|
||||
PaymentRecordStatusFailed = constants.PaymentRecordStatusFailed // 已失败
|
||||
PaymentRecordStatusRefunded = constants.PaymentRecordStatusRefunded // 已退款
|
||||
)
|
||||
|
||||
const (
|
||||
PaymentOrderTypePackage = "package_order"
|
||||
PaymentOrderTypeRecharge = "recharge_order"
|
||||
PaymentOrderTypePackage = "package_order" // 套餐订单
|
||||
PaymentOrderTypeRecharge = "recharge_order" // 客户充值订单
|
||||
PaymentOrderTypeAgentRecharge = "agent_recharge" // 代理充值订单
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
62
internal/query/agentrecharge/payment_status.go
Normal file
62
internal/query/agentrecharge/payment_status.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Package agentrecharge 提供代理充值本地状态只读投影。
|
||||
package agentrecharge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// PaymentStatusQuery 查询代理充值本地支付与到账事实。
|
||||
type PaymentStatusQuery struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewPaymentStatusQuery 创建代理充值支付状态 Query。
|
||||
func NewPaymentStatusQuery(db *gorm.DB) *PaymentStatusQuery {
|
||||
return &PaymentStatusQuery{db: db}
|
||||
}
|
||||
|
||||
// Get 读取当前数据范围内的充值单和支付单,不调用第三方渠道。
|
||||
func (q *PaymentStatusQuery) Get(ctx context.Context, rechargeID uint) (*dto.AgentRechargePaymentStatusResponse, error) {
|
||||
if q == nil || q.db == nil || rechargeID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "代理充值支付状态查询参数无效")
|
||||
}
|
||||
var recharge model.AgentRechargeRecord
|
||||
if err := q.db.WithContext(ctx).Where("id = ?", rechargeID).First(&recharge).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值状态失败")
|
||||
}
|
||||
var payment model.Payment
|
||||
if err := q.db.WithContext(ctx).
|
||||
Where("order_id = ? AND order_type = ?", recharge.ID, model.PaymentOrderTypeAgentRecharge).
|
||||
First(&payment).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询代理充值支付状态失败")
|
||||
}
|
||||
source, sourceName := constants.GetAgentRechargeSource(recharge.PaymentMethod)
|
||||
result := &dto.AgentRechargePaymentStatusResponse{
|
||||
RechargeID: recharge.ID, RechargeNo: recharge.RechargeNo,
|
||||
RechargeSource: source, RechargeSourceName: sourceName,
|
||||
Status: recharge.Status, StatusName: constants.GetRechargeStatusName(recharge.Status),
|
||||
PaymentStatus: payment.Status, PaymentStatusName: constants.GetPaymentRecordStatusName(payment.Status),
|
||||
}
|
||||
if payment.PaidAt != nil {
|
||||
paidAt := payment.PaidAt.Format("2006-01-02 15:04:05")
|
||||
result.PaidAt = &paidAt
|
||||
}
|
||||
if recharge.CompletedAt != nil {
|
||||
completedAt := recharge.CompletedAt.Format("2006-01-02 15:04:05")
|
||||
result.CompletedAt = &completedAt
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func registerAgentRechargeRoutes(router fiber.Router, handler *admin.AgentRechar
|
||||
Summary: "创建代理充值订单",
|
||||
Tags: []string{"代理预充值"},
|
||||
Input: new(dto.CreateAgentRechargeRequest),
|
||||
Output: new(dto.AgentRechargeResponse),
|
||||
Output: new(dto.AgentRechargeOnlineResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
@@ -37,6 +37,21 @@ func registerAgentRechargeRoutes(router fiber.Router, handler *admin.AgentRechar
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(group, doc, groupPath, "GET", "/payment-methods", handler.PaymentMethods, RouteSpec{
|
||||
Summary: "查询代理在线充值可用支付方式",
|
||||
Tags: []string{"代理预充值"},
|
||||
Output: new(dto.AgentRechargePaymentMethodsResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(group, doc, groupPath, "GET", "/:id/payment-status", handler.PaymentStatus, RouteSpec{
|
||||
Summary: "查询代理充值本地支付与到账状态",
|
||||
Tags: []string{"代理预充值"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: new(dto.AgentRechargePaymentStatusResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(group, doc, groupPath, "GET", "/:id", handler.Get, RouteSpec{
|
||||
Summary: "查询代理充值订单详情",
|
||||
Tags: []string{"代理预充值"},
|
||||
|
||||
@@ -95,20 +95,16 @@ func (s *Service) SetOfflineCreationService(service *agentrechargeapp.OfflineCre
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeRequest) (*dto.AgentRechargeResponse, error) {
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
userShopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
// 代理只能充自己店铺
|
||||
if userType == constants.UserTypeAgent && req.ShopID != userShopID {
|
||||
return nil, errors.New(errors.CodeForbidden, "代理只能为自己的店铺充值")
|
||||
if req.PaymentMethod != constants.RechargeMethodOffline {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "在线充值必须通过代理在线创建用例处理")
|
||||
}
|
||||
|
||||
// 线下充值仅平台可用
|
||||
if req.PaymentMethod == constants.RechargeMethodOffline && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
|
||||
if userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
|
||||
return nil, errors.New(errors.CodeForbidden, "线下充值仅平台管理员可操作")
|
||||
}
|
||||
|
||||
// 线下充值必须上传支付凭证
|
||||
if req.PaymentMethod == constants.RechargeMethodOffline && len(req.PaymentVoucherKey) == 0 {
|
||||
if req.ShopID == nil || *req.ShopID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "线下充值必须指定目标店铺")
|
||||
}
|
||||
if len(req.PaymentVoucherKey) == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "线下充值必须上传支付凭证")
|
||||
}
|
||||
|
||||
@@ -117,73 +113,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateAgentRechargeReques
|
||||
}
|
||||
|
||||
rechargeNo := s.generateRechargeNo()
|
||||
if req.PaymentMethod == constants.RechargeMethodOffline {
|
||||
return s.createOffline(ctx, req, userID, userType, rechargeNo)
|
||||
}
|
||||
|
||||
// 查找目标店铺的主钱包
|
||||
wallet, err := s.agentWalletStore.GetMainWallet(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "目标店铺主钱包不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺主钱包失败")
|
||||
}
|
||||
|
||||
// 查询店铺名称
|
||||
shop, err := s.shopStore.GetByID(ctx, req.ShopID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询店铺失败")
|
||||
}
|
||||
|
||||
// 在线支付需要查询生效的支付配置
|
||||
var paymentConfigID *uint
|
||||
var paymentChannel string
|
||||
if req.PaymentMethod == "wechat" {
|
||||
activeConfig, cfgErr := s.wechatConfigService.GetActiveConfig(ctx)
|
||||
if cfgErr != nil || activeConfig == nil {
|
||||
return nil, errors.New(errors.CodeNoPaymentConfig, "当前无可用的支付配置,请联系管理员")
|
||||
}
|
||||
paymentConfigID = &activeConfig.ID
|
||||
paymentChannel = activeConfig.ProviderType
|
||||
} else {
|
||||
paymentChannel = "offline"
|
||||
}
|
||||
|
||||
record := &model.AgentRechargeRecord{
|
||||
UserID: userID,
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: req.ShopID,
|
||||
RechargeNo: rechargeNo,
|
||||
Amount: req.Amount,
|
||||
PaymentMethod: req.PaymentMethod,
|
||||
PaymentChannel: &paymentChannel,
|
||||
PaymentConfigID: paymentConfigID,
|
||||
PaymentVoucherKey: model.StringJSONBArray(req.PaymentVoucherKey),
|
||||
Remark: req.Remark,
|
||||
Status: constants.RechargeStatusPending,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
}
|
||||
|
||||
if err := s.agentRechargeStore.Create(ctx, record); err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
|
||||
s.logger.Info("创建代理充值订单成功",
|
||||
zap.Uint("recharge_id", record.ID),
|
||||
zap.String("recharge_no", rechargeNo),
|
||||
zap.Int64("amount", req.Amount),
|
||||
zap.Uint("shop_id", req.ShopID),
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
resp := toResponse(record, shop.ShopName)
|
||||
resp.SubmitterName = s.loadSubmitterNameBestEffort(ctx, record.UserID)
|
||||
return resp, nil
|
||||
return s.createOffline(ctx, req, userID, userType, rechargeNo)
|
||||
}
|
||||
|
||||
func (s *Service) createOffline(
|
||||
@@ -199,7 +129,7 @@ func (s *Service) createOffline(
|
||||
result, err := s.offlineCreation.Execute(ctx, agentrechargeapp.CreateOfflineCommand{
|
||||
SubmitterAccountID: userID,
|
||||
SubmitterUserType: userType,
|
||||
ShopID: req.ShopID,
|
||||
ShopID: *req.ShopID,
|
||||
RechargeNo: rechargeNo,
|
||||
Amount: req.Amount,
|
||||
PaymentVoucherKeys: req.PaymentVoucherKey,
|
||||
@@ -522,6 +452,12 @@ func (s *Service) List(ctx context.Context, req *dto.AgentRechargeListRequest) (
|
||||
if req.Status != nil {
|
||||
query = query.Where("status = ?", *req.Status)
|
||||
}
|
||||
switch req.RechargeSource {
|
||||
case constants.AgentRechargeSourcePlatformOffline:
|
||||
query = query.Where("payment_method NOT IN ?", []string{constants.RechargeMethodWechat, constants.RechargeMethodAlipay})
|
||||
case constants.AgentRechargeSourceAgentOnline:
|
||||
query = query.Where("payment_method IN ?", []string{constants.RechargeMethodWechat, constants.RechargeMethodAlipay})
|
||||
}
|
||||
if req.StartDate != "" {
|
||||
query = query.Where("created_at >= ?", req.StartDate+" 00:00:00")
|
||||
}
|
||||
@@ -590,6 +526,7 @@ func (s *Service) generateRechargeNo() string {
|
||||
|
||||
// toResponse 将模型转换为响应 DTO
|
||||
func toResponse(record *model.AgentRechargeRecord, shopName string) *dto.AgentRechargeResponse {
|
||||
rechargeSource, rechargeSourceName := constants.GetAgentRechargeSource(record.PaymentMethod)
|
||||
resp := &dto.AgentRechargeResponse{
|
||||
ID: record.ID,
|
||||
RechargeNo: record.RechargeNo,
|
||||
@@ -598,6 +535,8 @@ func toResponse(record *model.AgentRechargeRecord, shopName string) *dto.AgentRe
|
||||
AgentWalletID: record.AgentWalletID,
|
||||
Amount: record.Amount,
|
||||
PaymentMethod: record.PaymentMethod,
|
||||
RechargeSource: rechargeSource,
|
||||
RechargeSourceName: rechargeSourceName,
|
||||
PaymentVoucherKey: []string(record.PaymentVoucherKey),
|
||||
Remark: record.Remark,
|
||||
Status: record.Status,
|
||||
|
||||
Reference in New Issue
Block a user