From 8ec85e2829f32377a14955808f1f3491ac926235 Mon Sep 17 00:00:00 2001 From: RockYang Date: Sun, 7 Jan 2024 14:36:02 +0800 Subject: [PATCH] feat: payjs payment channel is ready --- api/config.sample.toml | 39 ++++++- api/core/types/config.go | 3 +- api/handler/payment_handler.go | 150 ++++++++++++++++++-------- api/handler/sms_handler.go | 11 +- api/handler/test_handler.go | 24 ++++- api/main.go | 2 + api/service/mj/service.go | 5 +- api/service/payment/hupipay_serive.go | 36 +++++-- api/service/payment/payjs_service.go | 73 ++++++++----- web/src/views/Member.vue | 23 +++- web/src/views/admin/Functions.vue | 1 + 11 files changed, 279 insertions(+), 88 deletions(-) diff --git a/api/config.sample.toml b/api/config.sample.toml index 3e8b0d16..6282604f 100644 --- a/api/config.sample.toml +++ b/api/config.sample.toml @@ -58,6 +58,10 @@ WeChatBot = false BotToken = "" GuildId = "" ChanelId = "" + UseCDN = false #是否使用反向代理访问,设置为true下面的设置才会生效 + DiscordAPI = "https://mj.r9it.com:8001" # discord API 反代地址 + DiscordCDN = "https://mj.r9it.com:8002" # mj 图片反代地址 + DiscordGateway = "wss://mj.r9it.com:8003" # discord 机器人反代地址 [[MjConfigs]] Enabled = false @@ -65,6 +69,16 @@ WeChatBot = false BotToken = "" GuildId = "" ChanelId = "" + UseCDN = false #是否使用反向代理访问,设置为true下面的设置才会生效 + DiscordAPI = "https://mj.r9it.com:8001" # discord API 反代地址 + DiscordCDN = "https://mj.r9it.com:8002" # mj 图片反代地址 + DiscordGateway = "wss://mj.r9it.com:8003" # discord 机器人反代地址 + +[[SdConfigs]] + Enabled = false + ApiURL = "" + ApiKey = "" + Txt2ImgJsonPath = "res/sd/text2img.json" [[SdConfigs]] Enabled = false @@ -95,4 +109,27 @@ WeChatBot = false PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书 AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书 RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书 - NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址 \ No newline at end of file + NotifyURL = "https://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址 + +[HuPiPayConfig] + Enabled = false + Name = "wechat" + AppId = "201906161477" + AppSecret = "7f403199d510fb2c6f0b9f2311800e7c" + PayURL = "https://api.xunhupay.com/payment/do.html" + NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify" + +[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,所以如果需要使用邮件功能,请别用阿里云服务器 + Host = "smtp.163.com" + Port = 25 + AppName = "极客学长" + From = "test@163.com" # 发件邮箱人地址 + Password = "" #邮箱 stmp 服务授权码 + +[JPayConfig] # PayJs 支付配置 + Enabled = false + Name = "wechat" # 请不要改动 + AppId = "" # 商户 ID + PrivateKey = "" # 秘钥 + ApiURL = "https://payjs.cn/api/native" + NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的 \ No newline at end of file diff --git a/api/core/types/config.go b/api/core/types/config.go index 3723af5e..51e773b2 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -39,6 +39,7 @@ type SmtpConfig struct { // JPayConfig PayJs 支付配置 type JPayConfig struct { Enabled bool + Name string // 支付名称,默认 wechat AppId string // 商户 ID PrivateKey string // 私钥 ApiURL string // API 网关 @@ -96,8 +97,8 @@ type HuPiPayConfig struct { //虎皮椒第四方支付配置 Name string // 支付名称,如:wechat/alipay AppId string // App ID AppSecret string // app 密钥 - NotifyURL string // 异步通知回调 PayURL string // 支付网关 + NotifyURL string // 异步通知回调 } type XXLConfig struct { // XXL 任务调度配置 diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 422f133e..63dfdcda 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -11,17 +11,20 @@ import ( "embed" "encoding/base64" "fmt" - "github.com/gin-gonic/gin" - "gorm.io/gorm" + "math" "net/http" "net/url" "sync" "time" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" ) const ( PayWayAlipay = "支付宝" PayWayXunHu = "虎皮椒" + PayWayJs = "PayJS" ) // PaymentHandler 支付服务回调 handler @@ -29,16 +32,25 @@ type PaymentHandler struct { BaseHandler alipayService *payment.AlipayService huPiPayService *payment.HuPiPayService + js *payment.PayJS snowflake *service.Snowflake db *gorm.DB fs embed.FS lock sync.Mutex } -func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, huPiPayService *payment.HuPiPayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler { +func NewPaymentHandler( + server *core.AppServer, + alipayService *payment.AlipayService, + huPiPayService *payment.HuPiPayService, + js *payment.PayJS, + snowflake *service.Snowflake, + db *gorm.DB, + fs embed.FS) *PaymentHandler { h := PaymentHandler{ alipayService: alipayService, huPiPayService: huPiPayService, + js: js, snowflake: snowflake, fs: fs, db: db, @@ -81,17 +93,14 @@ func (h *PaymentHandler) DoPay(c *gin.Context) { c.Redirect(302, uri) return } else if payWay == "hupi" { // 虎皮椒支付 - params := map[string]string{ - "version": "1.1", - "trade_order_id": orderNo, - "total_fee": fmt.Sprintf("%f", order.Amount), - "title": order.Subject, - "notify_url": h.App.Config.HuPiPayConfig.NotifyURL, - "return_url": "", - "wap_name": "极客学长", - "callback_url": "", + params := payment.HuPiPayReq{ + Version: "1.1", + TradeOrderId: orderNo, + TotalFee: fmt.Sprintf("%f", order.Amount), + Title: order.Subject, + NotifyURL: h.App.Config.HuPiPayConfig.NotifyURL, + WapName: "极客学长", } - res, err := h.huPiPayService.Pay(params) if err != nil { resp.ERROR(c, "error with generate pay url: "+err.Error()) @@ -189,9 +198,14 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { return } - payWay := PayWayAlipay - if data.PayWay == "hupi" { + var payWay string + switch data.PayWay { + case "hupi": payWay = PayWayXunHu + case "payjs": + payWay = PayWayJs + default: + payWay = PayWayAlipay } // 创建订单 remark := types.OrderRemark{ @@ -219,6 +233,23 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { return } + // PayJs 单独处理,只能用官方生成的二维码 + if data.PayWay == "payjs" { + params := payment.JPayReq{ + TotalFee: int(math.Ceil(order.Amount * 100)), + OutTradeNo: order.OrderNo, + Subject: product.Name, + } + r := h.js.Pay(params) + if r.IsOK() { + resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode}) + return + } else { + resp.ERROR(c, "error with generating payment qrcode: "+r.ReturnMsg) + return + } + } + var logo string if data.PayWay == "alipay" { logo = "res/img/alipay.jpg" @@ -252,35 +283,6 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) { resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL}) } -// AlipayNotify 支付宝支付回调 -func (h *PaymentHandler) AlipayNotify(c *gin.Context) { - err := c.Request.ParseForm() - if err != nil { - c.String(http.StatusOK, "fail") - return - } - - // TODO:这里最好用支付宝的公钥签名签证一下交易真假 - //res := h.alipayService.TradeVerify(c.Request.Form) - r := h.alipayService.TradeQuery(c.Request.Form.Get("out_trade_no")) - logger.Infof("验证支付结果:%+v", r) - if !r.Success() { - c.String(http.StatusOK, "fail") - return - } - - h.lock.Lock() - defer h.lock.Unlock() - - err = h.notify(r.OutTradeNo) - if err != nil { - c.String(http.StatusOK, "fail") - return - } - - c.String(http.StatusOK, "success") -} - // 异步通知回调公共逻辑 func (h *PaymentHandler) notify(orderNo string) error { var order model.Order @@ -370,6 +372,9 @@ func (h *PaymentHandler) GetPayWays(c *gin.Context) { if h.App.Config.HuPiPayConfig.Enabled { data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name} } + if h.App.Config.JPayConfig.Enabled { + data["payjs"] = gin.H{"name": h.App.Config.JPayConfig.Name} + } resp.SUCCESS(c, data) } @@ -395,3 +400,60 @@ func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) { c.String(http.StatusOK, "success") } + +// AlipayNotify 支付宝支付回调 +func (h *PaymentHandler) AlipayNotify(c *gin.Context) { + err := c.Request.ParseForm() + if err != nil { + c.String(http.StatusOK, "fail") + return + } + + // TODO:这里最好用支付宝的公钥签名签证一下交易真假 + //res := h.alipayService.TradeVerify(c.Request.Form) + r := h.alipayService.TradeQuery(c.Request.Form.Get("out_trade_no")) + logger.Infof("验证支付结果:%+v", r) + if !r.Success() { + c.String(http.StatusOK, "fail") + return + } + + h.lock.Lock() + defer h.lock.Unlock() + + err = h.notify(r.OutTradeNo) + if err != nil { + c.String(http.StatusOK, "fail") + return + } + + c.String(http.StatusOK, "success") +} + +// PayJsNotify PayJs 支付异步回调 +func (h *PaymentHandler) PayJsNotify(c *gin.Context) { + err := c.Request.ParseForm() + if err != nil { + c.String(http.StatusOK, "fail") + return + } + + orderNo := c.Request.Form.Get("out_trade_no") + returnCode := c.Request.Form.Get("return_code") + logger.Infof("收到订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode) + // 支付失败 + if returnCode != "1" { + return + } + + h.lock.Lock() + defer h.lock.Unlock() + + err = h.notify(orderNo) + if err != nil { + c.String(http.StatusOK, "fail") + return + } + + c.String(http.StatusOK, "success") +} diff --git a/api/handler/sms_handler.go b/api/handler/sms_handler.go index bd33ae3d..c58eedb5 100644 --- a/api/handler/sms_handler.go +++ b/api/handler/sms_handler.go @@ -6,9 +6,10 @@ import ( "chatplus/service" "chatplus/utils" "chatplus/utils/resp" + "strings" + "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" - "strings" ) const CodeStorePrefix = "/verify/codes/" @@ -52,8 +53,16 @@ func (h *SmsHandler) SendCode(c *gin.Context) { code := utils.RandomNumber(6) var err error if strings.Contains(data.Receiver, "@") { // email + if !utils.ContainsStr(h.App.SysConfig.RegisterWays, "email") { + resp.ERROR(c, "系统已禁用邮箱注册!") + return + } err = h.smtp.SendVerifyCode(data.Receiver, code) } else { + if !utils.ContainsStr(h.App.SysConfig.RegisterWays, "mobile") { + resp.ERROR(c, "系统已禁用手机号注册!") + return + } err = h.sms.SendVerifyCode(data.Receiver, code) } if err != nil { diff --git a/api/handler/test_handler.go b/api/handler/test_handler.go index 74e9eb46..3cc1e749 100644 --- a/api/handler/test_handler.go +++ b/api/handler/test_handler.go @@ -2,6 +2,7 @@ package handler import ( "chatplus/service" + "chatplus/service/payment" "chatplus/store/model" "chatplus/utils" "chatplus/utils/resp" @@ -14,15 +15,30 @@ import ( type TestHandler struct { db *gorm.DB snowflake *service.Snowflake + js *payment.PayJS } -func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake) *TestHandler { - return &TestHandler{db: db, snowflake: snowflake} +func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.PayJS) *TestHandler { + return &TestHandler{db: db, snowflake: snowflake, js: js} } func (h *TestHandler) Test(c *gin.Context) { - h.initUserNickname(c) - h.initMjTaskId(c) + //h.initUserNickname(c) + //h.initMjTaskId(c) + + orderId, _ := h.snowflake.Next(false) + params := payment.JPayReq{ + TotalFee: 12345, + OutTradeNo: orderId, + Subject: "支付测试", + } + r := h.js.Pay(params) + if !r.IsOK() { + resp.ERROR(c, r.ReturnMsg) + return + } + resp.SUCCESS(c, r) + } func (h *TestHandler) initUserNickname(c *gin.Context) { diff --git a/api/main.go b/api/main.go index 35338099..879c93fb 100644 --- a/api/main.go +++ b/api/main.go @@ -170,6 +170,7 @@ func main() { fx.Provide(payment.NewAlipayService), fx.Provide(payment.NewHuPiPay), + fx.Provide(payment.NewPayJS), fx.Provide(service.NewSnowflake), fx.Provide(service.NewXXLJobExecutor), fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) { @@ -306,6 +307,7 @@ func main() { group.POST("qrcode", h.PayQrcode) group.POST("alipay/notify", h.AlipayNotify) group.POST("hupipay/notify", h.HuPiPayNotify) + group.POST("payjs/notify", h.PayJsNotify) }), fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) { group := s.Engine.Group("/api/admin/product/") diff --git a/api/service/mj/service.go b/api/service/mj/service.go index a9a0a49b..1c03de6d 100644 --- a/api/service/mj/service.go +++ b/api/service/mj/service.go @@ -4,10 +4,11 @@ import ( "chatplus/core/types" "chatplus/store" "chatplus/store/model" - "gorm.io/gorm" "strings" "sync/atomic" "time" + + "gorm.io/gorm" ) // Service MJ 绘画服务 @@ -121,7 +122,7 @@ func (s *Service) Notify(data CBReq) { return } - tx := s.db.Session(&gorm.Session{}).Order("id ASC") + tx := s.db.Session(&gorm.Session{}).Where("progress < ?", 100).Order("id ASC") if data.ReferenceId != "" { tx = tx.Where("reference_id = ?", data.ReferenceId) } else { diff --git a/api/service/payment/hupipay_serive.go b/api/service/payment/hupipay_serive.go index d42e9cb3..2e9da3b6 100644 --- a/api/service/payment/hupipay_serive.go +++ b/api/service/payment/hupipay_serive.go @@ -2,6 +2,7 @@ package payment import ( "chatplus/core/types" + "chatplus/utils" "crypto/md5" "fmt" "io" @@ -27,17 +28,37 @@ func NewHuPiPay(config *types.AppConfig) *HuPiPayService { } } +type HuPiPayReq struct { + AppId string `json:"appid"` + Version string `json:"version"` + TradeOrderId string `json:"trade_order_id"` + TotalFee string `json:"total_fee"` + Title string `json:"title"` + NotifyURL string `json:"notify_url"` + ReturnURL string `json:"return_url"` + WapName string `json:"wap_name"` + CallbackURL string `json:"callback_url"` + Time string `json:"time"` + NonceStr string `json:"nonce_str"` +} + // Pay 执行支付请求操作 -func (s *HuPiPayService) Pay(params map[string]string) (string, error) { +func (s *HuPiPayService) Pay(params HuPiPayReq) (string, error) { data := url.Values{} simple := strconv.FormatInt(time.Now().Unix(), 10) - params["appid"] = s.appId - params["time"] = simple - params["nonce_str"] = simple - for k, v := range params { - data.Add(k, v) + params.AppId = s.appId + params.Time = simple + params.NonceStr = simple + encode := utils.JsonEncode(params) + m := make(map[string]string) + _ = utils.JsonDecode(encode, &m) + for k, v := range m { + data.Add(k, fmt.Sprintf("%v", v)) } - data.Add("hash", s.Sign(params)) + encode = utils.JsonEncode(params) + m = make(map[string]string) + _ = utils.JsonDecode(encode, &m) + data.Add("hash", s.Sign(m)) resp, err := http.PostForm(s.host, data) if err != nil { return "error", err @@ -54,7 +75,6 @@ func (s *HuPiPayService) Pay(params map[string]string) (string, error) { func (s *HuPiPayService) Sign(params map[string]string) string { var data string keys := make([]string, 0, 0) - params["appid"] = s.appId for key := range params { keys = append(keys, key) } diff --git a/api/service/payment/payjs_service.go b/api/service/payment/payjs_service.go index 60db4a91..f4e27c21 100644 --- a/api/service/payment/payjs_service.go +++ b/api/service/payment/payjs_service.go @@ -26,9 +26,55 @@ func NewPayJS(appConfig *types.AppConfig) *PayJS { type JPayReq struct { TotalFee int `json:"total_fee"` OutTradeNo string `json:"out_trade_no"` - Body string `json:"body"` + Subject string `json:"body"` NotifyURL string `json:"notify_url"` } +type JPayReps struct { + CodeUrl string `json:"code_url"` + OutTradeNo string `json:"out_trade_no"` + OrderId string `json:"payjs_order_id"` + Qrcode string `json:"qrcode"` + ReturnCode int `json:"return_code"` + ReturnMsg string `json:"return_msg"` + Sign string `json:"sign"` + TotalFee string `json:"total_fee"` +} + +func (r JPayReps) IsOK() bool { + return r.ReturnMsg == "SUCCESS" +} + +func (js *PayJS) Pay(param JPayReq) JPayReps { + param.NotifyURL = js.config.NotifyURL + var p = url.Values{} + encode := utils.JsonEncode(param) + m := make(map[string]interface{}) + _ = utils.JsonDecode(encode, &m) + for k, v := range m { + p.Add(k, fmt.Sprintf("%v", v)) + } + p.Add("mchid", js.config.AppId) + + p.Add("sign", sign(p, js.config.PrivateKey)) + + cli := http.Client{} + r, err := cli.PostForm(js.config.ApiURL, p) + if err != nil { + return JPayReps{ReturnMsg: err.Error()} + } + defer r.Body.Close() + bs, err := io.ReadAll(r.Body) + if err != nil { + return JPayReps{ReturnMsg: err.Error()} + } + + var data JPayReps + err = utils.JsonDecode(string(bs), &data) + if err != nil { + return JPayReps{ReturnMsg: err.Error()} + } + return data +} func sign(params url.Values, priKey string) string { params.Del(`sign`) @@ -54,28 +100,3 @@ func sign(params url.Values, priKey string) string { md5res := hex.EncodeToString(md5bs[:]) return strings.ToUpper(md5res) } - -func (pj *PayJS) Pay(param JPayReq) (string, error) { - var p = url.Values{} - encode := utils.JsonEncode(param) - m := make(map[string]interface{}) - _ = utils.JsonDecode(encode, &m) - for k, v := range m { - p.Add(k, fmt.Sprintf("%v", v)) - } - p.Add("mchid", pj.config.AppId) - - p.Add("sign", sign(p, pj.config.PrivateKey)) - - cli := http.Client{} - r, err := cli.PostForm(pj.config.ApiURL, p) - if err != nil { - return "", err - } - defer r.Body.Close() - bs, err := io.ReadAll(r.Body) - if err != nil { - return "", err - } - return string(bs), nil -} diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index 5613db5e..32844030 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -71,6 +71,10 @@ 微信 支付宝 + + + 微信 + @@ -280,7 +284,20 @@ const huPiPay = (row) => { curPayProduct.value = row } genPayQrcode() +} +// PayJS 支付 +const PayJs = (row) => { + payName.value = '微信' + curPay.value = "payjs" + if (!user.value.id) { + showLoginDialog.value = true + return + } + if (row) { + curPayProduct.value = row + } + genPayQrcode() } const queryOrder = (orderNo) => { @@ -290,7 +307,11 @@ const queryOrder = (orderNo) => { queryOrder(orderNo) } else if (res.data.status === 2) { text.value = "支付成功,正在刷新页面" - setTimeout(() => location.reload(), 500) + if (curPay.value === "payjs") { + setTimeout(() => location.reload(), 3000) + } else { + setTimeout(() => location.reload(), 500) + } } else { // 如果当前订单没有过期,继续等待订单的下一个状态 if (activeOrderNo.value === orderNo) { diff --git a/web/src/views/admin/Functions.vue b/web/src/views/admin/Functions.vue index dc8a6e0d..16ac9817 100644 --- a/web/src/views/admin/Functions.vue +++ b/web/src/views/admin/Functions.vue @@ -284,6 +284,7 @@ const functionSet = (filed, row) => { const generateToken = () => { httpGet('/api/admin/function/token').then(res => { + ElMessage.success("生成 Token 成功") item.value.token = res.data }).catch(() => { ElMessage.error("生成 Token 失败")