让代理充值复用现有网页支付能力
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m7s
微信按当前 v2/v3 配置分别生成 MWEB/H5 链接,支付宝复用 C 端 WAP 链接,并让可用支付方式基于生效配置判断。 Constraint: 支付链接统一通过 qr_content 返回,由前端渲染二维码;按要求不运行测试 Rejected: 微信 Native 与支付宝当面付 | 会引入非当前商户配置所需的额外产品开通 Confidence: high Scope-risk: moderate Directive: 微信 H5/MWEB 二维码仅承诺系统相机或外部浏览器扫码链路 Tested: 相关 Go 包编译通过;gofmt 与 git diff --check 通过 Not-tested: 按用户要求未运行自动化测试及真实支付联调
This commit is contained in:
@@ -28,9 +28,10 @@ type CreateOnlineCommand struct {
|
||||
Amount int64
|
||||
PaymentMethod string
|
||||
RequestID string
|
||||
PayerClientIP string
|
||||
}
|
||||
|
||||
// CreateOnlineResult 返回在线充值单、支付单及原始付款内容。
|
||||
// CreateOnlineResult 返回在线充值单、支付单及支付链接。
|
||||
type CreateOnlineResult struct {
|
||||
Recharge *model.AgentRechargeRecord
|
||||
Payment *model.Payment
|
||||
@@ -55,7 +56,7 @@ func NewOnlineCreationService(db *gorm.DB, wechat, alipay OnlinePaymentPort) *On
|
||||
return &OnlineCreationService{db: db, wechat: wechat, alipay: alipay}
|
||||
}
|
||||
|
||||
// Execute 以短事务建单,事务外预下单,再条件保存付款内容或关闭失败订单。
|
||||
// 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, "代理在线充值能力未配置")
|
||||
@@ -90,14 +91,15 @@ func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlin
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
paymentResult, err := adapter.PreCreate(ctx, OnlinePaymentRequest{
|
||||
PaymentNo: result.Payment.PaymentNo, Description: "代理主钱包充值", Amount: command.Amount,
|
||||
ExpireAt: *result.Payment.ExpireAt, Config: config,
|
||||
paymentResult, err := adapter.CreatePaymentURL(ctx, OnlinePaymentRequest{
|
||||
PaymentID: result.Payment.ID, PaymentNo: result.Payment.PaymentNo, CorrelationID: result.Payment.PaymentNo,
|
||||
Description: "代理主钱包充值", Amount: command.Amount,
|
||||
ExpireAt: *result.Payment.ExpireAt, PayerClientIP: command.PayerClientIP, 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, apperrors.Wrap(apperrors.CodeDatabaseError, closeErr, "支付链接生成失败且关闭本地订单失败")
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
@@ -106,22 +108,22 @@ func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlin
|
||||
if closeErr := s.closeFailedCreation(ctx, result); closeErr != nil {
|
||||
return nil, closeErr
|
||||
}
|
||||
return nil, apperrors.New(apperrors.CodeServiceUnavailable, "支付渠道未返回付款内容")
|
||||
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, "保存扫码付款内容失败")
|
||||
return nil, apperrors.Wrap(apperrors.CodeDatabaseError, update.Error, "保存支付链接失败")
|
||||
}
|
||||
if update.RowsAffected != 1 {
|
||||
return nil, apperrors.New(apperrors.CodeConflict, "在线充值付款内容已变化")
|
||||
return nil, apperrors.New(apperrors.CodeConflict, "在线充值支付链接已变化")
|
||||
}
|
||||
result.Payment.QRContent = paymentResult.QRContent
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AvailablePaymentMethods 按固定顺序返回配置完整的扫码支付方式。
|
||||
// AvailablePaymentMethods 按固定顺序返回配置完整的在线支付方式。
|
||||
func (s *OnlineCreationService) AvailablePaymentMethods(ctx context.Context, userType int) (AvailablePaymentMethodsResult, error) {
|
||||
result := AvailablePaymentMethodsResult{
|
||||
Methods: []string{}, MinAmount: constants.AgentOnlineRechargeMinAmount, MaxAmount: constants.AgentRechargeMaxAmount,
|
||||
@@ -275,7 +277,7 @@ func (s *OnlineCreationService) loadReplay(
|
||||
}
|
||||
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.CodeInvalidStatus, "原在线充值请求支付链接生成失败")
|
||||
}
|
||||
return nil, true, apperrors.New(apperrors.CodeConflict, "在线充值请求正在处理中,请稍后重试")
|
||||
}
|
||||
@@ -283,7 +285,7 @@ func (s *OnlineCreationService) loadReplay(
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) closeFailedCreation(ctx context.Context, result *CreateOnlineResult) error {
|
||||
if result == nil || result.Recharge == nil || result.Payment == nil || !domain.CanCloseAfterPreCreateFailure(result.Recharge.Status) {
|
||||
if result == nil || result.Recharge == nil || result.Payment == nil || !domain.CanCloseAfterPaymentURLFailure(result.Recharge.Status) {
|
||||
return nil
|
||||
}
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
@@ -18,16 +18,19 @@ const (
|
||||
OnlinePaymentStateUnknown = "unknown"
|
||||
)
|
||||
|
||||
// OnlinePaymentRequest 描述扫码预下单所需的最小事实。
|
||||
// OnlinePaymentRequest 描述生成支付链接与主动查单所需的最小事实。
|
||||
type OnlinePaymentRequest struct {
|
||||
PaymentNo string
|
||||
Description string
|
||||
Amount int64
|
||||
ExpireAt time.Time
|
||||
Config *model.WechatConfig
|
||||
PaymentID uint
|
||||
PaymentNo string
|
||||
CorrelationID string
|
||||
Description string
|
||||
Amount int64
|
||||
ExpireAt time.Time
|
||||
PayerClientIP string
|
||||
Config *model.WechatConfig
|
||||
}
|
||||
|
||||
// OnlinePaymentResult 描述渠道返回的扫码付款内容。
|
||||
// OnlinePaymentResult 描述渠道返回的支付链接。
|
||||
type OnlinePaymentResult struct {
|
||||
QRContent string
|
||||
}
|
||||
@@ -40,9 +43,9 @@ type OnlinePaymentQueryResult struct {
|
||||
PaidAt *time.Time
|
||||
}
|
||||
|
||||
// OnlinePaymentPort 定义代理扫码充值需要的最小渠道能力。
|
||||
// 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)
|
||||
CreatePaymentURL(ctx context.Context, request OnlinePaymentRequest) (OnlinePaymentResult, error)
|
||||
Query(ctx context.Context, request OnlinePaymentRequest) (OnlinePaymentQueryResult, error)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// RecoverOnlinePaymentService 批量收敛长期待预下单或待支付的代理在线充值。
|
||||
// RecoverOnlinePaymentService 批量收敛长期缺少支付链接或待支付的代理在线充值。
|
||||
type RecoverOnlinePaymentService struct {
|
||||
db *gorm.DB
|
||||
wechat OnlinePaymentPort
|
||||
@@ -75,18 +75,23 @@ func (s *RecoverOnlinePaymentService) recoverOne(ctx context.Context, payment *m
|
||||
}
|
||||
if payment.QRContent == "" {
|
||||
if !adapter.Available(config) {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "代理充值预下单配置不可用")
|
||||
return errors.New(errors.CodeNoPaymentConfig, "代理充值支付配置不可用")
|
||||
}
|
||||
// 支付宝 WAP 链接由本地签名生成,可以安全重建;微信 H5 下单结果未知时只允许查单。
|
||||
if payment.PaymentMethod != constants.RechargeMethodAlipay {
|
||||
return s.queryPayment(ctx, adapter, payment, recharge, config)
|
||||
}
|
||||
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,
|
||||
result, err := adapter.CreatePaymentURL(ctx, OnlinePaymentRequest{
|
||||
PaymentID: payment.ID, PaymentNo: payment.PaymentNo, CorrelationID: payment.PaymentNo,
|
||||
Description: "代理主钱包充值", Amount: payment.Amount,
|
||||
ExpireAt: expireAt, Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
// 恢复阶段不能仅凭预下单错误推断未收款,保留本地状态等待下次查单。
|
||||
// 恢复阶段不能仅凭链接生成错误推断未收款,保留本地状态等待下次查单。
|
||||
return nil
|
||||
}
|
||||
if result.QRContent == "" {
|
||||
@@ -96,11 +101,17 @@ func (s *RecoverOnlinePaymentService) recoverOne(ctx context.Context, payment *m
|
||||
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 errors.Wrap(errors.CodeDatabaseError, update.Error, "恢复代理充值支付链接失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
queryResult, err := adapter.Query(ctx, payment.PaymentNo, config)
|
||||
return s.queryPayment(ctx, adapter, payment, recharge, config)
|
||||
}
|
||||
|
||||
func (s *RecoverOnlinePaymentService) queryPayment(ctx context.Context, adapter OnlinePaymentPort, payment *model.Payment, recharge *model.AgentRechargeRecord, config *model.WechatConfig) error {
|
||||
queryResult, err := adapter.Query(ctx, OnlinePaymentRequest{
|
||||
PaymentID: payment.ID, PaymentNo: payment.PaymentNo, CorrelationID: payment.PaymentNo, Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user