mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-17 16:56:38 +08:00
427 lines
11 KiB
Go
427 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
|
||
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,
|
||
snowflake *service.Snowflake,
|
||
fs embed.FS) *PaymentHandler {
|
||
return &PaymentHandler{
|
||
alipayService: alipayService,
|
||
huPiPayService: huPiPayService,
|
||
geekPayService: geekPayService,
|
||
wechatPayService: wechatPayService,
|
||
snowflake: snowflake,
|
||
fs: fs,
|
||
lock: sync.Mutex{},
|
||
BaseHandler: BaseHandler{
|
||
App: server,
|
||
DB: db,
|
||
},
|
||
signKey: utils.RandString(32),
|
||
}
|
||
}
|
||
|
||
func (h *PaymentHandler) Pay(c *gin.Context) {
|
||
payWay := c.Query("pay_way")
|
||
payType := c.Query("pay_type")
|
||
productId := c.Query("pid")
|
||
device := c.Query("device")
|
||
userId := c.Query("user_id")
|
||
|
||
var product model.Product
|
||
err := h.DB.First(&product, productId).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", userId).First(&user).Error
|
||
if err != nil {
|
||
resp.NotAuth(c)
|
||
return
|
||
}
|
||
// 创建订单
|
||
remark := types.OrderRemark{
|
||
Days: product.Days,
|
||
Power: product.Power,
|
||
Name: product.Name,
|
||
Price: product.Price,
|
||
Discount: product.Discount,
|
||
}
|
||
|
||
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
|
||
|
||
order := model.Order{
|
||
UserId: user.Id,
|
||
Username: user.Username,
|
||
ProductId: product.Id,
|
||
OrderNo: orderNo,
|
||
Subject: product.Name,
|
||
Amount: amount,
|
||
Status: types.OrderNotPaid,
|
||
PayWay: payWay,
|
||
PayType: payType,
|
||
Remark: utils.JsonEncode(remark),
|
||
}
|
||
err = h.DB.Create(&order).Error
|
||
if err != nil {
|
||
resp.ERROR(c, "error with create order: "+err.Error())
|
||
return
|
||
}
|
||
|
||
var payURL string
|
||
if payWay == "alipay" { // 支付宝
|
||
money := fmt.Sprintf("%.2f", order.Amount)
|
||
notifyURL := fmt.Sprintf("%s/api/payment/notify/alipay", h.App.Config.AlipayConfig.NotifyHost)
|
||
returnURL := fmt.Sprintf("%s/member", h.App.Config.AlipayConfig.NotifyHost)
|
||
if device == "mobile" {
|
||
payURL, err = h.alipayService.PayMobile(payment.AlipayParams{
|
||
OutTradeNo: orderNo,
|
||
Subject: product.Name,
|
||
TotalFee: money,
|
||
ReturnURL: returnURL,
|
||
NotifyURL: notifyURL,
|
||
})
|
||
} else {
|
||
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
|
||
}
|
||
} else if order.PayWay == "hupi" { // 虎皮椒支付
|
||
params := payment.HuPiPayReq{
|
||
Version: "1.1",
|
||
TradeOrderId: orderNo,
|
||
TotalFee: fmt.Sprintf("%f", order.Amount),
|
||
Title: order.Subject,
|
||
NotifyURL: fmt.Sprintf("%s/api/payment/notify/hupi", h.App.Config.HuPiPayConfig.NotifyHost),
|
||
WapName: "GeekAI助手",
|
||
}
|
||
r, err := h.huPiPayService.Pay(params)
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
c.Redirect(302, r.URL)
|
||
} else if order.PayWay == "wechat" {
|
||
//uri, err := h.wechatPayService.PayUrlNative(order.OrderNo, int(order.Amount*100), order.Subject)
|
||
uri, err := h.wechatPayService.PayUrlNative(payment.WechatPayParams{})
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
|
||
c.Redirect(302, uri)
|
||
} else if order.PayWay == "geek" {
|
||
notifyURL := fmt.Sprintf("%s/api/payment/notify/geek", h.App.Config.GeekPayConfig.NotifyHost)
|
||
returnURL := fmt.Sprintf("%s/member", h.App.Config.GeekPayConfig.NotifyHost)
|
||
params := payment.GeekPayParams{
|
||
OutTradeNo: orderNo,
|
||
Method: "web",
|
||
Name: order.Subject,
|
||
Money: fmt.Sprintf("%f", order.Amount),
|
||
ClientIP: c.ClientIP(),
|
||
Device: device,
|
||
Type: payType,
|
||
ReturnURL: returnURL,
|
||
NotifyURL: notifyURL,
|
||
}
|
||
|
||
res, err := h.geekPayService.Pay(params)
|
||
if err != nil {
|
||
resp.ERROR(c, err.Error())
|
||
return
|
||
}
|
||
payURL = res.PayURL
|
||
}
|
||
resp.SUCCESS(c, payURL)
|
||
}
|
||
|
||
// 异步通知回调公共逻辑
|
||
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
|
||
var order model.Order
|
||
res := h.DB.Where("order_no = ?", orderNo).First(&order)
|
||
if res.Error != nil {
|
||
err := fmt.Errorf("error with fetch order: %v", res.Error)
|
||
logger.Error(err)
|
||
return err
|
||
}
|
||
|
||
h.lock.Lock()
|
||
defer h.lock.Unlock()
|
||
|
||
// 已支付订单,直接返回
|
||
if order.Status == types.OrderPaidSuccess {
|
||
return nil
|
||
}
|
||
|
||
var user model.User
|
||
res = h.DB.First(&user, order.UserId)
|
||
if res.Error != nil {
|
||
err := fmt.Errorf("error with fetch user info: %v", res.Error)
|
||
logger.Error(err)
|
||
return err
|
||
}
|
||
|
||
var remark types.OrderRemark
|
||
err := utils.JsonDecode(order.Remark, &remark)
|
||
if err != nil {
|
||
err := fmt.Errorf("error with decode order remark: %v", err)
|
||
logger.Error(err)
|
||
return err
|
||
}
|
||
|
||
var opt string
|
||
var power int
|
||
if remark.Days > 0 { // VIP 充值
|
||
if user.ExpiredTime >= time.Now().Unix() {
|
||
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
|
||
opt = "VIP充值,VIP 没到期,只延期不增加算力"
|
||
} else {
|
||
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
|
||
user.Power += h.App.SysConfig.VipMonthPower
|
||
power = h.App.SysConfig.VipMonthPower
|
||
opt = "VIP充值"
|
||
}
|
||
user.Vip = true
|
||
} else { // 充值点卡,直接增加次数即可
|
||
user.Power += remark.Power
|
||
opt = "点卡充值"
|
||
power = remark.Power
|
||
}
|
||
|
||
// 更新用户信息
|
||
res = h.DB.Updates(&user)
|
||
if res.Error != nil {
|
||
err := fmt.Errorf("error with update user info: %v", res.Error)
|
||
logger.Error(err)
|
||
return err
|
||
}
|
||
|
||
// 更新订单状态
|
||
order.PayTime = time.Now().Unix()
|
||
order.Status = types.OrderPaidSuccess
|
||
order.TradeNo = tradeNo
|
||
res = h.DB.Updates(&order)
|
||
if res.Error != nil {
|
||
err := fmt.Errorf("error with update order info: %v", res.Error)
|
||
logger.Error(err)
|
||
return err
|
||
}
|
||
|
||
// 更新产品销量
|
||
h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
|
||
|
||
// 记录算力充值日志
|
||
if power > 0 {
|
||
h.DB.Create(&model.PowerLog{
|
||
UserId: user.Id,
|
||
Username: user.Username,
|
||
Type: types.PowerRecharge,
|
||
Amount: power,
|
||
Balance: user.Power,
|
||
Mark: types.PowerAdd,
|
||
Model: order.PayWay,
|
||
Remark: fmt.Sprintf("%s,金额:%f,订单号:%s", opt, order.Amount, order.OrderNo),
|
||
CreatedAt: time.Now(),
|
||
})
|
||
}
|
||
|
||
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": h.App.Config.HuPiPayConfig.Name})
|
||
}
|
||
if h.App.Config.GeekPayConfig.Enabled {
|
||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "alipay"})
|
||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "wxpay"})
|
||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "qqpay"})
|
||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "jdpay"})
|
||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "douyin"})
|
||
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "paypal"})
|
||
}
|
||
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("收到虎皮椒订单支付回调,订单 NO:%s,交易流水号:%s", orderNo, tradeNo)
|
||
|
||
if err = h.huPiPayService.Check(tradeNo); err != nil {
|
||
logger.Error("订单校验失败:", err)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
err = h.notify(orderNo, tradeNo)
|
||
if err != nil {
|
||
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
|
||
}
|
||
|
||
// TODO:验证交易签名
|
||
res := h.alipayService.TradeVerify(c.Request)
|
||
logger.Infof("验证支付结果:%+v", res)
|
||
if !res.Success() {
|
||
logger.Error("订单校验失败:", res.Message)
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
tradeNo := c.Request.Form.Get("trade_no")
|
||
err = h.notify(res.OutTradeNo, tradeNo)
|
||
if err != nil {
|
||
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 {
|
||
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)
|
||
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 {
|
||
c.String(http.StatusOK, "fail")
|
||
return
|
||
}
|
||
|
||
c.String(http.StatusOK, "success")
|
||
}
|