feat: integrate unified payment and wallet refunds
This commit is contained in:
139
backend/api/internal/logic/payment/provider.go
Normal file
139
backend/api/internal/logic/payment/provider.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Package payment 统一承载支付宝、微信和钱包支付请求及渠道回调。
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/smartwalle/alipay/v3"
|
||||
wechatcore "github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
wechatapp "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app"
|
||||
wechatjsapi "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||||
wechatnative "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
var ErrChannelUnavailable = errors.New("payment channel is not configured")
|
||||
|
||||
func money(amount int64) string { return fmt.Sprintf("%d.%02d", amount/100, amount%100) }
|
||||
|
||||
func createChannelOrder(ctx context.Context, order models.PaymentOrder, openID string) (string, error) {
|
||||
switch order.Channel {
|
||||
case "alipay":
|
||||
return createAlipayOrder(order)
|
||||
case "wechat":
|
||||
return createWechatOrder(ctx, order, openID)
|
||||
default:
|
||||
return "", errors.New("unsupported payment channel")
|
||||
}
|
||||
}
|
||||
|
||||
func alipayClient() (*alipay.Client, error) {
|
||||
cfg := config.Spec.Payment.Alipay
|
||||
if !cfg.Enabled || cfg.AppID == "" || cfg.PrivateKeyPath == "" {
|
||||
return nil, ErrChannelUnavailable
|
||||
}
|
||||
privateKey, err := os.ReadFile(cfg.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, err := alipay.New(cfg.AppID, string(privateKey), cfg.Production)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.LoadAppCertPublicKeyFromFile(cfg.AppPublicCertPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.LoadAliPayRootCertFromFile(cfg.AlipayRootCertPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = client.LoadAlipayCertPublicKeyFromFile(cfg.AlipayPublicCertPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func createAlipayOrder(order models.PaymentOrder) (string, error) {
|
||||
client, err := alipayClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
trade := alipay.Trade{NotifyURL: config.Spec.Payment.Alipay.NotifyURL, ReturnURL: config.Spec.Payment.Alipay.ReturnURL,
|
||||
Subject: order.Subject, OutTradeNo: order.PaymentNo, TotalAmount: money(order.Amount), TimeoutExpress: fmt.Sprintf("%dm", config.Spec.Payment.ExpireMinutes)}
|
||||
if order.PayType == "app" {
|
||||
trade.ProductCode = "QUICK_MSECURITY_PAY"
|
||||
return client.TradeAppPay(alipay.TradeAppPay{Trade: trade})
|
||||
}
|
||||
if order.PayType == "wap" {
|
||||
trade.ProductCode = "QUICK_WAP_WAY"
|
||||
value, err := client.TradeWapPay(alipay.TradeWapPay{Trade: trade})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value.String(), nil
|
||||
}
|
||||
return "", errors.New("unsupported alipay product")
|
||||
}
|
||||
|
||||
func wechatClient(ctx context.Context) (*wechatcore.Client, error) {
|
||||
cfg := config.Spec.Payment.Wechat
|
||||
if !cfg.Enabled || cfg.MerchantID == "" || cfg.MerchantPrivateKeyPath == "" || len(cfg.APIv3Key) != 32 {
|
||||
return nil, ErrChannelUnavailable
|
||||
}
|
||||
key, err := utils.LoadPrivateKeyWithPath(cfg.MerchantPrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wechatcore.NewClient(ctx, option.WithWechatPayAutoAuthCipher(cfg.MerchantID, cfg.MerchantCertificateSerial, key, cfg.APIv3Key))
|
||||
}
|
||||
|
||||
func createWechatOrder(ctx context.Context, order models.PaymentOrder, openID string) (string, error) {
|
||||
cfg := config.Spec.Payment.Wechat
|
||||
client, err := wechatClient(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
marshal := func(value any) (string, error) { raw, err := json.Marshal(value); return string(raw), err }
|
||||
switch order.PayType {
|
||||
case "app":
|
||||
resp, _, err := (&wechatapp.AppApiService{Client: client}).PrepayWithRequestPayment(ctx, wechatapp.PrepayRequest{
|
||||
Appid: wechatcore.String(cfg.AppAppID), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject),
|
||||
OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL), Amount: &wechatapp.Amount{Total: wechatcore.Int64(order.Amount)}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return marshal(resp)
|
||||
case "jsapi", "mini":
|
||||
if strings.TrimSpace(openID) == "" {
|
||||
return "", errors.New("openid is required")
|
||||
}
|
||||
appid := cfg.OfficialAccountAppID
|
||||
if order.PayType == "mini" {
|
||||
appid = cfg.MiniProgramAppID
|
||||
}
|
||||
resp, _, err := (&wechatjsapi.JsapiApiService{Client: client}).PrepayWithRequestPayment(ctx, wechatjsapi.PrepayRequest{
|
||||
Appid: wechatcore.String(appid), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject),
|
||||
OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL),
|
||||
Amount: &wechatjsapi.Amount{Total: wechatcore.Int64(order.Amount)}, Payer: &wechatjsapi.Payer{Openid: wechatcore.String(openID)}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return marshal(resp)
|
||||
case "native":
|
||||
resp, _, err := (&wechatnative.NativeApiService{Client: client}).Prepay(ctx, wechatnative.PrepayRequest{
|
||||
Appid: wechatcore.String(cfg.AppAppID), Mchid: wechatcore.String(cfg.MerchantID), Description: wechatcore.String(order.Subject),
|
||||
OutTradeNo: wechatcore.String(order.PaymentNo), TimeExpire: &order.ExpiresAt, NotifyUrl: wechatcore.String(cfg.NotifyURL), Amount: &wechatnative.Amount{Total: wechatcore.Int64(order.Amount)}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return marshal(resp)
|
||||
}
|
||||
return "", errors.New("unsupported wechat product")
|
||||
}
|
||||
Reference in New Issue
Block a user