This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user