package wepay import ( "context" "crypto/rsa" "encoding/json" "fmt" "net/http" "time" w "quyun/v2/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, ¬ifyData); 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, ¬ifyData); 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, ¬ifyData); 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, ¬ifyData); 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" )