diff --git a/CHANGELOG.md b/CHANGELOG.md index 04a64d10..f610f799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码 * 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选 * 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。 +* 功能优化:移除PayJS支付渠道支持,PayJs已经关闭注册服务,请使用其他支付方式。 +* 功能新增:新增GeeK易支付支付渠道,支持支付宝,微信支付,QQ钱包,京东支付,抖音支付,Paypal支付等支付方式 ## v4.1.3 * 功能优化:重构用户登录模块,给所有的登录组件增加行为验证码功能,支持用户绑定手机,邮箱和微信 diff --git a/api/core/types/config.go b/api/core/types/config.go index 9638f620..f6e59727 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -12,24 +12,23 @@ import ( ) type AppConfig struct { - Path string `toml:"-"` - Listen string - Session Session - AdminSession Session - ProxyURL string - MysqlDns string // mysql 连接地址 - StaticDir string // 静态资源目录 - StaticUrl string // 静态资源 URL - Redis RedisConfig // redis 连接信息 - ApiConfig ApiConfig // ChatPlus API authorization configs - SMS SMSConfig // send mobile message config - OSS OSSConfig // OSS config - + Path string `toml:"-"` + Listen string + Session Session + AdminSession Session + ProxyURL string + MysqlDns string // mysql 连接地址 + StaticDir string // 静态资源目录 + StaticUrl string // 静态资源 URL + Redis RedisConfig // redis 连接信息 + ApiConfig ApiConfig // ChatPlus API authorization configs + SMS SMSConfig // send mobile message config + OSS OSSConfig // OSS config + SmtpConfig SmtpConfig // 邮件发送配置 XXLConfig XXLConfig AlipayConfig AlipayConfig // 支付宝支付渠道配置 HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置 - SmtpConfig SmtpConfig // 邮件发送配置 - JPayConfig JPayConfig // payjs 支付配置 + GeekPayConfig GeekPayConfig // GEEK 支付配置 WechatPayConfig WechatPayConfig // 微信支付渠道配置 TikaHost string // TiKa 服务器地址 } @@ -83,10 +82,9 @@ type HuPiPayConfig struct { //虎皮椒第四方支付配置 ReturnURL string // 支付成功返回地址 } -// JPayConfig PayJs 支付配置 -type JPayConfig struct { +// GeekPayConfig GEEK支付配置 +type GeekPayConfig struct { Enabled bool - Name string // 支付名称,默认 wechat AppId string // 商户 ID PrivateKey string // 私钥 ApiURL string // API 网关 diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 5d807f79..8a0112a9 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -19,7 +19,6 @@ import ( "geekai/utils" "geekai/utils/resp" "github.com/shopspring/decimal" - "math" "net/http" "net/url" "sync" @@ -34,19 +33,12 @@ type PayWay struct { Value string `json:"value"` } -var ( - PayWayAlipay = PayWay{Name: "支付宝", Value: "alipay"} - PayWayXunHu = PayWay{Name: "虎皮椒", Value: "hupi"} - PayWayJs = PayWay{Name: "PayJS", Value: "payjs"} - PayWayWechat = PayWay{Name: "微信支付", Value: "wechat"} -) - // PaymentHandler 支付服务回调 handler type PaymentHandler struct { BaseHandler alipayService *payment.AlipayService huPiPayService *payment.HuPiPayService - jsPayService *payment.JPayService + geekPayService *payment.GeekPayService wechatPayService *payment.WechatPayService snowflake *service.Snowflake fs embed.FS @@ -58,7 +50,7 @@ func NewPaymentHandler( server *core.AppServer, alipayService *payment.AlipayService, huPiPayService *payment.HuPiPayService, - jsPayService *payment.JPayService, + geekPayService *payment.GeekPayService, wechatPayService *payment.WechatPayService, db *gorm.DB, snowflake *service.Snowflake, @@ -66,7 +58,7 @@ func NewPaymentHandler( return &PaymentHandler{ alipayService: alipayService, huPiPayService: huPiPayService, - jsPayService: jsPayService, + geekPayService: geekPayService, wechatPayService: wechatPayService, snowflake: snowflake, fs: fs, @@ -81,10 +73,9 @@ func NewPaymentHandler( func (h *PaymentHandler) DoPay(c *gin.Context) { orderNo := h.GetTrim(c, "order_no") - payWay := h.GetTrim(c, "pay_way") t := h.GetInt(c, "t", 0) sign := h.GetTrim(c, "sign") - signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, payWay, t, h.signKey) + signStr := fmt.Sprintf("%s-%d-%s", orderNo, t, h.signKey) newSign := utils.Sha256(signStr) if newSign != sign { resp.ERROR(c, "订单签名错误!") @@ -118,7 +109,7 @@ func (h *PaymentHandler) DoPay(c *gin.Context) { // 更新扫码状态 h.DB.Model(&order).UpdateColumn("status", types.OrderScanned) - if payWay == "alipay" { // 支付宝 + if order.PayWay == "alipay" { // 支付宝 amount := fmt.Sprintf("%.2f", order.Amount) uri, err := h.alipayService.PayUrlMobile(order.OrderNo, amount, order.Subject) if err != nil { @@ -128,7 +119,7 @@ func (h *PaymentHandler) DoPay(c *gin.Context) { c.Redirect(302, uri) return - } else if payWay == "hupi" { // 虎皮椒支付 + } else if order.PayWay == "hupi" { // 虎皮椒支付 params := payment.HuPiPayReq{ Version: "1.1", TradeOrderId: orderNo, @@ -144,15 +135,41 @@ 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) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + c.Redirect(302, uri) + } else if order.PayWay == "geek" { + params := payment.GeekPayParams{ + OutTradeNo: orderNo, + Method: "web", + Name: order.Subject, + Money: fmt.Sprintf("%f", order.Amount), + ClientIP: c.ClientIP(), + Device: "pc", + Type: "alipay", + } + + s, err := h.geekPayService.Pay(params) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + resp.SUCCESS(c, s) } - resp.ERROR(c, "Invalid operations") + //resp.ERROR(c, "Invalid operations") } // PayQrcode 生成支付 URL 二维码 func (h *PaymentHandler) PayQrcode(c *gin.Context) { var data struct { - PayWay string `json:"pay_way"` // 支付方式 - ProductId uint `json:"product_id"` + 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) @@ -177,24 +194,22 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { return } - var payWay string var notifyURL string switch data.PayWay { case "hupi": - payWay = PayWayXunHu.Value notifyURL = h.App.Config.HuPiPayConfig.NotifyURL break - case "payjs": - payWay = PayWayJs.Value - notifyURL = h.App.Config.JPayConfig.NotifyURL + case "geek": + notifyURL = h.App.Config.GeekPayConfig.NotifyURL break - case "alipay": - payWay = PayWayAlipay.Value + case "alipay": // 支付宝商户支付 notifyURL = h.App.Config.AlipayConfig.NotifyURL break - default: - payWay = PayWayWechat.Value + case "wechat": // 微信商户支付 notifyURL = h.App.Config.WechatPayConfig.NotifyURL + default: + resp.ERROR(c, "Invalid pay way") + return } // 创建订单 @@ -215,7 +230,8 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { Subject: product.Name, Amount: amount, Status: types.OrderNotPaid, - PayWay: payWay, + PayWay: data.PayWay, + PayType: data.PayType, Remark: utils.JsonEncode(remark), } res = h.DB.Create(&order) @@ -224,36 +240,26 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { return } - // PayJs 单独处理,只能用官方生成的二维码 - if data.PayWay == "payjs" { - params := payment.JPayReq{ - TotalFee: int(math.Ceil(order.Amount * 100)), - OutTradeNo: order.OrderNo, - Subject: product.Name, - } - r := h.jsPayService.Pay(params) - if r.IsOK() { - resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode}) - return - } else { - resp.ERROR(c, "error with generating payment qrcode: "+r.ReturnMsg) - return - } - } - var logo string - if data.PayWay == "alipay" { + switch data.PayType { + case "alipay": logo = "res/img/alipay.jpg" - } else if data.PayWay == "hupi" { - if h.App.Config.HuPiPayConfig.Name == "wechat" { - logo = "res/img/wechat-pay.jpg" - } else { - logo = "res/img/alipay.jpg" - } - } else if data.PayWay == "wechat" { + 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()) @@ -268,31 +274,21 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { timestamp := time.Now().Unix() signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, data.PayWay, timestamp, h.signKey) sign := utils.Sha256(signStr) - var imageURL string - if data.PayWay == "wechat" { - payUrl, err := h.wechatPayService.PayUrlNative(order.OrderNo, int(math.Floor(order.Amount*100)), product.Name) - if err != nil { - resp.ERROR(c, "error with generating wechat payment qrcode: "+err.Error()) - return - } else { - imageURL = payUrl - } - } else { - imageURL = fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s&t=%d&sign=%s", parse.Scheme, parse.Host, orderNo, data.PayWay, timestamp, sign) - } - imgData, err := utils.GenQrcode(imageURL, 400, file) + 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": imageURL}) + 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"` // 支付方式 + 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 { @@ -319,12 +315,10 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { } amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64() - var payWay string var notifyURL, returnURL string var payURL string switch data.PayWay { case "hupi": - payWay = PayWayXunHu.Name notifyURL = h.App.Config.HuPiPayConfig.NotifyURL returnURL = h.App.Config.HuPiPayConfig.ReturnURL parse, _ := url.Parse(h.App.Config.HuPiPayConfig.ReturnURL) @@ -349,20 +343,16 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { return } payURL = r.URL - case "payjs": - payWay = PayWayJs.Name - notifyURL = h.App.Config.JPayConfig.NotifyURL - returnURL = h.App.Config.JPayConfig.ReturnURL - 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.jsPayService.PayH5(params) + 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": - payWay = PayWayAlipay.Name payURL, err = h.alipayService.PayUrlMobile(orderNo, fmt.Sprintf("%.2f", amount), product.Name) if err != nil { errMsg := "error with generating Alipay URL: " + err.Error() @@ -370,7 +360,6 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { return } case "wechat": - payWay = PayWayWechat.Name payURL, err = h.wechatPayService.PayUrlH5(orderNo, int(amount*100), product.Name, c.ClientIP()) if err != nil { errMsg := "error with generating Wechat URL: " + err.Error() @@ -399,7 +388,8 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { Subject: product.Name, Amount: amount, Status: types.OrderNotPaid, - PayWay: payWay, + PayWay: data.PayWay, + PayType: data.PayType, Remark: utils.JsonEncode(remark), } res = h.DB.Create(&order) @@ -506,20 +496,25 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error { // GetPayWays 获取支付方式 func (h *PaymentHandler) GetPayWays(c *gin.Context) { - data := gin.H{} + payWays := make([]gin.H, 0) if h.App.Config.AlipayConfig.Enabled { - data["alipay"] = gin.H{"name": "alipay"} + payWays = append(payWays, gin.H{"pay_way": "alipay", "pay_type": "alipay"}) } if h.App.Config.HuPiPayConfig.Enabled { - data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name} + payWays = append(payWays, gin.H{"pay_way": "hupi", "pay_type": h.App.Config.HuPiPayConfig.Name}) } - if h.App.Config.JPayConfig.Enabled { - data["payjs"] = gin.H{"name": h.App.Config.JPayConfig.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": "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": "douyin"}) + payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": "paypal"}) } if h.App.Config.WechatPayConfig.Enabled { - data["wechat"] = gin.H{"name": "wechat"} + payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wechat"}) } - resp.SUCCESS(c, data) + resp.SUCCESS(c, payWays) } // HuPiPayNotify 虎皮椒支付异步回调 @@ -593,12 +588,12 @@ func (h *PaymentHandler) PayJsNotify(c *gin.Context) { // 校验订单支付状态 tradeNo := c.Request.Form.Get("payjs_order_id") - err = h.jsPayService.TradeVerify(tradeNo) - if err != nil { - logger.Error("订单校验失败:", err) - c.String(http.StatusOK, "fail") - return - } + //err = h.geekPayService.TradeVerify(tradeNo) + //if err != nil { + // logger.Error("订单校验失败:", err) + // c.String(http.StatusOK, "fail") + // return + //} err = h.notify(orderNo, tradeNo) if err != nil { diff --git a/api/handler/test_handler.go b/api/handler/test_handler.go index fe38bc38..88e95a2f 100644 --- a/api/handler/test_handler.go +++ b/api/handler/test_handler.go @@ -11,10 +11,10 @@ import ( type TestHandler struct { db *gorm.DB snowflake *service.Snowflake - js *payment.JPayService + js *payment.GeekPayService } -func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.JPayService) *TestHandler { +func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.GeekPayService) *TestHandler { return &TestHandler{db: db, snowflake: snowflake, js: js} } diff --git a/api/res/img/geek-pay.jpg b/api/res/img/geek-pay.jpg new file mode 100644 index 00000000..48baf80d Binary files /dev/null and b/api/res/img/geek-pay.jpg differ diff --git a/api/res/img/qq-pay.jpg b/api/res/img/qq-pay.jpg new file mode 100644 index 00000000..25a0b180 Binary files /dev/null and b/api/res/img/qq-pay.jpg differ diff --git a/api/service/payment/geekpay_service.go b/api/service/payment/geekpay_service.go new file mode 100644 index 00000000..2018f43f --- /dev/null +++ b/api/service/payment/geekpay_service.go @@ -0,0 +1,136 @@ +package payment + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * 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 ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "geekai/core/types" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +// GeekPayService Geek 支付服务 +type GeekPayService struct { + config *types.GeekPayConfig +} + +func NewJPayService(appConfig *types.AppConfig) *GeekPayService { + return &GeekPayService{ + config: &appConfig.GeekPayConfig, + } +} + +type GeekPayParams struct { + Method string `json:"method"` // 接口类型 + Device string `json:"device"` // 设备类型 + Type string `json:"type"` // 支付方式 + OutTradeNo string `json:"out_trade_no"` // 商户订单号 + Name string `json:"name"` // 商品名称 + Money string `json:"money"` // 商品金额 + ClientIP string `json:"clientip"` //用户IP地址 + SubOpenId string `json:"sub_openid"` // 微信用户 openid,仅小程序支付需要 + SubAppId string `json:"sub_appid"` // 小程序 AppId,仅小程序支付需要 +} + +// Pay 支付订单 +func (s *GeekPayService) Pay(params GeekPayParams) (string, error) { + if params.Type == "wechat" { + params.Type = "wxpay" + } + p := map[string]string{ + "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, + "timestamp": fmt.Sprintf("%d", time.Now().Unix()), + } + sign, err := s.Sign(p) + if err != nil { + return "", err + } + p["sign"] = sign + p["sign_type"] = "RSA" + return s.sendRequest(s.config.ApiURL, p) +} + +func (s *GeekPayService) Sign(params map[string]string) (string, error) { + // 按字母顺序排序参数 + var keys []string + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + // 构建待签名字符串 + var signStr strings.Builder + for _, k := range keys { + signStr.WriteString(k) + signStr.WriteString("=") + signStr.WriteString(params[k]) + signStr.WriteString("&") + } + signString := strings.TrimSuffix(signStr.String(), "&") + + // 使用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 +} + +func (s *GeekPayService) sendRequest(apiEndpoint string, params map[string]string) (string, error) { + form := url.Values{} + for k, v := range params { + form.Add(k, v) + } + + resp, err := http.PostForm(apiEndpoint, form) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(body), nil +} diff --git a/api/service/payment/payjs_service.go b/api/service/payment/payjs_service.go deleted file mode 100644 index 56a98471..00000000 --- a/api/service/payment/payjs_service.go +++ /dev/null @@ -1,153 +0,0 @@ -package payment - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * 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 ( - "crypto/md5" - "encoding/hex" - "errors" - "fmt" - "geekai/core/types" - "geekai/utils" - "io" - "net/http" - "net/url" - "sort" - "strings" -) - -type JPayService struct { - config *types.JPayConfig -} - -func NewJPayService(appConfig *types.AppConfig) *JPayService { - return &JPayService{ - config: &appConfig.JPayConfig, - } -} - -type JPayReq struct { - TotalFee int `json:"total_fee"` - OutTradeNo string `json:"out_trade_no"` - Subject string `json:"body"` - NotifyURL string `json:"notify_url"` - ReturnURL string `json:"callback_url"` -} -type JPayReps struct { - OutTradeNo string `json:"out_trade_no"` - OrderId string `json:"payjs_order_id"` - ReturnCode int `json:"return_code"` - ReturnMsg string `json:"return_msg"` - Sign string `json:"Sign"` - TotalFee string `json:"total_fee"` - CodeUrl string `json:"code_url,omitempty"` - Qrcode string `json:"qrcode,omitempty"` -} - -func (r JPayReps) IsOK() bool { - return r.ReturnMsg == "SUCCESS" -} - -func (js *JPayService) Pay(param JPayReq) JPayReps { - param.NotifyURL = js.config.NotifyURL - var p = url.Values{} - encode := utils.JsonEncode(param) - m := make(map[string]interface{}) - _ = utils.JsonDecode(encode, &m) - for k, v := range m { - p.Add(k, fmt.Sprintf("%v", v)) - } - p.Add("mchid", js.config.AppId) - - p.Add("sign", js.sign(p)) - - cli := http.Client{} - apiURL := fmt.Sprintf("%s/api/native", js.config.ApiURL) - r, err := cli.PostForm(apiURL, p) - if err != nil { - return JPayReps{ReturnMsg: err.Error()} - } - defer r.Body.Close() - bs, err := io.ReadAll(r.Body) - if err != nil { - return JPayReps{ReturnMsg: err.Error()} - } - - var data JPayReps - err = utils.JsonDecode(string(bs), &data) - if err != nil { - return JPayReps{ReturnMsg: err.Error()} - } - return data -} - -func (js *JPayService) PayH5(p url.Values) string { - p.Add("mchid", js.config.AppId) - p.Add("sign", js.sign(p)) - return fmt.Sprintf("%s/api/cashier?%s", js.config.ApiURL, p.Encode()) -} - -func (js *JPayService) sign(params url.Values) string { - params.Del(`sign`) - var keys = make([]string, 0, 0) - for key := range params { - if params.Get(key) != `` { - keys = append(keys, key) - } - } - sort.Strings(keys) - - var pList = make([]string, 0, 0) - for _, key := range keys { - var value = strings.TrimSpace(params.Get(key)) - if len(value) > 0 { - pList = append(pList, key+"="+value) - } - } - var src = strings.Join(pList, "&") - src += "&key=" + js.config.PrivateKey - - md5bs := md5.Sum([]byte(src)) - md5res := hex.EncodeToString(md5bs[:]) - return strings.ToUpper(md5res) -} - -// TradeVerify 查询订单支付状态 -// @param tradeNo 支付平台交易 ID -func (js *JPayService) TradeVerify(tradeNo string) error { - apiURL := fmt.Sprintf("%s/api/check", js.config.ApiURL) - params := url.Values{} - params.Add("payjs_order_id", tradeNo) - params.Add("sign", js.sign(params)) - data := strings.NewReader(params.Encode()) - resp, err := http.Post(apiURL, "application/x-www-form-urlencoded", data) - if err != nil { - return fmt.Errorf("error with http reqeust: %v", err) - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("error with reading response: %v", err) - } - - var r struct { - ReturnCode int `json:"return_code"` - Status int `json:"status"` - } - err = utils.JsonDecode(string(body), &r) - if err != nil { - return fmt.Errorf("error with decode response: %v", err) - } - - if r.ReturnCode == 1 && r.Status == 1 { - return nil - } else { - logger.Errorf("PayJs 支付验证响应:%s", string(body)) - return errors.New("order not paid") - } -} diff --git a/api/store/model/order.go b/api/store/model/order.go index a1c6929f..86e81ca7 100644 --- a/api/store/model/order.go +++ b/api/store/model/order.go @@ -18,6 +18,7 @@ type Order struct { Status types.OrderStatus Remark string PayTime int64 - PayWay string // 支付方式 + PayWay string // 支付渠道 + PayType string // 支付类型 DeletedAt gorm.DeletedAt } diff --git a/api/store/vo/order.go b/api/store/vo/order.go index c076d002..39693649 100644 --- a/api/store/vo/order.go +++ b/api/store/vo/order.go @@ -16,5 +16,6 @@ type Order struct { Status types.OrderStatus `json:"status"` PayTime int64 `json:"pay_time"` PayWay string `json:"pay_way"` + PayType string `json:"pay_type"` Remark types.OrderRemark `json:"remark"` } diff --git a/database/update-v4.1.4.sql b/database/update-v4.1.4.sql index 28d93e5c..0ea7f36f 100644 --- a/database/update-v4.1.4.sql +++ b/database/update-v4.1.4.sql @@ -11,4 +11,5 @@ ALTER TABLE `chatgpt_app_types`ADD PRIMARY KEY (`id`); ALTER TABLE `chatgpt_app_types` MODIFY `id` int NOT NULL AUTO_INCREMENT; ALTER TABLE `chatgpt_chat_roles` ADD `tid` INT NOT NULL COMMENT '分类ID' AFTER `name`; -ALTER TABLE `chatgpt_chat_history` ADD `total_tokens` INT NOT NULL COMMENT '消耗总Token长度' AFTER `tokens`; \ No newline at end of file +ALTER TABLE `chatgpt_chat_history` ADD `total_tokens` INT NOT NULL COMMENT '消耗总Token长度' AFTER `tokens`; +ALTER TABLE `chatgpt_orders` ADD `pay_type` VARCHAR(30) NOT NULL COMMENT '支付类型' AFTER `pay_way`; \ No newline at end of file diff --git a/web/src/assets/css/member.styl b/web/src/assets/css/member.styl index c1f61bb5..9dab8325 100644 --- a/web/src/assets/css/member.styl +++ b/web/src/assets/css/member.styl @@ -110,6 +110,7 @@ overflow hidden cursor pointer transition: all 0.3s ease; /* 添加过渡效果 */ + margin-bottom 20px .image-container { display flex @@ -175,10 +176,11 @@ .pay-way { padding 10px 0 display flex - justify-content: space-between + justify-content: center + flex-wrap wrap - .iconfont { - margin-right 5px + .el-button { + margin 10px 5px 0 5px } } } diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css index 31901bb5..a0959ced 100644 --- a/web/src/assets/iconfont/iconfont.css +++ b/web/src/assets/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4125778 */ - src: url('iconfont.woff2?t=1725929120246') format('woff2'), - url('iconfont.woff?t=1725929120246') format('woff'), - url('iconfont.ttf?t=1725929120246') format('truetype'); + src: url('iconfont.woff2?t=1726612860394') format('woff2'), + url('iconfont.woff?t=1726612860394') format('woff'), + url('iconfont.ttf?t=1726612860394') format('truetype'); } .iconfont { @@ -13,6 +13,22 @@ -moz-osx-font-smoothing: grayscale; } +.icon-douyin:before { + content: "\e852"; +} + +.icon-paypal:before { + content: "\e60f"; +} + +.icon-qq:before { + content: "\e69f"; +} + +.icon-jd-pay:before { + content: "\e8dd"; +} + .icon-luma:before { content: "\e704"; } diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js index ad94b5e2..32973473 100644 --- a/web/src/assets/iconfont/iconfont.js +++ b/web/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}})(window); \ No newline at end of file +window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,t,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(i=h,o=a.document,z=!1,s(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,p())})}function p(){z||(z=!0,i())}function s(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(s,50)}p()}})(window); \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json index f114daa2..7dc3877f 100644 --- a/web/src/assets/iconfont/iconfont.json +++ b/web/src/assets/iconfont/iconfont.json @@ -5,6 +5,34 @@ "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "22174321", + "name": "抖音支付", + "font_class": "douyin", + "unicode": "e852", + "unicode_decimal": 59474 + }, + { + "icon_id": "1238433", + "name": "social-paypal", + "font_class": "paypal", + "unicode": "e60f", + "unicode_decimal": 58895 + }, + { + "icon_id": "1244217", + "name": "qq", + "font_class": "qq", + "unicode": "e69f", + "unicode_decimal": 59039 + }, + { + "icon_id": "18166714", + "name": "京东支付", + "font_class": "jd-pay", + "unicode": "e8dd", + "unicode_decimal": 59613 + }, { "icon_id": "41645421", "name": "luma-logo", diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf index f00663ec..8b228359 100644 Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index ff63b66a..426fce0c 100644 Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index a6df0ee8..906588c2 100644 Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index fcf359f9..11412a2e 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -36,57 +36,63 @@ - - - + + +

