Files
NianAIGC/backend/internal/payments/wechat.go
T
2026-10-12 13:57:23 +08:00

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 }