Add Notify() to payment provider.

This commit is contained in:
Yang Luo 2022-03-14 02:07:55 +08:00
parent 5de417ecf7
commit 4dca3bd3f7
12 changed files with 200 additions and 113 deletions

View File

@ -21,7 +21,6 @@ import (
"github.com/astaxie/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay/alipay"
)
// GetPayments
@ -142,14 +141,16 @@ func (c *ApiController) DeletePayment() {
// @Success 200 {object} controllers.Response The Response object
// @router /notify-payment [post]
func (c *ApiController) NotifyPayment() {
bm, err := alipay.ParseNotifyToBodyMap(c.Ctx.Request)
if err != nil {
panic(err)
}
owner := c.Ctx.Input.Param(":owner")
providerName := c.Ctx.Input.Param(":provider")
productName := c.Ctx.Input.Param(":product")
paymentName := c.Ctx.Input.Param(":payment")
ok := object.NotifyPayment(bm)
body := c.Ctx.Input.RequestBody
ok := object.NotifyPayment(c.Ctx.Request, body, owner, providerName, productName, paymentName)
if ok {
_, err = c.Ctx.ResponseWriter.Write([]byte("success"))
_, err := c.Ctx.ResponseWriter.Write([]byte("success"))
if err != nil {
panic(err)
}

View File

@ -120,12 +120,12 @@ func (c *ApiController) DeleteProduct() {
// @Tag Product API
// @Description buy product
// @Param id query string true "The id of the product"
// @Param providerId query string true "The id of the provider"
// @Param providerName query string true "The name of the provider"
// @Success 200 {object} controllers.Response The Response object
// @router /buy-product [post]
func (c *ApiController) BuyProduct() {
id := c.Input().Get("id")
providerId := c.Input().Get("providerId")
providerName := c.Input().Get("providerName")
host := c.Ctx.Request.Host
userId := c.GetSessionUsername()
@ -140,7 +140,7 @@ func (c *ApiController) BuyProduct() {
return
}
payUrl, err := object.BuyProduct(id, providerId, user, host)
payUrl, err := object.BuyProduct(id, providerName, user, host)
if err != nil {
c.ResponseError(err.Error())
return

View File

@ -16,10 +16,9 @@ package object
import (
"fmt"
"net/http"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
"xorm.io/core"
)
@ -33,8 +32,8 @@ type Payment struct {
Type string `xorm:"varchar(100)" json:"type"`
Organization string `xorm:"varchar(100)" json:"organization"`
User string `xorm:"varchar(100)" json:"user"`
ProductId string `xorm:"varchar(100)" json:"productId"`
ProductName string `xorm:"varchar(100)" json:"productName"`
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
Detail string `xorm:"varchar(100)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
@ -143,57 +142,45 @@ func DeletePayment(payment *Payment) bool {
return affected != 0
}
func notifyPayment(bm gopay.BodyMap) (*Payment, error) {
owner := "admin"
productName := bm.Get("subject")
paymentId := bm.Get("out_trade_no")
priceString := bm.Get("total_amount")
price := util.ParseFloat(priceString)
productId := bm.Get("productId")
providerId := bm.Get("providerId")
product := getProduct(owner, productId)
if product == nil {
return nil, fmt.Errorf("the product: %s does not exist", productId)
func notifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) (*Payment, error) {
payment := getPayment(owner, paymentName)
if payment == nil {
return nil, fmt.Errorf("the payment: %s does not exist", paymentName)
}
if productName != product.DisplayName {
return nil, fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", productName, product.DisplayName)
product := getProduct(owner, productName)
if product == nil {
return nil, fmt.Errorf("the product: %s does not exist", productName)
}
provider, err := product.getProvider(providerName)
if err != nil {
return payment, err
}
pProvider, cert, err := provider.getPaymentProvider()
if err != nil {
return payment, err
}
productDisplayName, paymentName, price, productName, providerName, err := pProvider.Notify(request, body, cert.AuthorityPublicKey)
if err != nil {
return payment, err
}
if productDisplayName != "" && productDisplayName != product.DisplayName {
return nil, fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", productDisplayName, product.DisplayName)
}
if price != product.Price {
return nil, fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", price, product.Price)
}
payment := getPayment(owner, paymentId)
if payment == nil {
return nil, fmt.Errorf("the payment: %s does not exist", paymentId)
}
provider, err := product.getProvider(providerId)
if err != nil {
return payment, err
}
cert := getCert(owner, provider.Cert)
if cert == nil {
return payment, fmt.Errorf("the cert: %s does not exist", provider.Cert)
}
ok, err := alipay.VerifySignWithCert(cert.AuthorityPublicKey, bm)
if err != nil {
return payment, err
}
if !ok {
return payment, fmt.Errorf("VerifySignWithCert() failed: %v", ok)
}
return payment, nil
}
func NotifyPayment(bm gopay.BodyMap) bool {
payment, err := notifyPayment(bm)
func NotifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) bool {
payment, err := notifyPayment(request, body, owner, providerName, productName, paymentName)
if payment != nil {
if err != nil {

View File

@ -17,7 +17,6 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -153,54 +152,47 @@ func (product *Product) getProvider(providerId string) (*Provider, error) {
return provider, nil
}
func BuyProduct(id string, providerId string, user *User, host string) (string, error) {
func BuyProduct(id string, providerName string, user *User, host string) (string, error) {
product := GetProduct(id)
if product == nil {
return "", fmt.Errorf("the product: %s does not exist", id)
}
provider, err := product.getProvider(providerId)
provider, err := product.getProvider(providerName)
if err != nil {
return "", err
}
cert := &Cert{}
if provider.Cert != "" {
cert = getCert(product.Owner, provider.Cert)
if cert == nil {
return "", fmt.Errorf("the cert: %s does not exist", provider.Cert)
}
pProvider, _, err := provider.getPaymentProvider()
if err != nil {
return "", err
}
pProvider := pp.GetPaymentProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
if pProvider == nil {
return "", fmt.Errorf("the payment provider type: %s is not supported", provider.Type)
}
paymentId := util.GenerateTimeId()
productName := product.DisplayName
productId := product.Name
owner := product.Owner
productName := product.Name
paymentName := util.GenerateTimeId()
productDisplayName := product.DisplayName
originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/result", originFrontend, paymentId)
notifyUrl := fmt.Sprintf("%s/api/notify-payment", originBackend)
returnUrl := fmt.Sprintf("%s/payments/%s/result", originFrontend, paymentName)
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s/%s/%s", originBackend, owner, providerName, productName, paymentName)
payUrl, err := pProvider.Pay(productName, productId, providerId, paymentId, product.Price, returnUrl, notifyUrl)
payUrl, err := pProvider.Pay(providerName, productName, paymentName, productDisplayName, product.Price, returnUrl, notifyUrl)
if err != nil {
return "", err
}
payment := Payment{
Owner: product.Owner,
Name: paymentId,
Name: paymentName,
CreatedTime: util.GetCurrentTime(),
DisplayName: paymentId,
DisplayName: paymentName,
Provider: provider.Name,
Type: provider.Type,
Organization: user.Owner,
User: user.Name,
ProductId: productId,
ProductName: productName,
ProductDisplayName: productDisplayName,
Detail: product.Detail,
Tag: product.Tag,
Currency: product.Currency,

View File

@ -17,6 +17,7 @@ package object
import (
"fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util"
"xorm.io/core"
)
@ -182,6 +183,23 @@ func DeleteProvider(provider *Provider) bool {
return affected != 0
}
func (p *Provider) getPaymentProvider() (pp.PaymentProvider, *Cert, error) {
cert := &Cert{}
if p.Cert != "" {
cert = getCert(p.Owner, p.Cert)
if cert == nil {
return nil, nil, fmt.Errorf("the cert: %s does not exist", p.Cert)
}
}
pProvider := pp.GetPaymentProvider(p.Type, p.ClientId, p.ClientSecret, p.Host, cert.PublicKey, cert.PrivateKey, cert.AuthorityPublicKey, cert.AuthorityRootPublicKey)
if pProvider == nil {
return nil, cert, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
}
return pProvider, cert, nil
}
func (p *Provider) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name)
}

View File

@ -16,7 +16,10 @@ package pp
import (
"context"
"fmt"
"net/http"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
)
@ -42,19 +45,20 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
return pp
}
func (pp *AlipayPaymentProvider) Pay(productName string, productId string, providerId string, paymentId string, price float64, returnUrl string, notifyUrl string) (string, error) {
func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
//pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{}
bm.Set("providerName", providerName)
bm.Set("productName", productName)
bm.Set("return_url", returnUrl)
bm.Set("notify_url", notifyUrl)
bm.Set("subject", productName)
bm.Set("out_trade_no", paymentId)
bm.Set("subject", productDisplayName)
bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", getPriceString(price))
bm.Set("productId", productId)
bm.Set("providerId", productId)
payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil {
@ -62,3 +66,27 @@ func (pp *AlipayPaymentProvider) Pay(productName string, productId string, provi
}
return payUrl, nil
}
func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
bm, err := alipay.ParseNotifyToBodyMap(request)
if err != nil {
return "", "", 0, "", "", err
}
providerName := bm.Get("providerName")
productName := bm.Get("productName")
productDisplayName := bm.Get("subject")
paymentName := bm.Get("out_trade_no")
price := util.ParseFloat(bm.Get("total_amount"))
ok, err := alipay.VerifySignWithCert(authorityPublicKey, bm)
if err != nil {
return "", "", 0, "", "", err
}
if !ok {
return "", "", 0, "", "", fmt.Errorf("VerifySignWithCert() failed: %v", ok)
}
return productDisplayName, paymentName, price, productName, providerName, nil
}

View File

@ -22,6 +22,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/casdoor/casdoor/util"
@ -52,6 +53,20 @@ type GcPayRespInfo struct {
PayUrl string `json:"payurl"`
}
type GcNotifyRespInfo struct {
Xmpch string `json:"xmpch"`
OrderDate string `json:"orderdate"`
OrderNo string `json:"orderno"`
Amount float64 `json:"amount"`
Jylsh string `json:"jylsh"`
TradeNo string `json:"tradeno"`
PayMethod string `json:"paymethod"`
OrderState string `json:"orderstate"`
ReturnType string `json:"return_type"`
PayerId string `json:"payerid"`
PayerName string `json:"payername"`
}
type GcRequestBody struct {
Op string `json:"op"`
Xmpch string `json:"xmpch"`
@ -115,7 +130,7 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
return respBytes, nil
}
func (pp *GcPaymentProvider) Pay(productName string, productId string, providerId string, paymentId string, price float64, returnUrl string, notifyUrl string) (string, error) {
func (pp *GcPaymentProvider) Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error) {
payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(),
OrderNo: util.GenerateTimeId(),
@ -159,6 +174,10 @@ func (pp *GcPaymentProvider) Pay(productName string, productId string, providerI
return "", err
}
if respBody.ReturnCode != "SUCCESS" {
return "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
}
payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
if err != nil {
return "", err
@ -172,3 +191,42 @@ func (pp *GcPaymentProvider) Pay(productName string, productId string, providerI
return payRespInfo.PayUrl, nil
}
func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error) {
reqBody := GcRequestBody{}
m, err := url.ParseQuery(string(body))
if err != nil {
return "", "", 0, "", "", err
}
reqBody.Op = m["op"][0]
reqBody.Xmpch = m["xmpch"][0]
reqBody.Version = m["version"][0]
reqBody.Data = m["data"][0]
reqBody.RequestTime = m["requesttime"][0]
reqBody.Sign = m["sign"][0]
notifyReqInfoBytes, err := base64.StdEncoding.DecodeString(reqBody.Data)
if err != nil {
return "", "", 0, "", "", err
}
var notifyRespInfo GcNotifyRespInfo
err = json.Unmarshal(notifyReqInfoBytes, &notifyRespInfo)
if err != nil {
return "", "", 0, "", "", err
}
providerName := ""
productName := ""
productDisplayName := ""
paymentName := notifyRespInfo.OrderNo
price := notifyRespInfo.Amount
if notifyRespInfo.OrderState != "1" {
return "", "", 0, "", "", fmt.Errorf("error order state: %s", notifyRespInfo.OrderDate)
}
return productDisplayName, paymentName, price, productName, providerName, nil
}

View File

@ -14,8 +14,11 @@
package pp
import "net/http"
type PaymentProvider interface {
Pay(productName string, productId string, providerId string, paymentId string, price float64, returnUrl string, notifyUrl string) (string, error)
Pay(providerName string, productName string, paymentName string, productDisplayName string, price float64, returnUrl string, notifyUrl string) (string, error)
Notify(request *http.Request, body []byte, authorityPublicKey string) (string, string, float64, string, string, error)
}
func GetPaymentProvider(typ string, appId string, clientSecret string, host string, appPublicKey string, appPrivateKey string, authorityPublicKey string, authorityRootPublicKey string) PaymentProvider {

View File

@ -164,7 +164,7 @@ func initAPI() {
beego.Router("/api/update-payment", &controllers.ApiController{}, "POST:UpdatePayment")
beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment")
beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment")
beego.Router("/api/notify-payment", &controllers.ApiController{}, "POST:NotifyPayment")
beego.Router("/api/notify-payment/?:owner/?:provider/?:product/?:payment", &controllers.ApiController{}, "POST:NotifyPayment")
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")

View File

@ -34,8 +34,8 @@ class PaymentListPage extends BaseListPage {
type: "PayPal",
organization: "built-in",
user: "admin",
productId: "computer-1",
productName: "A notebook computer",
productName: "computer-1",
productDisplayName: "A notebook computer",
detail: "This is a computer with excellent CPU, memory and disk",
tag: "Promotion-1",
currency: "USD",
@ -172,11 +172,11 @@ class PaymentListPage extends BaseListPage {
},
{
title: i18next.t("payment:Product"),
dataIndex: 'productName',
key: 'productName',
dataIndex: 'productDisplayName',
key: 'productDisplayName',
// width: '160px',
sorter: true,
...this.getColumnSearchProps('productName'),
...this.getColumnSearchProps('productDisplayName'),
},
{
title: i18next.t("payment:Price"),

View File

@ -60,7 +60,7 @@ class PaymentResultPage extends React.Component {
}
<Result
status="success"
title={`${i18next.t("payment:You have successfully completed the payment")}: ${payment.productName}`}
title={`${i18next.t("payment:You have successfully completed the payment")}: ${payment.productDisplayName}`}
subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[
<Button type="primary" key="returnUrl" onClick={() => {
@ -80,7 +80,7 @@ class PaymentResultPage extends React.Component {
}
<Result
status="info"
title={`${i18next.t("payment:The payment is still under processing")}: ${payment.productName}, ${i18next.t("payment:the current state is")}: ${payment.state}, ${i18next.t("payment:please wait for a few seconds...")}`}
title={`${i18next.t("payment:The payment is still under processing")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}, ${i18next.t("payment:please wait for a few seconds...")}`}
subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[
<Spin size="large" tip={i18next.t("payment:Processing...")} />,
@ -96,7 +96,7 @@ class PaymentResultPage extends React.Component {
}
<Result
status="error"
title={`${i18next.t("payment:The payment has failed")}: ${payment.productName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
title={`${i18next.t("payment:The payment has failed")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[
<Button type="primary" key="returnUrl" onClick={() => {

View File

@ -56,7 +56,7 @@ export function deleteProduct(product) {
}
export function buyProduct(owner, name, providerId) {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerId=${providerId}`, {
return fetch(`${Setting.ServerUrl}/api/buy-product?id=${owner}/${encodeURIComponent(name)}&providerName=${providerId}`, {
method: 'POST',
credentials: 'include',
}).then(res => res.json());