mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-18 01:06:39 +08:00
444 lines
11 KiB
Go
444 lines
11 KiB
Go
package handler
|
||
|
||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
||
// * Use of this source code is governed by a Apache-2.0 license
|
||
// * that can be found in the LICENSE file.
|
||
// * @Author yangjian102621@163.com
|
||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||
|
||
import (
|
||
"embed"
|
||
"fmt"
|
||
"geekai/core"
|
||
"geekai/core/types"
|
||
"geekai/service"
|
||
"geekai/service/payment"
|
||
"geekai/store/model"
|
||
"geekai/utils"
|
||
"geekai/utils/resp"
|
||
"github.com/shopspring/decimal"
|
||
"net/http"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type PayWay struct {
|
||
Name string `json:"name"`
|
||
Value string `json:"value"`
|
||
}
|
||
|
||
// PaymentHandler 支付服务回调 handler
|
||
type PaymentHandler struct {
|
||
BaseHandler
|
||
alipayService *payment.AlipayService
|
||
huPiPayService *payment.HuPiPayService
|
||
geekPayService *payment.GeekPayService
|
||
wechatPayService *payment.WechatPayService
|
||
snowflake *service.Snowflake
|
||
userService *service.UserService
|
||
fs embed.FS
|
||
lock sync.Mutex
|
||
signKey string // 用来签名的随机秘钥
|
||
}
|
||
|
||
func NewPaymentHandler(
|
||
server *core.AppServer,
|
||
alipayService *payment.AlipayService,
|
||
huPiPayService *payment.HuPiPayService,
|
||
geekPayService *payment.GeekPayService,
|
||
wechatPayService *payment.WechatPayService,
|
||
db *gorm.DB,
|
||
userService *service.UserService,
|
||
snowflake *service.Snowflake,
|
||
fs embed.FS) *PaymentHandler {
|
||
return &PaymentHandler{
|
||
alipayService: alipayService,
|
||
huPiPayService: huPiPayService,
|
||
geekPayService: geekPayService,
|
||
wechatPayService: wechatPayService,
|
||
snowflake: snowflake,
|
||
userService: userService,
|
||
fs: fs,
|
||
lock: sync.Mutex{},
|
||
BaseHandler: BaseHandler{
|
||
App: server,
|
||
DB: db,
|
||
},
|
||
signKey: utils.RandString(32),
|
||
}
|
||
}
|
||
|
||
func (h *PaymentHandler) Pay(c *gin.Context) {
|
||
var data struct {
|
||
PayWay string `json:"pay_way"`
|
||
PayType string `json:"pay_type"`
|
||
ProductId int `json:"product_id"`
|
||
UserId int `json:"user_id"`
|
||
Device string `json:"device"`
|
||
Host string `json:"host"`
|
||
}
|
||
if err := c.ShouldBindJSON(&data); err != nil {
|
||
resp.ERROR(c, types.InvalidArgs)
|
||
return
|
||
}
|
||
|
||
var product model.Product
|
||
err := h.DB.Where("id", data.ProductId).First(&product).Error
|
||
if err != nil {
|
||
resp.ERROR(c, "Product not found")
|
||
return
|
||
}
|
||
|
||
orderNo, err := h.snowflake.Next(false)
|
||
if err != nil {
|
||
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||
return
|
||
}
|
||
var user model.User
|
||
err = h.DB.Where("id", data.UserId).First(&user).Error
|
||
if err != nil {
|
||
resp.NotAuth(c)
|
||
return
|
||
}
|
||
|
||
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
|
||
var payURL, returnURL, notifyURL string
|
||
switch data.PayWay {
|
||
case "alipay":
|
||
if h.App.Config.AlipayConfig.NotifyURL != "" { // 用于本地调试支付
|
||
notifyURL = h.App.Config.AlipayConfig.NotifyURL
|
||
} else {
|
||
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Host)
|
||
}
|
||
if h.App.Config.AlipayConfig.ReturnURL != "" { // 用于本地调试支付
|
||
returnURL = h.App.Config.AlipayConfig.ReturnURL
|
||
} else {
|
||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||
}
|
||
money := fmt.Sprintf("%.2f", amount)
|
||
payURL, err = h.alipayService.PayPC(payment.AlipayParams{
|
||
OutTradeNo: orderNo,
|
||
Subject: product.Name,
|
||
TotalFee: money,
|
||
ReturnURL: returnURL,
|
||
NotifyURL: notifyURL,
|
||
})
|
||
if err != nil {
|
||
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||
return
|
||
}
|
||
break
|
||
case "wechat":
|
||
if h.App.Config.WechatPayConfig.NotifyURL != "" {
|
||
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
|
||
} else {
|
||
notifyURL = fmt.Sprintf("%s/api/payment/notify/wechat", data.Host)
|
||
}
|
||
if data.Device == "wechat" {
|
||
payURL, err = h.wechatPayService.PayUrlH5(payment.WechatPayParams{
|
||
OutTradeNo: orderNo,
|
||
TotalFee: int(amount * 100),
|
||
Subject: product.Name,
|
||
NotifyURL: notifyURL,
|
||
ClientIP: c.ClientIP(),
|
||
})
|
||
} else {
|
||
payURL, err = h.wechatPayService.PayUrlNative(payment.WechatPayParams{
|
||
OutTradeNo: orderNo,
|
||
TotalFee: int(amount * 100),
|
||
Subject: product.Name,
|
||
NotifyURL: notifyURL,
|
||
})
|
||
}
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
break
|
||
case "hupi":
|
||
if h.App.Config.HuPiPayConfig.NotifyURL != "" {
|
||
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
|
||
} else {
|
||
notifyURL = fmt.Sprintf("%s/api/payment/notify/hupi", data.Host)
|
||
}
|
||
if h.App.Config.HuPiPayConfig.ReturnURL != "" {
|
||
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
|
||
} else {
|
||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||
}
|
||
r, err := h.huPiPayService.Pay(payment.HuPiPayParams{
|
||
Version: "1.1",
|
||
TradeOrderId: orderNo,
|
||
TotalFee: fmt.Sprintf("%f", amount),
|
||
Title: product.Name,
|
||
NotifyURL: notifyURL,
|
||
ReturnURL: returnURL,
|
||
WapName: "GeekAI助手",
|
||
})
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
payURL = r.URL
|
||
break
|
||
case "geek":
|
||
if h.App.Config.GeekPayConfig.NotifyURL != "" {
|
||
notifyURL = h.App.Config.GeekPayConfig.NotifyURL
|
||
} else {
|
||
notifyURL = fmt.Sprintf("%s/api/payment/notify/geek", data.Host)
|
||
}
|
||
if h.App.Config.GeekPayConfig.ReturnURL != "" {
|
||
data.Host = utils.GetBaseURL(h.App.Config.GeekPayConfig.ReturnURL)
|
||
}
|
||
if data.Device == "wechat" { // 微信客户端打开,调回手机端用户中心页面
|
||
returnURL = fmt.Sprintf("%s/mobile/profile", data.Host)
|
||
} else {
|
||
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
|
||
}
|
||
params := payment.GeekPayParams{
|
||
OutTradeNo: orderNo,
|
||
Method: "web",
|
||
Name: product.Name,
|
||
Money: fmt.Sprintf("%f", amount),
|
||
ClientIP: c.ClientIP(),
|
||
Device: data.Device,
|
||
Type: data.PayType,
|
||
ReturnURL: returnURL,
|
||
NotifyURL: notifyURL,
|
||
}
|
||
|
||
res, err := h.geekPayService.Pay(params)
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
payURL = res.PayURL
|
||
default:
|
||
resp.ERROR(c, "不支持的支付渠道")
|
||
return
|
||
}
|
||
|
||
// 创建订单
|
||
remark := types.OrderRemark{
|
||
Days: product.Days,
|
||
Power: product.Power,
|
||
Name: product.Name,
|
||
Price: product.Price,
|
||
Discount: product.Discount,
|
||
}
|
||
order := model.Order{
|
||
UserId: user.Id,
|
||
Username: user.Username,
|
||
ProductId: product.Id,
|
||
OrderNo: orderNo,
|
||
Subject: product.Name,
|
||
Amount: amount,
|
||
Status: types.OrderNotPaid,
|
||
PayWay: data.PayWay,
|
||
PayType: data.PayType,
|
||
Remark: utils.JsonEncode(remark),
|
||
}
|
||
err = h.DB.Create(&order).Error
|
||
if err != nil {
|
||
resp.ERROR(c, "error with create order: "+err.Error())
|
||
return
|
||
}
|
||
resp.SUCCESS(c, payURL)
|
||
}
|
||
|
||
// 异步通知回调公共逻辑
|
||
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||
var order model.Order
|
||
err := h.DB.Where("order_no = ?", orderNo).First(&order).Error
|
||
if err != nil {
|
||
return fmt.Errorf("error with fetch order: %v", err)
|
||
}
|
||
|
||
h.lock.Lock()
|
||
defer h.lock.Unlock()
|
||
|
||
// 已支付订单,直接返回
|
||
if order.Status == types.OrderPaidSuccess {
|
||
return nil
|
||
}
|
||
|
||
var user model.User
|
||
err = h.DB.First(&user, order.UserId).Error
|
||
if err != nil {
|
||
return fmt.Errorf("error with fetch user info: %v", err)
|
||
}
|
||
|
||
var remark types.OrderRemark
|
||
err = utils.JsonDecode(order.Remark, &remark)
|
||
if err != nil {
|
||
return fmt.Errorf("error with decode order remark: %v", err)
|
||
}
|
||
|
||
// 增加用户算力
|
||
err = h.userService.IncreasePower(int(order.UserId), remark.Power, model.PowerLog{
|
||
Type: types.PowerRecharge,
|
||
Model: order.PayWay,
|
||
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 更新订单状态
|
||
order.PayTime = time.Now().Unix()
|
||
order.Status = types.OrderPaidSuccess
|
||
order.TradeNo = tradeNo
|
||
err = h.DB.Updates(&order).Error
|
||
if err != nil {
|
||
return fmt.Errorf("error with update order info: %v", err)
|
||
}
|
||
|
||
// 更新产品销量
|
||
err = h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).
|
||
UpdateColumn("sales", gorm.Expr("sales + ?", 1)).Error
|
||
if err != nil {
|
||
return fmt.Errorf("error with update product sales: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetPayWays 获取支付方式
|
||
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
|
||
payWays := make([]gin.H, 0)
|
||
if h.App.Config.AlipayConfig.Enabled {
|
||
payWays = append(payWays, gin.H{"pay_way": "alipay", "pay_type": "alipay"})
|
||
}
|
||
if h.App.Config.HuPiPayConfig.Enabled {
|
||
payWays = append(payWays, gin.H{"pay_way": "hupi", "pay_type": "wxpay"})
|
||
}
|
||
if h.App.Config.GeekPayConfig.Enabled {
|
||
for _, v := range h.App.Config.GeekPayConfig.Methods {
|
||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": v})
|
||
}
|
||
}
|
||
if h.App.Config.WechatPayConfig.Enabled {
|
||
payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wxpay"})
|
||
}
|
||
resp.SUCCESS(c, payWays)
|
||
}
|
||
|
||
// HuPiPayNotify 虎皮椒支付异步回调
|
||
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
|
||
err := c.Request.ParseForm()
|
||
if err != nil {
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
orderNo := c.Request.Form.Get("trade_order_id")
|
||
tradeNo := c.Request.Form.Get("open_order_id")
|
||
logger.Infof("收到虎皮椒订单支付回调,%+v", c.Request.Form)
|
||
|
||
if err = h.huPiPayService.Check(orderNo); err != nil {
|
||
logger.Error("订单校验失败:", err)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
err = h.notify(orderNo, tradeNo)
|
||
if err != nil {
|
||
logger.Error(err)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
c.String(http.StatusOK, "success")
|
||
}
|
||
|
||
// AlipayNotify 支付宝支付回调
|
||
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||
err := c.Request.ParseForm()
|
||
if err != nil {
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
result := h.alipayService.TradeVerify(c.Request)
|
||
logger.Infof("收到支付宝商号订单支付回调:%+v", result)
|
||
if !result.Success() {
|
||
logger.Error("订单校验失败:", result.Message)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
tradeNo := c.Request.Form.Get("trade_no")
|
||
err = h.notify(result.OutTradeNo, tradeNo)
|
||
if err != nil {
|
||
logger.Error(err)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
c.String(http.StatusOK, "success")
|
||
}
|
||
|
||
// GeekPayNotify 支付异步回调
|
||
func (h *PaymentHandler) GeekPayNotify(c *gin.Context) {
|
||
var params = make(map[string]string)
|
||
for k := range c.Request.URL.Query() {
|
||
params[k] = c.Query(k)
|
||
}
|
||
|
||
logger.Infof("收到GeekPay订单支付回调:%+v", params)
|
||
// 检查支付状态
|
||
if params["trade_status"] != "TRADE_SUCCESS" {
|
||
c.String(http.StatusOK, "success")
|
||
return
|
||
}
|
||
|
||
sign := h.geekPayService.Sign(params)
|
||
if sign != c.Query("sign") {
|
||
logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign"))
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
err := h.notify(params["out_trade_no"], params["trade_no"])
|
||
if err != nil {
|
||
logger.Error(err)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
c.String(http.StatusOK, "success")
|
||
}
|
||
|
||
// WechatPayNotify 微信商户支付异步回调
|
||
func (h *PaymentHandler) WechatPayNotify(c *gin.Context) {
|
||
err := c.Request.ParseForm()
|
||
if err != nil {
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
result := h.wechatPayService.TradeVerify(c.Request)
|
||
logger.Infof("收到微信商号订单支付回调:%+v", result)
|
||
if !result.Success() {
|
||
logger.Error("订单校验失败:", err)
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"code": "FAIL",
|
||
"message": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
err = h.notify(result.OutTradeNo, result.TradeId)
|
||
if err != nil {
|
||
logger.Error(err)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
c.String(http.StatusOK, "success")
|
||
}
|