package api import ( "context" "errors" "fmt" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "github.com/smartwalle/alipay/v3" wxcore "github.com/wechatpay-apiv3/wechatpay-go/core" "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/refunddomestic" wxutils "github.com/wechatpay-apiv3/wechatpay-go/utils" "gorm.io/gorm" "log" "net/http" "qnc-server/config" "qnc-server/db" "qnc-server/global" "qnc-server/model/model" "qnc-server/model/request" "qnc-server/model/response" "qnc-server/service" "qnc-server/utils" "strconv" "time" ) type Pay struct { } var ctx = context.Background() var productService service.ProductService // 注册路由 func InitPay(group *gin.RouterGroup) { var p Pay { payPublicGroup := group.Group("pay") payPublicGroup.POST("callback/:platform", p.Callback) // 微信支付支付回调 payPublicGroup.GET("refund_details/:id", p.RefundDetailsHTML) // 内部退款订单详情页面 payPublicGroup.POST("refund/:id", p.Refund) // 内部退款按钮 payPublicGroup.POST("refund_callback/:platform", p.RefundCallback) // 微信退款回调 payPublicGroup.POST("ali_callback", p.AlipayCallback) // 阿里退款回调 payPublicGroup.POST("complaint_callback/:platform", p.WxPayComplaintCallback) } { payPrivateGroup := group.Group("pay") payPrivateGroup.Use(JWTAuth()) payPrivateGroup.POST("prepay", p.Prepay) // 创建微信支付订单 payPrivateGroup.GET("order_list", p.GetOrderList) // 获取订单列表 payPrivateGroup.POST("ali_prepay", p.AliPrepay) // 创建支付宝支付订单 payPrivateGroup.GET("get_query_cache", p.GetQueryCache) // 获取网页的查询缓存 } } func (p *Pay) GetOrderList(c *gin.Context) { // 从查询参数中获取分页参数 pageSizeStr := c.DefaultQuery("page_size", "10") pageNumStr := c.DefaultQuery("page_num", "1") userId := utils.GetUserID(c) pageSize, err := strconv.Atoi(pageSizeStr) if err != nil { response.FailWithMessage(err.Error(), c) return } pageNum, err := strconv.Atoi(pageNumStr) if err != nil { response.FailWithMessage(err.Error(), c) return } list, err := orderService.GetList(pageSize, pageNum, userId) if err != nil { log.Println("get order list error:", err) response.FailWithMessage(err.Error(), c) return } response.OkWithData(list, c) } func (p *Pay) Prepay(c *gin.Context) { Claims, err := utils.GetClaims(c) if err != nil { log.Println("get claims error:", err) response.FailWithMessage(err.Error(), c) return } var reqBody request.PrepayReq err = c.ShouldBindJSON(&reqBody) if err != nil { response.FailWithMessage("Failed to read request body, need product", c) return } var ( userid = Claims.Userid openid string appid string mchID string ) // 获取用户ip clientIP := c.ClientIP() switch reqBody.Platform { case model.PlatformMPWEIXIN: openid = Claims.AuthIdentifiers.OpenID appid = config.ConfigData.System.WxAppId mchID = config.ConfigData.WxPay.MchID case model.PlatformMPH5: openid = Claims.AuthIdentifiers.OpenID appid = config.ConfigData.System.WxH5AppId mchID = config.ConfigData.WxPay.MchH5ID case model.PlatformH5: appid = config.ConfigData.System.WxH5AppId mchID = config.ConfigData.WxPay.MchH5ID default: response.FailWithMessage("ProductName Must be wx or mp-h5 or h5", c) return } product, err := productService.GetProduct(reqBody.ProductName) if err != nil { log.Println(err) response.FailWithMessage(err.Error(), c) return } // 查看用户是否内部号 user, err := userService.GetUserByUserid(Claims.Userid) if err != nil { log.Println(err) response.FailWithMessage(err.Error(), c) return } var amount int64 if user.Inside { amount = int64(1) } else { amount = int64(product.SellPrice) } var outTradeNo = fmt.Sprintf("wx_%s", utils.GenerateOrderNumber()) var resp interface{} if reqBody.Platform == model.PlatformMPWEIXIN { resp, err = orderService.WechatJSAPIPrepay(appid, mchID, product, outTradeNo, amount, openid, model.PlatformMPWEIXIN, global.GlobalData.PayClient) if err != nil { log.Printf("【创建微信支付订单】创建支付失败,系统错误: %v", err) response.FailWithMessage("创建支付失败,系统错误", c) return } } else if reqBody.Platform == model.PlatformMPH5 { resp, err = orderService.WechatJSAPIPrepay(appid, mchID, product, outTradeNo, amount, model.PlatformMPH5, openid, global.GlobalData.PayH5Client) if err != nil { log.Printf("【创建微信支付订单】创建支付失败,系统错误: %v", err) response.FailWithMessage("创建支付失败,系统错误", c) return } } else { resp, err = orderService.WechatH5Prepay(appid, mchID, product, outTradeNo, amount, clientIP, global.GlobalData.PayH5Client) if err != nil { log.Printf("【创建微信支付订单】创建支付失败,系统错误: %v", err) response.FailWithMessage("创建支付失败,系统错误", c) return } } var payOrder = model.PayOrder{ OutTradeNo: outTradeNo, Amount: amount, Userid: userid, PayStatus: model.PayStatusNotPay, ProductID: product.ID, Product: &product, Platform: reqBody.Platform, PaymentMethod: model.PaymentMethod_WECHAT, } err = db.DB.Create(&payOrder).Error if err != nil { log.Printf("create payOrder error:%v", err) } response.OkWithData(resp, c) } func (p *Pay) Callback(c *gin.Context) { ctx := c //这个参数是context.Background() cRequest := c.Request //这个值是*http.Request platform := c.Param("platform") var mchID string var mchCertificateSerialNumber string var mchAPIv3Key string var privateKeyPath string switch platform { case model.PlatformMPWEIXIN: mchID = config.ConfigData.WxPay.MchID mchCertificateSerialNumber = config.ConfigData.WxPay.MchCertificateSerialNumber mchAPIv3Key = config.ConfigData.WxPay.MchAPIv3Key privateKeyPath = "merchant/mp/apiclient_key.pem" case model.PlatformH5, model.PlatformMPH5: mchID = config.ConfigData.WxPay.MchH5ID mchCertificateSerialNumber = config.ConfigData.WxPay.MchH5CertificateSerialNumber mchAPIv3Key = config.ConfigData.WxPay.MchH5APIv3Key privateKeyPath = "merchant/mph5/apiclient_key.pem" } mchPrivateKey, err := wxutils.LoadPrivateKeyWithPath(privateKeyPath) if err != nil { log.Printf("load merchant private key error") return } // 1. 使用 `RegisterDownloaderWithPrivateKey` 注册下载器 err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, mchPrivateKey, mchCertificateSerialNumber, mchID, mchAPIv3Key) if err != nil { log.Printf("register downloader with private key error") return } // 2. 获取商户号对应的微信支付平台证书访问器 certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) // 3. 使用证书访问器初始化 `notify.Handler` handler := notify.NewNotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) transaction := new(payments.Transaction) notifyReq, err := handler.ParseNotifyRequest(context.Background(), cRequest, transaction) // 如果验签未通过,或者解密失败 if err != nil { log.Printf("parse notify request error") return } log.Printf("微信支付回调响应: %+v", notifyReq) var status string if notifyReq.Summary == "支付成功" { status = model.PayStatusSuccess } else { status = model.PayStatusPayError } err = db.DB.Model(&model.PayOrder{}).Where("out_trade_no = ?", transaction.OutTradeNo).Updates(model.PayOrder{PayStatus: status, TransactionId: *transaction.TransactionId}).Error if err != nil { log.Printf("订单回调处理错误%v", err) } c.String(http.StatusOK, "success") } func (p *Pay) RefundDetailsHTML(c *gin.Context) { encryptedOrderID := c.Param("id") // 解密订单ID decryptedOrderID, err := utils.IDDecrypt(encryptedOrderID, "njbh287yfbuyh18suygbhd98") if err != nil { log.Printf("解密订单ID(%s)失败:%v", decryptedOrderID, err) c.String(http.StatusNotFound, "") return } // 将字符串转换为 uint64 u64, err := strconv.ParseUint(decryptedOrderID, 10, 64) if err != nil { log.Printf("uint64转换失败: %v", err) c.String(http.StatusNotFound, "") return } // 将 uint64 转换为 uint orderID := uint(u64) order, err := orderService.GetOrderByid(orderID) if err != nil { log.Printf("订单(%d)获取失败:%v", orderID, err) c.String(http.StatusNotFound, "") return } type RenderOrder struct { ID string OutTradeNo string TransactionId string Userid uint CreatedAt string Product string Amount float64 PayStatus string } renderOrder := RenderOrder{ ID: encryptedOrderID, OutTradeNo: order.OutTradeNo, TransactionId: order.TransactionId, Userid: order.Userid, CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"), Product: order.Product.ProductName, Amount: float64(order.Amount) / 100, PayStatus: order.PayStatus, } c.HTML(http.StatusOK, "refund.html", renderOrder) } func (p *Pay) Refund(c *gin.Context) { encryptedOrderID := c.Param("id") // 解密订单ID decryptedOrderID, err := utils.IDDecrypt(encryptedOrderID, "njbh287yfbuyh18suygbhd98") if err != nil { log.Printf("解密订单ID(%s)失败:%v", decryptedOrderID, err) c.String(http.StatusNotFound, "") return } // 将字符串转换为 uint64 u64, err := strconv.ParseUint(decryptedOrderID, 10, 64) if err != nil { log.Printf("uint64转换失败: %v", err) c.String(http.StatusNotFound, "") return } // 将 uint64 转换为 uint orderID := uint(u64) order, err := orderService.GetOrderByid(orderID) if err != nil { log.Printf("订单(%d)获取失败:%v", orderID, err) c.String(http.StatusNotFound, "") return } var payClient *wxcore.Client switch order.Platform { case model.PlatformMPWEIXIN: payClient = global.GlobalData.PayClient case model.PlatformH5, model.PlatformMPH5: payClient = global.GlobalData.PayH5Client } outRefundNo := utils.GenerateOrderRefundNumber() svc := refunddomestic.RefundsApiService{Client: payClient} resp, result, err := svc.Create(ctx, refunddomestic.CreateRequest{ OutTradeNo: wxcore.String(order.OutTradeNo), OutRefundNo: wxcore.String(outRefundNo), //退款单号 NotifyUrl: wxcore.String(fmt.Sprintf("%s/%s", config.ConfigData.WxPay.RefundNotifyURL, order.Platform)), Amount: &refunddomestic.AmountReq{ Currency: wxcore.String("CNY"), Refund: wxcore.Int64(order.Amount), Total: wxcore.Int64(order.Amount), }, }, ) if err != nil { // 处理错误 log.Printf("微信订单申请退款错误:%s", err) response.Fail(c) } else { // 处理返回结果 log.Printf("微信订单申请退款 response status=%d resp=%s", result.Response.StatusCode, resp) order.PayStatus = model.PayStatusUnderRefund err = db.DB.Save(order).Error if err != nil { log.Printf("微信订单退款状态修改失败 underRefund Error:%v", err) } response.Ok(c) } } func (p *Pay) RefundCallback(c *gin.Context) { ctx := c //这个参数是context.Background() cRequest := c.Request //这个值是*http.Request platform := c.Param("platform") var mchID string var mchCertificateSerialNumber string var mchAPIv3Key string var privateKeyPath string switch platform { case model.PlatformMPWEIXIN: mchID = config.ConfigData.WxPay.MchID mchCertificateSerialNumber = config.ConfigData.WxPay.MchCertificateSerialNumber mchAPIv3Key = config.ConfigData.WxPay.MchAPIv3Key privateKeyPath = "merchant/mp/apiclient_key.pem" case model.PlatformH5, model.PlatformMPH5: mchID = config.ConfigData.WxPay.MchH5ID mchCertificateSerialNumber = config.ConfigData.WxPay.MchH5CertificateSerialNumber mchAPIv3Key = config.ConfigData.WxPay.MchH5APIv3Key privateKeyPath = "merchant/mph5/apiclient_key.pem" } mchPrivateKey, err := wxutils.LoadPrivateKeyWithPath(privateKeyPath) if err != nil { log.Printf("load merchant private key error") return } // 1. 使用 `RegisterDownloaderWithPrivateKey` 注册下载器 err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, mchPrivateKey, mchCertificateSerialNumber, mchID, mchAPIv3Key) if err != nil { log.Printf("register downloader with private key error") return } // 2. 获取商户号对应的微信支付平台证书访问器 certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) // 3. 使用证书访问器初始化 `notify.Handler` handler := notify.NewNotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) transaction := new(payments.Transaction) notifyReq, err := handler.ParseNotifyRequest(context.Background(), cRequest, transaction) // 如果验签未通过,或者解密失败 if err != nil { log.Printf("parse notify request error") return } log.Printf("微信退款回调响应 notifyReq: %+v", notifyReq) log.Printf("微信退款回调响应 transaction: %+v", transaction) order := model.PayOrder{} err = db.DB.Preload("Product").Where("out_trade_no = ?", transaction.OutTradeNo).First(&order).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { log.Printf("【微信退款回调响应】找不到回调相关的订单。OutTradeNo:%s", transaction.OutTradeNo) return } else { log.Printf("退款回调处理错误%v", err) } } if notifyReq.EventType == "REFUND.SUCCESS" { order.PayStatus = model.PayStatusRefund err = db.DB.Save(&order).Error if err != nil { log.Printf("退款回调处理错误%v", err) } else { notifyService.SendNotification("退款成功", order.Product.ProductName, order.Userid, order.ID) } } else if notifyReq.EventType == "REFUND.ABNORMAL" { order.PayStatus = model.PayStatusRefundError err = db.DB.Save(&order).Error if err != nil { log.Printf("退款回调处理错误%v", err) } else { notifyService.SendNotification("退款异常,请及时处理", order.Product.ProductName, order.Userid, order.ID) } } //log.Printf("微信退款回调响应: %+v", transaction) c.String(http.StatusOK, "success") } // AliPrepay 阿里支付 func (p *Pay) AliPrepay(c *gin.Context) { var reqBody request.H5PrepayReq err := c.ShouldBindJSON(&reqBody) if err != nil { response.FailWithMessage("Failed to read request body, need product", c) return } Claims, err := utils.GetClaims(c) if err != nil { log.Println("get claims error:", err) response.FailWithMessage(err.Error(), c) return } // 获取商品信息 product, err := productService.GetProduct(reqBody.ProductName) if err != nil { log.Println(err) response.FailWithMessage(err.Error(), c) return } // 生成单号 outTradeNo := fmt.Sprintf("ali_%s", utils.GenerateOrderNumber()) // 查看用户是否内部号 user, err := userService.GetUserByUserid(Claims.Userid) if err != nil { log.Println(err) response.FailWithMessage(err.Error(), c) return } var amount string var orderAmount int64 log.Printf("is inside%+v,is %t", user, user.Inside) if user.Inside { amount = "0.01" orderAmount = 1 } else { amount = utils.ConvertCentsToYuan(product.SellPrice) orderAmount = int64(product.SellPrice) } pay := alipay.TradeWapPay{} pay.NotifyURL = config.ConfigData.AliPay.NotifyURL // 替换为您的回调地址 pay.ReturnURL = fmt.Sprintf("%s%s?callback=true", config.ConfigData.Server.Domain, reqBody.Href) // 支付成功后跳转的地址 pay.Subject = product.ProductName // 订单标题 pay.OutTradeNo = outTradeNo // 生成的唯一订单号 pay.TotalAmount = amount // 支付金额,单位元 pay.ProductCode = "QUICK_WAP_PAY" // WAP支付的产品代码 payURL, err := global.GlobalData.AliPayClient.TradeWapPay(pay) if err != nil { log.Printf("【阿里支付创建】创建订单错误:%v", err) response.Fail(c) return } ctx := context.Background() expiration := 5 * time.Minute // 5分钟超时 err = db.RedisClient.Set(ctx, fmt.Sprintf("alipay_%s", outTradeNo), reqBody.QueryData, expiration).Err() if err != nil { log.Printf("【阿里支付创建】前端请求数据缓存错误:%v", err) response.Fail(c) return } var payOrder = model.PayOrder{ OutTradeNo: outTradeNo, Amount: orderAmount, Userid: Claims.Userid, PayStatus: model.PayStatusNotPay, ProductID: product.ID, Product: &product, Platform: reqBody.Platform, PaymentMethod: model.PaymentMethod_ALIPAY, } err = db.DB.Create(&payOrder).Error if err != nil { log.Printf("【阿里支付创建】订单保存错误:%v", err) response.Fail(c) return } response.OkWithData(gin.H{ "PayUrl": payURL.String(), }, c) } // AlipayCallback 阿里支付回调 func (p *Pay) AlipayCallback(c *gin.Context) { // 解析表单 err := c.Request.ParseForm() if err != nil { log.Printf("ali pay callback解析请求表单失败:%v", err) return } // DecodeNotification 内部已调用 VerifySign 方法验证签名 noti, err := global.GlobalData.AliPayClient.DecodeNotification(c.Request.Form) if err != nil { log.Printf("【阿里支付回调】通知解码失败失败:%v", err) return } log.Printf("【阿里支付回调】接收到支付宝回调,商户订单号:%s", noti.OutTradeNo) log.Printf("【阿里支付回调】接收到支付宝回调,支付宝订单号:%s", noti.OutTradeNo) log.Printf("【阿里支付回调】交易状态:%s", noti.TradeStatus) if noti.TradeStatus == alipay.TradeStatusSuccess { log.Printf("【阿里支付回调】交易成功") err = db.DB.Model(&model.PayOrder{}).Where("out_trade_no = ?", noti.OutTradeNo).Updates(model.PayOrder{PayStatus: model.PayStatusSuccess, AliTradeNo: noti.TradeNo}).Error if err != nil { log.Printf("【阿里支付回调】订单回调处理错误%v", err) } // 确认收到通知消息,不然支付宝后续会继续推送相同的消息 alipay.ACKNotification(c.Writer) } } func (p *Pay) GetQueryCache(c *gin.Context) { var reqBody request.QueryDataReq err := c.ShouldBindQuery(&reqBody) if err != nil { response.FailWithMessage("Failed to read request body, need out_trade_no", c) return } ctx := context.Background() key := fmt.Sprintf("alipay_%s", reqBody.OutTradeNo) result, err := db.RedisClient.Get(ctx, key).Result() if errors.Is(err, redis.Nil) { response.FailWithMessage("超时,请重新手动输入数据查询", c) return } else if err != nil { log.Printf("【阿里支付】获取缓存表单数据错误%v", err) response.FailWithMessage("请重新手动输入数据", c) return } response.OkWithData(result, c) } func (p *Pay) WxPayComplaintCallback(c *gin.Context) { ctx := context.Background() cRequest := c.Request platform := c.Param("platform") var mchID string var mchCertificateSerialNumber string var mchAPIv3Key string var privateKeyPath string var platformString string switch platform { case model.PlatformMPWEIXIN: platformString = "微信小程序" mchID = config.ConfigData.WxPay.MchID mchCertificateSerialNumber = config.ConfigData.WxPay.MchCertificateSerialNumber mchAPIv3Key = config.ConfigData.WxPay.MchAPIv3Key privateKeyPath = "merchant/mp/apiclient_key.pem" case model.PlatformH5, model.PlatformMPH5: platformString = "公众号" mchID = config.ConfigData.WxPay.MchH5ID mchCertificateSerialNumber = config.ConfigData.WxPay.MchH5CertificateSerialNumber mchAPIv3Key = config.ConfigData.WxPay.MchH5APIv3Key privateKeyPath = "merchant/mph5/apiclient_key.pem" } notifyService.SendComplaintNotification(fmt.Sprintf("%s投诉", platformString), "", platformString, "") mchPrivateKey, err := wxutils.LoadPrivateKeyWithPath(privateKeyPath) if err != nil { log.Printf("【%s投诉通知回调】加载商户私钥失败: %v", platformString, err) return } // 注册下载器 err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, mchPrivateKey, mchCertificateSerialNumber, mchID, mchAPIv3Key) if err != nil { log.Printf("【%s投诉通知回调】注册下载器失败: %v", platformString, err) return } // 获取微信支付平台证书访问器 certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) // 初始化 notify.Handler handler := notify.NewNotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) // 解析并验证通知请求 content := make(map[string]interface{}) notifyReq, err := handler.ParseNotifyRequest(ctx, cRequest, &content) if err != nil { log.Printf("【%s投诉通知回调】解析通知请求失败: %v", platformString, err) return } // 打印通知摘要和解析后的内容 log.Printf("【%s投诉通知回调】通知信息: %s", platformString, notifyReq.Summary) log.Printf("【%s投诉通知回调】通知数据: %s", platformString, content) // 根据解析后的数据处理通知 actionType, ok := content["action_type"].(string) if !ok { log.Printf("【%s投诉通知回调】通知内容中缺少action_type字段", platformString) return } complaintID, ok := content["complaint_id"].(string) if !ok { log.Printf("【%s投诉通知回调】通知内容中缺少complaint_id字段", platformString) return } switch actionType { case "CREATE_COMPLAINT": message := "用户提交了新的投诉" notifyService.SendComplaintNotification(message, "新投诉", platformString, complaintID) case "CONTINUE_COMPLAINT": message := "用户继续投诉" notifyService.SendComplaintNotification(message, "继续投诉", platformString, complaintID) case "USER_RESPONSE": message := "用户在投诉单中添加了新留言" notifyService.SendComplaintNotification(message, "用户留言", platformString, complaintID) case "RESPONSE_BY_PLATFORM": message := "平台在投诉单中添加了新留言" notifyService.SendComplaintNotification(message, "平台留言", platformString, complaintID) case "SELLER_REFUND": message := "商户发起了全额退款" notifyService.SendComplaintNotification(message, "全额退款", platformString, complaintID) case "MERCHANT_RESPONSE": message := "商户对投诉进行了回复" notifyService.SendComplaintNotification(message, "商户回复", platformString, complaintID) case "MERCHANT_CONFIRM_COMPLETE": message := "商户标记投诉处理完成" notifyService.SendComplaintNotification(message, "投诉处理完成", platformString, complaintID) case "MERCHANT_APPROVE_REFUND": message := "商户同意了退款" notifyService.SendComplaintNotification(message, "同意退款", platformString, complaintID) case "MERCHANT_REJECT_REFUND": message := "商户拒绝了退款" notifyService.SendComplaintNotification(message, "拒绝退款", platformString, complaintID) case "REFUND_SUCCESS": message := "退款已成功到账" notifyService.SendComplaintNotification(message, "退款到账", platformString, complaintID) default: message := "未知投诉类型" notifyService.SendComplaintNotification(message, "未知投诉类型", platformString, complaintID) log.Printf("【%s投诉通知回调】收到未知类型的投诉通知: %s", platformString, actionType) } // 返回成功响应 c.String(http.StatusOK, "success") }