添加微信商户号支付支持

This commit is contained in:
futuresnail 2024-04-29 21:09:10 +08:00
parent fbfa2a71a9
commit a90f00f7a4
18 changed files with 644 additions and 34 deletions

View File

@ -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"

View File

@ -21,9 +21,9 @@ type AppConfig struct {
MjPlusConfigs []MjPlusConfig // MJ plus config
WeChatBot bool // 是否启用微信机器人
SdConfigs []StableDiffusionConfig // sd AI draw service pool
XXLConfig XXLConfig
AlipayConfig AlipayConfig
WxpayConfig WxpayConfig
HuPiPayConfig HuPiPayConfig
SmtpConfig SmtpConfig // 邮件发送配置
JPayConfig JPayConfig // payjs 支付配置
@ -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

View File

@ -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

View File

@ -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=

View File

@ -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)
}

View File

@ -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()
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()

View File

@ -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 {

View File

@ -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)

View File

@ -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
}

View File

@ -13,6 +13,8 @@ type User struct {
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 会员

View File

@ -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",

View File

@ -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')

View File

@ -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',

View File

@ -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) {

103
web/src/utils/wechatAuth.js Normal file
View File

@ -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()
})
}

View File

@ -74,6 +74,9 @@
<el-button type="primary" @click="alipay(scope.item)" size="small" v-if="payWays['alipay']">
<i class="iconfont icon-alipay"></i> 支付宝
</el-button>
<el-button type="success" @click="wxpay(scope.item)" size="small" v-if="payWays['wxpay']">
<span><i class="iconfont icon-wechat-pay"></i> 微信</span>
</el-button>
<el-button type="success" @click="huPiPay(scope.item)" size="small" v-if="payWays['hupi']">
<span v-if="payWays['hupi']['name'] === 'wechat'"><i class="iconfont icon-wechat-pay"></i> 微信</span>
<span v-else><i class="iconfont icon-alipay"></i> 支付宝</span>
@ -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" ? '微信' : '支付宝'

View File

@ -0,0 +1,187 @@
<template>
<div class="payment-content container">
<van-nav-bar :title="title" :left-arrow="true" @click-left="onClickLeft"/>
<div class="content">
<div class="pay-price">
<div class="pay-expire">支付倒计时
<van-count-down :time="data.expire" :auto-start="data.autoStart" format="mm:ss" @finish="finishPay"
ref="countDownRef"/>
</div>
<div class="pay-title">支付金额</div>
<div class="pay-amount">¥<span>{{ data.amount }}</span></div>
</div>
<div class="pay-btn">
<van-button round block type="primary" @click="pay">支付</van-button>
</div>
</div>
</div>
</template>
<script setup>
import wx from "weixin-js-sdk";
import {ref} from "vue";
import {ElMessage} from "element-plus";
import {checkSession} from "@/action/session";
import {httpGet, httpPost} from "@/utils/http";
import {authResult, authUrl, getCode, getSignature} from "@/utils/wechatAuth";
import {isWeChat} from "@/utils/libs";
import {useRouter} from "vue-router";
import {setUserToken} from "@/store/session";
import {prevRoute} from "@/router";
const title = ref('支付')
const moment = require('moment');
const countDownRef = ref(null)
const router = useRouter()
const data = ref({
amount: '-',
expire: 0,
autoStart: false,
orderTimeout: 1800
})
const orderNo = router.currentRoute.value.query["order_no"]
const payWay = router.currentRoute.value.query["pay_way"]
if (isWeChat()) {
checkSession().then(() => {
httpGet("/api/config/get?key=system").then(res => {
data.value.orderTimeout = res.data['order_pay_timeout']
if (authResult()) {
//访访
const queryParam = getCode()
queryParam.login_type = "1"
httpPost('/api/user/wxLogin', queryParam).then(() => {
data.value.autoStart = false
httpPost("/api/payment/queryOrder", {order_no: orderNo}).then(res => {
const {amount, createTime} = res.data
data.value.amount = amount
let expire = createTime + data.value.orderTimeout - moment().unix()
if (expire <= 0) {
finishPay()
} else {
data.value.expire = Math.min(expire, data.value.orderTimeout) * 1000
data.value.autoStart = true
if (countDownRef.value) {
countDownRef.value.start()
}
}
}).catch(e => {
ElMessage.error("查询支付状态失败:" + e.message)
})
}).catch((e) => {
ElMessage.error('登录失败,' + e.message)
})
} else {
window.location.href = authUrl(res.data['wxAppId'])
}
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
}).catch(() => {
router.push('/login')
})
} else {
ElMessage.warning("请使用微信支付")
}
const pay = () => {
if (!isWeChat()) {
ElMessage.warning("请使用微信支付")
return
}
httpGet(`/api/payment/doPay?order_no=${orderNo}&pay_way=${payWay}`).then(res => {
const {nonceStr, paySign, signType, timeStamp} = res.data
getSignature(res.data, () => {
wx.chooseWXPay({
timestamp: timeStamp, // jssdk使timestamp使timeStampS
nonceStr: nonceStr, // 32
package: res.data['package'], // prepay_idprepay_id=\*\*\*
signType: signType, // V3RSA,V2V2
paySign: paySign, //
success: function () {
//
ElMessage.success('支付成功')
let timer = setTimeout(() => {
clearTimeout(timer)
router.push('/mobile/profile')
}, 1000)
if (countDownRef.value) {
countDownRef.value.stop()
}
},
fail: function () {
ElMessage.error('支付失败')
}
})
})
}).catch(e => {
ElMessage.error("查询支付状态失败:" + e.message)
})
}
const onClickLeft = () => router.push('/mobile/profile');
const finishPay = () => {
ElMessage.error('支付超时')
let timer = setTimeout(() => {
clearTimeout(timer)
router.push('/mobile/profile')
}, 1000)
}
</script>
<style lang="stylus">
.payment-content {
.content {
padding-top 60px
.van-cell__value {
.van-image {
width 100%
}
}
.pay-price {
text-align center
margin-bottom 30px
padding 10px 15px
.pay-expire {
color #2778FF
display flex
flex-direction row
align-items center
justify-content center
font-size 15px
.van-count-down {
color #2778FF
font-size 15px
}
}
.pay-title {
color #333
font-size 15px
margin-top 30px
}
.pay-amount {
color #666
font-size 15px
span {
font-size 36px
font-weight bold
}
}
}
.pay-btn {
padding 10px 15px
}
}
}
</style>

View File

@ -53,7 +53,9 @@
<span v-if="payWays['hupi']['name'] === 'wechat'"><i class="iconfont icon-wechat-pay"></i> 微信</span>
<span v-else><i class="iconfont icon-alipay"></i> 支付宝</span>
</van-button>
<van-button type="success" @click="pay('wxpay',item)" size="small" v-if="payWays['wxpay']">
<span><i class="iconfont icon-wechat-pay"></i> 微信</span>
</van-button>
<van-button type="success" @click="pay('payjs',item)" size="small" v-if="payWays['payjs']">
<span><i class="iconfont icon-wechat-pay"></i> 微信</span>
</van-button>
@ -250,7 +252,11 @@ const pay = (payWay, item) => {
user_id: loginUser.value.id
}).then(res => {
// console.log(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)
})