Files

140 lines
5.3 KiB
Go

// 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")
}