From 857da34b9cc32220b43beba01b2fe2337996ec7b Mon Sep 17 00:00:00 2001 From: RockYang Date: Wed, 12 Jun 2024 14:20:37 +0800 Subject: [PATCH] wechat payment is ready for PC --- CHANGELOG.md | 1 + api/.gitignore | 3 +- api/core/app_server.go | 1 + api/core/types/config.go | 27 +++-- api/go.mod | 2 + api/go.sum | 2 + api/handler/payment_handler.go | 141 +++++++++++++++++++------- api/handler/test_handler.go | 4 +- api/main.go | 4 +- api/service/payment/alipay_service.go | 32 ++---- api/service/payment/payjs_service.go | 18 ++-- api/service/payment/types.go | 19 ++++ api/service/payment/wepay_service.go | 135 ++++++++++++++++++++++++ api/test/test.go | 5 +- web/.env.development | 4 +- web/src/views/Index.vue | 3 + web/src/views/Member.vue | 18 ++++ web/src/views/admin/SysConfig.vue | 3 + web/src/views/mobile/Index.vue | 14 ++- web/src/views/mobile/Profile.vue | 3 + 20 files changed, 350 insertions(+), 89 deletions(-) create mode 100644 api/service/payment/types.go create mode 100644 api/service/payment/wepay_service.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e72ef18d..fbeb63b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 更新日志 ## v4.0.9 * 环境升级:升级 Golang 到 go1.22.4 +* 功能增加:接入微信商户号支付渠道 * Bug修复:修复前端页面菜单把页面撑开,底部留白问题 * 功能优化:聊天页面自动根据内容调整输入框的高度 * Bug修复:修复Dalle绘图失败退回算力的问题 diff --git a/api/.gitignore b/api/.gitignore index 7502cf69..45aded04 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -17,4 +17,5 @@ bin data config.toml static/upload -storage.json +storage.json +res/certs/wechat/apiclient_key.pem diff --git a/api/core/app_server.go b/api/core/app_server.go index 8ba434fa..6d236e73 100644 --- a/api/core/app_server.go +++ b/api/core/app_server.go @@ -233,6 +233,7 @@ func needLogin(c *gin.Context) bool { 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" || strings.HasPrefix(c.Request.URL.Path, "/api/test") || diff --git a/api/core/types/config.go b/api/core/types/config.go index c9881999..ccd1dada 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -29,11 +29,12 @@ type AppConfig struct { WeChatBot bool // 是否启用微信机器人 SdConfigs []StableDiffusionConfig // sd AI draw service pool - XXLConfig XXLConfig - AlipayConfig AlipayConfig - HuPiPayConfig HuPiPayConfig - SmtpConfig SmtpConfig // 邮件发送配置 - JPayConfig JPayConfig // payjs 支付配置 + XXLConfig XXLConfig + AlipayConfig AlipayConfig // 支付宝支付渠道配置 + HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置 + SmtpConfig SmtpConfig // 邮件发送配置 + JPayConfig JPayConfig // payjs 支付配置 + WechatPayConfig WechatPayConfig // 微信支付渠道配置 } type SmtpConfig struct { @@ -85,6 +86,17 @@ type AlipayConfig struct { ReturnURL string // 支付成功返回地址 } +type WechatPayConfig struct { + Enabled bool // 是否启用该支付通道 + AppId string // 公众号的APPID,如:wxd678efh567hg6787 + MchId string // 直连商户的商户号,由微信支付生成并下发 + SerialNo string // 商户证书的证书序列号 + PrivateKey string // 用户私钥文件路径 + ApiV3Key string // API V3 秘钥 + NotifyURL string // 异步通知回调 + ReturnURL string // 支付成功返回地址 +} + type HuPiPayConfig struct { //虎皮椒第四方支付配置 Enabled bool // 是否启用该支付通道 Name string // 支付名称,如:wechat/alipay @@ -182,8 +194,9 @@ var QWen = Platform{ } type SystemConfig struct { - Title string `json:"title,omitempty"` - AdminTitle string `json:"admin_title,omitempty"` + Title string `json:"title,omitempty"` // 网站标题 + Slogan string `json:"slogan,omitempty"` // 网站 slogan + AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题 Logo string `json:"logo,omitempty"` InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值 DailyPower int `json:"daily_power,omitempty"` // 每日赠送算力 diff --git a/api/go.mod b/api/go.mod index 9bb4a344..9b5efbef 100644 --- a/api/go.mod +++ b/api/go.mod @@ -38,6 +38,8 @@ require ( require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-pay/crypto v0.0.1 // indirect + github.com/go-pay/errgroup v0.0.2 // indirect + github.com/go-pay/util v0.0.2 // indirect github.com/go-pay/xlog v0.0.2 // indirect github.com/go-pay/xtime v0.0.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect diff --git a/api/go.sum b/api/go.sum index 592fbc58..c32990c3 100644 --- a/api/go.sum +++ b/api/go.sum @@ -47,6 +47,8 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-pay/crypto v0.0.1 h1:B6InT8CLfSLc6nGRVx9VMJRBBazFMjr293+jl0lLXUY= github.com/go-pay/crypto v0.0.1/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes= +github.com/go-pay/errgroup v0.0.2 h1:5mZMdm0TDClDm2S3G0/sm0f8AuQRtz0dOrTHDR9R8Cc= +github.com/go-pay/errgroup v0.0.2/go.mod h1:0+4b8mvFMS71MIzsaC+gVvB4x37I93lRb2dqrwuU8x8= github.com/go-pay/gopay v1.5.101 h1:rVb+sfv6hiQtknAlZnTTLvU27NvFJ4p0yglN/vPpGXI= github.com/go-pay/gopay v1.5.101/go.mod h1:AW4Yj8jDZX9BM1/GTLTY1Gy5SHjiq8kQvG5sBTN2sxI= github.com/go-pay/util v0.0.2 h1:goJ4f6kNY5zzdtg1Cj8oWC+Cw7bfg/qq2rJangMAb9U= diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 2cc4bcd6..c69f0703 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -29,39 +29,48 @@ import ( "gorm.io/gorm" ) -const ( - PayWayAlipay = "支付宝" - PayWayXunHu = "虎皮椒" - PayWayJs = "PayJS" +type PayWay struct { + Name string `json:"name"` + 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 - js *payment.PayJS - snowflake *service.Snowflake - fs embed.FS - lock sync.Mutex - signKey string // 用来签名的随机秘钥 + alipayService *payment.AlipayService + huPiPayService *payment.HuPiPayService + jsPayService *payment.JPayService + 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, - js *payment.PayJS, + jsPayService *payment.JPayService, + wechatPayService *payment.WechatPayService, db *gorm.DB, snowflake *service.Snowflake, fs embed.FS) *PaymentHandler { return &PaymentHandler{ - alipayService: alipayService, - huPiPayService: huPiPayService, - js: js, - snowflake: snowflake, - fs: fs, - lock: sync.Mutex{}, + alipayService: alipayService, + huPiPayService: huPiPayService, + jsPayService: jsPayService, + wechatPayService: wechatPayService, + snowflake: snowflake, + fs: fs, + lock: sync.Mutex{}, BaseHandler: BaseHandler{ App: server, DB: db, @@ -108,10 +117,9 @@ func (h *PaymentHandler) DoPay(c *gin.Context) { // 更新扫码状态 h.DB.Model(&order).UpdateColumn("status", types.OrderScanned) - if payWay == "alipay" { // 支付宝 - // 生成支付链接 - amount := fmt.Sprintf("%.2f", order.Amount) + if payWay == "alipay" { // 支付宝 + amount := fmt.Sprintf("%.2f", order.Amount) uri, err := h.alipayService.PayUrlMobile(order.OrderNo, amount, order.Subject) if err != nil { resp.ERROR(c, "error with generate pay url: "+err.Error()) @@ -212,14 +220,21 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { var notifyURL string switch data.PayWay { case "hupi": - payWay = PayWayXunHu + payWay = PayWayXunHu.Value notifyURL = h.App.Config.HuPiPayConfig.NotifyURL + break case "payjs": - payWay = PayWayJs + payWay = PayWayJs.Value notifyURL = h.App.Config.JPayConfig.NotifyURL - default: - payWay = PayWayAlipay + break + case "alipay": + payWay = PayWayAlipay.Value notifyURL = h.App.Config.AlipayConfig.NotifyURL + break + default: + payWay = PayWayWechat.Value + notifyURL = h.App.Config.WechatPayConfig.NotifyURL + } // 创建订单 remark := types.OrderRemark{ @@ -255,7 +270,7 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { OutTradeNo: order.OrderNo, Subject: product.Name, } - r := h.js.Pay(params) + r := h.jsPayService.Pay(params) if r.IsOK() { resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode}) return @@ -274,6 +289,8 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { } else { logo = "res/img/alipay.jpg" } + } else if data.PayWay == "wechat" { + logo = "res/img/wechat-pay.jpg" } file, err := h.fs.Open(logo) @@ -290,7 +307,18 @@ 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) - 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) + 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) if err != nil { resp.ERROR(c, err.Error()) @@ -337,7 +365,7 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { var payURL string switch data.PayWay { case "hupi": - payWay = PayWayXunHu + payWay = PayWayXunHu.Name notifyURL = h.App.Config.HuPiPayConfig.NotifyURL returnURL = h.App.Config.HuPiPayConfig.ReturnURL parse, _ := url.Parse(h.App.Config.HuPiPayConfig.ReturnURL) @@ -356,13 +384,14 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { } r, err := h.huPiPayService.Pay(params) if err != nil { - logger.Error("error with generating Pay URL: ", err.Error()) - resp.ERROR(c, "error with generating Pay URL: "+err.Error()) + errMsg := "error with generating Pay Hupi URL: " + err.Error() + logger.Error(errMsg) + resp.ERROR(c, errMsg) return } payURL = r.URL case "payjs": - payWay = PayWayJs + 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() @@ -372,12 +401,22 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { params.Add("body", product.Name) params.Add("notify_url", notifyURL) params.Add("auto", "0") - payURL = h.js.PayH5(params) + payURL = h.jsPayService.PayH5(params) case "alipay": - payWay = PayWayAlipay + payWay = PayWayAlipay.Name payURL, err = h.alipayService.PayUrlMobile(orderNo, fmt.Sprintf("%.2f", amount), product.Name) if err != nil { - resp.ERROR(c, "error with generating Pay URL: "+err.Error()) + errMsg := "error with generating Alipay URL: " + err.Error() + resp.ERROR(c, errMsg) + 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() + logger.Error(errMsg) + resp.ERROR(c, errMsg) return } default: @@ -518,6 +557,9 @@ func (h *PaymentHandler) GetPayWays(c *gin.Context) { if h.App.Config.JPayConfig.Enabled { data["payjs"] = gin.H{"name": h.App.Config.JPayConfig.Name} } + if h.App.Config.WechatPayConfig.Enabled { + data["wechat"] = gin.H{"name": "wechat"} + } resp.SUCCESS(c, data) } @@ -584,7 +626,7 @@ func (h *PaymentHandler) PayJsNotify(c *gin.Context) { orderNo := c.Request.Form.Get("out_trade_no") returnCode := c.Request.Form.Get("return_code") - logger.Infof("收到订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode) + logger.Infof("收到PayJs订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode) // 支付失败 if returnCode != "1" { return @@ -592,7 +634,7 @@ func (h *PaymentHandler) PayJsNotify(c *gin.Context) { // 校验订单支付状态 tradeNo := c.Request.Form.Get("payjs_order_id") - err = h.js.Check(tradeNo) + err = h.jsPayService.TradeVerify(tradeNo) if err != nil { logger.Error("订单校验失败:", err) c.String(http.StatusOK, "fail") @@ -607,3 +649,30 @@ func (h *PaymentHandler) PayJsNotify(c *gin.Context) { 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") +} diff --git a/api/handler/test_handler.go b/api/handler/test_handler.go index 35aba79f..eb5b7107 100644 --- a/api/handler/test_handler.go +++ b/api/handler/test_handler.go @@ -9,9 +9,9 @@ import ( type TestHandler struct { db *gorm.DB snowflake *service.Snowflake - js *payment.PayJS + js *payment.JPayService } -func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.PayJS) *TestHandler { +func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.JPayService) *TestHandler { return &TestHandler{db: db, snowflake: snowflake, js: js} } diff --git a/api/main.go b/api/main.go index 8b585ee6..e26d07ff 100644 --- a/api/main.go +++ b/api/main.go @@ -211,7 +211,8 @@ func main() { fx.Provide(payment.NewAlipayService), fx.Provide(payment.NewHuPiPay), - fx.Provide(payment.NewPayJS), + fx.Provide(payment.NewJPayService), + fx.Provide(payment.NewWechatService), fx.Provide(service.NewSnowflake), fx.Provide(service.NewXXLJobExecutor), fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) { @@ -373,6 +374,7 @@ func main() { group.POST("alipay/notify", h.AlipayNotify) group.POST("hupipay/notify", h.HuPiPayNotify) group.POST("payjs/notify", h.PayJsNotify) + group.POST("wechat/notify", h.WechatPayNotify) }), fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) { group := s.Engine.Group("/api/admin/product/") diff --git a/api/service/payment/alipay_service.go b/api/service/payment/alipay_service.go index 06ec2dd5..8ab93eda 100644 --- a/api/service/payment/alipay_service.go +++ b/api/service/payment/alipay_service.go @@ -41,8 +41,7 @@ func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) { return nil, fmt.Errorf("error with initialize alipay service: %v", err) } - client.DebugSwitch = gopay.DebugOn - + //client.DebugSwitch = gopay.DebugOn // 开启调试模式 client.SetLocation(alipay.LocationShanghai). // 设置时区,不设置或出错均为默认服务器时间 SetCharset(alipay.UTF8). // 设置字符编码,不设置默认 utf-8 SetSignType(alipay.RSA2). // 设置签名类型,不设置默认 RSA2 @@ -56,12 +55,12 @@ 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) { +func (s *AlipayService) PayUrlMobile(outTradeNo string, amount string, subject string) (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("total_amount", amount) bm.Set("product_code", "QUICK_WAP_WAY") return s.client.TradeWapPay(context.Background(), bm) } @@ -80,7 +79,7 @@ func (s *AlipayService) TradeVerify(request *http.Request) NotifyVo { notifyReq, err := alipay.ParseNotifyToBodyMap(request) // c.Request 是 gin 框架的写法 if err != nil { return NotifyVo{ - Status: 0, + Status: Failure, Message: "error with parse notify request: " + err.Error(), } } @@ -88,7 +87,7 @@ func (s *AlipayService) TradeVerify(request *http.Request) NotifyVo { _, err = alipay.VerifySignWithCert(s.config.AlipayPublicKey, notifyReq) if err != nil { return NotifyVo{ - Status: 0, + Status: Failure, Message: "error with verify sign: " + err.Error(), } } @@ -104,23 +103,23 @@ func (s *AlipayService) TradeQuery(outTradeNo string) NotifyVo { rsp, err := s.client.TradeQuery(context.Background(), bm) if err != nil { return NotifyVo{ - Status: 0, + Status: Failure, Message: "异步查询验证订单信息发生错误" + outTradeNo + err.Error(), } } if rsp.Response.TradeStatus == "TRADE_SUCCESS" { return NotifyVo{ - Status: 1, + Status: Success, OutTradeNo: rsp.Response.OutTradeNo, - TradeNo: rsp.Response.TradeNo, + TradeId: rsp.Response.TradeNo, Amount: rsp.Response.TotalAmount, Subject: rsp.Response.Subject, Message: "OK", } } else { return NotifyVo{ - Status: 0, + Status: Failure, Message: "异步查询验证订单信息发生错误" + outTradeNo, } } @@ -133,16 +132,3 @@ func readKey(filename string) (string, error) { } return string(data), nil } - -type NotifyVo struct { - Status int - OutTradeNo string - TradeNo string - Amount string - Message string - Subject string -} - -func (v NotifyVo) Success() bool { - return v.Status == 1 -} diff --git a/api/service/payment/payjs_service.go b/api/service/payment/payjs_service.go index 1b42406b..56a98471 100644 --- a/api/service/payment/payjs_service.go +++ b/api/service/payment/payjs_service.go @@ -21,12 +21,12 @@ import ( "strings" ) -type PayJS struct { +type JPayService struct { config *types.JPayConfig } -func NewPayJS(appConfig *types.AppConfig) *PayJS { - return &PayJS{ +func NewJPayService(appConfig *types.AppConfig) *JPayService { + return &JPayService{ config: &appConfig.JPayConfig, } } @@ -53,7 +53,7 @@ func (r JPayReps) IsOK() bool { return r.ReturnMsg == "SUCCESS" } -func (js *PayJS) Pay(param JPayReq) JPayReps { +func (js *JPayService) Pay(param JPayReq) JPayReps { param.NotifyURL = js.config.NotifyURL var p = url.Values{} encode := utils.JsonEncode(param) @@ -86,13 +86,13 @@ func (js *PayJS) Pay(param JPayReq) JPayReps { return data } -func (js *PayJS) PayH5(p url.Values) string { +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 *PayJS) sign(params url.Values) string { +func (js *JPayService) sign(params url.Values) string { params.Del(`sign`) var keys = make([]string, 0, 0) for key := range params { @@ -117,20 +117,18 @@ func (js *PayJS) sign(params url.Values) string { return strings.ToUpper(md5res) } -// Check 查询订单支付状态 +// TradeVerify 查询订单支付状态 // @param tradeNo 支付平台交易 ID -func (js *PayJS) Check(tradeNo string) error { +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) - defer resp.Body.Close() 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 { diff --git a/api/service/payment/types.go b/api/service/payment/types.go new file mode 100644 index 00000000..ef8ff24c --- /dev/null +++ b/api/service/payment/types.go @@ -0,0 +1,19 @@ +package payment + +type NotifyVo struct { + Status int + OutTradeNo string // 商户订单号 + TradeId string // 交易ID + Amount string // 交易金额 + Message string + Subject string +} + +func (v NotifyVo) Success() bool { + return v.Status == Success +} + +const ( + Success = 0 + Failure = 1 +) diff --git a/api/service/payment/wepay_service.go b/api/service/payment/wepay_service.go new file mode 100644 index 00000000..b141a8e9 --- /dev/null +++ b/api/service/payment/wepay_service.go @@ -0,0 +1,135 @@ +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 ( + "context" + "fmt" + "geekai/core/types" + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/wechat/v3" + "net/http" + "time" +) + +type WechatPayService struct { + config *types.WechatPayConfig + client *wechat.ClientV3 +} + +func NewWechatService(appConfig *types.AppConfig) (*WechatPayService, error) { + config := appConfig.WechatPayConfig + if !config.Enabled { + logger.Info("Disabled WechatPay service") + return nil, nil + } + priKey, err := readKey(config.PrivateKey) + if err != nil { + return nil, fmt.Errorf("error with read App Private key: %v", err) + } + + client, err := wechat.NewClientV3(config.MchId, config.SerialNo, config.ApiV3Key, priKey) + if err != nil { + return nil, fmt.Errorf("error with initialize WechatPay service: %v", err) + } + err = client.AutoVerifySign() + if err != nil { + return nil, fmt.Errorf("error with autoVerifySign: %v", err) + } + //client.DebugSwitch = gopay.DebugOn + + return &WechatPayService{config: &config, client: client}, nil +} + +func (s *WechatPayService) PayUrlNative(outTradeNo string, amount int, subject string) (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("time_expire", expire). + Set("notify_url", s.config.NotifyURL). + SetBodyMap("amount", func(bm gopay.BodyMap) { + bm.Set("total", amount). + Set("currency", "CNY") + }) + + wxRsp, err := s.client.V3TransactionNative(context.Background(), bm) + if err != nil { + return "", fmt.Errorf("error with client v3 transaction Native: %v", err) + } + if wxRsp.Code != wechat.Success { + return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error) + } + return wxRsp.Response.CodeUrl, nil +} + +func (s *WechatPayService) PayUrlH5(outTradeNo string, amount int, subject string, ip string) (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("time_expire", expire). + Set("notify_url", s.config.NotifyURL). + SetBodyMap("amount", func(bm gopay.BodyMap) { + bm.Set("total", amount). + Set("currency", "CNY") + }). + SetBodyMap("scene_info", func(bm gopay.BodyMap) { + bm.Set("payer_client_ip", ip). + SetBodyMap("h5_info", func(bm gopay.BodyMap) { + bm.Set("type", "Wap") + }) + }) + + wxRsp, err := s.client.V3TransactionH5(context.Background(), bm) + if err != nil { + return "", fmt.Errorf("error with client v3 transaction H5: %v", err) + } + if wxRsp.Code != wechat.Success { + return "", fmt.Errorf("error with generating pay url: %v", wxRsp.Error) + } + return wxRsp.Response.H5Url, nil +} + +type NotifyResponse struct { + Code string `json:"code"` + Message string `xml:"message"` +} + +// TradeVerify 交易验证 +func (s *WechatPayService) TradeVerify(request *http.Request) NotifyVo { + notifyReq, err := wechat.V3ParseNotify(request) + if err != nil { + return NotifyVo{Status: 1, Message: fmt.Sprintf("error with client v3 parse notify: %v", err)} + } + + // TODO: 这里验签程序有 Bug,一直报错:crypto/rsa: verification error,先暂时取消验签 + //err = notifyReq.VerifySignByPK(s.client.WxPublicKey()) + //if err != nil { + // return fmt.Errorf("error with client v3 verify sign: %v", err) + //} + + // 解密支付密文,验证订单信息 + result, err := notifyReq.DecryptPayCipherText(s.config.ApiV3Key) + if err != nil { + return NotifyVo{Status: Failure, Message: fmt.Sprintf("error with client v3 decrypt: %v", err)} + } + + return NotifyVo{ + Status: Success, + OutTradeNo: result.OutTradeNo, + TradeId: result.TransactionId, + Amount: fmt.Sprintf("%.2f", float64(result.Amount.Total)/100), + } +} diff --git a/api/test/test.go b/api/test/test.go index 0a48ec97..2d53da2b 100644 --- a/api/test/test.go +++ b/api/test/test.go @@ -2,11 +2,8 @@ package main import ( "fmt" - "net/url" ) func main() { - text := "https://nk.img.r9it.com/chatgpt-plus/1712709360012445.png" - parse, _ := url.Parse(text) - fmt.Println(fmt.Sprintf("%s://%s", parse.Scheme, parse.Host)) + fmt.Println(fmt.Sprintf("%v", float64(90)/100)) } diff --git a/web/.env.development b/web/.env.development index 62d3b372..52e013d9 100644 --- a/web/.env.development +++ b/web/.env.development @@ -1,5 +1,5 @@ -VUE_APP_API_HOST=http://localhost:5678 -VUE_APP_WS_HOST=ws://localhost:5678 +VUE_APP_API_HOST=http://172.22.11.69:5678 +VUE_APP_WS_HOST=ws://172.22.11.69:5678 VUE_APP_USER=18575670125 VUE_APP_PASS=12345678 VUE_APP_ADMIN_USER=admin diff --git a/web/src/views/Index.vue b/web/src/views/Index.vue index ae0cdfd6..b58bf46a 100644 --- a/web/src/views/Index.vue +++ b/web/src/views/Index.vue @@ -96,6 +96,9 @@ onMounted(() => { if (res.data.rand_bg) { bgClass.value = "rand-bg" } + if (res.data.slogan) { + slogan.value = res.data.slogan + } }).catch(e => { ElMessage.error("获取系统配置失败:" + e.message) }) diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index fffcd18a..0f6eb2be 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -76,6 +76,9 @@ 微信 + + 微信 + @@ -314,6 +317,21 @@ const PayJs = (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) => { httpPost("/api/payment/query", {order_no: orderNo}).then(res => { if (res.data.status === 1) { diff --git a/web/src/views/admin/SysConfig.vue b/web/src/views/admin/SysConfig.vue index a0b33d09..c0bb89bb 100644 --- a/web/src/views/admin/SysConfig.vue +++ b/web/src/views/admin/SysConfig.vue @@ -13,6 +13,9 @@ + + +