mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Geek Pay notify is ready
This commit is contained in:
		@@ -219,10 +219,6 @@ func needLogin(c *gin.Context) bool {
 | 
			
		||||
		c.Request.URL.Path == "/api/product/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/menu/list" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/markMap/client" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/alipay/notify" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/hupipay/notify" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/payjs/notify" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/wechat/notify" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/doPay" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/payment/payWays" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/client" ||
 | 
			
		||||
@@ -231,6 +227,7 @@ func needLogin(c *gin.Context) bool {
 | 
			
		||||
		c.Request.URL.Path == "/api/download" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/video/client" ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
 | 
			
		||||
 
 | 
			
		||||
@@ -57,8 +57,7 @@ type AlipayConfig struct {
 | 
			
		||||
	PublicKey       string // 用户公钥文件路径
 | 
			
		||||
	AlipayPublicKey string // 支付宝公钥文件路径
 | 
			
		||||
	RootCert        string // Root 秘钥路径
 | 
			
		||||
	NotifyURL       string // 异步通知回调
 | 
			
		||||
	ReturnURL       string // 支付成功返回地址
 | 
			
		||||
	NotifyHost      string // 通知回调地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WechatPayConfig struct {
 | 
			
		||||
@@ -68,18 +67,16 @@ type WechatPayConfig struct {
 | 
			
		||||
	SerialNo   string // 商户证书的证书序列号
 | 
			
		||||
	PrivateKey string // 用户私钥文件路径
 | 
			
		||||
	ApiV3Key   string // API V3 秘钥
 | 
			
		||||
	NotifyURL  string // 异步通知回调
 | 
			
		||||
	ReturnURL  string // 支付成功返回地址
 | 
			
		||||
	NotifyHost string // 通知回调地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HuPiPayConfig struct { //虎皮椒第四方支付配置
 | 
			
		||||
	Enabled   bool   // 是否启用该支付通道
 | 
			
		||||
	Name      string // 支付名称,如:wechat/alipay
 | 
			
		||||
	AppId     string // App ID
 | 
			
		||||
	AppSecret string // app 密钥
 | 
			
		||||
	ApiURL    string // 支付网关
 | 
			
		||||
	NotifyURL string // 异步通知回调
 | 
			
		||||
	ReturnURL string // 支付成功返回地址
 | 
			
		||||
	Enabled    bool   // 是否启用该支付通道
 | 
			
		||||
	Name       string // 支付名称,如:wechat/alipay
 | 
			
		||||
	AppId      string // App ID
 | 
			
		||||
	AppSecret  string // app 密钥
 | 
			
		||||
	ApiURL     string // 支付网关
 | 
			
		||||
	NotifyHost string // 通知回调地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GeekPayConfig GEEK支付配置
 | 
			
		||||
@@ -88,8 +85,7 @@ type GeekPayConfig struct {
 | 
			
		||||
	AppId      string // 商户 ID
 | 
			
		||||
	PrivateKey string // 私钥
 | 
			
		||||
	ApiURL     string // API 网关
 | 
			
		||||
	NotifyURL  string // 异步回调地址
 | 
			
		||||
	ReturnURL  string // 支付成功返回地址
 | 
			
		||||
	NotifyHost string // 通知回调地址
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type XXLConfig struct { // XXL 任务调度配置
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ package handler
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
@@ -20,7 +19,6 @@ import (
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"github.com/shopspring/decimal"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@@ -71,62 +69,94 @@ func NewPaymentHandler(
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *PaymentHandler) DoPay(c *gin.Context) {
 | 
			
		||||
	orderNo := h.GetTrim(c, "order_no")
 | 
			
		||||
	t := h.GetInt(c, "t", 0)
 | 
			
		||||
	sign := h.GetTrim(c, "sign")
 | 
			
		||||
	signStr := fmt.Sprintf("%s-%d-%s", orderNo, t, h.signKey)
 | 
			
		||||
	newSign := utils.Sha256(signStr)
 | 
			
		||||
	if newSign != sign {
 | 
			
		||||
		resp.ERROR(c, "订单签名错误!")
 | 
			
		||||
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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查二维码是否过期
 | 
			
		||||
	if time.Now().Unix()-int64(t) > int64(h.App.SysConfig.OrderPayTimeout) {
 | 
			
		||||
		resp.ERROR(c, "支付二维码已过期,请重新生成!")
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if orderNo == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var order model.Order
 | 
			
		||||
	res := h.DB.Where("order_no = ?", orderNo).First(&order)
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		resp.ERROR(c, "Order not found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// fix: 这里先检查一下订单状态,如果已经支付了,就直接返回
 | 
			
		||||
	if order.Status == types.OrderPaidSuccess {
 | 
			
		||||
		resp.ERROR(c, "订单已支付成功,无需重复支付!")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 更新扫码状态
 | 
			
		||||
	h.DB.Model(&order).UpdateColumn("status", types.OrderScanned)
 | 
			
		||||
 | 
			
		||||
	if order.PayWay == "alipay" { // 支付宝
 | 
			
		||||
		amount := fmt.Sprintf("%.2f", order.Amount)
 | 
			
		||||
		uri, err := h.alipayService.PayUrlMobile(order.OrderNo, amount, order.Subject)
 | 
			
		||||
	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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Redirect(302, uri)
 | 
			
		||||
		return
 | 
			
		||||
	} else if order.PayWay == "hupi" { // 虎皮椒支付
 | 
			
		||||
		params := payment.HuPiPayReq{
 | 
			
		||||
			Version:      "1.1",
 | 
			
		||||
			TradeOrderId: orderNo,
 | 
			
		||||
			TotalFee:     fmt.Sprintf("%f", order.Amount),
 | 
			
		||||
			Title:        order.Subject,
 | 
			
		||||
			NotifyURL:    h.App.Config.HuPiPayConfig.NotifyURL,
 | 
			
		||||
			WapName:      "极客学长",
 | 
			
		||||
			NotifyURL:    fmt.Sprintf("%s/api/payment/notify/hupi", h.App.Config.HuPiPayConfig.NotifyHost),
 | 
			
		||||
			WapName:      "GeekAI助手",
 | 
			
		||||
		}
 | 
			
		||||
		r, err := h.huPiPayService.Pay(params)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -136,7 +166,8 @@ func (h *PaymentHandler) DoPay(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
		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(order.OrderNo, int(order.Amount*100), order.Subject)
 | 
			
		||||
		uri, err := h.wechatPayService.PayUrlNative(payment.WechatPayParams{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
@@ -144,261 +175,28 @@ func (h *PaymentHandler) DoPay(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
		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:     "pc",
 | 
			
		||||
			Type:       "alipay",
 | 
			
		||||
			Device:     device,
 | 
			
		||||
			Type:       payType,
 | 
			
		||||
			ReturnURL:  returnURL,
 | 
			
		||||
			NotifyURL:  notifyURL,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		s, err := h.geekPayService.Pay(params)
 | 
			
		||||
		res, err := h.geekPayService.Pay(params)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			resp.ERROR(c, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		resp.SUCCESS(c, s)
 | 
			
		||||
		payURL = res.PayURL
 | 
			
		||||
	}
 | 
			
		||||
	//resp.ERROR(c, "Invalid operations")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PayQrcode 生成支付 URL 二维码
 | 
			
		||||
func (h *PaymentHandler) PayQrcode(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		PayWay    string `json:"pay_way"`    // 支付方式
 | 
			
		||||
		PayType   string `json:"pay_type"`   // 支付类别:wechat,alipay,qq...
 | 
			
		||||
		ProductId uint   `json:"product_id"` // 支付产品ID
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var product model.Product
 | 
			
		||||
	res := h.DB.First(&product, data.ProductId)
 | 
			
		||||
	if res.Error != 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
 | 
			
		||||
	}
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var notifyURL string
 | 
			
		||||
	switch data.PayWay {
 | 
			
		||||
	case "hupi":
 | 
			
		||||
		notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
 | 
			
		||||
		break
 | 
			
		||||
	case "geek":
 | 
			
		||||
		notifyURL = h.App.Config.GeekPayConfig.NotifyURL
 | 
			
		||||
		break
 | 
			
		||||
	case "alipay": // 支付宝商户支付
 | 
			
		||||
		notifyURL = h.App.Config.AlipayConfig.NotifyURL
 | 
			
		||||
		break
 | 
			
		||||
	case "wechat": // 微信商户支付
 | 
			
		||||
		notifyURL = h.App.Config.WechatPayConfig.NotifyURL
 | 
			
		||||
	default:
 | 
			
		||||
		resp.ERROR(c, "Invalid pay way")
 | 
			
		||||
		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:    data.PayWay,
 | 
			
		||||
		PayType:   data.PayType,
 | 
			
		||||
		Remark:    utils.JsonEncode(remark),
 | 
			
		||||
	}
 | 
			
		||||
	res = h.DB.Create(&order)
 | 
			
		||||
	if res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "error with create order: "+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var logo string
 | 
			
		||||
	switch data.PayType {
 | 
			
		||||
	case "alipay":
 | 
			
		||||
		logo = "res/img/alipay.jpg"
 | 
			
		||||
		break
 | 
			
		||||
	case "wechat":
 | 
			
		||||
		logo = "res/img/wechat-pay.jpg"
 | 
			
		||||
		break
 | 
			
		||||
	case "qq":
 | 
			
		||||
		logo = "res/img/qq-pay.jpg"
 | 
			
		||||
		break
 | 
			
		||||
	default:
 | 
			
		||||
		logo = "res/img/geek-pay.jpg"
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	if data.PayType == "alipay" {
 | 
			
		||||
		logo = "res/img/alipay.jpg"
 | 
			
		||||
	} else if data.PayType == "wechat" {
 | 
			
		||||
		logo = "res/img/wechat-pay.jpg"
 | 
			
		||||
	}
 | 
			
		||||
	file, err := h.fs.Open(logo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, "error with open qrcode log file: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parse, err := url.Parse(notifyURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	timestamp := time.Now().Unix()
 | 
			
		||||
	signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, data.PayWay, timestamp, h.signKey)
 | 
			
		||||
	sign := utils.Sha256(signStr)
 | 
			
		||||
	payUrl := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s&pay_type=%s&t=%d&sign=%s", parse.Scheme, parse.Host, orderNo, data.PayWay, data.PayType, timestamp, sign)
 | 
			
		||||
	imgData, err := utils.GenQrcode(payUrl, 400, file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	imgDataBase64 := base64.StdEncoding.EncodeToString(imgData)
 | 
			
		||||
	resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": payUrl})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mobile 移动端支付
 | 
			
		||||
func (h *PaymentHandler) Mobile(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		PayWay    string `json:"pay_way"`  // 支付方式
 | 
			
		||||
		PayType   string `json:"pay_type"` // 支付类别:wechat,alipay,qq...
 | 
			
		||||
		ProductId uint   `json:"product_id"`
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.ShouldBindJSON(&data); err != nil {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var product model.Product
 | 
			
		||||
	res := h.DB.First(&product, data.ProductId)
 | 
			
		||||
	if res.Error != 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
 | 
			
		||||
	}
 | 
			
		||||
	user, err := h.GetLoginUser(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.NotAuth(c)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
 | 
			
		||||
	var notifyURL, returnURL string
 | 
			
		||||
	var payURL string
 | 
			
		||||
	switch data.PayWay {
 | 
			
		||||
	case "hupi":
 | 
			
		||||
		notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
 | 
			
		||||
		returnURL = h.App.Config.HuPiPayConfig.ReturnURL
 | 
			
		||||
		parse, _ := url.Parse(h.App.Config.HuPiPayConfig.ReturnURL)
 | 
			
		||||
		baseURL := fmt.Sprintf("%s://%s", parse.Scheme, parse.Host)
 | 
			
		||||
		params := payment.HuPiPayReq{
 | 
			
		||||
			Version:      "1.1",
 | 
			
		||||
			TradeOrderId: orderNo,
 | 
			
		||||
			TotalFee:     fmt.Sprintf("%f", amount),
 | 
			
		||||
			Title:        product.Name,
 | 
			
		||||
			NotifyURL:    notifyURL,
 | 
			
		||||
			ReturnURL:    returnURL,
 | 
			
		||||
			CallbackURL:  returnURL,
 | 
			
		||||
			WapName:      "极客学长",
 | 
			
		||||
			WapUrl:       baseURL,
 | 
			
		||||
			Type:         "WAP",
 | 
			
		||||
		}
 | 
			
		||||
		r, err := h.huPiPayService.Pay(params)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errMsg := "error with generating Pay Hupi URL: " + err.Error()
 | 
			
		||||
			logger.Error(errMsg)
 | 
			
		||||
			resp.ERROR(c, errMsg)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		payURL = r.URL
 | 
			
		||||
	case "geek":
 | 
			
		||||
		//totalFee := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Mul(decimal.NewFromInt(100)).IntPart()
 | 
			
		||||
		//params := url.Values{}
 | 
			
		||||
		//params.Add("total_fee", fmt.Sprintf("%d", totalFee))
 | 
			
		||||
		//params.Add("out_trade_no", orderNo)
 | 
			
		||||
		//params.Add("body", product.Name)
 | 
			
		||||
		//params.Add("notify_url", notifyURL)
 | 
			
		||||
		//params.Add("auto", "0")
 | 
			
		||||
		//payURL = h.geekPayService.Pay(params)
 | 
			
		||||
	case "alipay":
 | 
			
		||||
		payURL, err = h.alipayService.PayUrlMobile(orderNo, fmt.Sprintf("%.2f", amount), product.Name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errMsg := "error with generating Alipay URL: " + err.Error()
 | 
			
		||||
			resp.ERROR(c, errMsg)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	case "wechat":
 | 
			
		||||
		payURL, err = h.wechatPayService.PayUrlH5(orderNo, int(amount*100), product.Name, c.ClientIP())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			errMsg := "error with generating Wechat URL: " + err.Error()
 | 
			
		||||
			logger.Error(errMsg)
 | 
			
		||||
			resp.ERROR(c, errMsg)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		resp.ERROR(c, "Unsupported pay way: "+data.PayWay)
 | 
			
		||||
		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),
 | 
			
		||||
	}
 | 
			
		||||
	res = h.DB.Create(&order)
 | 
			
		||||
	if res.Error != nil || res.RowsAffected == 0 {
 | 
			
		||||
		resp.ERROR(c, "error with create order: "+res.Error.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp.SUCCESS(c, gin.H{"url": payURL, "order_no": orderNo})
 | 
			
		||||
	resp.SUCCESS(c, payURL)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 异步通知回调公共逻辑
 | 
			
		||||
@@ -505,14 +303,14 @@ func (h *PaymentHandler) GetPayWays(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	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": "wechat"})
 | 
			
		||||
		payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "qq"})
 | 
			
		||||
		payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "jd"})
 | 
			
		||||
		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": "wechat"})
 | 
			
		||||
		payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wxpay"})
 | 
			
		||||
	}
 | 
			
		||||
	resp.SUCCESS(c, payWays)
 | 
			
		||||
}
 | 
			
		||||
@@ -570,32 +368,28 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
 | 
			
		||||
	c.String(http.StatusOK, "success")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PayJsNotify PayJs 支付异步回调
 | 
			
		||||
func (h *PaymentHandler) PayJsNotify(c *gin.Context) {
 | 
			
		||||
	err := c.Request.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
// 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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	orderNo := c.Request.Form.Get("out_trade_no")
 | 
			
		||||
	returnCode := c.Request.Form.Get("return_code")
 | 
			
		||||
	logger.Infof("收到PayJs订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode)
 | 
			
		||||
	// 支付失败
 | 
			
		||||
	if returnCode != "1" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 校验订单支付状态
 | 
			
		||||
	tradeNo := c.Request.Form.Get("payjs_order_id")
 | 
			
		||||
	//err = h.geekPayService.TradeVerify(tradeNo)
 | 
			
		||||
	//if err != nil {
 | 
			
		||||
	//	logger.Error("订单校验失败:", err)
 | 
			
		||||
	//	c.String(http.StatusOK, "fail")
 | 
			
		||||
	//	return
 | 
			
		||||
	//}
 | 
			
		||||
 | 
			
		||||
	err = h.notify(orderNo, tradeNo)
 | 
			
		||||
	err := h.notify(params["out_trade_no"], params["trade_no"])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.String(http.StatusOK, "fail")
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								api/main.go
									
									
									
									
									
								
							@@ -372,14 +372,11 @@ func main() {
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/payment/")
 | 
			
		||||
			group.GET("doPay", h.DoPay)
 | 
			
		||||
			group.GET("doPay", h.Pay)
 | 
			
		||||
			group.GET("payWays", h.GetPayWays)
 | 
			
		||||
			group.POST("qrcode", h.PayQrcode)
 | 
			
		||||
			group.POST("mobile", h.Mobile)
 | 
			
		||||
			group.POST("alipay/notify", h.AlipayNotify)
 | 
			
		||||
			group.POST("hupipay/notify", h.HuPiPayNotify)
 | 
			
		||||
			group.POST("payjs/notify", h.PayJsNotify)
 | 
			
		||||
			group.POST("wechat/notify", h.WechatPayNotify)
 | 
			
		||||
			group.POST("notify/alipay", h.AlipayNotify)
 | 
			
		||||
			group.GET("notify/geek", h.GeekPayNotify)
 | 
			
		||||
			group.POST("notify/wechat", h.WechatPayNotify)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/admin/product/")
 | 
			
		||||
 
 | 
			
		||||
@@ -44,9 +44,7 @@ func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
 | 
			
		||||
	//client.DebugSwitch = gopay.DebugOn // 开启调试模式
 | 
			
		||||
	client.SetLocation(alipay.LocationShanghai). // 设置时区,不设置或出错均为默认服务器时间
 | 
			
		||||
		SetCharset(alipay.UTF8). // 设置字符编码,不设置默认 utf-8
 | 
			
		||||
		SetSignType(alipay.RSA2). // 设置签名类型,不设置默认 RSA2
 | 
			
		||||
		SetReturnUrl(config.ReturnURL). // 设置返回URL
 | 
			
		||||
		SetNotifyUrl(config.NotifyURL)
 | 
			
		||||
		SetSignType(alipay.RSA2) // 设置签名类型,不设置默认 RSA2
 | 
			
		||||
 | 
			
		||||
	if err = client.SetCertSnByPath(config.PublicKey, config.RootCert, config.AlipayPublicKey); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error with load payment public key: %v", err)
 | 
			
		||||
@@ -55,23 +53,33 @@ func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
 | 
			
		||||
	return &AlipayService{config: &config, client: client}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *AlipayService) PayUrlMobile(outTradeNo string, amount string, subject string) (string, error) {
 | 
			
		||||
type AlipayParams struct {
 | 
			
		||||
	OutTradeNo string `json:"out_trade_no"`
 | 
			
		||||
	Subject    string `json:"subject"`
 | 
			
		||||
	TotalFee   string `json:"total_fee"`
 | 
			
		||||
	ReturnURL  string `json:"return_url"`
 | 
			
		||||
	NotifyURL  string `json:"notify_url"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *AlipayService) PayMobile(params AlipayParams) (string, error) {
 | 
			
		||||
	bm := make(gopay.BodyMap)
 | 
			
		||||
	bm.Set("subject", subject)
 | 
			
		||||
	bm.Set("out_trade_no", outTradeNo)
 | 
			
		||||
	bm.Set("quit_url", s.config.ReturnURL)
 | 
			
		||||
	bm.Set("total_amount", amount)
 | 
			
		||||
	bm.Set("subject", params.Subject)
 | 
			
		||||
	bm.Set("out_trade_no", params.OutTradeNo)
 | 
			
		||||
	bm.Set("quit_url", params.ReturnURL)
 | 
			
		||||
	bm.Set("total_amount", params.TotalFee)
 | 
			
		||||
	bm.Set("return_url", params.ReturnURL)
 | 
			
		||||
	bm.Set("notify_url", params.NotifyURL)
 | 
			
		||||
	bm.Set("product_code", "QUICK_WAP_WAY")
 | 
			
		||||
	return s.client.TradeWapPay(context.Background(), bm)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *AlipayService) PayUrlPc(outTradeNo string, amount string, subject string) (string, error) {
 | 
			
		||||
func (s *AlipayService) PayPC(params AlipayParams) (string, error) {
 | 
			
		||||
	bm := make(gopay.BodyMap)
 | 
			
		||||
	bm.Set("subject", subject)
 | 
			
		||||
	bm.Set("out_trade_no", outTradeNo)
 | 
			
		||||
	bm.Set("total_amount", amount)
 | 
			
		||||
	bm.Set("subject", params.Subject)
 | 
			
		||||
	bm.Set("out_trade_no", params.OutTradeNo)
 | 
			
		||||
	bm.Set("total_amount", params.TotalFee)
 | 
			
		||||
	bm.Set("product_code", "FAST_INSTANT_TRADE_PAY")
 | 
			
		||||
	return s.client.TradePagePay(context.Background(), bm)
 | 
			
		||||
	return s.client.SetNotifyUrl(params.NotifyURL).SetReturnUrl(params.ReturnURL).TradePagePay(context.Background(), bm)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TradeVerify 交易验证
 | 
			
		||||
 
 | 
			
		||||
@@ -8,15 +8,11 @@ package payment
 | 
			
		||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"crypto/rsa"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"crypto/x509"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/pem"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"geekai/core/types"
 | 
			
		||||
	"geekai/utils"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
@@ -46,41 +42,37 @@ type GeekPayParams struct {
 | 
			
		||||
	ClientIP   string `json:"clientip"`     //用户IP地址
 | 
			
		||||
	SubOpenId  string `json:"sub_openid"`   // 微信用户 openid,仅小程序支付需要
 | 
			
		||||
	SubAppId   string `json:"sub_appid"`    // 小程序 AppId,仅小程序支付需要
 | 
			
		||||
	NotifyURL  string `json:"notify_url"`
 | 
			
		||||
	ReturnURL  string `json:"return_url"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Pay 支付订单
 | 
			
		||||
func (s *GeekPayService) Pay(params GeekPayParams) (string, error) {
 | 
			
		||||
	if params.Type == "wechat" {
 | 
			
		||||
		params.Type = "wxpay"
 | 
			
		||||
	}
 | 
			
		||||
func (s *GeekPayService) Pay(params GeekPayParams) (*GeekPayResp, error) {
 | 
			
		||||
	p := map[string]string{
 | 
			
		||||
		"pid":          s.config.AppId,
 | 
			
		||||
		"method":       params.Method,
 | 
			
		||||
		"pid": s.config.AppId,
 | 
			
		||||
		//"method":       params.Method,
 | 
			
		||||
		"device":       params.Device,
 | 
			
		||||
		"type":         params.Type,
 | 
			
		||||
		"out_trade_no": params.OutTradeNo,
 | 
			
		||||
		"name":         params.Name,
 | 
			
		||||
		"money":        params.Money,
 | 
			
		||||
		"clientip":     params.ClientIP,
 | 
			
		||||
		"sub_openid":   params.SubOpenId,
 | 
			
		||||
		"sub_appid":    params.SubAppId,
 | 
			
		||||
		"notify_url":   s.config.NotifyURL,
 | 
			
		||||
		"return_url":   s.config.ReturnURL,
 | 
			
		||||
		"notify_url":   params.NotifyURL,
 | 
			
		||||
		"return_url":   params.ReturnURL,
 | 
			
		||||
		"timestamp":    fmt.Sprintf("%d", time.Now().Unix()),
 | 
			
		||||
	}
 | 
			
		||||
	sign, err := s.Sign(p)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	p["sign"] = sign
 | 
			
		||||
	p["sign_type"] = "RSA"
 | 
			
		||||
	p["sign"] = s.Sign(p)
 | 
			
		||||
	p["sign_type"] = "MD5"
 | 
			
		||||
	return s.sendRequest(s.config.ApiURL, p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *GeekPayService) Sign(params map[string]string) (string, error) {
 | 
			
		||||
func (s *GeekPayService) Sign(params map[string]string) string {
 | 
			
		||||
	// 按字母顺序排序参数
 | 
			
		||||
	var keys []string
 | 
			
		||||
	for k := range params {
 | 
			
		||||
		if params[k] == "" || k == "sign" || k == "sign_type" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		keys = append(keys, k)
 | 
			
		||||
	}
 | 
			
		||||
	sort.Strings(keys)
 | 
			
		||||
@@ -93,44 +85,48 @@ func (s *GeekPayService) Sign(params map[string]string) (string, error) {
 | 
			
		||||
		signStr.WriteString(params[k])
 | 
			
		||||
		signStr.WriteString("&")
 | 
			
		||||
	}
 | 
			
		||||
	signString := strings.TrimSuffix(signStr.String(), "&")
 | 
			
		||||
	signString := strings.TrimSuffix(signStr.String(), "&") + s.config.PrivateKey
 | 
			
		||||
 | 
			
		||||
	// 使用RSA私钥签名
 | 
			
		||||
	block, _ := pem.Decode([]byte(s.config.PrivateKey))
 | 
			
		||||
	if block == nil {
 | 
			
		||||
		return "", fmt.Errorf("failed to decode private key")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("failed to parse private key: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hashed := sha256.Sum256([]byte(signString))
 | 
			
		||||
	signature, err := rsa.SignPKCS1v15(rand.Reader, priKey, crypto.SHA256, hashed[:])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(fmt.Sprintf("failed to sign: %v", err))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return base64.StdEncoding.EncodeToString(signature), nil
 | 
			
		||||
	return utils.Md5(signString)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *GeekPayService) sendRequest(apiEndpoint string, params map[string]string) (string, error) {
 | 
			
		||||
type GeekPayResp struct {
 | 
			
		||||
	Code      int    `json:"code"`
 | 
			
		||||
	Msg       string `json:"msg"`
 | 
			
		||||
	TradeNo   string `json:"trade_no"`
 | 
			
		||||
	PayURL    string `json:"payurl"`
 | 
			
		||||
	QrCode    string `json:"qrcode"`
 | 
			
		||||
	UrlScheme string `json:"urlscheme"` // 小程序跳转支付链接
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *GeekPayService) sendRequest(endpoint string, params map[string]string) (*GeekPayResp, error) {
 | 
			
		||||
	form := url.Values{}
 | 
			
		||||
	for k, v := range params {
 | 
			
		||||
		form.Add(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.PostForm(apiEndpoint, form)
 | 
			
		||||
	apiURL := fmt.Sprintf("%s/mapi.php", endpoint)
 | 
			
		||||
	logger.Infof(apiURL)
 | 
			
		||||
 | 
			
		||||
	resp, err := http.PostForm(apiURL, form)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	body, err := io.ReadAll(resp.Body)
 | 
			
		||||
	logger.Debugf(string(body))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return string(body), nil
 | 
			
		||||
	var r GeekPayResp
 | 
			
		||||
	err = json.Unmarshal(body, &r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.New("当前支付渠道暂不支持")
 | 
			
		||||
	}
 | 
			
		||||
	if r.Code != 1 {
 | 
			
		||||
		return nil, errors.New(r.Msg)
 | 
			
		||||
	}
 | 
			
		||||
	return &r, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -46,18 +46,28 @@ func NewWechatService(appConfig *types.AppConfig) (*WechatPayService, error) {
 | 
			
		||||
	return &WechatPayService{config: &config, client: client}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *WechatPayService) PayUrlNative(outTradeNo string, amount int, subject string) (string, error) {
 | 
			
		||||
type WechatPayParams struct {
 | 
			
		||||
	OutTradeNo string `json:"out_trade_no"`
 | 
			
		||||
	TotalFee   int    `json:"total_fee"`
 | 
			
		||||
	Subject    string `json:"subject"`
 | 
			
		||||
	ClientIP   string `json:"client_ip"`
 | 
			
		||||
	ReturnURL  string `json:"return_url"`
 | 
			
		||||
	NotifyURL  string `json:"notify_url"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *WechatPayService) PayUrlNative(params WechatPayParams) (string, error) {
 | 
			
		||||
	expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
 | 
			
		||||
	// 初始化 BodyMap
 | 
			
		||||
	bm := make(gopay.BodyMap)
 | 
			
		||||
	bm.Set("appid", s.config.AppId).
 | 
			
		||||
		Set("mchid", s.config.MchId).
 | 
			
		||||
		Set("description", subject).
 | 
			
		||||
		Set("out_trade_no", outTradeNo).
 | 
			
		||||
		Set("description", params.Subject).
 | 
			
		||||
		Set("out_trade_no", params.OutTradeNo).
 | 
			
		||||
		Set("time_expire", expire).
 | 
			
		||||
		Set("notify_url", s.config.NotifyURL).
 | 
			
		||||
		Set("notify_url", params.NotifyURL).
 | 
			
		||||
		Set("return_url", params.ReturnURL).
 | 
			
		||||
		SetBodyMap("amount", func(bm gopay.BodyMap) {
 | 
			
		||||
			bm.Set("total", amount).
 | 
			
		||||
			bm.Set("total", params.TotalFee).
 | 
			
		||||
				Set("currency", "CNY")
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
@@ -71,22 +81,23 @@ func (s *WechatPayService) PayUrlNative(outTradeNo string, amount int, subject s
 | 
			
		||||
	return wxRsp.Response.CodeUrl, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *WechatPayService) PayUrlH5(outTradeNo string, amount int, subject string, ip string) (string, error) {
 | 
			
		||||
func (s *WechatPayService) PayUrlH5(params WechatPayParams) (string, error) {
 | 
			
		||||
	expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
 | 
			
		||||
	// 初始化 BodyMap
 | 
			
		||||
	bm := make(gopay.BodyMap)
 | 
			
		||||
	bm.Set("appid", s.config.AppId).
 | 
			
		||||
		Set("mchid", s.config.MchId).
 | 
			
		||||
		Set("description", subject).
 | 
			
		||||
		Set("out_trade_no", outTradeNo).
 | 
			
		||||
		Set("description", params.Subject).
 | 
			
		||||
		Set("out_trade_no", params.OutTradeNo).
 | 
			
		||||
		Set("time_expire", expire).
 | 
			
		||||
		Set("notify_url", s.config.NotifyURL).
 | 
			
		||||
		Set("notify_url", params.NotifyURL).
 | 
			
		||||
		Set("return_url", params.ReturnURL).
 | 
			
		||||
		SetBodyMap("amount", func(bm gopay.BodyMap) {
 | 
			
		||||
			bm.Set("total", amount).
 | 
			
		||||
			bm.Set("total", params.TotalFee).
 | 
			
		||||
				Set("currency", "CNY")
 | 
			
		||||
		}).
 | 
			
		||||
		SetBodyMap("scene_info", func(bm gopay.BodyMap) {
 | 
			
		||||
			bm.Set("payer_client_ip", ip).
 | 
			
		||||
			bm.Set("payer_client_ip", params.ClientIP).
 | 
			
		||||
				SetBodyMap("h5_info", func(bm gopay.BodyMap) {
 | 
			
		||||
					bm.Set("type", "Wap")
 | 
			
		||||
				})
 | 
			
		||||
 
 | 
			
		||||
@@ -181,6 +181,27 @@
 | 
			
		||||
 | 
			
		||||
              .el-button {
 | 
			
		||||
                margin 10px 5px 0 5px
 | 
			
		||||
                padding 0
 | 
			
		||||
 | 
			
		||||
                .icon-alipay,.icon-wechat-pay {
 | 
			
		||||
                  color #ffffff
 | 
			
		||||
                }
 | 
			
		||||
                .icon-qq {
 | 
			
		||||
                  color #15A6E8
 | 
			
		||||
                  font-size 24px
 | 
			
		||||
                }
 | 
			
		||||
                .icon-jd-pay {
 | 
			
		||||
                  color #ffffff
 | 
			
		||||
                  font-size 24px
 | 
			
		||||
                }
 | 
			
		||||
                .icon-douyin {
 | 
			
		||||
                  color #0a0a0a
 | 
			
		||||
                  font-size 22px
 | 
			
		||||
                }
 | 
			
		||||
                .icon-paypal {
 | 
			
		||||
                  font-size 14px
 | 
			
		||||
                  color #009CDE
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "iconfont"; /* Project id 4125778 */
 | 
			
		||||
  src: url('iconfont.woff2?t=1726612860394') format('woff2'),
 | 
			
		||||
       url('iconfont.woff?t=1726612860394') format('woff'),
 | 
			
		||||
       url('iconfont.ttf?t=1726612860394') format('truetype');
 | 
			
		||||
  src: url('iconfont.woff2?t=1726622198991') format('woff2'),
 | 
			
		||||
       url('iconfont.woff?t=1726622198991') format('woff'),
 | 
			
		||||
       url('iconfont.ttf?t=1726622198991') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.iconfont {
 | 
			
		||||
@@ -13,12 +13,12 @@
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-douyin:before {
 | 
			
		||||
  content: "\e852";
 | 
			
		||||
.icon-paypal:before {
 | 
			
		||||
  content: "\e666";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-paypal:before {
 | 
			
		||||
  content: "\e60f";
 | 
			
		||||
.icon-douyin:before {
 | 
			
		||||
  content: "\e8db";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-qq:before {
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -6,18 +6,18 @@
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "glyphs": [
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "22174321",
 | 
			
		||||
      "name": "抖音支付",
 | 
			
		||||
      "font_class": "douyin",
 | 
			
		||||
      "unicode": "e852",
 | 
			
		||||
      "unicode_decimal": 59474
 | 
			
		||||
      "icon_id": "7443846",
 | 
			
		||||
      "name": "PayPal",
 | 
			
		||||
      "font_class": "paypal",
 | 
			
		||||
      "unicode": "e666",
 | 
			
		||||
      "unicode_decimal": 58982
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "1238433",
 | 
			
		||||
      "name": "social-paypal",
 | 
			
		||||
      "font_class": "paypal",
 | 
			
		||||
      "unicode": "e60f",
 | 
			
		||||
      "unicode_decimal": 58895
 | 
			
		||||
      "icon_id": "18166694",
 | 
			
		||||
      "name": "抖音",
 | 
			
		||||
      "font_class": "douyin",
 | 
			
		||||
      "unicode": "e8db",
 | 
			
		||||
      "unicode_decimal": 59611
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "icon_id": "1244217",
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="user-bill" v-loading="loading">
 | 
			
		||||
  <div class="user-bill" v-loading="loading" element-loading-background="rgba(255,255,255,.3)">
 | 
			
		||||
    <el-row v-if="items.length > 0">
 | 
			
		||||
      <el-table :data="items" :row-key="row => row.id" table-layout="auto" border
 | 
			
		||||
                style="--el-table-border-color:#373C47;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <div class="member custom-scroll">
 | 
			
		||||
    <div class="member custom-scroll" v-loading="loading" element-loading-background="rgba(255,255,255,.3)" :element-loading-text="loadingText">
 | 
			
		||||
      <div class="inner">
 | 
			
		||||
        <div class="user-profile">
 | 
			
		||||
          <user-profile :key="profileKey"/>
 | 
			
		||||
@@ -22,10 +22,6 @@
 | 
			
		||||
              <el-button type="primary" @click="showRedeemVerifyDialog = true">卡密兑换
 | 
			
		||||
              </el-button>
 | 
			
		||||
            </el-col>
 | 
			
		||||
 | 
			
		||||
            <el-col :span="24" style="padding-top: 30px" v-if="isLogin">
 | 
			
		||||
              <el-button type="danger" round @click="logout">退出登录</el-button>
 | 
			
		||||
            </el-col>
 | 
			
		||||
          </el-row>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@@ -67,23 +63,23 @@
 | 
			
		||||
 | 
			
		||||
                  <div class="pay-way">
 | 
			
		||||
 | 
			
		||||
                    <span type="primary" v-for="payWay in payWays" @click="genPayQrcode(item,payWay)" :key="payWay">
 | 
			
		||||
                      <el-button v-if="payWay.pay_type==='alipay'" color="#409eff" circle>
 | 
			
		||||
                    <span type="primary" v-for="payWay in payWays" @click="pay(item,payWay)" :key="payWay">
 | 
			
		||||
                      <el-button v-if="payWay.pay_type==='alipay'" color="#15A6E8" circle>
 | 
			
		||||
                        <i class="iconfont icon-alipay" ></i>
 | 
			
		||||
                      </el-button>
 | 
			
		||||
                      <el-button v-else-if="payWay.pay_type==='qq'" class="qq" circle>
 | 
			
		||||
                        <i class="iconfont icon-qq" ></i>
 | 
			
		||||
                      <el-button v-else-if="payWay.pay_type==='qqpay'" circle>
 | 
			
		||||
                        <i class="iconfont icon-qq"></i>
 | 
			
		||||
                      </el-button>
 | 
			
		||||
                      <el-button v-else-if="payWay.pay_type==='paypal'" class="paypal" circle>
 | 
			
		||||
                      <el-button v-else-if="payWay.pay_type==='paypal'" class="paypal" round>
 | 
			
		||||
                        <i class="iconfont icon-paypal"></i>
 | 
			
		||||
                      </el-button>
 | 
			
		||||
                      <el-button v-else-if="payWay.pay_type==='jd'" class="jd" circle>
 | 
			
		||||
                      <el-button v-else-if="payWay.pay_type==='jdpay'" color="#E1251B" circle>
 | 
			
		||||
                        <i class="iconfont icon-jd-pay"></i>
 | 
			
		||||
                      </el-button>
 | 
			
		||||
                      <el-button v-else-if="payWay.pay_type==='douyin'" class="douyin" circle>
 | 
			
		||||
                         <i class="iconfont icon-douyin"></i>
 | 
			
		||||
                      </el-button>
 | 
			
		||||
                      <el-button v-else circle class="wechat">
 | 
			
		||||
                      <el-button v-else circle class="wechat" color="#67C23A">
 | 
			
		||||
                        <i class="iconfont icon-wechat-pay"></i>
 | 
			
		||||
                      </el-button>
 | 
			
		||||
                    </span>
 | 
			
		||||
@@ -92,7 +88,7 @@
 | 
			
		||||
              </div>
 | 
			
		||||
            </el-col>
 | 
			
		||||
          </el-row>
 | 
			
		||||
          <el-empty description="暂无数据" />
 | 
			
		||||
          <el-empty description="暂无数据" v-else />
 | 
			
		||||
 | 
			
		||||
          <h2 class="headline">消费账单</h2>
 | 
			
		||||
 | 
			
		||||
@@ -102,47 +98,12 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"
 | 
			
		||||
                       @logout="logout"/>
 | 
			
		||||
 | 
			
		||||
      <password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"/>
 | 
			
		||||
      <bind-mobile v-if="isLogin" :show="showBindMobileDialog" @hide="showBindMobileDialog = false"/>
 | 
			
		||||
 | 
			
		||||
      <bind-email v-if="isLogin" :show="showBindEmailDialog" @hide="showBindEmailDialog = false"/>
 | 
			
		||||
      <third-login v-if="isLogin" :show="showThirdLoginDialog" @hide="showThirdLoginDialog = false"/>
 | 
			
		||||
 | 
			
		||||
      <redeem-verify v-if="isLogin" :show="showRedeemVerifyDialog" @hide="redeemCallback"/>
 | 
			
		||||
 | 
			
		||||
      <el-dialog
 | 
			
		||||
          v-model="showPayDialog"
 | 
			
		||||
          :close-on-click-modal="false"
 | 
			
		||||
          :show-close="true"
 | 
			
		||||
          :width="400"
 | 
			
		||||
          @close="closeOrder"
 | 
			
		||||
          :title="'实付金额:¥'+amount">
 | 
			
		||||
        <div class="pay-container">
 | 
			
		||||
          <div class="count-down">
 | 
			
		||||
            <count-down :second="orderTimeout" @timeout="refreshPayCode" ref="countDownRef"/>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="pay-qrcode" v-loading="loading">
 | 
			
		||||
            <el-image :src="qrcode"/>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="tip success" v-if="text !== ''">
 | 
			
		||||
            <el-icon>
 | 
			
		||||
              <SuccessFilled/>
 | 
			
		||||
            </el-icon>
 | 
			
		||||
            <span class="text">{{ text }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="tip" v-else>
 | 
			
		||||
            <el-icon>
 | 
			
		||||
              <InfoFilled/>
 | 
			
		||||
            </el-icon>
 | 
			
		||||
            <span class="text">请打开手机扫码支付</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
      </el-dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -151,46 +112,34 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, ref} from "vue"
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {InfoFilled, SuccessFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpGet} from "@/utils/http";
 | 
			
		||||
import {checkSession, getSystemInfo} from "@/store/cache";
 | 
			
		||||
import UserProfile from "@/components/UserProfile.vue";
 | 
			
		||||
import PasswordDialog from "@/components/PasswordDialog.vue";
 | 
			
		||||
import BindMobile from "@/components/BindMobile.vue";
 | 
			
		||||
import RedeemVerify from "@/components/RedeemVerify.vue";
 | 
			
		||||
import {useRouter} from "vue-router";
 | 
			
		||||
import {removeUserToken} from "@/store/session";
 | 
			
		||||
import UserOrder from "@/components/UserOrder.vue";
 | 
			
		||||
import CountDown from "@/components/CountDown.vue";
 | 
			
		||||
import {useSharedStore} from "@/store/sharedata";
 | 
			
		||||
import BindEmail from "@/components/BindEmail.vue";
 | 
			
		||||
import ThirdLogin from "@/components/ThirdLogin.vue";
 | 
			
		||||
 | 
			
		||||
const list = ref([])
 | 
			
		||||
const showPayDialog = ref(false)
 | 
			
		||||
const vipImg = ref("/images/vip.png")
 | 
			
		||||
const enableReward = ref(false) // 是否启用众筹功能
 | 
			
		||||
const rewardImg = ref('/images/reward.png')
 | 
			
		||||
const qrcode = ref("")
 | 
			
		||||
const showPasswordDialog = ref(false)
 | 
			
		||||
const showBindMobileDialog = ref(false)
 | 
			
		||||
const showBindEmailDialog = ref(false)
 | 
			
		||||
const showRedeemVerifyDialog = ref(false)
 | 
			
		||||
const showThirdLoginDialog = ref(false)
 | 
			
		||||
const text = ref("")
 | 
			
		||||
const user = ref(null)
 | 
			
		||||
const isLogin = ref(false)
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const activeOrderNo = ref("")
 | 
			
		||||
const countDownRef = ref(null)
 | 
			
		||||
const orderTimeout = ref(1800)
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
const loadingText = ref("加载中...")
 | 
			
		||||
const orderPayInfoText = ref("")
 | 
			
		||||
 | 
			
		||||
const payWays = ref([])
 | 
			
		||||
const amount = ref(0)
 | 
			
		||||
const curPayProduct = ref(null)
 | 
			
		||||
const curPayWay = ref({pay_way: "", pay_type:""})
 | 
			
		||||
const vipInfoText = ref("")
 | 
			
		||||
const store = useSharedStore()
 | 
			
		||||
const profileKey = ref(0)
 | 
			
		||||
@@ -206,6 +155,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
  httpGet("/api/product/list").then((res) => {
 | 
			
		||||
    list.value = res.data
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("获取产品套餐失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
@@ -229,74 +179,30 @@ onMounted(() => {
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// refresh payment qrcode
 | 
			
		||||
const refreshPayCode = () => {
 | 
			
		||||
  genPayQrcode(curPayProduct.value, curPayWay.value)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const genPayQrcode = (product, payWay) => {
 | 
			
		||||
const pay = (product, payWay) => {
 | 
			
		||||
  if (!isLogin.value) {
 | 
			
		||||
    store.setShowLoginDialog(true)
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  text.value = ""
 | 
			
		||||
  curPayProduct.value = product
 | 
			
		||||
  curPayWay.value = payWay
 | 
			
		||||
  amount.value = (product.price - product.discount).toFixed(2)
 | 
			
		||||
  httpPost("/api/payment/qrcode", {
 | 
			
		||||
  loadingText.value = "正在生成支付订单..."
 | 
			
		||||
  httpGet(`${process.env.VUE_APP_API_HOST}/api/payment/doPay`, {
 | 
			
		||||
    product_id: product.id,
 | 
			
		||||
    pay_way: payWay.pay_way,
 | 
			
		||||
    pay_type: payWay.pay_type,
 | 
			
		||||
    product_id: curPayProduct.value.id,
 | 
			
		||||
    user_id: user.value.id,
 | 
			
		||||
    device: "jump"
 | 
			
		||||
  }).then(res => {
 | 
			
		||||
    showPayDialog.value = true
 | 
			
		||||
    qrcode.value = res.data['image']
 | 
			
		||||
    activeOrderNo.value = res.data['order_no']
 | 
			
		||||
    queryOrder(activeOrderNo.value)
 | 
			
		||||
   window.open(res.data, '_blank');
 | 
			
		||||
    loading.value = false
 | 
			
		||||
    // 重置计数器
 | 
			
		||||
    if (countDownRef.value) {
 | 
			
		||||
      countDownRef.value.resetTimer()
 | 
			
		||||
    }
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("生成支付订单失败:" + e.message)
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      ElMessage.error("生成支付订单失败:" + e.message)
 | 
			
		||||
      loading.value = false
 | 
			
		||||
    }, 500)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const queryOrder = (orderNo) => {
 | 
			
		||||
  httpGet("/api/order/query", {order_no: orderNo}).then(res => {
 | 
			
		||||
    if (res.data.status === 1) {
 | 
			
		||||
      text.value = "扫码成功,请在手机上进行支付!"
 | 
			
		||||
      queryOrder(orderNo)
 | 
			
		||||
    } else if (res.data.status === 2) {
 | 
			
		||||
      text.value = "支付成功,正在刷新页面"
 | 
			
		||||
      setTimeout(() => location.reload(), 500)
 | 
			
		||||
    } else {
 | 
			
		||||
      // 如果当前订单没有过期,继续等待订单的下一个状态
 | 
			
		||||
      if (activeOrderNo.value === orderNo) {
 | 
			
		||||
        queryOrder(orderNo)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("查询支付状态失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const logout = function () {
 | 
			
		||||
  httpGet('/api/user/logout').then(() => {
 | 
			
		||||
    removeUserToken();
 | 
			
		||||
    router.push('/login');
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    ElMessage.error('注销失败!');
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const closeOrder = () => {
 | 
			
		||||
  activeOrderNo.value = ''
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const redeemCallback = (success) => {
 | 
			
		||||
  showRedeemVerifyDialog.value = false
 | 
			
		||||
  if (success) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user