package handler import ( "chatplus/core" "chatplus/core/types" "chatplus/service" "chatplus/service/payment" "chatplus/store/model" "chatplus/utils" "chatplus/utils/resp" "embed" "encoding/base64" "fmt" "math" "net/http" "net/url" "sync" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) const ( PayWayAlipay = "支付宝" PayWayXunHu = "虎皮椒" PayWayJs = "PayJS" ) // PaymentHandler 支付服务回调 handler 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, 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, lock: sync.Mutex{}, } h.App = server return &h } func (h *PaymentHandler) DoPay(c *gin.Context) { orderNo := h.GetTrim(c, "order_no") payWay := h.GetTrim(c, "pay_way") if orderNo == "" { resp.ERROR(c, types.InvalidArgs) return } var order model.Order res := h.db.Where("order_no = ?", orderNo).First(&order) if res.Error != nil { resp.ERROR(c, "Order not found") return } // 更新扫码状态 h.db.Model(&order).UpdateColumn("status", types.OrderScanned) if payWay == "alipay" { // 支付宝 // 生成支付链接 notifyURL := h.App.Config.AlipayConfig.NotifyURL returnURL := "" // 关闭同步回跳 amount := fmt.Sprintf("%.2f", order.Amount) uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject) if err != nil { resp.ERROR(c, "error with generate pay url: "+err.Error()) return } c.Redirect(302, uri) return } else if payWay == "hupi" { // 虎皮椒支付 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()) 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") } // OrderQuery 查询订单状态 func (h *PaymentHandler) OrderQuery(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 } if order.Status == types.OrderPaidSuccess { resp.SUCCESS(c, gin.H{"status": order.Status}) return } counter := 0 for { time.Sleep(time.Second) var item model.Order h.db.Where("order_no = ?", data.OrderNo).First(&item) if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status { order.Status = item.Status break } counter++ } resp.SUCCESS(c, gin.H{"status": order.Status}) } // PayQrcode 生成支付 URL 二维码 func (h *PaymentHandler) PayQrcode(c *gin.Context) { var data struct { PayWay string `json:"pay_way"` // 支付方式 ProductId uint `json:"product_id"` UserId int `json:"user_id"` } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) return } var product model.Product res := h.db.First(&product, data.ProductId) if res.Error != nil { resp.ERROR(c, "Product not found") return } orderNo, err := h.snowflake.Next(false) if err != nil { resp.ERROR(c, "error with generate trade no: "+err.Error()) return } var user model.User res = h.db.First(&user, data.UserId) if res.Error != nil { resp.ERROR(c, "Invalid user ID") return } var payWay string var notifyURL string switch data.PayWay { case "hupi": payWay = PayWayXunHu notifyURL = h.App.Config.HuPiPayConfig.NotifyURL case "payjs": payWay = PayWayJs notifyURL = h.App.Config.JPayConfig.NotifyURL default: payWay = PayWayAlipay notifyURL = h.App.Config.AlipayConfig.NotifyURL } // 创建订单 remark := types.OrderRemark{ Days: product.Days, Calls: product.Calls, ImgCalls: product.ImgCalls, Name: product.Name, Price: product.Price, Discount: product.Discount, } order := model.Order{ UserId: user.Id, Username: user.Username, ProductId: product.Id, OrderNo: orderNo, Subject: product.Name, Amount: product.Price - product.Discount, Status: types.OrderNotPaid, PayWay: payWay, Remark: utils.JsonEncode(remark), } res = h.db.Create(&order) if res.Error != nil || res.RowsAffected == 0 { resp.ERROR(c, "error with create order: "+res.Error.Error()) 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" } else if data.PayWay == "hupi" { if h.App.Config.HuPiPayConfig.Name == "wechat" { logo = "res/img/wechat-pay.jpg" } else { logo = "res/img/alipay.jpg" } } file, err := h.fs.Open(logo) if err != nil { resp.ERROR(c, "error with open qrcode log file: "+err.Error()) return } parse, err := url.Parse(notifyURL) if err != nil { resp.ERROR(c, err.Error()) return } imageURL := fmt.Sprintf("%s://%s/api/payment/doPay?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()) return } imgDataBase64 := base64.StdEncoding.EncodeToString(imgData) resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL}) } // 异步通知回调公共逻辑 func (h *PaymentHandler) notify(orderNo string) error { var order model.Order res := h.db.Where("order_no = ?", orderNo).First(&order) if res.Error != nil { err := fmt.Errorf("error with fetch order: %v", res.Error) logger.Error(err) return err } h.lock.Lock() defer h.lock.Unlock() // 已支付订单,直接返回 if order.Status == types.OrderPaidSuccess { return nil } var user model.User res = h.db.First(&user, order.UserId) if res.Error != nil { err := fmt.Errorf("error with fetch user info: %v", res.Error) logger.Error(err) return err } var remark types.OrderRemark err := utils.JsonDecode(order.Remark, &remark) if err != nil { err := fmt.Errorf("error with decode order remark: %v", err) logger.Error(err) return err } if user.Vip { // 已经是 VIP 用户 if remark.Days > 0 { // 只延期 VIP,不增加调用次数 user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix() } else { // 充值点卡,直接增加次数即可 user.Calls += remark.Calls user.ImgCalls += remark.ImgCalls } } 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 user.ImgCalls += h.App.SysConfig.VipMonthImgCalls user.Vip = true } else { //点卡:days == 0, calls > 0 user.Calls += remark.Calls user.ImgCalls += remark.ImgCalls } } // 更新用户信息 res = h.db.Updates(&user) if res.Error != nil { err := fmt.Errorf("error with update user info: %v", res.Error) logger.Error(err) return err } // 更新订单状态 order.PayTime = time.Now().Unix() order.Status = types.OrderPaidSuccess res = h.db.Updates(&order) if res.Error != nil { err := fmt.Errorf("error with update order info: %v", res.Error) logger.Error(err) return err } // 更新产品销量 h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1)) return nil } // GetPayWays 获取支付方式 func (h *PaymentHandler) GetPayWays(c *gin.Context) { data := gin.H{} if h.App.Config.AlipayConfig.Enabled { data["alipay"] = gin.H{"name": "alipay"} } 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) } // HuPiPayNotify 虎皮椒支付异步回调 func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) { err := c.Request.ParseForm() if err != nil { c.String(http.StatusOK, "fail") return } orderNo := c.Request.Form.Get("trade_order_id") logger.Infof("收到订单支付回调,订单 NO:%s", orderNo) // TODO 是否要保存订单交易流水号 err = h.notify(orderNo) if err != nil { c.String(http.StatusOK, "fail") return } 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 } 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 } err = h.notify(orderNo) if err != nil { c.String(http.StatusOK, "fail") return } c.String(http.StatusOK, "success") }