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

View File

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

View File

@ -16,10 +16,9 @@ package object
import ( import (
"fmt" "fmt"
"net/http"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
"xorm.io/core" "xorm.io/core"
) )
@ -29,12 +28,12 @@ type Payment struct {
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Provider string `xorm:"varchar(100)" json:"provider"` Provider string `xorm:"varchar(100)" json:"provider"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
Organization string `xorm:"varchar(100)" json:"organization"` Organization string `xorm:"varchar(100)" json:"organization"`
User string `xorm:"varchar(100)" json:"user"` User string `xorm:"varchar(100)" json:"user"`
ProductId string `xorm:"varchar(100)" json:"productId"` ProductName string `xorm:"varchar(100)" json:"productName"`
ProductName string `xorm:"varchar(100)" json:"productName"` ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
Detail string `xorm:"varchar(100)" json:"detail"` Detail string `xorm:"varchar(100)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"` Tag string `xorm:"varchar(100)" json:"tag"`
@ -143,57 +142,45 @@ func DeletePayment(payment *Payment) bool {
return affected != 0 return affected != 0
} }
func notifyPayment(bm gopay.BodyMap) (*Payment, error) { func notifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) (*Payment, error) {
owner := "admin" payment := getPayment(owner, paymentName)
productName := bm.Get("subject") if payment == nil {
paymentId := bm.Get("out_trade_no") return nil, fmt.Errorf("the payment: %s does not exist", paymentName)
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)
} }
if productName != product.DisplayName { product := getProduct(owner, productName)
return nil, fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", productName, product.DisplayName) 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 { if price != product.Price {
return nil, fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", 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 return payment, nil
} }
func NotifyPayment(bm gopay.BodyMap) bool { func NotifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string) bool {
payment, err := notifyPayment(bm) payment, err := notifyPayment(request, body, owner, providerName, productName, paymentName)
if payment != nil { if payment != nil {
if err != nil { if err != nil {

View File

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

View File

@ -17,6 +17,7 @@ package object
import ( import (
"fmt" "fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"xorm.io/core" "xorm.io/core"
) )
@ -182,6 +183,23 @@ func DeleteProvider(provider *Provider) bool {
return affected != 0 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 { func (p *Provider) GetId() string {
return fmt.Sprintf("%s/%s", p.Owner, p.Name) return fmt.Sprintf("%s/%s", p.Owner, p.Name)
} }

View File

@ -16,7 +16,10 @@ package pp
import ( import (
"context" "context"
"fmt"
"net/http"
"github.com/casdoor/casdoor/util"
"github.com/go-pay/gopay" "github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay" "github.com/go-pay/gopay/alipay"
) )
@ -42,19 +45,20 @@ func NewAlipayPaymentProvider(appId string, appPublicKey string, appPrivateKey s
return pp 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 //pp.Client.DebugSwitch = gopay.DebugOn
bm := gopay.BodyMap{} bm := gopay.BodyMap{}
bm.Set("providerName", providerName)
bm.Set("productName", productName)
bm.Set("return_url", returnUrl) bm.Set("return_url", returnUrl)
bm.Set("notify_url", notifyUrl) bm.Set("notify_url", notifyUrl)
bm.Set("subject", productName) bm.Set("subject", productDisplayName)
bm.Set("out_trade_no", paymentId) bm.Set("out_trade_no", paymentName)
bm.Set("total_amount", getPriceString(price)) bm.Set("total_amount", getPriceString(price))
bm.Set("productId", productId)
bm.Set("providerId", productId)
payUrl, err := pp.Client.TradePagePay(context.Background(), bm) payUrl, err := pp.Client.TradePagePay(context.Background(), bm)
if err != nil { if err != nil {
@ -62,3 +66,27 @@ func (pp *AlipayPaymentProvider) Pay(productName string, productId string, provi
} }
return payUrl, nil 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"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -52,6 +53,20 @@ type GcPayRespInfo struct {
PayUrl string `json:"payurl"` 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 { type GcRequestBody struct {
Op string `json:"op"` Op string `json:"op"`
Xmpch string `json:"xmpch"` Xmpch string `json:"xmpch"`
@ -115,7 +130,7 @@ func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
return respBytes, nil 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{ payReqInfo := GcPayReqInfo{
OrderDate: util.GenerateSimpleTimeId(), OrderDate: util.GenerateSimpleTimeId(),
OrderNo: util.GenerateTimeId(), OrderNo: util.GenerateTimeId(),
@ -159,6 +174,10 @@ func (pp *GcPaymentProvider) Pay(productName string, productId string, providerI
return "", err return "", err
} }
if respBody.ReturnCode != "SUCCESS" {
return "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
}
payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data) payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
if err != nil { if err != nil {
return "", err return "", err
@ -172,3 +191,42 @@ func (pp *GcPaymentProvider) Pay(productName string, productId string, providerI
return payRespInfo.PayUrl, nil 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 package pp
import "net/http"
type PaymentProvider interface { 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 { 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/update-payment", &controllers.ApiController{}, "POST:UpdatePayment")
beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment") beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment")
beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment") 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-email", &controllers.ApiController{}, "POST:SendEmail")
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms") beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")

View File

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

View File

@ -60,7 +60,7 @@ class PaymentResultPage extends React.Component {
} }
<Result <Result
status="success" 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")} subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[ extra={[
<Button type="primary" key="returnUrl" onClick={() => { <Button type="primary" key="returnUrl" onClick={() => {
@ -80,7 +80,7 @@ class PaymentResultPage extends React.Component {
} }
<Result <Result
status="info" 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")} subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[ extra={[
<Spin size="large" tip={i18next.t("payment:Processing...")} />, <Spin size="large" tip={i18next.t("payment:Processing...")} />,
@ -96,7 +96,7 @@ class PaymentResultPage extends React.Component {
} }
<Result <Result
status="error" 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")} subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[ extra={[
<Button type="primary" key="returnUrl" onClick={() => { <Button type="primary" key="returnUrl" onClick={() => {

View File

@ -56,7 +56,7 @@ export function deleteProduct(product) {
} }
export function buyProduct(owner, name, providerId) { 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', method: 'POST',
credentials: 'include', credentials: 'include',
}).then(res => res.json()); }).then(res => res.json());