diff --git a/controller/game_card.go b/controller/game_card.go index 55808d9..ea516e3 100644 --- a/controller/game_card.go +++ b/controller/game_card.go @@ -3382,9 +3382,9 @@ type WxPayRefundPlaintext struct { UserReceivedAccount string `json:"user_received_account"` } -// 0 元购 微信推送支付通知 +// PushWXPayRefundNotice 微信退款消息通知(明慧) func PushWXPayRefundNotice(c *gin.Context) { - fmt.Println("微信推送支付通知") + fmt.Println("微信推送退款通知") //body, err := ioutil.ReadAll(c.Request.Body) //if err != nil { // logger.Error(err) @@ -3393,11 +3393,103 @@ func PushWXPayRefundNotice(c *gin.Context) { mchID := "1609877389" mchAPIv3Key := "DeovoMingHuiRengTianTang45675123" // 商户APIv3密钥 mchCertificateSerialNumber := "7540301D8FD52CCF7D6267DCF7CD2BC0AB467EFF" // 商户证书序列号 - mchPrivateKey, err := wechatpayutils.LoadPrivateKeyWithPath("./configs/merchant/apiclient_key.pem") if err != nil { log.Print("load merchant private key error") } + + ctx := context.Background() + // 1. 使用 `RegisterDownloaderWithPrivateKey` 注册下载器 + err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, mchPrivateKey, mchCertificateSerialNumber, mchID, mchAPIv3Key) + if err != nil { + fmt.Println(err) + return + } + // 2. 获取商户号对应的微信支付平台证书访问器 + certVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) + // 3. 使用证书访问器初始化 `notify.Handler` + handler := notify.NewNotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certVisitor)) + + transaction := new(payments.Transaction) + notifyReq, err := handler.ParseNotifyRequest(context.Background(), c.Request, transaction) + // 如果验签未通过,或者解密失败 + if err != nil { + fmt.Println(err) + return + } + // 处理通知内容 + //fmt.Println(notifyReq.Summary) + //fmt.Println(transaction.TransactionId) + + //transactionJson, _ := json.Marshal(transaction) + //fmt.Println("transactionJson:", string(transactionJson)) + //notifyReqJson, _ := json.Marshal(notifyReq) + //fmt.Println("notifyReqJson:", string(notifyReqJson)) + if notifyReq.EventType == "REFUND.SUCCESS" { + plaintext := new(WxPayRefundPlaintext) + err = json.Unmarshal([]byte(notifyReq.Resource.Plaintext), plaintext) + if err != nil { + logger.Error("unmarshal plaintext err:", err) + return + } + count, err := model.NewFundRecordQuerySet(model.DB).RefundIdEq(plaintext.RefundId).Count() + if err != nil { + logger.Error("count refund id err:", err) + return + } + plaintextJson, _ := json.Marshal(plaintext) + fmt.Println("plaintextJson:", string(plaintextJson)) + + if count == 0 { + openMemberRecord := new(model.UserOpenMemberRecord) + err = model.NewUserOpenMemberRecordQuerySet(model.DB).OpenNoEq(plaintext.OutRefundNo).One(openMemberRecord) + if err != nil { + logger.Error("user open member record err:", err) + return + } + //fundType := model.FundTypeExpressFeeRefund + //if openMemberRecord.OrderType == 6 { + // fundType = model.FundTypeBuyGoodsRefund + //} + fundRecord := &model.FundRecord{ + Uid: openMemberRecord.Uid, + FundType: GetFundRecordFundType(openMemberRecord.OrderType), + Amount: int64(plaintext.Amount.Refund) * (-1), + TransactionId: plaintext.TransactionId, + OutTradeNo: plaintext.OutTradeNo, + RefundId: plaintext.RefundId, + Status: 2, + Remark: GetFundRecordRemark(openMemberRecord.OrderType), + } + err = model.DB.Create(fundRecord).Error + if err != nil { + logger.Error("create fund record err:", err) + return + } + } + } + + RespNotice(c, "SUCCESS", "成功") + return + //logger.Error("xml Request.Body1:", string(body)) +} + +// DwPushWXPayRefundNotice 微信退款消息通知(迪为) +func DwPushWXPayRefundNotice(c *gin.Context) { + fmt.Println("微信推送退款通知") + //body, err := ioutil.ReadAll(c.Request.Body) + //if err != nil { + // logger.Error(err) + //} + + mchID := "1494954322" + mchAPIv3Key := "hTCTqF9jHsWlFOO8ZuL05BDo2UlrJwVv" // 商户APIv3密钥 + mchCertificateSerialNumber := "5D98B7F99C24BFD8649E2045635AFBBCDD5B29C1" // 商户证书序列号 + mchPrivateKey, err := wechatpayutils.LoadPrivateKeyWithPath("./configs/dw_merchant/apiclient_key.pem") + if err != nil { + log.Print("load merchant private key error") + } + ctx := context.Background() // 1. 使用 `RegisterDownloaderWithPrivateKey` 注册下载器 err = downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx, mchPrivateKey, mchCertificateSerialNumber, mchID, mchAPIv3Key) diff --git a/controller/mall.go b/controller/mall.go index 57fa7ec..d832ce8 100644 --- a/controller/mall.go +++ b/controller/mall.go @@ -481,7 +481,7 @@ func MallOrderCreate(c *gin.Context) { RespOK(c, ret) } else { // 其他则默认明慧账户收费 - webPay, err := wxpay.HmJsPayUnifiedOrderForBuyGoods(order.SerialNo, order.Rm, user.WxOpenID, configInfo.NotifyUrl) + webPay, err := wxpay.WebPay(order.SerialNo, order.Rm, user.WxOpenID, "N", wxpay.WxPayBuyGoods, configInfo.NotifyUrl, false) if err != nil { logger.Error(errors.New("WebPay err")) RespJson(c, status.InternalServerError, nil) @@ -495,6 +495,21 @@ func MallOrderCreate(c *gin.Context) { } RespOK(c, ret) + + //webPay, err := wxpay.HmJsPayUnifiedOrderForBuyGoods(order.SerialNo, order.Rm, user.WxOpenID, configInfo.NotifyUrl) + //if err != nil { + // logger.Error(errors.New("WebPay err")) + // RespJson(c, status.InternalServerError, nil) + // return + //} + // + //ret := map[string]interface{}{ + // "web_pay": webPay, + // "order_id": order.ID, + // "order": order, + //} + // + //RespOK(c, ret) } return @@ -643,6 +658,7 @@ func MallOrderRefund(c *gin.Context) { RespJson(c, status.InternalServerError, nil) return } + if goodsOrder.Amount == 0 && goodsOrder.DeliverStoreId == 0 { // 新机预售券 // 查询新机预售券状态,只有未使用才能退 var newMachineCoupon model.UserCoupon @@ -697,6 +713,7 @@ func MallOrderRefund(c *gin.Context) { RespJson(c, status.InternalServerError, nil) return } + RespOK(c, goodsOrder) return } diff --git a/lib/wxpay/wx_pay.go b/lib/wxpay/wx_pay.go index 147f953..7277a8e 100644 --- a/lib/wxpay/wx_pay.go +++ b/lib/wxpay/wx_pay.go @@ -27,7 +27,6 @@ import ( mathrand "math/rand" "mh-server/config" "mh-server/lib/utils" - "net/http" "sort" "strconv" @@ -70,7 +69,7 @@ const ( TestHmPubKeySwitchPubFp = "/Users/max/Documents/code/deovo/mh_server/pack/configs/hm_pay/switch_pub_private_key.pem" ) -// web 微信支付 +// WebPay web 微信支付 func WebPay(orderId string, totalFee uint32, openId, profitSharing, attach, notifyUrl string, flag bool) (*Sextuple, error) { now := time.Now().Local() strTime := fmt.Sprintf("%04d%02d%02d%02d%02d%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) @@ -176,9 +175,69 @@ func WebPay(orderId string, totalFee uint32, openId, profitSharing, attach, noti return &sextuple, nil } +// WechatRefund 微信退款 +func WechatRefund(orderId, refundId string, totalFee uint32, attach, notifyUrl string, flag bool) (*RefundResp, error) { + if notifyUrl == "" { + logger.Error("NotifyUrl is null") + return nil, errors.New("NotifyUrl is null") + } + + fmt.Println("MchId:", config.AppConfig.WxMchID) + fmt.Println("AppId:", config.AppConfig.WxAppId) + fmt.Println("MchSecret:", config.AppConfig.WxMchSecret) + + nonce := utils.GenRandStr(NonceStringLength) + refundReq := RefundReq{ + AppId: config.AppConfig.WxAppId, + MchId: config.AppConfig.WxMchID, + NonceStr: nonce, + Sign: "", + SignType: "MD5", + OutTradeNo: orderId, + OutRefundNo: refundId, + TotalFee: int(totalFee), + RefundFee: int(totalFee), + RefundFeeType: "CNY", + RefundDesc: attach, + NotifyUrl: notifyUrl, + } + if flag { + refundReq.MchId = config.AppConfig.WxDwMchID + } + + fmt.Println("OutTradeNo:", refundReq.OutTradeNo) + + m, err := struct2Map(refundReq) + if err != nil { + logger.Error(err) + return nil, err + } + + payKey := config.AppConfig.WxMchSecret + if flag { + payKey = config.AppConfig.WxDwMchSecret + } + + sign, err := GenWxPaySign(m, payKey) + if err != nil { + logger.Error(err) + return nil, err + } + refundReq.Sign = strings.ToUpper(sign) + + refundResp, err := WxRefund(refundReq) + if err != nil { + logger.Errorf("WxUnifiedOrder unified order error %#v", err) + return nil, err + } + + return &refundResp, nil +} + const ( NonceStringLength = 32 UnifiedOrderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder" + RefundUrl = "https://api.mch.weixin.qq.com/secapi/pay/refund" ) type ( @@ -312,6 +371,57 @@ type ( Timestamp string `json:"timestamp,omitempty"` Sign string `json:"sign,omitempty"` } + + RefundReq struct { + AppId string `xml:"appid" json:"appid"` //微信分配的小程序ID,必须 + MchId string `xml:"mch_id" json:"mch_id"` //微信支付分配的商户号,必须 + NonceStr string `xml:"nonce_str" json:"nonce_str"` //随机字符串,必须 + Sign string `xml:"sign" json:"sign"` //签名,必须 + SignType string `xml:"sign_type" json:"sign_type"` //"HMAC-SHA256"或者"MD5",非必须,默认MD5 + TransactionId string `xml:"transaction_id" json:"transaction_id"` //微信支付订单号 + OutTradeNo string `xml:"out_trade_no" json:"out_trade_no"` //商户系统内部订单号,transaction_id、out_trade_no二选一 + OutRefundNo string `xml:"out_refund_no" json:"out_refund_no"` //商户退款单号,必须 + TotalFee int `xml:"total_fee" json:"total_fee"` //订单金额,单位分,必须 + RefundFee int `xml:"refund_fee" json:"refund_fee"` //退款金额,单位分,必须 + RefundFeeType string `xml:"refund_fee_type" json:"refund_fee_type"` //退款货币种类,非必须 + RefundDesc string `xml:"refund_desc" json:"refund_desc"` //退款原因,非必须 + RefundAccount string `xml:"refund_account" json:"refund_account"` //退款资金来源,非必须 + NotifyUrl string `xml:"notify_url" json:"notify_url"` //退款结果通知url,非必须 + } + + RefundResp struct { + ReturnCode string `xml:"return_code"` + ReturnMsg string `xml:"return_msg"` + ResultCode string `xml:"result_code"` + ErrCode string `xml:"err_code"` + ErrCodeDes string `xml:"err_code_des"` + AppId string `xml:"appid"` //微信分配的小程序ID,必须 + MchId string `xml:"mch_id"` //微信支付分配的商户号,必须 + NonceStr string `xml:"nonce_str"` //随机字符串,必须 + Sign string `xml:"sign"` //签名,必须 + TransactionId string `xml:"transaction_id"` //微信支付订单号,必须 + OutTradeNo string `xml:"out_trade_no"` //商户订单号,必须 + OutRefundNo string `xml:"out_refund_no"` //商户退款单号,必须 + RefundId string `xml:"refund_id"` //微信退款单号,必须 + RefundFee int `xml:"refund_fee"` //退款金额,单位分,必须 + SettlementRefundFee int `xml:"settlement_refund_fee"` //应结退款金额,非必须 + TotalFee int `xml:"total_fee"` //标价金额,单位分,必须 + SettlementTotalFee int `xml:"settlement_total_fee"` //应结订单金额,非必须 + FeeType string `xml:"fee_type,omitempty"` // 标价币种 + CashFee int `xml:"cash_fee"` // 现金支付金额 + CashFeeType string `xml:"cash_fee_type,omitempty"` // 现金支付币种 + CashRefundFee int `xml:"cash_refund_fee,omitempty"` // 现金退款金额 + CouponRefundFee int `xml:"coupon_refund_fee,omitempty"` // 代金券退款总金额 + CouponRefundCount int `xml:"coupon_refund_count,omitempty"` // 退款代金券使用数量 + Coupons []RefundCoupon `xml:"coupons,omitempty"` // 代金券明细 + } + + // RefundCoupon 表示单个代金券的退款明细(可多个) + RefundCoupon struct { + CouponType string `xml:"coupon_type"` // 代金券类型:CASH/NO_CASH + CouponID string `xml:"coupon_refund_id"` // 退款代金券ID + CouponAmount int `xml:"coupon_refund_fee"` // 单个代金券退款金额 + } ) type T struct { @@ -543,6 +653,51 @@ func WxUnifiedOrder(r UnifiedOrderReq) (UnifiedOrderResp, error) { return payResp, nil } +func WxRefund(r RefundReq) (RefundResp, error) { + var rResp RefundResp + + data, err := xml.Marshal(r) + if err != nil { + logger.Error(err) + return rResp, err + } + + logger.Error("xml:", string(data)) + client := http.Client{} + req, err := http.NewRequest("POST", RefundUrl, bytes.NewBuffer(data)) + if err != nil { + logger.Error(err) + return rResp, err + } + req.Header.Set("Content-Type", "application/xml; charset=utf-8") + resp, err := client.Do(req) + if err != nil { + logger.Error(err) + return rResp, err + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + logger.Error(err) + return rResp, err + } + fmt.Println("body:", string(body)) + + defer resp.Body.Close() + + err = xml.Unmarshal(body, &rResp) + if err != nil { + logger.Error(err) + return rResp, err + } + + if rResp.ReturnCode != "SUCCESS" { + return rResp, errors.New(rResp.ReturnMsg) + } + + return rResp, nil +} + func WxPayTransactionOrderClose(outTradeNo, mchid string) error { // url := fmt.Sprintf("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close", outTradeNo) // para := map[string]interface{}{ diff --git a/model/user.go b/model/user.go index 97094a3..7e0d44f 100644 --- a/model/user.go +++ b/model/user.go @@ -7,6 +7,7 @@ import ( "github.com/codinl/go-logger" "github.com/jinzhu/gorm" "github.com/rs/zerolog/log" + "math/rand" "mh-server/lib/utils" "mh-server/lib/wxpay" "sort" @@ -884,6 +885,23 @@ func (m *UserInviteListReq) InviteUserList() (*UserInviteListResp, error) { return resp, nil } +// GenerateRefundOrderNo 生成退款订单号(例如:R202505271545301234567890) +func GenerateRefundOrderNo() string { + // 设置随机种子 + rand.Seed(time.Now().UnixNano()) + + // 当前时间:年月日时分秒(14位) + timestamp := time.Now().Format("20060102150405") + + // 生成 10 位随机数:范围是 1000000000 ~ 9999999999 + randomNum := rand.Int63n(9000000000) + 1000000000 // int63 支持更大范围 + + // 拼接最终退款订单号 + refundOrderNo := fmt.Sprintf("R%s%d", timestamp, randomNum) + + return refundOrderNo +} + func GetOrderSn() string { var orderSn string for { diff --git a/router/router_app.go b/router/router_app.go index 8da4a9b..b1da49a 100644 --- a/router/router_app.go +++ b/router/router_app.go @@ -32,12 +32,13 @@ func ConfigAppRouter(r gin.IRouter) { // //api.POST("upload_user_info", controller.UploadUserInfo) // 上传用户信息 //api.POST("wxpay/notice", controller.PushWXPayNotice) // 微信推送支付通知 // TODO两边都改 - api.GET("wxpay/notice", controller.HmPushWXPayNotice) // 河马付推送支付通知 - api.POST("wxpay/notice", controller.PushWXPayNotice) // 微信推送支付通知 - api.POST("wxpay_refund/notice", controller.PushWXPayRefundNotice) // 微信推送支付退款通知 - api.POST("aliyun/sts_token", controller.AliyunStsTokenGet) // 阿里云上传图片token - api.POST("auto_reply/focus", controller.AutoReplyFocusMsg) // 自动回复 - api.GET("auto_reply/focus", controller.CustomerServiceMessageCheck) // 客服校验 + api.GET("wxpay/notice", controller.HmPushWXPayNotice) // 河马付推送支付通知 + api.POST("wxpay/notice", controller.PushWXPayNotice) // 微信推送支付通知 + api.POST("wxpay_refund/notice", controller.PushWXPayRefundNotice) // 微信推送支付退款通知(明慧) + api.POST("wxpay_refund/dw/notice", controller.DwPushWXPayRefundNotice) // 微信推送支付退款通知(迪为) + api.POST("aliyun/sts_token", controller.AliyunStsTokenGet) // 阿里云上传图片token + api.POST("auto_reply/focus", controller.AutoReplyFocusMsg) // 自动回复 + api.GET("auto_reply/focus", controller.CustomerServiceMessageCheck) // 客服校验 // api.GET("wx_cs/message", controller.CustomerServiceMessageCheck) // 客服校验 // api.POST("wx_cs/message", controller.CustomerServiceMessage) // 客服