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 @@
+
+
+
+
+
+
支付倒计时:
+
+
+
支付金额
+
¥{{ data.amount }}
+
+
+ 支付
+
+
+
+
+
+
+
+
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
+