From f8c14b38269226695acd8bd471916e3b7c33076f Mon Sep 17 00:00:00 2001 From: HFO4 <912394456@qq.com> Date: Mon, 17 Feb 2020 12:33:54 +0800 Subject: [PATCH] Feat: vas alipay --- go.mod | 1 + go.sum | 6 ++++ models/order.go | 12 ++++++++ pkg/payment/alipay.go | 42 ++++++++++++++++++++++++++++ pkg/payment/order.go | 48 +++++++++++++++++++++++++++++--- pkg/payment/purchase.go | 57 ++++++++++++++++++++++++++++++++++++++ routers/controllers/vas.go | 41 +++++++++++++++++++++++++++ routers/router.go | 7 +++++ service/vas/purchase.go | 15 ++++++++++ 9 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 pkg/payment/alipay.go diff --git a/go.mod b/go.mod index c42dafe..e4d37d4 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/qiniu/api.v7/v7 v7.4.0 github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1 github.com/robfig/cron/v3 v3.0.1 + github.com/smartwalle/alipay/v3 v3.0.13 github.com/smartystreets/goconvey v1.6.4 // indirect github.com/speps/go-hashids v2.0.0+incompatible github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum index 0952587..473cc3a 100644 --- a/go.sum +++ b/go.sum @@ -173,6 +173,11 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartwalle/alipay v1.0.2 h1:y/uC+6OcVDVxKJ5Bs1qi6aogjDEXwD3XswDXJ5zJurk= +github.com/smartwalle/alipay/v3 v3.0.13 h1:f1Cdnxh6TfbaziLw0i/4h+f8tw9RJwG8y4xye7vTTgY= +github.com/smartwalle/alipay/v3 v3.0.13/go.mod h1:cZUMCCnsux9YAxA0/f3PWUR+7wckWtE1BqxbVRtGij0= +github.com/smartwalle/crypto4go v1.0.2 h1:9DUEOOsPhmp00438L4oBdcL8EZG1zumecft5bWj5phI= +github.com/smartwalle/crypto4go v1.0.2/go.mod h1:LQ7vCZIb7BE5+MuMtJBuO8ORkkQ01m4DXDBWPzLbkMY= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw= @@ -200,6 +205,7 @@ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/models/order.go b/models/order.go index 9800bf2..27c7182 100644 --- a/models/order.go +++ b/models/order.go @@ -45,3 +45,15 @@ func (order *Order) Create() (uint, error) { } return order.ID, nil } + +// UpdateStatus 更新订单状态 +func (order *Order) UpdateStatus(status int) { + DB.Model(order).Update("status", status) +} + +// GetOrderByNo 根据商户订单号查询订单 +func GetOrderByNo(id string) (*Order, error) { + var order Order + err := DB.Where("order_no = ?", id).First(&order).Error + return &order, err +} diff --git a/pkg/payment/alipay.go b/pkg/payment/alipay.go new file mode 100644 index 0000000..a1fbf2c --- /dev/null +++ b/pkg/payment/alipay.go @@ -0,0 +1,42 @@ +package payment + +import ( + "fmt" + model "github.com/HFO4/cloudreve/models" + "github.com/HFO4/cloudreve/pkg/serializer" + alipay "github.com/smartwalle/alipay/v3" + "net/url" +) + +// Alipay 支付宝当面付支付处理 +type Alipay struct { + Client *alipay.Client +} + +// Create 创建订单 +func (pay *Alipay) Create(order *model.Order, pack *serializer.PackProduct, group *serializer.GroupProducts, user *model.User) (*OrderCreateRes, error) { + gateway, _ := url.Parse("/api/v3/callback/alipay") + var p = alipay.TradePreCreate{ + Trade: alipay.Trade{ + NotifyURL: model.GetSiteURL().ResolveReference(gateway).String(), + Subject: order.Name, + OutTradeNo: order.OrderNo, + TotalAmount: fmt.Sprintf("%.2f", float64(order.Price*order.Num)/100), + }, + } + + if _, err := order.Create(); err != nil { + return nil, ErrInsertOrder.WithError(err) + } + + res, err := pay.Client.TradePreCreate(p) + if err != nil { + return nil, ErrIssueOrder.WithError(err) + } + + return &OrderCreateRes{ + Payment: true, + QRCode: res.Content.QRCode, + ID: order.OrderNo, + }, nil +} diff --git a/pkg/payment/order.go b/pkg/payment/order.go index 9b82ff5..426e9ba 100644 --- a/pkg/payment/order.go +++ b/pkg/payment/order.go @@ -4,6 +4,9 @@ import ( "fmt" model "github.com/HFO4/cloudreve/models" "github.com/HFO4/cloudreve/pkg/serializer" + "github.com/smartwalle/alipay/v3" + "math/rand" + "time" ) var ( @@ -23,6 +26,12 @@ var ( ErrGroupInvalid = serializer.NewError(serializer.CodeNoPermissionErr, "用户组不可用", nil) // ErrUpgradeGroup 用户组冲突 ErrUpgradeGroup = serializer.NewError(serializer.CodeDBError, "无法升级用户组", nil) + // ErrUInitPayment 无法初始化支付实例 + ErrUInitPayment = serializer.NewError(serializer.CodeInternalSetting, "无法初始化支付实例", nil) + // ErrIssueOrder 订单接口请求失败 + ErrIssueOrder = serializer.NewError(serializer.CodeInternalSetting, "无法创建订单", nil) + // ErrOrderNotFound 订单不存在 + ErrOrderNotFound = serializer.NewError(serializer.CodeNotFound, "订单不存在", nil) ) // Pay 支付处理接口 @@ -32,16 +41,39 @@ type Pay interface { // OrderCreateRes 订单创建结果 type OrderCreateRes struct { - Payment bool `json:"payment"` // 是否需要支付 + Payment bool `json:"payment"` // 是否需要支付 + ID string `json:"id,omitempty"` // 订单号 + QRCode string `json:"qr_code,omitempty"` // 支付二维码指向的地址 + Redirect string `json:"redirect,omitempty"` // 支付跳转连接,和二维码二选一,不需要的留空 } // NewPaymentInstance 获取新的支付实例 func NewPaymentInstance(method string) (Pay, error) { - if method == "score" { + switch method { + case "score": return &ScorePayment{}, nil - } + case "alipay": + options := model.GetSettingByNames("alipay_enabled", "appid", "appkey", "shopid") + if options["alipay_enabled"] != "1" { + return nil, ErrUnknownPaymentMethod + } + + // 初始化支付宝客户端 + var client, err = alipay.New(options["appid"], options["appkey"], true) + if err != nil { + return nil, ErrUInitPayment.WithError(err) + } + + // 加载支付宝公钥 + err = client.LoadAliPayPublicKey(options["shopid"]) + if err != nil { + return nil, ErrUInitPayment.WithError(err) + } - return nil, ErrUnknownPaymentMethod + return &Alipay{Client: client}, nil + default: + return nil, ErrUnknownPaymentMethod + } } // NewOrder 创建新订单 @@ -78,6 +110,7 @@ func NewOrder(pack *serializer.PackProduct, group *serializer.GroupProducts, num // 创建订单记录 order := &model.Order{ UserID: user.ID, + OrderNo: orderID(), Type: orderType, Method: method, ProductID: productID, @@ -89,3 +122,10 @@ func NewOrder(pack *serializer.PackProduct, group *serializer.GroupProducts, num return pay.Create(order, pack, group, user) } + +func orderID() string { + return fmt.Sprintf("%s%d%d", + time.Now().Format("20060102150405"), + 10000+rand.Intn(90000), + time.Now().UnixNano()) +} diff --git a/pkg/payment/purchase.go b/pkg/payment/purchase.go index a1bd574..02ada23 100644 --- a/pkg/payment/purchase.go +++ b/pkg/payment/purchase.go @@ -1,6 +1,8 @@ package payment import ( + "encoding/json" + "errors" model "github.com/HFO4/cloudreve/models" "github.com/HFO4/cloudreve/pkg/serializer" "time" @@ -69,3 +71,58 @@ func GiveProduct(user *model.User, pack *serializer.PackProduct, group *serializ return GiveScore(user, num) } } + +// OrderPaid 订单已支付处理 +func OrderPaid(orderNo string) error { + order, err := model.GetOrderByNo(orderNo) + if err != nil { + return ErrOrderNotFound.WithError(err) + } + + // 更新订单状态为 已支付 + order.UpdateStatus(model.OrderPaid) + + user, err := model.GetActiveUserByID(order.UserID) + if err != nil { + return errors.New("用户不存在") + } + + // 查询商品 + options := model.GetSettingByNames("pack_data", "group_sell_data") + + var ( + packs []serializer.PackProduct + groups []serializer.GroupProducts + ) + if err := json.Unmarshal([]byte(options["pack_data"]), &packs); err != nil { + return err + } + if err := json.Unmarshal([]byte(options["group_sell_data"]), &groups); err != nil { + return err + } + + // 查找要购买的商品 + var ( + pack *serializer.PackProduct + group *serializer.GroupProducts + ) + if order.Type == model.GroupOrderType { + for _, v := range groups { + if v.ID == order.ProductID { + group = &v + break + } + } + } else if order.Type == model.PackOrderType { + for _, v := range packs { + if v.ID == order.ProductID { + pack = &v + break + } + } + } + + // "发货" + return GiveProduct(&user, pack, group, order.Num) + +} diff --git a/routers/controllers/vas.go b/routers/controllers/vas.go index 883b477..3bb3c6a 100644 --- a/routers/controllers/vas.go +++ b/routers/controllers/vas.go @@ -1,8 +1,11 @@ package controllers import ( + "github.com/HFO4/cloudreve/pkg/payment" + "github.com/HFO4/cloudreve/pkg/util" "github.com/HFO4/cloudreve/service/vas" "github.com/gin-gonic/gin" + "github.com/smartwalle/alipay/v3" ) // GetQuota 获取容量配额信息 @@ -38,6 +41,17 @@ func NewOrder(c *gin.Context) { } } +// OrderStatus 查询订单状态 +func OrderStatus(c *gin.Context) { + var service vas.OrderService + if err := c.ShouldBindUri(&service); err == nil { + res := service.Status(c, CurrentUser(c)) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} + // GetRedeemInfo 获取兑换码信息 func GetRedeemInfo(c *gin.Context) { var service vas.RedeemService @@ -59,3 +73,30 @@ func DoRedeem(c *gin.Context) { c.JSON(200, ErrorResponse(err)) } } + +// AlipayCallback 支付宝回调 +func AlipayCallback(c *gin.Context) { + pay, err := payment.NewPaymentInstance("alipay") + if err != nil { + util.Log().Debug("[支付宝回调] 无法创建支付宝客户端, %s", err) + c.Status(400) + return + } + + res, err := pay.(*payment.Alipay).Client.GetTradeNotification(c.Request) + if err != nil { + util.Log().Debug("[支付宝回调] 回调验证失败, %s", err) + c.Status(403) + return + } + + if res != nil && res.TradeStatus == "TRADE_SUCCESS" { + // 支付成功 + if err := payment.OrderPaid(res.OutTradeNo); err != nil { + util.Log().Debug("[支付宝回调] 支付处理失败, %s", err) + } + } + + // 确认收到通知消息 + alipay.AckNotification(c.Writer) +} diff --git a/routers/router.go b/routers/router.go index ee7d88f..0d78337 100644 --- a/routers/router.go +++ b/routers/router.go @@ -127,6 +127,11 @@ func InitMasterRouter() *gin.Engine { // 回调接口 callback := v3.Group("callback") { + // 支付宝回调 + callback.POST( + "alipay", + controllers.AlipayCallback, + ) // 远程策略上传回调 callback.POST( "remote/:key", @@ -363,6 +368,8 @@ func InitMasterRouter() *gin.Engine { vas.GET("product", controllers.GetProduct) // 新建支付订单 vas.POST("order", controllers.NewOrder) + // 查询订单状态 + vas.GET("order/:id", controllers.OrderStatus) // 获取兑换码信息 vas.GET("redeem/:code", controllers.GetRedeemInfo) // 执行兑换 diff --git a/service/vas/purchase.go b/service/vas/purchase.go index 1bff342..363eaa3 100644 --- a/service/vas/purchase.go +++ b/service/vas/purchase.go @@ -21,6 +21,21 @@ type RedeemService struct { Code string `uri:"code" binding:"required,max=64"` } +// OrderService 订单查询 +type OrderService struct { + ID string `uri:"id" binding:"required"` +} + +// Status 查询订单状态 +func (service *OrderService) Status(c *gin.Context, user *model.User) serializer.Response { + order, _ := model.GetOrderByNo(service.ID) + if order == nil || order.UserID != user.ID { + return serializer.Err(serializer.CodeNotFound, "订单不存在", nil) + } + + return serializer.Response{Data: order.Status} +} + // Redeem 开始兑换 func (service *RedeemService) Redeem(c *gin.Context, user *model.User) serializer.Response { redeem, err := model.GetAvailableRedeem(service.Code)