Files
quyun/backend/providers/wepay/pay.go
2025-05-06 21:16:23 +08:00

331 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package wepay
import (
"context"
"crypto/rsa"
"encoding/json"
"fmt"
"net/http"
"time"
w "quyun/providers/wechat"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
"github.com/go-pay/util/js"
"github.com/gofiber/fiber/v3"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/opt"
)
type Config struct{}
func Provide(opts ...opt.Option) error {
o := opt.New(opts...)
var config Config
if err := o.UnmarshalConfig(&config); err != nil {
return err
}
return container.Container.Provide(func(wechatConfig *w.Config) (*Client, error) {
client, err := wechat.NewClientV3(
wechatConfig.Pay.MchID,
wechatConfig.Pay.SerialNo,
wechatConfig.Pay.ApiV3Key,
wechatConfig.Pay.PrivateKey,
)
if err != nil {
return nil, err
}
client.DebugSwitch = gopay.DebugOff
if wechatConfig.DevMode {
client.DebugSwitch = gopay.DebugOn
}
err = client.AutoVerifySignByPublicKey([]byte(wechatConfig.Pay.PublicKey), wechatConfig.Pay.PublicKeyID)
if err != nil {
return nil, errors.Wrap(err, "AutoVerifySignByPublicKey")
}
return &Client{
payClient: client,
config: wechatConfig,
}, nil
}, o.DiOptions()...)
}
type Client struct {
payClient *wechat.ClientV3
config *w.Config
}
func (c *Client) GetClient() *wechat.ClientV3 {
return c.payClient
}
// WxPublicKeyMap
func (c *Client) WxPublicKeyMap() map[string]*rsa.PublicKey {
return c.payClient.WxPublicKeyMap()
}
type PrepayData struct {
client *Client
AppID string `json:"app_id"`
PrepayID string `json:"prepay_id"`
}
// PaySignOfJSAPI
func (pay *PrepayData) PaySignOfJSAPI() (*wechat.JSAPIPayParams, error) {
return pay.client.payClient.PaySignOfJSAPI(pay.AppID, pay.PrepayID)
}
func (c *Client) Refund(ctx context.Context, f func(*BodyMap)) (*wechat.RefundOrderResponse, error) {
bm := NewRefundBodyMap(c.config)
f(bm)
resp, err := c.payClient.V3Refund(ctx, bm.bm)
if err != nil {
return nil, err
}
if resp.Code != wechat.Success {
log.Errorf("WePay Refund error: %s", resp.Error)
return nil, errors.New(resp.Error)
}
return resp.Response, nil
}
func (c *Client) V3TransactionJsapi(ctx context.Context, f func(*BodyMap)) (*PrepayData, error) {
bm := NewBodyMap(c.config)
f(bm)
resp, err := c.payClient.V3TransactionJsapi(ctx, bm.bm)
if err != nil {
return nil, err
}
if resp.Code != wechat.Success {
b, _ := json.Marshal(resp)
log.Errorf("WePay V3TransactionJsapi error: %s", b)
return nil, errors.New(resp.Error)
}
return &PrepayData{
client: c,
AppID: c.config.AppID,
PrepayID: resp.Response.PrepayId,
}, nil
}
func (c *Client) ParseNotify(
ctx fiber.Ctx,
payCallback func(fiber.Ctx, *wechat.V3DecryptPayResult) error,
refundCallback func(fiber.Ctx, *wechat.V3DecryptRefundResult) error,
) error {
body := ctx.Body()
si := &wechat.SignInfo{
HeaderTimestamp: ctx.Get(wechat.HeaderTimestamp),
HeaderNonce: ctx.Get(wechat.HeaderNonce),
HeaderSignature: ctx.Get(wechat.HeaderSignature),
HeaderSerial: ctx.Get(wechat.HeaderSerial),
SignBody: string(body),
}
notifyReq := &wechat.V3NotifyReq{SignInfo: si}
if err := js.UnmarshalBytes(body, notifyReq); err != nil {
log.Errorf("json unmarshal error:%v", err)
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": fmt.Sprintf("json unmarshal error:%v", err)})
}
// 获取微信平台证书
certMap := c.WxPublicKeyMap()
// 验证异步通知的签名
if err := notifyReq.VerifySignByPKMap(certMap); err != nil {
log.Errorf("verify sign error:%v", err)
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid signature"})
}
// TRANSACTION.SUCCESS :支付成功通知
// REFUND.SUCCESS退款成功通知
// REFUND.ABNORMAL退款异常通知
// REFUND.CLOSED退款关闭通知
switch notifyReq.EventType {
case "TRANSACTION.SUCCESS":
var notifyData wechat.V3DecryptPayResult
if err := notifyReq.DecryptCipherTextToStruct(c.config.Pay.ApiV3Key, &notifyData); err != nil {
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid cipher text"})
}
log.Infof("Successfully decrypted cipher text for pay notify data: %+v", notifyData)
if err := payCallback(ctx, &notifyData); err != nil {
log.Errorf("payCallback error:%v", err)
return err
}
case "REFUND.SUCCESS", "REFUND.ABNORMAL", "REFUND.CLOSED":
var notifyData wechat.V3DecryptRefundResult
if err := notifyReq.DecryptCipherTextToStruct(c.config.Pay.ApiV3Key, &notifyData); err != nil {
return ctx.Status(http.StatusBadRequest).JSON(fiber.Map{"error": "Invalid cipher text"})
}
log.Infof("Successfully decrypted cipher text for refund notify data: %+v", notifyData)
if err := refundCallback(ctx, &notifyData); err != nil {
log.Errorf("refundCallback error:%v", err)
return err
}
}
return ctx.Status(http.StatusOK).JSON(&wechat.V3NotifyRsp{
Code: gopay.SUCCESS,
Message: "成功",
})
}
type BodyMap struct {
bm gopay.BodyMap
}
func NewRefundBodyMap(c *w.Config) *BodyMap {
bm := make(gopay.BodyMap)
bm.Set("notify_url", c.Pay.NotifyURL)
return &BodyMap{
bm: bm,
}
}
func NewBodyMap(c *w.Config) *BodyMap {
bm := make(gopay.BodyMap)
bm.Set("appid", c.AppID).
Set("mchid", c.Pay.MchID).
Set("notify_url", c.Pay.NotifyURL)
// .
// SetBodyMap("amount", func(bm gopay.BodyMap) {
// bm.Set("total", 1).
// Set("currency", "CNY")
// })
return &BodyMap{
bm: bm,
}
}
func (b *BodyMap) Set(key string, value interface{}) *BodyMap {
b.bm.Set(key, value)
return b
}
func (b *BodyMap) SetBodyMap(key string, f func(bm gopay.BodyMap)) *BodyMap {
b.bm.SetBodyMap(key, f)
return b
}
// Expire time
func (b *BodyMap) Expire(t time.Duration) *BodyMap {
return b.Set("time_expire", time.Now().Add(t).Format(time.RFC3339))
}
// Description
func (b *BodyMap) Description(desc string) *BodyMap {
return b.Set("description", desc)
}
// OutTradeNo
func (b *BodyMap) OutTradeNo(outTradeNo string) *BodyMap {
return b.Set("out_trade_no", outTradeNo)
}
// TransactionID
func (b *BodyMap) TransactionID(transactionID string) *BodyMap {
return b.Set("transaction_id", transactionID)
}
// OutRefundNo
func (b *BodyMap) OutRefundNo(outRefundNo string) *BodyMap {
return b.Set("out_refund_no", outRefundNo)
}
// RefundReason
func (b *BodyMap) RefundReason(refundReason string) *BodyMap {
return b.Set("reason", refundReason)
}
// RefundAmount
func (b *BodyMap) RefundAmount(total, refund int64, currency CURRENCY) *BodyMap {
return b.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.
Set("total", total).
Set("refund", refund).
Set("currency", currency.String())
})
}
func (b *BodyMap) CNYRefundAmount(total, refund int64) *BodyMap {
return b.RefundAmount(total, refund, CNY)
}
type RefundGoodsInfo struct {
MerchantGoodsID string `json:"merchant_goods_id"`
GoodsName string `json:"goods_name"`
RefundQuantity int64 `json:"refund_quantity"`
RefundAmount int64 `json:"refund_amount"`
UnitPrice int64 `json:"unit_price"`
}
// RefundGoodsInfo
func (b *BodyMap) RefundGoods(goods []RefundGoodsInfo) *BodyMap {
return b.Set("goods_detail", goods)
}
// Amount
func (b *BodyMap) Amount(total int64, currency CURRENCY) *BodyMap {
return b.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.
Set("total", total).
Set("currency", currency.String())
})
}
func (b *BodyMap) CNYAmount(total int64) *BodyMap {
return b.Amount(total, CNY)
}
type GoodsInfo struct {
MerchantGoodsID string `json:"merchant_goods_id"`
GoodsName string `json:"goods_name"`
Quantity int64 `json:"quantity"`
UnitPrice int64 `json:"unit_price"`
}
func (b *BodyMap) Detail(goods []GoodsInfo) *BodyMap {
return b.SetBodyMap("detail", func(bm gopay.BodyMap) {
bm.Set("goods_detail", goods)
})
}
// Payer
func (b *BodyMap) Payer(spOpenId string) *BodyMap {
return b.SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", spOpenId)
})
}
// SubMchId
func (b *BodyMap) SubMchId(subMchId string) *BodyMap {
return b.Set("sub_mchid", subMchId)
}
type CURRENCY string
func (c CURRENCY) String() string {
return string(c)
}
const (
CNY CURRENCY = "CNY"
USD CURRENCY = "USD"
EUR CURRENCY = "EUR"
)