让代理充值复用现有网页支付能力
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:
2026-07-30 11:41:50 +08:00
parent 0f4f0d1176
commit 8fc667daee
24 changed files with 692 additions and 352 deletions

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -272,8 +272,8 @@ func initServices(s *stores, deps *Dependencies) *services {
paymentIntegration := integrationlog.NewRepository(deps.DB)
agentRechargeOnline := agentrechargeApp.NewOnlineCreationService(
deps.DB,
paymentInfra.NewWechatNativeAdapter(wechat.NewRedisCache(deps.Redis), paymentIntegration, deps.Logger),
paymentInfra.NewAlipayPreCreateAdapter(paymentIntegration),
paymentInfra.NewWechatWebAdapter(wechat.NewRedisCache(deps.Redis), paymentIntegration, deps.Logger),
paymentInfra.NewAlipayWapAdapter(paymentIntegration, deps.Logger),
)
agentRechargePaymentConfirm := agentrechargeApp.NewConfirmOnlinePaymentService(
deps.DB,

View File

@@ -24,7 +24,7 @@ func ValidateOnlineCreation(userType int, amount int64, paymentMethod string) er
}
}
// CanCloseAfterPreCreateFailure 判断预下单明确失败后能否关闭充值单。
func CanCloseAfterPreCreateFailure(status int) bool {
// CanCloseAfterPaymentURLFailure 判断支付链接生成明确失败后能否关闭充值单。
func CanCloseAfterPaymentURLFailure(status int) bool {
return status == constants.RechargeStatusPending
}

View File

@@ -79,7 +79,7 @@ func (h *AgentRechargeHandler) createOnline(c *fiber.Ctx, req dto.CreateAgentRec
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,
PaymentMethod: req.PaymentMethod, RequestID: req.RequestID, PayerClientIP: c.IP(),
})
if err != nil {
return err

View File

@@ -2,7 +2,7 @@ package payment
import (
"context"
"strings"
"strconv"
"time"
sdkalipay "github.com/smartwalle/alipay/v3"
@@ -13,82 +13,47 @@ import (
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"
"go.uber.org/zap"
)
// AlipayPreCreateAdapter 使用现有 smartwalle/alipay 实现当面付预下单与查单。
type AlipayPreCreateAdapter struct {
// AlipayWapAdapter 使用现有 C 端支付宝能力生成 WAP 支付链接并主动查单。
type AlipayWapAdapter struct {
integration *integrationlog.Repository
logger *zap.Logger
}
// NewAlipayPreCreateAdapter 创建支付宝当面付适配器。
func NewAlipayPreCreateAdapter(integration *integrationlog.Repository) *AlipayPreCreateAdapter {
return &AlipayPreCreateAdapter{integration: integration}
// NewAlipayWapAdapter 创建支付宝 WAP 支付适配器。
func NewAlipayWapAdapter(integration *integrationlog.Repository, logger *zap.Logger) *AlipayWapAdapter {
return &AlipayWapAdapter{integration: integration, logger: logger}
}
// Available 判断配置是否完整支持支付宝预下单、验签与查单。
func (a *AlipayPreCreateAdapter) Available(config *model.WechatConfig) bool {
// Available 判断配置是否完整支持支付宝 WAP 支付、验签与查单。
func (a *AlipayWapAdapter) 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)
// CreatePaymentURL 使用与 C 端相同的手机网站支付能力生成签名 URL
func (a *AlipayWapAdapter) CreatePaymentURL(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentResult, error) {
payment := &model.Payment{PaymentNo: request.PaymentNo, Amount: request.Amount, ExpireAt: &request.ExpireAt}
payURL, err := alipaypkg.BuildWapPayURL(ctx, request.Config, payment, request.Description)
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
return agentrecharge.OnlinePaymentResult{QRContent: payURL}, nil
}
// Query 查询支付宝当面付订单状态。
func (a *AlipayPreCreateAdapter) Query(ctx context.Context, paymentNo string, config *model.WechatConfig) (agentrecharge.OnlinePaymentQueryResult, error) {
client, err := a.queryClient(config)
// Query 查询支付宝 WAP 支付单状态。
func (a *AlipayWapAdapter) Query(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentQueryResult, error) {
client, err := a.queryClient(request.Config)
if err != nil {
return agentrecharge.OnlinePaymentQueryResult{}, err
}
attempt, err := a.startAttempt(ctx, paymentNo, constants.IntegrationOperationPaymentQuery, config.ID, 0)
attempt, err := a.startAttempt(ctx, request, constants.IntegrationOperationPaymentQuery, request.Config.ID, 0)
if err != nil {
return agentrecharge.OnlinePaymentQueryResult{}, err
}
startedAt := time.Now()
response, callErr := client.TradeQuery(ctx, sdkalipay.TradeQuery{OutTradeNo: paymentNo})
response, callErr := client.TradeQuery(ctx, sdkalipay.TradeQuery{OutTradeNo: request.PaymentNo})
if callErr != nil {
return agentrecharge.OnlinePaymentQueryResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
}
@@ -123,14 +88,7 @@ func (a *AlipayPreCreateAdapter) Query(ctx context.Context, paymentNo string, co
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) {
func (a *AlipayWapAdapter) queryClient(config *model.WechatConfig) (*sdkalipay.Client, error) {
if !alipayConfigComplete(config, false) {
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "支付宝查单配置不可用")
}
@@ -142,19 +100,25 @@ func alipayConfigComplete(config *model.WechatConfig, requireActive bool) bool {
config.AliPublicKey != "" && config.AliNotifyURL != ""
}
func (a *AlipayPreCreateAdapter) startAttempt(ctx context.Context, paymentNo, operation string, configID uint, amount int64) (*model.IntegrationLog, error) {
resourceID := paymentNo
func (a *AlipayWapAdapter) startAttempt(ctx context.Context, request agentrecharge.OnlinePaymentRequest, operation string, configID uint, amount int64) (*model.IntegrationLog, error) {
resourceID, resourceKey := strconv.FormatUint(uint64(request.PaymentID), 10), request.PaymentNo
series := "agent-recharge-payment:" + resourceID + ":" + operation
correlationID := request.CorrelationID
return a.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderAlipay, Direction: constants.IntegrationDirectionOutbound,
Operation: operation, ResourceType: constants.IntegrationResourceTypeAgentRechargePayment,
ResourceID: &resourceID, ExternalID: &resourceID,
ResourceID: &resourceID, ResourceKey: &resourceKey, ExternalID: &resourceKey,
TriggerSeries: &series, CorrelationID: &correlationID,
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 {
func (a *AlipayWapAdapter) completeUnknown(ctx context.Context, integrationID string, startedAt time.Time, cause error) error {
if a.logger != nil {
a.logger.Warn("支付宝支付请求结果未知", zap.String("integration_id", integrationID), zap.Error(cause))
}
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", ProviderMessage: cause.Error(),
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", ProviderMessage: "支付宝支付请求结果未知",
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
RecoveryStrategy: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
})

View File

@@ -1,171 +0,0 @@
// 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
}
}

View File

@@ -0,0 +1,208 @@
// Package payment 提供代理在线充值使用的支付渠道薄适配器。
package payment
import (
"context"
"strconv"
"time"
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
sdkpayment "github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
"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"
)
// WechatWebAdapter 按当前支付配置生成微信 H5/MWEB 支付链接并查单。
type WechatWebAdapter struct {
cache kernel.CacheInterface
integration *integrationlog.Repository
logger *zap.Logger
}
// NewWechatWebAdapter 创建微信 H5/MWEB 支付适配器。
func NewWechatWebAdapter(cache kernel.CacheInterface, integration *integrationlog.Repository, logger *zap.Logger) *WechatWebAdapter {
return &WechatWebAdapter{cache: cache, integration: integration, logger: logger}
}
// Available 判断当前协议配置是否完整支持 H5/MWEB 下单、验签与查单。
func (a *WechatWebAdapter) Available(config *model.WechatConfig) bool {
return wechatConfigComplete(config, true) || wechatV2ConfigComplete(config, true)
}
// CreatePaymentURL 按当前协议生成微信 H5 或 MWEB 支付链接。
func (a *WechatWebAdapter) CreatePaymentURL(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentResult, error) {
attempt, err := a.startAttempt(ctx, request, constants.IntegrationOperationPaymentPreCreate, request.Config.ID, request.Amount)
if err != nil {
return agentrecharge.OnlinePaymentResult{}, err
}
startedAt := time.Now()
response, callErr := a.createH5Order(ctx, request, &wechatpay.H5SceneInfo{
PayerClientIP: request.PayerClientIP,
H5Type: "Wap",
})
if callErr != nil {
return agentrecharge.OnlinePaymentResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
}
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.H5URL}, nil
}
// Query 按创建支付单时的协议查询微信支付状态。
func (a *WechatWebAdapter) Query(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentQueryResult, error) {
attempt, err := a.startAttempt(ctx, request, constants.IntegrationOperationPaymentQuery, request.Config.ID, 0)
if err != nil {
return agentrecharge.OnlinePaymentQueryResult{}, err
}
startedAt := time.Now()
info, callErr := a.queryOrder(ctx, request.Config, request.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, ok := parseWechatPaidAt(info.SuccessTime); ok {
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 parseWechatPaidAt(value string) (time.Time, bool) {
for _, layout := range []string{time.RFC3339, "20060102150405"} {
paidAt, err := time.ParseInLocation(layout, value, time.Local)
if err == nil {
return paidAt, true
}
}
return time.Time{}, false
}
func (a *WechatWebAdapter) paymentApp(config *model.WechatConfig) (*sdkpayment.Payment, error) {
if !wechatConfigComplete(config, false) {
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "微信 H5 支付配置不可用")
}
app, err := wechatpay.NewPaymentAppFromConfig(config, config.OaAppID, a.cache, a.logger)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信 H5 支付配置不可用")
}
return app, nil
}
func (a *WechatWebAdapter) createH5Order(ctx context.Context, request agentrecharge.OnlinePaymentRequest, sceneInfo *wechatpay.H5SceneInfo) (*wechatpay.H5PayResult, error) {
switch request.Config.ProviderType {
case model.ProviderTypeWechat:
app, err := a.paymentApp(request.Config)
if err != nil {
return nil, err
}
return wechatpay.NewPaymentService(app, a.logger).CreateH5Order(ctx, request.PaymentNo, request.Description, int(request.Amount), sceneInfo)
case model.ProviderTypeWechatV2:
service, err := wechatpay.NewPaymentV2ServiceFromConfig(request.Config, request.Config.OaAppID, a.logger)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信 MWEB 支付配置不可用")
}
return service.CreateH5Order(ctx, request.PaymentNo, request.Description, int(request.Amount), sceneInfo)
default:
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "当前支付渠道不支持微信网页支付")
}
}
func (a *WechatWebAdapter) queryOrder(ctx context.Context, config *model.WechatConfig, paymentNo string) (*wechatpay.OrderInfo, error) {
switch config.ProviderType {
case model.ProviderTypeWechat:
app, err := a.queryPaymentApp(config)
if err != nil {
return nil, err
}
return wechatpay.NewPaymentService(app, a.logger).QueryOrder(ctx, paymentNo)
case model.ProviderTypeWechatV2:
service, err := wechatpay.NewPaymentV2ServiceFromConfig(config, config.OaAppID, a.logger)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信 v2 查单配置不可用")
}
return service.QueryOrder(ctx, paymentNo)
default:
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "当前支付渠道不支持微信查单")
}
}
func (a *WechatWebAdapter) 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 wechatV2ConfigComplete(config *model.WechatConfig, requireActive bool) bool {
return config != nil && (!requireActive || config.IsActive) && config.ProviderType == model.ProviderTypeWechatV2 &&
config.OaAppID != "" && config.WxMchID != "" && config.WxAPIV2Key != "" && config.WxNotifyURL != ""
}
func (a *WechatWebAdapter) startAttempt(ctx context.Context, request agentrecharge.OnlinePaymentRequest, operation string, configID uint, amount int64) (*model.IntegrationLog, error) {
resourceID, resourceKey := strconv.FormatUint(uint64(request.PaymentID), 10), request.PaymentNo
series := "agent-recharge-payment:" + resourceID + ":" + operation
correlationID := request.CorrelationID
return a.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWechatPay, Direction: constants.IntegrationDirectionOutbound,
Operation: operation, ResourceType: constants.IntegrationResourceTypeAgentRechargePayment,
ResourceID: &resourceID, ResourceKey: &resourceKey, ExternalID: &resourceKey,
TriggerSeries: &series, CorrelationID: &correlationID,
RequestSummary: map[string]any{"payment_config_id": configID, "amount": amount},
})
}
func (a *WechatWebAdapter) completeUnknown(ctx context.Context, integrationID string, startedAt time.Time, cause error) error {
if a.logger != nil {
a.logger.Warn("微信支付请求结果未知", zap.String("integration_id", integrationID), zap.Error(cause))
}
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", ProviderMessage: "微信支付请求结果未知",
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
}
}