137 lines
5.0 KiB
Go
137 lines
5.0 KiB
Go
package payments
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
|
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
|
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
|
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
|
wechatpayments "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
|
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
|
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
|
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
|
)
|
|
|
|
type wechatProvider struct {
|
|
service native.NativeApiService
|
|
refunds refunddomestic.RefundsApiService
|
|
notifier *notify.Handler
|
|
config Config
|
|
}
|
|
|
|
func NewWeChatProvider(config Config) (Provider, error) {
|
|
if config.MchID == "" || config.AppID == "" || config.NotifyURL == "" || config.MerchantSerial == "" || config.APIv3Key == "" || config.WechatPublicKeyID == "" || config.PrivateKeyPEM == "" || config.WechatPublicKeyPEM == "" {
|
|
return nil, fmt.Errorf("wechat payment configuration incomplete")
|
|
}
|
|
if len(config.APIv3Key) != 32 {
|
|
return nil, fmt.Errorf("APIv3 key must be 32 bytes")
|
|
}
|
|
privateKey, err := utils.LoadPrivateKey(config.PrivateKeyPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("merchant private key: %w", err)
|
|
}
|
|
publicKey, err := utils.LoadPublicKey(config.WechatPublicKeyPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wechat public key: %w", err)
|
|
}
|
|
client, err := core.NewClient(context.Background(),
|
|
option.WithWechatPayPublicKeyAuthCipher(config.MchID, config.MerchantSerial, privateKey, config.WechatPublicKeyID, publicKey),
|
|
option.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wechat client: %w", err)
|
|
}
|
|
return &wechatProvider{service: native.NativeApiService{Client: client}, refunds: refunddomestic.RefundsApiService{Client: client},
|
|
notifier: notify.NewNotifyHandler(config.APIv3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(config.WechatPublicKeyID, *publicKey)),
|
|
config: config}, nil
|
|
}
|
|
|
|
func (p *wechatProvider) Create(ctx context.Context, order Order) (string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 12*time.Second)
|
|
defer cancel()
|
|
resp, _, err := p.service.Prepay(ctx, native.PrepayRequest{
|
|
Appid: &p.config.AppID, Mchid: &p.config.MchID, Description: ptr("智念AIGC平台使用额度"), OutTradeNo: &order.ID,
|
|
TimeExpire: &order.ExpiresAt, NotifyUrl: &p.config.NotifyURL,
|
|
Amount: &native.Amount{Total: &order.AmountFen, Currency: ptr("CNY")},
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp == nil || resp.CodeUrl == nil || !validCodeURL(*resp.CodeUrl) {
|
|
return "", ErrVerification
|
|
}
|
|
return *resp.CodeUrl, nil
|
|
}
|
|
|
|
func (p *wechatProvider) Query(ctx context.Context, id string) (Transaction, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 12*time.Second)
|
|
defer cancel()
|
|
resp, _, err := p.service.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{OutTradeNo: &id, Mchid: &p.config.MchID})
|
|
if err != nil {
|
|
var apiErr *core.APIError
|
|
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
|
return Transaction{}, ErrRemoteNotFound
|
|
}
|
|
return Transaction{}, err
|
|
}
|
|
return transactionFromSDK(resp)
|
|
}
|
|
|
|
func (p *wechatProvider) Close(ctx context.Context, id string) error {
|
|
ctx, cancel := context.WithTimeout(ctx, 12*time.Second)
|
|
defer cancel()
|
|
_, err := p.service.CloseOrder(ctx, native.CloseOrderRequest{OutTradeNo: &id, Mchid: &p.config.MchID})
|
|
return err
|
|
}
|
|
|
|
func (p *wechatProvider) ParseNotification(ctx context.Context, request *http.Request) (Transaction, error) {
|
|
if request == nil || request.Body == nil {
|
|
return Transaction{}, ErrVerification
|
|
}
|
|
request.Body = http.MaxBytesReader(nil, request.Body, 128*1024)
|
|
var sdkTxn wechatpayments.Transaction
|
|
n, err := p.notifier.ParseNotifyRequest(ctx, request, &sdkTxn)
|
|
if err != nil {
|
|
return Transaction{}, fmt.Errorf("%w: %v", ErrVerification, err)
|
|
}
|
|
if n.EventType != "TRANSACTION.SUCCESS" || n.ResourceType != "encrypt-resource" {
|
|
return Transaction{}, ErrVerification
|
|
}
|
|
return transactionFromSDK(&sdkTxn)
|
|
}
|
|
|
|
func transactionFromSDK(t *wechatpayments.Transaction) (Transaction, error) {
|
|
if t == nil || t.OutTradeNo == nil || t.TradeState == nil || t.Mchid == nil || t.Appid == nil {
|
|
return Transaction{}, ErrVerification
|
|
}
|
|
txn := Transaction{OrderID: *t.OutTradeNo, MchID: *t.Mchid, AppID: *t.Appid, TradeState: *t.TradeState}
|
|
if t.TransactionId != nil {
|
|
txn.TransactionID = *t.TransactionId
|
|
}
|
|
if t.Amount != nil {
|
|
if t.Amount.Total != nil {
|
|
txn.AmountFen = *t.Amount.Total
|
|
}
|
|
if t.Amount.Currency != nil {
|
|
txn.Currency = *t.Amount.Currency
|
|
}
|
|
}
|
|
if txn.TradeState == "SUCCESS" {
|
|
if txn.TransactionID == "" || txn.AmountFen <= 0 || txn.Currency != "CNY" || t.SuccessTime == nil {
|
|
return Transaction{}, ErrVerification
|
|
}
|
|
parsed, err := time.Parse(time.RFC3339, *t.SuccessTime)
|
|
if err != nil {
|
|
return Transaction{}, ErrVerification
|
|
}
|
|
txn.SuccessTime = parsed
|
|
}
|
|
return txn, nil
|
|
}
|
|
|
|
func ptr(s string) *string { return &s }
|