This commit is contained in:
58
backend_v1/providers/wepay/config.go
Normal file
58
backend_v1/providers/wepay/config.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package wepay
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
const DefaultPrefix = "WePay"
|
||||
|
||||
func DefaultProvider() container.ProviderContainer {
|
||||
return container.ProviderContainer{
|
||||
Provider: Provide,
|
||||
Options: []opt.Option{
|
||||
opt.Prefix(DefaultPrefix),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type PayNotify struct {
|
||||
Mchid string `json:"mchid"`
|
||||
Appid string `json:"appid"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
TradeType string `json:"trade_type"`
|
||||
TradeState string `json:"trade_state"`
|
||||
TradeStateDesc string `json:"trade_state_desc"`
|
||||
BankType string `json:"bank_type"`
|
||||
Attach string `json:"attach"`
|
||||
SuccessTime time.Time `json:"success_time"`
|
||||
Payer struct {
|
||||
Openid string `json:"openid"`
|
||||
} `json:"payer"`
|
||||
Amount struct {
|
||||
Total int64 `json:"total"`
|
||||
PayerTotal int64 `json:"payer_total"`
|
||||
Currency string `json:"currency"`
|
||||
PayerCurrency string `json:"payer_currency"`
|
||||
} `json:"amount"`
|
||||
}
|
||||
|
||||
type RefundNotify struct {
|
||||
Mchid string `json:"mchid"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
OutTradeNo string `json:"out_trade_no"`
|
||||
RefundID string `json:"refund_id"`
|
||||
OutRefundNo string `json:"out_refund_no"`
|
||||
RefundStatus string `json:"refund_status"`
|
||||
SuccessTime time.Time `json:"success_time"`
|
||||
UserReceivedAccount string `json:"user_received_account"`
|
||||
Amount struct {
|
||||
Total int `json:"total"`
|
||||
Refund int `json:"refund"`
|
||||
PayerTotal int `json:"payer_total"`
|
||||
PayerRefund int `json:"payer_refund"`
|
||||
} `json:"amount"`
|
||||
}
|
||||
330
backend_v1/providers/wepay/pay.go
Normal file
330
backend_v1/providers/wepay/pay.go
Normal file
@@ -0,0 +1,330 @@
|
||||
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"
|
||||
)
|
||||
76
backend_v1/providers/wepay/pay_test.go
Normal file
76
backend_v1/providers/wepay/pay_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package wepay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"quyun/app/service/testx"
|
||||
|
||||
"github.com/go-pay/gopay/wechat/v3"
|
||||
"github.com/go-pay/util/js"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
type WePayInjectParams struct {
|
||||
dig.In
|
||||
Initials []contracts.Initial `group:"initials"`
|
||||
|
||||
Client *Client
|
||||
}
|
||||
|
||||
type WePayTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
WePayInjectParams
|
||||
}
|
||||
|
||||
func Test_WePay(t *testing.T) {
|
||||
providers := testx.Default().With(Provide)
|
||||
testx.Serve(providers, t, func(params WePayInjectParams) {
|
||||
suite.Run(t, &WePayTestSuite{WePayInjectParams: params})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WePayTestSuite) Test_PrePay() {
|
||||
Convey("get prepay", s.T(), func() {
|
||||
Convey("prepay", func() {
|
||||
resp, err := s.Client.V3TransactionJsapi(context.Background(), func(bm *BodyMap) {
|
||||
bm.
|
||||
OutTradeNo(fmt.Sprintf("test_trade_no_%d", time.Now().Unix())).
|
||||
Description("Test transaction").
|
||||
Payer("o5Bzk644x3LOMJsKSZRlqWin74IU")
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
s.T().Logf("prepay response: %+v", resp)
|
||||
|
||||
sign, err := resp.PaySignOfJSAPI()
|
||||
So(err, ShouldBeNil)
|
||||
s.T().Logf("Sign: %+v", sign)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WePayTestSuite) Test_parseNotify() {
|
||||
Convey("parse notify", s.T(), func() {
|
||||
Convey("prepay", func() {
|
||||
content := `{"id":"43d17a94-eb1e-5641-bb11-f59e5b6e8749","summary":"支付成功","resource":{"nonce":"avbpSc2seCN5","algorithm":"AEAD_AES_256_GCM","ciphertext":"20VGA2uItmbqFvGBxBug2K3eORRyy/xYswoDA7v4+Yi2ArHnXCXzScVn6kD3ZVpKLiFY7zcTPpTxk2JFJF3vG/6WGG7uuD8DDK7keJk0PZoAfvmSPskQzieOVz3Tgmqp3SkE74mJHX1MeMZHMXMmzMJ4Mp1OmYD2YpiWsF7jlAtiGqxHSC//YlKGaJ/9r0QG4TwZcFpm+X4qkdBNX+DcSCjYeXGyWIm2bVujj63rO43DEA5x0nytdBSrpup/T85khZzNVue1EcyF5XY7PguePU3Q2o+e1c/LnoL9nN7S+n2ljm+nN3uCAhz8eqkPn4uowiq37Tw4JZ2rx2rXCb9jYKmt+I8JHpOij4SgX6oQd7fLeZHsbHC/05s0A1qdLzeF5AKgrAOQT/T1yQ+LsWTnY2ftXAP6mnqGE8Z+vQm5PGo8xsQ8AycVaAhwaRLFvn/XtwlkumfuduAojimFRSNElWwHcApnT+ekqzBrKnAvKo8hdeygf9QWHENcNWVwqwjUWIHe/fGWgJbc6u595bEHb4MkcI8ESD/6bpay/Wk6SyvZCJHqS1WWaPaU0xh9","original_type":"transaction","associated_data":"transaction"},"event_type":"TRANSACTION.SUCCESS","create_time":"2025-04-30T19:25:51+08:00","resource_type":"encrypt-resource"}`
|
||||
var notifyReq wechat.V3NotifyReq
|
||||
err := js.UnmarshalBytes([]byte(content), ¬ifyReq)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
s.T().Logf("notifyReq: %+v", notifyReq)
|
||||
|
||||
var obj struct{}
|
||||
err = notifyReq.DecryptCipherTextToStruct("5UBDkxVDY44AKafkqN6YgYxgtkXP6Mw6", &obj)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
s.T().Logf("Decrypted object: %+v", obj)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user