diff --git a/CHANGELOG.md b/CHANGELOG.md index a90f7261..defc683e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * 功能新增:新增网站公告,可以在管理后台自定义配置 * 功能新增:新增阿里通义千问大模型支持 * Bug修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug +* 功能优化:新增虎皮椒和PayJS订单状态校验功能,增加安全性 ## v3.2.5 diff --git a/api/core/types/config.go b/api/core/types/config.go index 393e79e4..813ff196 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -38,16 +38,6 @@ type SmtpConfig struct { 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 { ApiURL string AppId string @@ -105,10 +95,20 @@ type HuPiPayConfig struct { //虎皮椒第四方支付配置 Name string // 支付名称,如:wechat/alipay AppId string // App ID AppSecret string // app 密钥 - PayURL string // 支付网关 + ApiURL 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 任务调度配置 Enabled bool ServerAddr string diff --git a/api/handler/payment_handler.go b/api/handler/payment_handler.go index 09a18085..d6141d6a 100644 --- a/api/handler/payment_handler.go +++ b/api/handler/payment_handler.go @@ -101,30 +101,12 @@ func (h *PaymentHandler) DoPay(c *gin.Context) { NotifyURL: h.App.Config.HuPiPayConfig.NotifyURL, WapName: "极客学长", } - res, err := h.huPiPayService.Pay(params) + r, err := h.huPiPayService.Pay(params) if err != nil { - resp.ERROR(c, "error with generate pay url: "+err.Error()) + resp.ERROR(c, err.Error()) 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) } 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 res := h.db.Where("order_no = ?", orderNo).First(&order) if res.Error != nil { @@ -329,7 +311,7 @@ func (h *PaymentHandler) notify(orderNo string) error { user.ImgCalls += remark.ImgCalls } - } else { // 非 VIP 用户 + } else { // 非 VIP 用户 if remark.Days > 0 { // vip 套餐:days > 0, calls == 0 user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix() user.Calls += h.App.SysConfig.VipMonthCalls @@ -353,6 +335,7 @@ func (h *PaymentHandler) notify(orderNo string) error { // 更新订单状态 order.PayTime = time.Now().Unix() order.Status = types.OrderPaidSuccess + order.TradeNo = tradeNo res = h.db.Updates(&order) if res.Error != nil { 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") 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 { c.String(http.StatusOK, "fail") return @@ -409,16 +397,17 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) { 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() { + // TODO:验证交易签名 + res := h.alipayService.TradeVerify(c.Request.Form) + logger.Infof("验证支付结果:%+v", res) + if !res.Success() { + logger.Error("订单校验失败:", res.Message) c.String(http.StatusOK, "fail") return } - err = h.notify(r.OutTradeNo) + tradeNo := c.Request.Form.Get("trade_no") + err = h.notify(res.OutTradeNo, tradeNo) if err != nil { c.String(http.StatusOK, "fail") return @@ -443,7 +432,16 @@ func (h *PaymentHandler) PayJsNotify(c *gin.Context) { 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 { c.String(http.StatusOK, "fail") return diff --git a/api/service/payment/hupipay_serive.go b/api/service/payment/hupipay_serive.go index 2e9da3b6..2edb1643 100644 --- a/api/service/payment/hupipay_serive.go +++ b/api/service/payment/hupipay_serive.go @@ -4,6 +4,7 @@ import ( "chatplus/core/types" "chatplus/utils" "crypto/md5" + "errors" "fmt" "io" "net/http" @@ -17,14 +18,14 @@ import ( type HuPiPayService struct { appId string appSecret string - host string + apiURL string } func NewHuPiPay(config *types.AppConfig) *HuPiPayService { return &HuPiPayService{ appId: config.HuPiPayConfig.AppId, appSecret: config.HuPiPayConfig.AppSecret, - host: config.HuPiPayConfig.PayURL, + apiURL: config.HuPiPayConfig.ApiURL, } } @@ -42,8 +43,16 @@ type HuPiPayReq struct { 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 执行支付请求操作 -func (s *HuPiPayService) Pay(params HuPiPayReq) (string, error) { +func (s *HuPiPayService) Pay(params HuPiPayReq) (HuPiResp, error) { data := url.Values{} simple := strconv.FormatInt(time.Now().Unix(), 10) params.AppId = s.appId @@ -58,21 +67,34 @@ func (s *HuPiPayService) Pay(params HuPiPayReq) (string, error) { 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) + data.Add("hash", s.sign(m)) + apiURL := fmt.Sprintf("%s/payment/do.html", s.apiURL) + logger.Info(apiURL) + resp, err := http.PostForm(apiURL, data) if err != nil { - return "error", err + return HuPiResp{}, fmt.Errorf("error with requst api: %v", err) } defer resp.Body.Close() all, err := io.ReadAll(resp.Body) 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 签名方法 -func (s *HuPiPayService) Sign(params map[string]string) string { +func (s *HuPiPayService) sign(params map[string]string) string { var data string keys := make([]string, 0, 0) for key := range params { @@ -90,3 +112,50 @@ func (s *HuPiPayService) Sign(params map[string]string) string { sign := fmt.Sprintf("%x", m.Sum(nil)) 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") + } +} diff --git a/api/service/payment/payjs_service.go b/api/service/payment/payjs_service.go index f4e27c21..e6cc07d3 100644 --- a/api/service/payment/payjs_service.go +++ b/api/service/payment/payjs_service.go @@ -5,6 +5,7 @@ import ( "chatplus/utils" "crypto/md5" "encoding/hex" + "errors" "fmt" "io" "net/http" @@ -55,10 +56,11 @@ func (js *PayJS) Pay(param JPayReq) JPayReps { } p.Add("mchid", js.config.AppId) - p.Add("sign", sign(p, js.config.PrivateKey)) + p.Add("sign", js.sign(p)) 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 { return JPayReps{ReturnMsg: err.Error()} } @@ -76,7 +78,7 @@ func (js *PayJS) Pay(param JPayReq) JPayReps { return data } -func sign(params url.Values, priKey string) string { +func (js *PayJS) sign(params url.Values) string { params.Del(`sign`) var keys = make([]string, 0, 0) for key := range params { @@ -94,9 +96,45 @@ func sign(params url.Values, priKey string) string { } } var src = strings.Join(pList, "&") - src += "&key=" + priKey + src += "&key=" + js.config.PrivateKey md5bs := md5.Sum([]byte(src)) md5res := hex.EncodeToString(md5bs[:]) 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") + } +} diff --git a/api/store/model/order.go b/api/store/model/order.go index 73d2607b..a9c6c66d 100644 --- a/api/store/model/order.go +++ b/api/store/model/order.go @@ -12,6 +12,7 @@ type Order struct { ProductId uint Username string OrderNo string + TradeNo string Subject string Amount float64 Status types.OrderStatus diff --git a/api/store/vo/order.go b/api/store/vo/order.go index e046de61..bd7087a7 100644 --- a/api/store/vo/order.go +++ b/api/store/vo/order.go @@ -10,6 +10,7 @@ type Order struct { ProductId uint `json:"product_id"` Username string `json:"username"` OrderNo string `json:"order_no"` + TradeNo string `json:"trade_no"` Subject string `json:"subject"` Amount float64 `json:"amount"` Status types.OrderStatus `json:"status"` diff --git a/database/update-v3.2.6.sql b/database/update-v3.2.6.sql index 5afef275..8d2fe042 100644 --- a/database/update-v3.2.6.sql +++ b/database/update-v3.2.6.sql @@ -1,2 +1,4 @@ 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`; \ No newline at end of file +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`; \ No newline at end of file diff --git a/deploy/conf/config.toml b/deploy/conf/config.toml index f58c3ad5..7d3f1453 100644 --- a/deploy/conf/config.toml +++ b/deploy/conf/config.toml @@ -101,7 +101,7 @@ WeChatBot = false Name = "wechat" AppId = "" AppSecret = "" - PayURL = "https://api.xunhupay.com/payment/do.html" + ApiURL = "https://api.xunhupay.com" NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify" [SmtpConfig] # 注意,阿里云服务器禁用了25号端口,所以如果需要使用邮件功能,请别用阿里云服务器 @@ -116,5 +116,5 @@ WeChatBot = false Name = "wechat" # 请不要改动 AppId = "" # 商户 ID PrivateKey = "" # 秘钥 - ApiURL = "https://payjs.cn/api/native" + ApiURL = "https://payjs.cn" NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的 \ No newline at end of file