feat: support WeChat Pay via JSAPI (#2488)

* feat: support wechat jsapi payment

* feat: add log

* feat: update sign

* feat: process wechat pay result

* feat: process wechat pay result

* feat: save wechat openid for different app

* feat: save wechat openid for different app

* feat: add SetUserOAuthProperties for signup

* feat: fix openid for wechat

* feat: get user extra property in buyproduct

* feat: remove log

* feat: remove log

* feat: gofumpt code

* feat: change lr->crlf

* feat: change crlf->lf

* feat: improve code
This commit is contained in:
haiwu
2023-11-11 17:16:57 +08:00
committed by GitHub
parent d090e9c860
commit 0ac2b69f5a
18 changed files with 324 additions and 97 deletions

View File

@ -49,20 +49,24 @@ func NewAlipayPaymentProvider(appId string, appCertificate string, appPrivateKey
return pp, nil
}
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *AlipayPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
pp.Client.SetReturnUrl(returnUrl)
pp.Client.SetNotifyUrl(notifyUrl)
bm.Set("subject", joinAttachString([]string{productName, productDisplayName, providerName}))
bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", priceFloat64ToString(price))
pp.Client.SetReturnUrl(r.ReturnUrl)
pp.Client.SetNotifyUrl(r.NotifyUrl)
bm.Set("subject", joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName}))
bm.Set("out_trade_no", r.PaymentName)
bm.Set("total_amount", priceFloat64ToString(r.Price))
payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil {
return "", "", err
return nil, err
}
return payUrl, paymentName, nil
payResp := &PayResp{
PayUrl: payUrl,
OrderId: r.PaymentName,
}
return payResp, nil
}
func (pp *AlipayPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -21,8 +21,10 @@ func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
return pp, nil
}
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
return returnUrl, "", nil
func (pp *DummyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
return &PayResp{
PayUrl: r.ReturnUrl,
}, nil
}
func (pp *DummyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -153,22 +153,22 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
return respBytes, nil
}
func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *GcPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(),
OrderNo: paymentName,
Amount: getPriceString(price),
OrderNo: r.PaymentName,
Amount: getPriceString(r.Price),
Xmpch: pp.Xmpch,
Body: productDisplayName,
ReturnUrl: returnUrl,
NotifyUrl: notifyUrl,
Remark1: payerName,
Remark2: productName,
Body: r.ProductDisplayName,
ReturnUrl: r.ReturnUrl,
NotifyUrl: r.NotifyUrl,
Remark1: r.PayerName,
Remark2: r.ProductName,
}
b, err := json.Marshal(payReqInfo)
if err != nil {
return "", "", err
return nil, err
}
body := GcRequestBody{
@ -184,36 +184,38 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", "", err
return nil, err
}
respBytes, err := pp.doPost(bodyBytes)
if err != nil {
return "", "", err
return nil, err
}
var respBody GcResponseBody
err = json.Unmarshal(respBytes, &respBody)
if err != nil {
return "", "", err
return nil, err
}
if respBody.ReturnCode != "SUCCESS" {
return "", "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
return nil, fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
}
payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
if err != nil {
return "", "", err
return nil, err
}
var payRespInfo GcPayRespInfo
err = json.Unmarshal(payRespInfoBytes, &payRespInfo)
if err != nil {
return "", "", err
return nil, err
}
return payRespInfo.PayUrl, "", nil
payResp := &PayResp{
PayUrl: payRespInfo.PayUrl,
}
return payResp, nil
}
func (pp *GcPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -49,16 +49,16 @@ func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentPro
return pp, nil
}
func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *PaypalPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// https://github.com/go-pay/gopay/blob/main/doc/paypal.md
units := make([]*paypal.PurchaseUnit, 0, 1)
unit := &paypal.PurchaseUnit{
ReferenceId: util.GetRandomString(16),
Amount: &paypal.Amount{
CurrencyCode: currency, // e.g."USD"
Value: priceFloat64ToString(price), // e.g."100.00"
CurrencyCode: r.Currency, // e.g."USD"
Value: priceFloat64ToString(r.Price), // e.g."100.00"
},
Description: joinAttachString([]string{productDisplayName, productName, providerName}),
Description: joinAttachString([]string{r.ProductDisplayName, r.ProductName, r.ProviderName}),
}
units = append(units, unit)
@ -68,23 +68,27 @@ func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, pa
bm.SetBodyMap("application_context", func(b gopay.BodyMap) {
b.Set("brand_name", "Casdoor")
b.Set("locale", "en-PT")
b.Set("return_url", returnUrl)
b.Set("cancel_url", returnUrl)
b.Set("return_url", r.ReturnUrl)
b.Set("cancel_url", r.ReturnUrl)
})
ppRsp, err := pp.Client.CreateOrder(context.Background(), bm)
if err != nil {
return "", "", err
return nil, err
}
if ppRsp.Code != paypal.Success {
return "", "", errors.New(ppRsp.Error)
return nil, errors.New(ppRsp.Error)
}
// {"id":"9BR68863NE220374S","status":"CREATED",
// "links":[{"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"self","method":"GET"},
// {"href":"https://www.sandbox.paypal.com/checkoutnow?token=9BR68863NE220374S","rel":"approve","method":"GET"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"update","method":"PATCH"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S/capture","rel":"capture","method":"POST"}]}
return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil
payResp := &PayResp{
PayUrl: ppRsp.Response.Links[1].Href,
OrderId: ppRsp.Response.Id,
}
return payResp, nil
}
func (pp *PaypalPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -24,6 +24,32 @@ const (
PaymentStateError PaymentState = "Error"
)
const (
PaymentEnvWechatBrowser = "WechatBrowser"
)
type PayReq struct {
ProviderName string
ProductName string
PayerName string
PayerId string
PaymentName string
ProductDisplayName string
Price float64
Currency string
ReturnUrl string
NotifyUrl string
PaymentEnv string
}
type PayResp struct {
PayUrl string
OrderId string
AttachInfo map[string]interface{}
}
type NotifyResult struct {
PaymentName string
PaymentStatus PaymentState
@ -39,7 +65,7 @@ type NotifyResult struct {
}
type PaymentProvider interface {
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error)
Pay(req *PayReq) (*PayResp, error)
Notify(body []byte, orderId string) (*NotifyResult, error)
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
GetResponseError(err error) string

View File

@ -46,30 +46,30 @@ func NewStripePaymentProvider(PublishableKey, SecretKey string) (*StripePaymentP
return pp, nil
}
func (pp *StripePaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (payUrl string, orderId string, err error) {
func (pp *StripePaymentProvider) Pay(r *PayReq) (*PayResp, error) {
// Create a temp product
description := joinAttachString([]string{productName, productDisplayName, providerName})
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
productParams := &stripe.ProductParams{
Name: stripe.String(productDisplayName),
Name: stripe.String(r.ProductDisplayName),
Description: stripe.String(description),
DefaultPriceData: &stripe.ProductDefaultPriceDataParams{
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
Currency: stripe.String(currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(r.Price)),
Currency: stripe.String(r.Currency),
},
}
sProduct, err := stripeProduct.New(productParams)
if err != nil {
return "", "", err
return nil, err
}
// Create a price for an existing product
priceParams := &stripe.PriceParams{
Currency: stripe.String(currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
Currency: stripe.String(r.Currency),
UnitAmount: stripe.Int64(priceFloat64ToInt64(r.Price)),
Product: stripe.String(sProduct.ID),
}
sPrice, err := stripePrice.New(priceParams)
if err != nil {
return "", "", err
return nil, err
}
// Create a Checkout Session
checkoutParams := &stripe.CheckoutSessionParams{
@ -80,17 +80,21 @@ func (pp *StripePaymentProvider) Pay(providerName string, productName string, pa
},
},
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
SuccessURL: stripe.String(returnUrl),
CancelURL: stripe.String(returnUrl),
ClientReferenceID: stripe.String(paymentName),
SuccessURL: stripe.String(r.ReturnUrl),
CancelURL: stripe.String(r.ReturnUrl),
ClientReferenceID: stripe.String(r.PaymentName),
ExpiresAt: stripe.Int64(time.Now().Add(30 * time.Minute).Unix()),
}
checkoutParams.AddMetadata("product_description", description)
sCheckout, err := stripeCheckout.New(checkoutParams)
if err != nil {
return "", "", err
return nil, err
}
return sCheckout.URL, sCheckout.ID, nil
payResp := &PayResp{
PayUrl: sCheckout.URL,
OrderId: sCheckout.ID,
}
return payResp, nil
}
func (pp *StripePaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {

View File

@ -63,27 +63,66 @@ func NewWechatPaymentProvider(mchId string, apiV3Key string, appId string, seria
return pp, nil
}
func (pp *WechatPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
func (pp *WechatPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
bm := gopay.BodyMap{}
bm.Set("attach", joinAttachString([]string{productDisplayName, productName, providerName}))
desc := joinAttachString([]string{r.ProductDisplayName, r.ProductName, r.ProviderName})
bm.Set("attach", desc)
bm.Set("appid", pp.AppId)
bm.Set("description", productDisplayName)
bm.Set("notify_url", notifyUrl)
bm.Set("out_trade_no", paymentName)
bm.Set("description", r.ProductDisplayName)
bm.Set("notify_url", r.NotifyUrl)
bm.Set("out_trade_no", r.PaymentName)
bm.SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", priceFloat64ToInt64(price))
bm.Set("currency", currency)
bm.Set("total", priceFloat64ToInt64(r.Price))
bm.Set("currency", r.Currency)
})
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return "", "", err
// In Wechat browser, we use JSAPI
if r.PaymentEnv == PaymentEnvWechatBrowser {
if r.PayerId == "" {
return nil, errors.New("failed to get the payer's openid, please retry login")
}
bm.SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", r.PayerId) // If the account is signup via Wechat, the PayerId is the Wechat OpenId e.g.oxW9O1ZDvgreSHuBSQDiQ2F055PI
})
jsapiRsp, err := pp.Client.V3TransactionJsapi(context.Background(), bm)
if err != nil {
return nil, err
}
if jsapiRsp.Code != wechat.Success {
return nil, errors.New(jsapiRsp.Error)
}
// use RSA256 to sign the pay request
params, err := pp.Client.PaySignOfJSAPI(pp.AppId, jsapiRsp.Response.PrepayId)
if err != nil {
return nil, err
}
payResp := &PayResp{
PayUrl: "",
OrderId: r.PaymentName, // Wechat can use paymentName as the OutTradeNo to query order status
AttachInfo: map[string]interface{}{
"appId": params.AppId,
"timeStamp": params.TimeStamp,
"nonceStr": params.NonceStr,
"package": params.Package,
"signType": "RSA",
"paySign": params.PaySign,
},
}
return payResp, nil
} else {
// In other case, we use NativeAPI
nativeRsp, err := pp.Client.V3TransactionNative(context.Background(), bm)
if err != nil {
return nil, err
}
if nativeRsp.Code != wechat.Success {
return nil, errors.New(nativeRsp.Error)
}
payResp := &PayResp{
PayUrl: nativeRsp.Response.CodeUrl,
OrderId: r.PaymentName, // Wechat can use paymentName as the OutTradeNo to query order status
}
return payResp, nil
}
if nativeRsp.Code != wechat.Success {
return "", "", errors.New(nativeRsp.Error)
}
return nativeRsp.Response.CodeUrl, paymentName, nil // Wechat can use paymentName as the OutTradeNo to query order status
}
func (pp *WechatPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {