From a90f00f7a465bfabe46d8a4ad58a30eef68c34a9 Mon Sep 17 00:00:00 2001 From: futuresnail <1364706201@qq.com> Date: Mon, 29 Apr 2024 21:09:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=BE=AE=E4=BF=A1=E5=95=86?= =?UTF-8?q?=E6=88=B7=E5=8F=B7=E6=94=AF=E4=BB=98=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/config.sample.toml | 13 +- api/core/types/config.go | 25 +++- api/go.mod | 3 + api/go.sum | 9 ++ api/handler/config_handler.go | 2 +- api/handler/payment_handler.go | 94 +++++++++++++- api/handler/user_handler.go | 40 ++++++ api/main.go | 4 + api/service/payment/wxpay_service.go | 115 ++++++++++++++++ api/store/model/user.go | 30 +++-- web/package.json | 4 +- web/src/main.js | 4 +- web/src/router.js | 7 +- web/src/utils/libs.js | 9 ++ web/src/utils/wechatAuth.js | 103 +++++++++++++++ web/src/views/Member.vue | 17 +++ web/src/views/mobile/Payment.vue | 187 +++++++++++++++++++++++++++ web/src/views/mobile/Profile.vue | 12 +- 18 files changed, 644 insertions(+), 34 deletions(-) create mode 100644 api/service/payment/wxpay_service.go create mode 100644 web/src/utils/wechatAuth.js create mode 100644 web/src/views/mobile/Payment.vue diff --git a/api/config.sample.toml b/api/config.sample.toml index 1f7849d5..e5366fa0 100644 --- a/api/config.sample.toml +++ b/api/config.sample.toml @@ -99,7 +99,16 @@ WeChatBot = false AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书 RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书 NotifyURL = "https://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址 - +[WxpayConfig] + Enabled = false # 启用微信支付通道 + SandBox = false # 是否启用沙盒模式 + AppId = "" # AppId + WxAppSecret = "" + MchId = "" # 商户ID + MchKey = "" # 应用私钥mchAPIv3Key + CertificateSerialNo = "" #证书序列号 + PrivateKey = "certs/wx/apiclient_key.pem" # 应用私钥证书 + NotifyURL = "https://ai.r9it.com/api/payment/wxpay/notify" # 支付异步回调地址 [HuPiPayConfig] Enabled = false Name = "wechat" @@ -121,4 +130,4 @@ WeChatBot = false AppId = "" # 商户 ID PrivateKey = "" # 秘钥 ApiURL = "https://payjs.cn" - NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的 \ No newline at end of file + NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的 diff --git a/api/core/types/config.go b/api/core/types/config.go index 24a94e78..b651f5ee 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -21,12 +21,12 @@ type AppConfig struct { MjPlusConfigs []MjPlusConfig // MJ plus config WeChatBot bool // 是否启用微信机器人 SdConfigs []StableDiffusionConfig // sd AI draw service pool - - XXLConfig XXLConfig - AlipayConfig AlipayConfig - HuPiPayConfig HuPiPayConfig - SmtpConfig SmtpConfig // 邮件发送配置 - JPayConfig JPayConfig // payjs 支付配置 + XXLConfig XXLConfig + AlipayConfig AlipayConfig + WxpayConfig WxpayConfig + HuPiPayConfig HuPiPayConfig + SmtpConfig SmtpConfig // 邮件发送配置 + JPayConfig JPayConfig // payjs 支付配置 } type SmtpConfig struct { @@ -77,6 +77,19 @@ type AlipayConfig struct { ReturnURL string // 支付成功返回地址 } +type WxpayConfig struct { + Enabled bool // 是否启用该支付通道 + SandBox bool // 是否沙盒环境 + AppId string // 应用 ID + WxAppSecret string // 应用 Secret + MchId string // 商户 ID + MchKey string // 商户key + CertificateSerialNo string // 商户key + PrivateKey string // 商户私密钥文件地址 + NotifyURL string // 异步通知回调 + ReturnURL string // 支付成功返回地址 +} + type HuPiPayConfig struct { //虎皮椒第四方支付配置 Enabled bool // 是否启用该支付通道 Name string // 支付名称,如:wechat/alipay diff --git a/api/go.mod b/api/go.mod index fc131837..cfd06288 100644 --- a/api/go.mod +++ b/api/go.mod @@ -26,12 +26,15 @@ require ( require github.com/xxl-job/xxl-job-executor-go v1.2.0 require ( + github.com/chanxuehong/wechat v0.0.0-20230222024006-36f0325263cd github.com/mojocn/base64Captcha v1.3.1 github.com/shopspring/decimal v1.3.1 github.com/syndtr/goleveldb v1.0.0 + github.com/wechatpay-apiv3/wechatpay-go v0.2.18 ) require ( + github.com/chanxuehong/rand v0.0.0-20211009035549-2f07823e8e99 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect diff --git a/api/go.sum b/api/go.sum index e5c987ce..0b6b0eaa 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4= github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs= github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k= @@ -12,6 +14,11 @@ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chanxuehong/rand v0.0.0-20211009035549-2f07823e8e99 h1:K62Lb6bsgLOB++z/VAvRvtiEBdNCuMfmQGTGGWMdPpM= +github.com/chanxuehong/rand v0.0.0-20211009035549-2f07823e8e99/go.mod h1:9+sJ9zvvkXC5sPjPEZM3Jpb9n2Q2VtcrGZly0UHYF5I= +github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13/go.mod h1:XEYt99iTxMqkv+gW85JX/DdUINHUe43Sbe5AtqSaDAQ= +github.com/chanxuehong/wechat v0.0.0-20230222024006-36f0325263cd h1:v3JNsFZmplLO/Cmiyr/rGvR7lW1ld9lB+d5h4yR0MTI= +github.com/chanxuehong/wechat v0.0.0-20230222024006-36f0325263cd/go.mod h1:mysjrtCs9MmN8hqDf4/mc4eQ26Rt9s1p5oO+fhJlLB4= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= @@ -211,6 +218,8 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/wechatpay-apiv3/wechatpay-go v0.2.18 h1:vj5tvSmnEIz3ZsnFNNUzg+3Z46xgNMJbrO4aD4wP15w= +github.com/wechatpay-apiv3/wechatpay-go v0.2.18/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ= github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= diff --git a/api/handler/config_handler.go b/api/handler/config_handler.go index ee58a94a..ce5b3b84 100644 --- a/api/handler/config_handler.go +++ b/api/handler/config_handler.go @@ -34,6 +34,6 @@ func (h *ConfigHandler) Get(c *gin.Context) { resp.ERROR(c, err.Error()) return } - + value["wxAppId"] = h.App.Config.WxpayConfig.AppId resp.SUCCESS(c, value) } diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 2b8f5dce..790b0cbb 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -24,6 +24,7 @@ import ( const ( PayWayAlipay = "支付宝" + PayWayWxPay = "微信" PayWayXunHu = "虎皮椒" PayWayJs = "PayJS" ) @@ -32,6 +33,7 @@ const ( type PaymentHandler struct { BaseHandler alipayService *payment.AlipayService + wxpayService *payment.WxpayService huPiPayService *payment.HuPiPayService js *payment.PayJS snowflake *service.Snowflake @@ -42,6 +44,7 @@ type PaymentHandler struct { func NewPaymentHandler( server *core.AppServer, alipayService *payment.AlipayService, + wxService *payment.WxpayService, huPiPayService *payment.HuPiPayService, js *payment.PayJS, db *gorm.DB, @@ -49,6 +52,7 @@ func NewPaymentHandler( fs embed.FS) *PaymentHandler { return &PaymentHandler{ alipayService: alipayService, + wxpayService: wxService, huPiPayService: huPiPayService, js: js, snowflake: snowflake, @@ -99,6 +103,23 @@ func (h *PaymentHandler) DoPay(c *gin.Context) { c.Redirect(302, uri) return + } else if payWay == "wxpay" { // 微信 + userId := h.GetLoginUserId(c) + var user model.User + res = h.DB.First(&user, userId) + if res.Error != nil { + resp.ERROR(c, "Invalid user ID") + return + } + // 生成支付签名 + signInfo, err := h.wxpayService.PayUrlMobile(user, order) + if err != nil { + resp.ERROR(c, "error with generating Pay URL: "+err.Error()) + return + } + + resp.SUCCESS(c, signInfo) + return } else if payWay == "hupi" { // 虎皮椒支付 params := payment.HuPiPayReq{ Version: "1.1", @@ -106,7 +127,7 @@ func (h *PaymentHandler) DoPay(c *gin.Context) { TotalFee: fmt.Sprintf("%f", order.Amount), Title: order.Subject, NotifyURL: h.App.Config.HuPiPayConfig.NotifyURL, - WapName: "极客学长", + WapName: "AI小墨", } r, err := h.huPiPayService.Pay(params) if err != nil { @@ -153,7 +174,27 @@ func (h *PaymentHandler) OrderQuery(c *gin.Context) { counter++ } - resp.SUCCESS(c, gin.H{"status": order.Status}) + resp.SUCCESS(c, gin.H{"status": order.Status, "amount": order.Amount, "expire": ""}) +} + +// OrderQueryAmount 查询订单状态 +func (h *PaymentHandler) OrderQueryAmount(c *gin.Context) { + var data struct { + OrderNo string `json:"order_no"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + var order model.Order + res := h.DB.Where("order_no = ?", data.OrderNo).First(&order) + if res.Error != nil { + resp.ERROR(c, "Order not found") + return + } + h.DB.Model(&order).UpdateColumn("status", types.OrderScanned) + resp.SUCCESS(c, gin.H{"status": order.Status, "amount": order.Amount, "createTime": order.CreatedAt.Unix()}) } // PayQrcode 生成支付 URL 二维码 @@ -196,6 +237,9 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { case "payjs": payWay = PayWayJs notifyURL = h.App.Config.JPayConfig.NotifyURL + case "wxpay": + payWay = PayWayWxPay + notifyURL = h.App.Config.WxpayConfig.NotifyURL default: payWay = PayWayAlipay notifyURL = h.App.Config.AlipayConfig.NotifyURL @@ -247,6 +291,8 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { var logo string if data.PayWay == "alipay" { logo = "res/img/alipay.jpg" + } else if data.PayWay == "wxpay" { + logo = "res/img/wechat-pay.jpg" } else if data.PayWay == "hupi" { if h.App.Config.HuPiPayConfig.Name == "wechat" { logo = "res/img/wechat-pay.jpg" @@ -268,6 +314,9 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { } imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s", parse.Scheme, parse.Host, orderNo, data.PayWay) + if data.PayWay == "wxpay" { + imageURL = fmt.Sprintf("%s://%s/mobile/payment?order_no=%s&pay_way=%s", parse.Scheme, parse.Host, orderNo, data.PayWay) + } imgData, err := utils.GenQrcode(imageURL, 400, file) if err != nil { resp.ERROR(c, err.Error()) @@ -325,7 +374,7 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { NotifyURL: notifyURL, ReturnURL: returnURL, CallbackURL: returnURL, - WapName: "极客学长", + WapName: "AI小墨", } r, err := h.huPiPayService.Pay(params) if err != nil { @@ -346,6 +395,16 @@ func (h *PaymentHandler) Mobile(c *gin.Context) { params.Add("notify_url", notifyURL) params.Add("auto", "0") payURL = h.js.PayH5(params) + case "wxpay": + payWay = PayWayWxPay + notifyURL = h.App.Config.WxpayConfig.NotifyURL + returnURL = h.App.Config.WxpayConfig.ReturnURL + payURL = orderNo + //signInfo, err = h.wxpayService.Pay(h.App.Config.WxpayConfig.MchId, h.App.Config.WxpayConfig.AppId, h.App.Config.WxpayConfig.MchKey) + //if err != nil { + // resp.ERROR(c, "error with generating Pay URL: "+err.Error()) + // return + //} case "alipay": payWay = PayWayAlipay notifyURL = h.App.Config.AlipayConfig.NotifyURL @@ -430,8 +489,13 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error { opt = "VIP充值,VIP 没到期,只延期不增加算力" } else { user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix() - user.Power += h.App.SysConfig.VipMonthPower - power = h.App.SysConfig.VipMonthPower + if remark.Days == 1 { + user.Power += remark.Power + power = remark.Power + } else { + user.Power += h.App.SysConfig.VipMonthPower + power = h.App.SysConfig.VipMonthPower + } opt = "VIP充值" } user.Vip = true @@ -487,6 +551,9 @@ func (h *PaymentHandler) GetPayWays(c *gin.Context) { if h.App.Config.AlipayConfig.Enabled { data["alipay"] = gin.H{"name": "alipay"} } + if h.App.Config.WxpayConfig.Enabled { + data["wxpay"] = gin.H{"name": "wxpay"} + } if h.App.Config.HuPiPayConfig.Enabled { data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name} } @@ -522,6 +589,23 @@ func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) { c.String(http.StatusOK, "success") } +// WxpayNotify 微信支付回调 +func (h *PaymentHandler) WxpayNotify(c *gin.Context) { + orderNo, outTradeNo, code := h.wxpayService.TradeVerify(c) + logger.Infof("验证支付结果:%+v", code) + if code != 200 { + logger.Error("订单校验失败") + c.String(http.StatusUnauthorized, "fail") + return + } + err := h.notify(orderNo, outTradeNo) + if err != nil { + c.String(http.StatusOK, "fail") + return + } + c.String(code, "fail") +} + // AlipayNotify 支付宝支付回调 func (h *PaymentHandler) AlipayNotify(c *gin.Context) { err := c.Request.ParseForm() diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index db4d4066..92b0aa1c 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -8,12 +8,14 @@ import ( "chatplus/utils" "chatplus/utils/resp" "fmt" + "github.com/chanxuehong/wechat/oauth2" "strings" "time" "github.com/go-redis/redis/v8" "github.com/golang-jwt/jwt/v5" + openoath "github.com/chanxuehong/wechat/open/oauth2" "github.com/gin-gonic/gin" "github.com/lionsoul2014/ip2region/binding/golang/xdb" "gorm.io/gorm" @@ -155,6 +157,44 @@ func (h *UserHandler) Register(c *gin.Context) { resp.SUCCESS(c, tokenString) } +// WxLogin 微信内公众号一键授权(支持改造为微信登录) +func (h *UserHandler) WxLogin(c *gin.Context) { + var data struct { + Code string `json:"code"` + State string `json:"state"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + oauth2Endpoint := openoath.NewEndpoint(h.App.Config.WxpayConfig.AppId, h.App.Config.WxpayConfig.WxAppSecret) + oaClient := oauth2.Client{Endpoint: oauth2Endpoint} + oaToken, errToken := oaClient.ExchangeToken(data.Code) + if errToken != nil { + logger.Error("errToken=", errToken) + resp.ERROR(c, "登录超时,请重试") + return + } + userinfo, err := openoath.GetUserInfo(oaToken.AccessToken, oaToken.OpenId, openoath.LanguageZhCN, nil) + if err != nil { + logger.Error("err=", err) + resp.ERROR(c, "用户信息获取失败,请重试") + return + } + var user model.User + userId := h.GetLoginUserId(c) + res := h.DB.Where("id = ?", userId).First(&user) + user.OfficialOpenid = userinfo.OpenId + user.Unionid = userinfo.UnionId + res = h.DB.Updates(&user) + if res.Error != nil { + resp.ERROR(c, "保存数据失败") + logger.Error(res.Error) + return + } + resp.SUCCESS(c, "微信授权成功") +} + // Login 用户登录 func (h *UserHandler) Login(c *gin.Context) { var data struct { diff --git a/api/main.go b/api/main.go index e5b8f3cf..89eea8f0 100644 --- a/api/main.go +++ b/api/main.go @@ -188,6 +188,7 @@ func main() { }), fx.Provide(payment.NewAlipayService), + fx.Provide(payment.NewWxpayService), fx.Provide(payment.NewHuPiPay), fx.Provide(payment.NewPayJS), fx.Provide(service.NewSnowflake), @@ -209,6 +210,7 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) { group := s.Engine.Group("/api/user/") group.POST("register", h.Register) + group.POST("wxLogin", h.WxLogin) group.POST("login", h.Login) group.GET("logout", h.Logout) group.GET("session", h.Session) @@ -341,8 +343,10 @@ func main() { group.GET("doPay", h.DoPay) group.GET("payWays", h.GetPayWays) group.POST("query", h.OrderQuery) + group.POST("queryOrder", h.OrderQueryAmount) group.POST("qrcode", h.PayQrcode) group.POST("mobile", h.Mobile) + group.POST("wxpay/notify", h.WxpayNotify) group.POST("alipay/notify", h.AlipayNotify) group.POST("hupipay/notify", h.HuPiPayNotify) group.POST("payjs/notify", h.PayJsNotify) diff --git a/api/service/payment/wxpay_service.go b/api/service/payment/wxpay_service.go new file mode 100644 index 00000000..6f62ddf4 --- /dev/null +++ b/api/service/payment/wxpay_service.go @@ -0,0 +1,115 @@ +package payment + +import ( + "chatplus/core/types" + "chatplus/store/model" + chatPlusUtils "chatplus/utils" + "context" + "github.com/gin-gonic/gin" + "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" + "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" + "github.com/wechatpay-apiv3/wechatpay-go/core/notify" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" + "log" + "time" + + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/utils" +) + +type WxpayService struct { + config *types.WxpayConfig + client *core.Client + certificateVisitor core.CertificateGetter +} + +func NewWxpayService(appConfig *types.AppConfig) *WxpayService { + config := appConfig.WxpayConfig + if !config.Enabled { + logger.Info("Disabled Wxpay service") + return nil + } + // 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(config.PrivateKey) + if err != nil { + log.Print("load merchant private key error") + return nil + } + + ctx := context.Background() + // 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力 + opts := []core.ClientOption{ + option.WithWechatPayAutoAuthCipher(config.MchId, config.CertificateSerialNo, mchPrivateKey, config.MchKey), + } + client, err := core.NewClient(ctx, opts...) + if err != nil { + return nil + } + // 1. 使用 `RegisterDownloaderWithPrivateKey` 注册下载器 + err2 := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, mchPrivateKey, config.CertificateSerialNo, config.MchId, config.MchKey) + if err2 != nil { + logger.Error("支付回调校验失败,请检查应用私钥配置文件") + return nil + } + // 2. 获取商户号对应的微信支付平台证书访问器 + certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(config.MchId) + return &WxpayService{&config, client, certificateVisitor} +} + +func (s *WxpayService) Pay(user model.User, order model.Order) (resp *jsapi.PrepayWithRequestPaymentResponse, err error) { + return s.PayUrlMobile(user, order) +} + +func (s *WxpayService) PayUrlMobile(user model.User, order model.Order) (resp *jsapi.PrepayWithRequestPaymentResponse, err error) { + svc := jsapi.JsapiApiService{Client: s.client} + var outTradeNo = chatPlusUtils.RandString(16) + resp, result, err := svc.PrepayWithRequestPayment(context.Background(), + jsapi.PrepayRequest{ + Appid: core.String(s.config.AppId), + Mchid: core.String(s.config.MchId), + Description: core.String(order.Subject), + OutTradeNo: core.String(outTradeNo), + TimeExpire: core.Time(time.Now()), + Attach: core.String(order.OrderNo), + NotifyUrl: core.String(s.config.NotifyURL), + SupportFapiao: core.Bool(false), + Amount: &jsapi.Amount{ + Currency: core.String("CNY"), + Total: core.Int64(int64(order.Amount * 100)), + }, + Payer: &jsapi.Payer{ + Openid: core.String(user.OfficialOpenid), + }, + SceneInfo: &jsapi.SceneInfo{ + PayerClientIp: core.String("127.0.0.1"), + }, + SettleInfo: &jsapi.SettleInfo{ + ProfitSharing: core.Bool(false), + }, + }, + ) + + if err != nil { + // 处理错误 + log.Printf("call Prepay err:%s", err) + } else { + // 处理返回结果 + log.Printf("status=%d resp=%s", result.Response.StatusCode, resp) + } + return resp, err +} + +func (s *WxpayService) TradeVerify(c *gin.Context) (orderNo string, tradeNo string, code int) { + // 3. 使用证书访问器初始化 `notify.Handler` + handler, _ := notify.NewRSANotifyHandler(s.config.MchKey, verifiers.NewSHA256WithRSAVerifier(s.certificateVisitor)) + // 2. 获取商户号对应的微信支付平台证书访问器 + transaction := new(payments.Transaction) + _, err3 := handler.ParseNotifyRequest(context.Background(), c.Request, transaction) + // 如果验签未通过,或者解密失败 + if err3 != nil { + return "0", "0", 401 + } + return *transaction.Attach, *transaction.OutTradeNo, 200 +} diff --git a/api/store/model/user.go b/api/store/model/user.go index 41d09905..1154aba0 100644 --- a/api/store/model/user.go +++ b/api/store/model/user.go @@ -2,18 +2,20 @@ package model type User struct { BaseModel - Username string - Nickname string - Password string - Avatar string - Salt string // 密码盐 - Power int // 剩余算力 - ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json - ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色 - ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型 - ExpiredTime int64 // 账户到期时间 - Status bool `gorm:"default:true"` // 当前状态 - LastLoginAt int64 // 最后登录时间 - LastLoginIp string // 最后登录 IP - Vip bool // 是否 VIP 会员 + Username string + Nickname string + Password string + Avatar string + Salt string // 密码盐 + Power int // 剩余算力 + ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json + ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色 + ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型 + ExpiredTime int64 // 账户到期时间 + Status bool `gorm:"default:true"` // 当前状态 + OfficialOpenid string + Unionid string + LastLoginAt int64 // 最后登录时间 + LastLoginIp string // 最后登录 IP + Vip bool // 是否 VIP 会员 } diff --git a/web/package.json b/web/package.json index c3151cb9..173d8e70 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,7 @@ "markdown-it-latex2img": "^0.0.6", "markdown-it-mathjax": "^2.0.0", "md-editor-v3": "^2.2.1", + "moment": "^2.30.1", "pinia": "^2.1.4", "qrcode": "^1.5.3", "qs": "^6.11.1", @@ -30,7 +31,8 @@ "v3-waterfall": "^1.2.1", "vant": "^4.5.0", "vue": "^3.2.13", - "vue-router": "^4.0.15" + "vue-router": "^4.0.15", + "weixin-js-sdk": "^1.6.0" }, "devDependencies": { "@babel/core": "7.18.6", diff --git a/web/src/main.js b/web/src/main.js index 64227b94..66323eda 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -46,7 +46,8 @@ import { Tabs, Tag, TextEllipsis, - Uploader + Uploader, + CountDown, } from "vant"; import {router} from "@/router"; import 'v3-waterfall/dist/style.css' @@ -97,6 +98,7 @@ app.use(Lazyload); app.use(ImagePreview); app.use(Tab); app.use(Tabs); +app.use(CountDown); app.use(router).use(ElementPlus).mount('#app') diff --git a/web/src/router.js b/web/src/router.js index b17f6c51..f63e6ec7 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -212,6 +212,11 @@ const routes = [ name: 'mobile-chat-session', component: () => import('@/views/mobile/ChatSession.vue'), }, + { + path: '/mobile/payment', + name: 'mobile-payment', + component: () => import('@/views/mobile/Payment.vue'), + }, { path: '/mobile/chat/export', name: 'mobile-chat-export', @@ -248,4 +253,4 @@ router.beforeEach((to, from, next) => { next() }) -export {router, prevRoute}; \ No newline at end of file +export {router, prevRoute}; diff --git a/web/src/utils/libs.js b/web/src/utils/libs.js index 646608e6..363baecc 100644 --- a/web/src/utils/libs.js +++ b/web/src/utils/libs.js @@ -30,6 +30,15 @@ export function isMobile() { return mobileRegex.test(userAgent); } +/** + * 判断是否微信浏览器 + * @returns {boolean} + */ +export function isWeChat() { + const userAgent = navigator.userAgent.toLowerCase(); + return userAgent.indexOf('micromessenger') !== -1; +} + // 格式化日期 export function dateFormat(timestamp, format) { if (!timestamp) { diff --git a/web/src/utils/wechatAuth.js b/web/src/utils/wechatAuth.js new file mode 100644 index 00000000..e967b48d --- /dev/null +++ b/web/src/utils/wechatAuth.js @@ -0,0 +1,103 @@ +//微信相关 +import wx from "weixin-js-sdk"; + +/** + * 获取公众号授权URL + * @returns {string} + */ +export function authUrl(appid, url = null) { + if (url == null) { + url = window.location.href + } + return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${encodeURIComponent(url)}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect` +} + +/** + * 获取授权回调code + */ +export function getCode() { + let url = window.location.href + let urlStr = url.split('?')[1] + const urlSearchParams = new URLSearchParams(urlStr) + return Object.fromEntries(urlSearchParams.entries()) +} + +/** + * 获取授权结果 + * @returns {boolean} + */ +export function authResult() { + const queryBean = getCode() + return queryBean !=null && queryBean.code !== undefined +} + + +export function getSignature (data, callback) { + const {appId, nonceStr, paySign, timeStamp} = data + // qryWxSignature 这个是调用后台获取签名的接口 + wx.config({ + beta: true, + debug: false, + appId: appId, + timestamp: timeStamp, + nonceStr: nonceStr, + signature: paySign, + // 这里是把所有的方法都写出来了 如果只需要一个方法可以只写一个 + jsApiList: [ + 'checkJsApi', + 'onMenuShareTimeline', + 'onMenuShareAppMessage', + 'onMenuShareQQ', + 'onMenuShareWeibo', + 'hideMenuItems', + 'showMenuItems', + 'hideAllNonBaseMenuItem', + 'showAllNonBaseMenuItem', + 'translateVoice', + 'startRecord', + 'stopRecord', + 'onRecordEnd', + 'playVoice', + 'pauseVoice', + 'stopVoice', + 'uploadVoice', + 'downloadVoice', + 'chooseImage', + 'previewImage', + 'uploadImage', + 'downloadImage', + 'getNetworkType', + 'openLocation', + 'getLocation', + 'hideOptionMenu', + 'showOptionMenu', + 'closeWindow', + 'scanQRCode', + 'chooseWXPay', + 'openProductSpecificView', + 'addCard', + 'chooseCard', + 'openCard', + 'openWXDeviceLib', + 'closeWXDeviceLib', + 'configWXDeviceWiFi', + 'getWXDeviceInfos', + 'sendDataToWXDevice', + 'startScanWXDevice', + 'stopScanWXDevice', + 'connectWXDevice', + 'disconnectWXDevice', + 'getWXDeviceTicket', + 'WeixinJSBridgeReady', + 'onWXDeviceBindStateChange', + 'onWXDeviceStateChange', + 'onScanWXDeviceResult', + 'onReceiveDataFromWXDevice', + 'onWXDeviceBluetoothStateChange' + ] + }) + wx.ready(function () { + console.log(callback, 'callback') + if (callback) callback() + }) +} diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index e586d5e9..7441f444 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -74,6 +74,9 @@ 支付宝 + + 微信 + 微信 支付宝 @@ -290,7 +293,21 @@ const alipay = (row) => { } genPayQrcode() } +//微信支付 +const wxpay = (row) => { + payName.value = "微信" + curPay.value = "wxpay" + amount.value = (row.price - row.discount).toFixed(2) + if (!isLogin.value) { + showLoginDialog.value = true + return + } + if (row) { + curPayProduct.value = row + } + genPayQrcode() +} // 虎皮椒支付 const huPiPay = (row) => { payName.value = payWays.value["hupi"]["name"] === "wechat" ? '微信' : '支付宝' diff --git a/web/src/views/mobile/Payment.vue b/web/src/views/mobile/Payment.vue new file mode 100644 index 00000000..f281a685 --- /dev/null +++ b/web/src/views/mobile/Payment.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/web/src/views/mobile/Profile.vue b/web/src/views/mobile/Profile.vue index 3a22a2aa..8a087a3b 100644 --- a/web/src/views/mobile/Profile.vue +++ b/web/src/views/mobile/Profile.vue @@ -53,7 +53,9 @@ 微信 支付宝 - + + 微信 + 微信 @@ -250,7 +252,11 @@ const pay = (payWay, item) => { user_id: loginUser.value.id }).then(res => { // console.log(res.data) - location.href = res.data + if (payWay === 'wxpay') { + router.push({path: "payment", query: {order_no: res.data, pay_way: 'wxpay'}}) + } else { + location.href = res.data + } }).catch(e => { showFailToast("生成支付订单失败:" + e.message) }) @@ -302,4 +308,4 @@ const pay = (payWay, item) => { } } } - \ No newline at end of file +