消费账单

@@ -132,7 +138,7 @@ - 请打开手机{{ payName }}扫码支付 + 请打开手机扫码支付 @@ -146,7 +152,6 @@ import {onMounted, ref} from "vue" import {ElMessage} from "element-plus"; import {httpGet, httpPost} from "@/utils/http"; -import ItemList from "@/components/ItemList.vue"; import {InfoFilled, SuccessFilled} from "@element-plus/icons-vue"; import {checkSession, getSystemInfo} from "@/store/cache"; import UserProfile from "@/components/UserProfile.vue"; @@ -176,19 +181,16 @@ const text = ref("") const user = ref(null) const isLogin = ref(false) const router = useRouter() -const curPayProduct = ref(null) const activeOrderNo = ref("") const countDownRef = ref(null) const orderTimeout = ref(1800) const loading = ref(true) const orderPayInfoText = ref("") -const vipMonthPower = ref(0) -const powerPrice = ref(0) -const payWays = ref({}) +const payWays = ref([]) const amount = ref(0) -const payName = ref("支付宝") -const curPay = ref("alipay") // 当前支付方式 +const curPayProduct = ref(null) +const curPayWay = ref({pay_way: "", pay_type:""}) const vipInfoText = ref("") const store = useSharedStore() const profileKey = ref(0) @@ -215,8 +217,6 @@ onMounted(() => { if (res.data['order_pay_timeout'] > 0) { orderTimeout.value = res.data['order_pay_timeout'] } - vipMonthPower.value = res.data['vip_month_power'] - powerPrice.value = res.data['power_price'] vipInfoText.value = res.data['vip_info_text'] }).catch(e => { ElMessage.error("获取系统配置失败:" + e.message) @@ -231,22 +231,24 @@ onMounted(() => { // refresh payment qrcode const refreshPayCode = () => { - if (curPay.value === 'alipay') { - alipay(curPayProduct.value) - } else if (curPay.value === 'hupi') { - huPiPay(curPayProduct.value) - } else if (curPay.value === 'payjs') { - PayJs(curPayProduct.value) - } + genPayQrcode(curPayProduct.value, curPayWay.value) } -const genPayQrcode = () => { +const genPayQrcode = (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", { - pay_way: curPay.value, + pay_way: payWay.pay_way, + pay_type: payWay.pay_type, product_id: curPayProduct.value.id, - user_id: user.value.id }).then(res => { showPayDialog.value = true qrcode.value = res.data['image'] @@ -262,67 +264,6 @@ const genPayQrcode = () => { }) } -const alipay = (row) => { - payName.value = "支付宝" - curPay.value = "alipay" - amount.value = (row.price - row.discount).toFixed(2) - if (!isLogin.value) { - store.setShowLoginDialog(true) - return - } - - if (row) { - curPayProduct.value = row - } - genPayQrcode() -} - -// 虎皮椒支付 -const huPiPay = (row) => { - payName.value = payWays.value["hupi"]["name"] === "wechat" ? '微信' : '支付宝' - curPay.value = "hupi" - amount.value = (row.price - row.discount).toFixed(2) - if (!isLogin.value) { - store.setShowLoginDialog(true) - return - } - - if (row) { - curPayProduct.value = row - } - genPayQrcode() -} - -// PayJS 支付 -const PayJs = (row) => { - payName.value = '微信' - curPay.value = "payjs" - amount.value = (row.price - row.discount).toFixed(2) - if (!isLogin.value) { - store.setShowLoginDialog(true) - return - } - - if (row) { - curPayProduct.value = row - } - genPayQrcode() -} - -const wechatPay = (row) => { - payName.value = '微信' - curPay.value = "wechat" - amount.value = (row.price - row.discount).toFixed(2) - if (!isLogin.value) { - store.setShowLoginDialog(true) - return - } - - if (row) { - curPayProduct.value = row - } - genPayQrcode() -} const queryOrder = (orderNo) => { httpGet("/api/order/query", {order_no: orderNo}).then(res => { @@ -331,11 +272,7 @@ const queryOrder = (orderNo) => { queryOrder(orderNo) } else if (res.data.status === 2) { text.value = "支付成功,正在刷新页面" - if (curPay.value === "payjs") { - setTimeout(() => location.reload(), 3000) - } else { - setTimeout(() => location.reload(), 500) - } + setTimeout(() => location.reload(), 500) } else { // 如果当前订单没有过期,继续等待订单的下一个状态 if (activeOrderNo.value === orderNo) {