opt: verify the order in notify callback

This commit is contained in:
RockYang 2024-01-22 13:58:25 +08:00
parent 66c7717f04
commit 3ad8065e20
9 changed files with 168 additions and 58 deletions

View File

@ -8,6 +8,7 @@
* 功能新增:新增网站公告,可以在管理后台自定义配置 * 功能新增:新增网站公告,可以在管理后台自定义配置
* 功能新增:新增阿里通义千问大模型支持 * 功能新增:新增阿里通义千问大模型支持
* Bug修复修复 MJ 放大任务失败时候 img_call 会增加的 Bug * Bug修复修复 MJ 放大任务失败时候 img_call 会增加的 Bug
* 功能优化新增虎皮椒和PayJS订单状态校验功能增加安全性
## v3.2.5 ## v3.2.5

View File

@ -38,16 +38,6 @@ type SmtpConfig struct {
Password string // 发件人邮箱密码 Password string // 发件人邮箱密码
} }
// JPayConfig PayJs 支付配置
type JPayConfig struct {
Enabled bool
Name string // 支付名称,默认 wechat
AppId string // 商户 ID
PrivateKey string // 私钥
ApiURL string // API 网关
NotifyURL string // 异步回调地址
}
type ChatPlusApiConfig struct { type ChatPlusApiConfig struct {
ApiURL string ApiURL string
AppId string AppId string
@ -105,10 +95,20 @@ type HuPiPayConfig struct { //虎皮椒第四方支付配置
Name string // 支付名称wechat/alipay Name string // 支付名称wechat/alipay
AppId string // App ID AppId string // App ID
AppSecret string // app 密钥 AppSecret string // app 密钥
PayURL string // 支付网关 ApiURL string // 支付网关
NotifyURL string // 异步通知回调 NotifyURL string // 异步通知回调
} }
// JPayConfig PayJs 支付配置
type JPayConfig struct {
Enabled bool
Name string // 支付名称,默认 wechat
AppId string // 商户 ID
PrivateKey string // 私钥
ApiURL string // API 网关
NotifyURL string // 异步回调地址
}
type XXLConfig struct { // XXL 任务调度配置 type XXLConfig struct { // XXL 任务调度配置
Enabled bool Enabled bool
ServerAddr string ServerAddr string

View File

@ -101,30 +101,12 @@ func (h *PaymentHandler) DoPay(c *gin.Context) {
NotifyURL: h.App.Config.HuPiPayConfig.NotifyURL, NotifyURL: h.App.Config.HuPiPayConfig.NotifyURL,
WapName: "极客学长", WapName: "极客学长",
} }
res, err := h.huPiPayService.Pay(params) r, err := h.huPiPayService.Pay(params)
if err != nil { if err != nil {
resp.ERROR(c, "error with generate pay url: "+err.Error()) resp.ERROR(c, err.Error())
return return
} }
var r struct {
Openid interface{} `json:"openid"`
UrlQrcode string `json:"url_qrcode"`
URL string `json:"url"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg,omitempty"`
}
err = utils.JsonDecode(res, &r)
if err != nil {
logger.Debugf(res)
resp.ERROR(c, "error with decode payment result: "+err.Error())
return
}
if r.ErrCode != 0 {
resp.ERROR(c, "error with generate pay url: "+r.ErrMsg)
return
}
c.Redirect(302, r.URL) c.Redirect(302, r.URL)
} }
resp.ERROR(c, "Invalid operations") resp.ERROR(c, "Invalid operations")
@ -288,7 +270,7 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) {
} }
// 异步通知回调公共逻辑 // 异步通知回调公共逻辑
func (h *PaymentHandler) notify(orderNo string) error { func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
var order model.Order var order model.Order
res := h.db.Where("order_no = ?", orderNo).First(&order) res := h.db.Where("order_no = ?", orderNo).First(&order)
if res.Error != nil { if res.Error != nil {
@ -353,6 +335,7 @@ func (h *PaymentHandler) notify(orderNo string) error {
// 更新订单状态 // 更新订单状态
order.PayTime = time.Now().Unix() order.PayTime = time.Now().Unix()
order.Status = types.OrderPaidSuccess order.Status = types.OrderPaidSuccess
order.TradeNo = tradeNo
res = h.db.Updates(&order) res = h.db.Updates(&order)
if res.Error != nil { if res.Error != nil {
err := fmt.Errorf("error with update order info: %v", res.Error) err := fmt.Errorf("error with update order info: %v", res.Error)
@ -390,9 +373,14 @@ func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
orderNo := c.Request.Form.Get("trade_order_id") orderNo := c.Request.Form.Get("trade_order_id")
logger.Infof("收到订单支付回调,订单 NO%s", orderNo) logger.Infof("收到订单支付回调,订单 NO%s", orderNo)
// TODO 是否要保存订单交易流水号
err = h.notify(orderNo) tradeNo := c.Request.Form.Get("transaction_id")
if err = h.huPiPayService.Check(tradeNo); err != nil {
logger.Error("订单校验失败:", err)
c.String(http.StatusOK, "fail")
return
}
err = h.notify(orderNo, tradeNo)
if err != nil { if err != nil {
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
return return
@ -409,16 +397,17 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
return return
} }
// TODO这里最好用支付宝的公钥签名签证一下交易真假 // TODO验证交易签名
//res := h.alipayService.TradeVerify(c.Request.Form) res := h.alipayService.TradeVerify(c.Request.Form)
r := h.alipayService.TradeQuery(c.Request.Form.Get("out_trade_no")) logger.Infof("验证支付结果:%+v", res)
logger.Infof("验证支付结果:%+v", r) if !res.Success() {
if !r.Success() { logger.Error("订单校验失败:", res.Message)
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
return return
} }
err = h.notify(r.OutTradeNo) tradeNo := c.Request.Form.Get("trade_no")
err = h.notify(res.OutTradeNo, tradeNo)
if err != nil { if err != nil {
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
return return
@ -443,7 +432,16 @@ func (h *PaymentHandler) PayJsNotify(c *gin.Context) {
return return
} }
err = h.notify(orderNo) // 校验订单支付状态
tradeNo := c.Request.Form.Get("payjs_order_id")
err = h.js.Check(tradeNo)
if err != nil {
logger.Error("订单校验失败:", err)
c.String(http.StatusOK, "fail")
return
}
err = h.notify(orderNo, tradeNo)
if err != nil { if err != nil {
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
return return

View File

@ -4,6 +4,7 @@ import (
"chatplus/core/types" "chatplus/core/types"
"chatplus/utils" "chatplus/utils"
"crypto/md5" "crypto/md5"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -17,14 +18,14 @@ import (
type HuPiPayService struct { type HuPiPayService struct {
appId string appId string
appSecret string appSecret string
host string apiURL string
} }
func NewHuPiPay(config *types.AppConfig) *HuPiPayService { func NewHuPiPay(config *types.AppConfig) *HuPiPayService {
return &HuPiPayService{ return &HuPiPayService{
appId: config.HuPiPayConfig.AppId, appId: config.HuPiPayConfig.AppId,
appSecret: config.HuPiPayConfig.AppSecret, appSecret: config.HuPiPayConfig.AppSecret,
host: config.HuPiPayConfig.PayURL, apiURL: config.HuPiPayConfig.ApiURL,
} }
} }
@ -42,8 +43,16 @@ type HuPiPayReq struct {
NonceStr string `json:"nonce_str"` NonceStr string `json:"nonce_str"`
} }
type HuPiResp struct {
Openid interface{} `json:"openid"`
UrlQrcode string `json:"url_qrcode"`
URL string `json:"url"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg,omitempty"`
}
// Pay 执行支付请求操作 // Pay 执行支付请求操作
func (s *HuPiPayService) Pay(params HuPiPayReq) (string, error) { func (s *HuPiPayService) Pay(params HuPiPayReq) (HuPiResp, error) {
data := url.Values{} data := url.Values{}
simple := strconv.FormatInt(time.Now().Unix(), 10) simple := strconv.FormatInt(time.Now().Unix(), 10)
params.AppId = s.appId params.AppId = s.appId
@ -58,21 +67,34 @@ func (s *HuPiPayService) Pay(params HuPiPayReq) (string, error) {
encode = utils.JsonEncode(params) encode = utils.JsonEncode(params)
m = make(map[string]string) m = make(map[string]string)
_ = utils.JsonDecode(encode, &m) _ = utils.JsonDecode(encode, &m)
data.Add("hash", s.Sign(m)) data.Add("hash", s.sign(m))
resp, err := http.PostForm(s.host, data) apiURL := fmt.Sprintf("%s/payment/do.html", s.apiURL)
logger.Info(apiURL)
resp, err := http.PostForm(apiURL, data)
if err != nil { if err != nil {
return "error", err return HuPiResp{}, fmt.Errorf("error with requst api: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
all, err := io.ReadAll(resp.Body) all, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "error", err return HuPiResp{}, fmt.Errorf("error with reading response: %v", err)
} }
return string(all), err
var res HuPiResp
err = utils.JsonDecode(string(all), &res)
if err != nil {
return HuPiResp{}, fmt.Errorf("error with decode payment result: %v", err)
}
if res.ErrCode != 0 {
return HuPiResp{}, fmt.Errorf("error with generate pay url: %s", res.ErrMsg)
}
return res, nil
} }
// Sign 签名方法 // Sign 签名方法
func (s *HuPiPayService) Sign(params map[string]string) string { func (s *HuPiPayService) sign(params map[string]string) string {
var data string var data string
keys := make([]string, 0, 0) keys := make([]string, 0, 0)
for key := range params { for key := range params {
@ -90,3 +112,50 @@ func (s *HuPiPayService) Sign(params map[string]string) string {
sign := fmt.Sprintf("%x", m.Sum(nil)) sign := fmt.Sprintf("%x", m.Sum(nil))
return sign return sign
} }
// Check 校验订单状态
func (s *HuPiPayService) Check(tradeNo string) error {
data := url.Values{}
data.Add("appid", s.appId)
data.Add("open_order_id", tradeNo)
stamp := strconv.FormatInt(time.Now().Unix(), 10)
data.Add("time", stamp)
data.Add("nonce_str", stamp)
// 生成签名
encode := utils.JsonEncode(data)
m := make(map[string]string)
err := utils.JsonDecode(encode, &m)
data.Add("sign", s.sign(m))
apiURL := fmt.Sprintf("%s/payment/query.html", s.apiURL)
resp, err := http.PostForm(apiURL, data)
if err != nil {
return fmt.Errorf("error with http reqeust: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error with reading response: %v", err)
}
var r struct {
ErrCode int `json:"errcode"`
Data struct {
Status string `json:"status"`
OpenOrderId string `json:"open_order_id"`
} `json:"data,omitempty"`
ErrMsg string `json:"errmsg"`
Hash string `json:"hash"`
}
err = utils.JsonDecode(string(body), &r)
if err != nil {
return fmt.Errorf("error with decode response: %v", err)
}
if r.ErrCode == 0 && r.Data.Status == "OD" {
return nil
} else {
return errors.New("order not paid")
}
}

View File

@ -5,6 +5,7 @@ import (
"chatplus/utils" "chatplus/utils"
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -55,10 +56,11 @@ func (js *PayJS) Pay(param JPayReq) JPayReps {
} }
p.Add("mchid", js.config.AppId) p.Add("mchid", js.config.AppId)
p.Add("sign", sign(p, js.config.PrivateKey)) p.Add("sign", js.sign(p))
cli := http.Client{} cli := http.Client{}
r, err := cli.PostForm(js.config.ApiURL, p) apiURL := fmt.Sprintf("%s/api/native", js.config.ApiURL)
r, err := cli.PostForm(apiURL, p)
if err != nil { if err != nil {
return JPayReps{ReturnMsg: err.Error()} return JPayReps{ReturnMsg: err.Error()}
} }
@ -76,7 +78,7 @@ func (js *PayJS) Pay(param JPayReq) JPayReps {
return data return data
} }
func sign(params url.Values, priKey string) string { func (js *PayJS) sign(params url.Values) string {
params.Del(`sign`) params.Del(`sign`)
var keys = make([]string, 0, 0) var keys = make([]string, 0, 0)
for key := range params { for key := range params {
@ -94,9 +96,45 @@ func sign(params url.Values, priKey string) string {
} }
} }
var src = strings.Join(pList, "&") var src = strings.Join(pList, "&")
src += "&key=" + priKey src += "&key=" + js.config.PrivateKey
md5bs := md5.Sum([]byte(src)) md5bs := md5.Sum([]byte(src))
md5res := hex.EncodeToString(md5bs[:]) md5res := hex.EncodeToString(md5bs[:])
return strings.ToUpper(md5res) return strings.ToUpper(md5res)
} }
// Check 查询订单支付状态
// @param tradeNo 支付平台交易 ID
func (js *PayJS) Check(tradeNo string) error {
apiURL := fmt.Sprintf("%s/api/check", js.config.ApiURL)
params := url.Values{}
params.Add("payjs_order_id", tradeNo)
params.Add("sign", js.sign(params))
data := strings.NewReader(params.Encode())
resp, err := http.Post(apiURL, "application/x-www-form-urlencoded", data)
defer resp.Body.Close()
if err != nil {
return fmt.Errorf("error with http reqeust: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error with reading response: %v", err)
}
var r struct {
ReturnCode int `json:"return_code"`
Status int `json:"status"`
}
err = utils.JsonDecode(string(body), &r)
if err != nil {
return fmt.Errorf("error with decode response: %v", err)
}
if r.ReturnCode == 1 && r.Status == 1 {
return nil
} else {
return errors.New("order not paid")
}
}

View File

@ -12,6 +12,7 @@ type Order struct {
ProductId uint ProductId uint
Username string Username string
OrderNo string OrderNo string
TradeNo string
Subject string Subject string
Amount float64 Amount float64
Status types.OrderStatus Status types.OrderStatus

View File

@ -10,6 +10,7 @@ type Order struct {
ProductId uint `json:"product_id"` ProductId uint `json:"product_id"`
Username string `json:"username"` Username string `json:"username"`
OrderNo string `json:"order_no"` OrderNo string `json:"order_no"`
TradeNo string `json:"trade_no"`
Subject string `json:"subject"` Subject string `json:"subject"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Status types.OrderStatus `json:"status"` Status types.OrderStatus `json:"status"`

View File

@ -1,2 +1,4 @@
ALTER TABLE `chatgpt_mj_jobs` ADD `publish` TINYINT(1) NOT NULL COMMENT '是否发布' AFTER `use_proxy`; ALTER TABLE `chatgpt_mj_jobs` ADD `publish` TINYINT(1) NOT NULL COMMENT '是否发布' AFTER `use_proxy`;
ALTER TABLE `chatgpt_sd_jobs` ADD `publish` TINYINT(1) NOT NULL COMMENT '是否发布' AFTER `progress`; ALTER TABLE `chatgpt_sd_jobs` ADD `publish` TINYINT(1) NOT NULL COMMENT '是否发布' AFTER `progress`;
ALTER TABLE `chatgpt_orders` ADD `trade_no` VARCHAR(60) NOT NULL COMMENT '支付平台交易流水号' AFTER `order_no`;

View File

@ -101,7 +101,7 @@ WeChatBot = false
Name = "wechat" Name = "wechat"
AppId = "" AppId = ""
AppSecret = "" AppSecret = ""
PayURL = "https://api.xunhupay.com/payment/do.html" ApiURL = "https://api.xunhupay.com"
NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify" NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify"
[SmtpConfig] # 注意阿里云服务器禁用了25号端口所以如果需要使用邮件功能请别用阿里云服务器 [SmtpConfig] # 注意阿里云服务器禁用了25号端口所以如果需要使用邮件功能请别用阿里云服务器
@ -116,5 +116,5 @@ WeChatBot = false
Name = "wechat" # 请不要改动 Name = "wechat" # 请不要改动
AppId = "" # 商户 ID AppId = "" # 商户 ID
PrivateKey = "" # 秘钥 PrivateKey = "" # 秘钥
ApiURL = "https://payjs.cn/api/native" ApiURL = "https://payjs.cn"
NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的 NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的