All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m17s
extractPaymentResult 只处理 map[string]any,而 V2 自建实现返回 map[string]string,类型断言静默失败导致所有字段为空字符串。 增加对 map[string]string 的处理分支,兼容 V1 和 V2 两种支付渠道。
185 lines
6.0 KiB
Go
185 lines
6.0 KiB
Go
package client_order
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
|
||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||
)
|
||
|
||
// PaymentProvider C 端支付提供者接口。
|
||
// 抽象不同支付渠道(微信、富友等)的预下单能力。
|
||
type PaymentProvider interface {
|
||
// CreateJSAPIPayment 创建 JSAPI 预下单,返回前端调起支付的参数。
|
||
CreateJSAPIPayment(ctx context.Context, orderNo, description, openID string, amount int) (*PaymentResult, error)
|
||
}
|
||
|
||
// PaymentResult 预下单结果,前端用这些参数调起支付。
|
||
type PaymentResult struct {
|
||
AppID string
|
||
TimeStamp string
|
||
NonceStr string
|
||
Package string
|
||
SignType string
|
||
PaySign string
|
||
}
|
||
|
||
// wechatPaymentProvider 微信支付适配器。
|
||
type wechatPaymentProvider struct {
|
||
paymentService *wechat.PaymentService
|
||
}
|
||
|
||
// CreateJSAPIPayment 创建微信 JSAPI 预下单。
|
||
func (w *wechatPaymentProvider) CreateJSAPIPayment(ctx context.Context, orderNo, description, openID string, amount int) (*PaymentResult, error) {
|
||
result, err := w.paymentService.CreateJSAPIOrder(ctx, orderNo, description, openID, amount)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return extractPaymentResult(result.PayConfig), nil
|
||
}
|
||
|
||
// wechatV2PaymentProvider 微信支付 v2 适配器。
|
||
type wechatV2PaymentProvider struct {
|
||
paymentService *wechat.PaymentV2Service
|
||
}
|
||
|
||
// CreateJSAPIPayment 创建微信 v2 JSAPI 预下单。
|
||
func (w *wechatV2PaymentProvider) CreateJSAPIPayment(ctx context.Context, orderNo, description, openID string, amount int) (*PaymentResult, error) {
|
||
result, err := w.paymentService.CreateJSAPIOrder(ctx, orderNo, description, openID, amount)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return extractPaymentResult(result.PayConfig), nil
|
||
}
|
||
|
||
// fuiouPaymentProvider 富友支付适配器。
|
||
type fuiouPaymentProvider struct {
|
||
client *fuiou.Client
|
||
appID string
|
||
tradeType string
|
||
}
|
||
|
||
// CreateJSAPIPayment 创建富友 JSAPI 预下单。
|
||
func (f *fuiouPaymentProvider) CreateJSAPIPayment(ctx context.Context, orderNo, description, openID string, amount int) (*PaymentResult, error) {
|
||
amountStr := strconv.Itoa(amount)
|
||
resp, err := f.client.WxPreCreate(orderNo, amountStr, description, fuiou.GetServerIP(), f.tradeType, f.appID, openID)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "富友预下单失败")
|
||
}
|
||
|
||
return &PaymentResult{
|
||
AppID: resp.SdkAppid,
|
||
TimeStamp: resp.SdkTimestamp,
|
||
NonceStr: resp.SdkNoncestr,
|
||
Package: resp.SdkPackage,
|
||
SignType: resp.SdkSigntype,
|
||
PaySign: resp.SdkPaysign,
|
||
}, nil
|
||
}
|
||
|
||
// newPaymentProvider 根据支付配置创建支付提供者。
|
||
// appType: "official_account" 或 "miniapp"
|
||
func (s *Service) newPaymentProvider(config *model.WechatConfig, appID, appType string) (PaymentProvider, error) {
|
||
switch config.ProviderType {
|
||
case model.ProviderTypeFuiou:
|
||
tradeType := "JSAPI"
|
||
if appType == "miniapp" {
|
||
tradeType = "LETPAY"
|
||
}
|
||
client, err := fuiou.NewClient(
|
||
config.FyInsCd,
|
||
config.FyMchntCd,
|
||
config.FyTermID,
|
||
config.FyAPIURL,
|
||
config.FyNotifyURL,
|
||
config.FyPrivateKey,
|
||
config.FyPublicKey,
|
||
s.logger,
|
||
)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "创建富友客户端失败")
|
||
}
|
||
return &fuiouPaymentProvider{client: client, appID: appID, tradeType: tradeType}, nil
|
||
case model.ProviderTypeWechatV2:
|
||
svc, err := wechat.NewPaymentV2ServiceFromConfig(config, appID, s.logger)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "创建微信支付 v2 应用失败")
|
||
}
|
||
return &wechatV2PaymentProvider{paymentService: svc}, nil
|
||
default:
|
||
cache := wechat.NewRedisCache(s.redis)
|
||
paymentApp, err := wechat.NewPaymentAppFromConfig(config, appID, cache, s.logger)
|
||
if err != nil {
|
||
return nil, errors.Wrap(errors.CodeWechatPayFailed, err, "创建微信支付应用失败")
|
||
}
|
||
return &wechatPaymentProvider{paymentService: wechat.NewPaymentService(paymentApp, s.logger)}, nil
|
||
}
|
||
}
|
||
|
||
// buildClientPayConfigFromResult 将预下单结果转换为客户端支付参数。
|
||
func buildClientPayConfigFromResult(result *PaymentResult) *dto.ClientPayConfig {
|
||
if result == nil {
|
||
result = &PaymentResult{}
|
||
}
|
||
|
||
return &dto.ClientPayConfig{
|
||
AppID: result.AppID,
|
||
Timestamp: result.TimeStamp,
|
||
NonceStr: result.NonceStr,
|
||
PackageVal: result.Package,
|
||
SignType: result.SignType,
|
||
PaySign: result.PaySign,
|
||
}
|
||
}
|
||
|
||
// extractPaymentResult 从微信 SDK 的 map 配置中提取统一支付参数。
|
||
// 支持两种类型:
|
||
// - map[string]any:V1 标准微信 SDK(PowerWeChat)返回
|
||
// - map[string]string:V2 自建实现返回
|
||
func extractPaymentResult(payConfig any) *PaymentResult {
|
||
if configMap, ok := payConfig.(map[string]any); ok {
|
||
return &PaymentResult{
|
||
AppID: payFirstNonEmpty(payStringFromAny(configMap["appId"])),
|
||
TimeStamp: payFirstNonEmpty(payStringFromAny(configMap["timeStamp"]), payStringFromAny(configMap["timestamp"])),
|
||
NonceStr: payStringFromAny(configMap["nonceStr"]),
|
||
Package: payStringFromAny(configMap["package"]),
|
||
SignType: payStringFromAny(configMap["signType"]),
|
||
PaySign: payStringFromAny(configMap["paySign"]),
|
||
}
|
||
}
|
||
// V2 自建实现返回 map[string]string,原 map[string]any 断言会失败导致字段全空
|
||
if configMap, ok := payConfig.(map[string]string); ok {
|
||
return &PaymentResult{
|
||
AppID: configMap["appId"],
|
||
TimeStamp: payFirstNonEmpty(configMap["timeStamp"], configMap["timestamp"]),
|
||
NonceStr: configMap["nonceStr"],
|
||
Package: configMap["package"],
|
||
SignType: configMap["signType"],
|
||
PaySign: configMap["paySign"],
|
||
}
|
||
}
|
||
return &PaymentResult{}
|
||
}
|
||
|
||
func payStringFromAny(value any) string {
|
||
if value == nil {
|
||
return ""
|
||
}
|
||
return fmt.Sprint(value)
|
||
}
|
||
|
||
func payFirstNonEmpty(values ...string) string {
|
||
for _, value := range values {
|
||
if strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|