|
|
|
|
package wechat
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"encoding/pem"
|
|
|
|
|
"encoding/xml"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
"golang.org/x/crypto/pkcs12"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"recook/internal/domain"
|
|
|
|
|
"recook/internal/v2/model/recook/after"
|
|
|
|
|
"recook/tools"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
AppID = "wx21724a42aebe20cc"
|
|
|
|
|
APIKey = "83f8932eb742257316e3168ba9e920dk"
|
|
|
|
|
MchID = "1545449631"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
PayCallbackUrl = domain.GetName() + "/api/v1/pay/wxpay/callback"
|
|
|
|
|
RefundCallbackUrl = domain.GetName() + "/api/v1/pay/wxpay/refund/callback"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
CreateOrderURL = "https://api.mch.weixin.qq.com/pay/unifiedorder" // 统一下单
|
|
|
|
|
OrderQueryURL = "https://api.mch.weixin.qq.com/pay/orderquery" // 订单查询
|
|
|
|
|
RefundURL = "https://api.mch.weixin.qq.com/secapi/pay/refund" // 退款
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type RefundParam struct {
|
|
|
|
|
XMLName xml.Name `xml:"xml"`
|
|
|
|
|
AppID string `xml:"appid"`
|
|
|
|
|
MchId string `xml:"mch_id"` // 商户号
|
|
|
|
|
NonceStr string `xml:"nonce_str"` // 随机字符串
|
|
|
|
|
OutRefundNo string `xml:"out_refund_no"` // 商户系统内部的退款单号,商户系统内部唯一,同一退款单号多次请求只退一笔。
|
|
|
|
|
OutTradeNo string `xml:"out_trade_no" ` // 商户订单号
|
|
|
|
|
TotalFee uint `xml:"total_fee"`
|
|
|
|
|
RefundFee uint `xml:"refund_fee"`
|
|
|
|
|
NotifyUrl string `xml:"notify_url"` // 接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。
|
|
|
|
|
Sign string `xml:"sign"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *RefundParam) GenerateSign() {
|
|
|
|
|
signStr := "appid=" + AppID
|
|
|
|
|
signStr = signStr + "&" + "mch_id=" + MchID
|
|
|
|
|
signStr = signStr + "&" + "nonce_str=" + r.NonceStr
|
|
|
|
|
signStr = signStr + "&" + "notify_url=" + r.NotifyUrl
|
|
|
|
|
signStr = signStr + "&" + "out_refund_no=" + r.OutRefundNo
|
|
|
|
|
signStr = signStr + "&" + "out_trade_no=" + r.OutTradeNo
|
|
|
|
|
signStr = signStr + "&" + "refund_fee=" + fmt.Sprintf("%d", r.RefundFee)
|
|
|
|
|
signStr = signStr + "&" + "total_fee=" + fmt.Sprintf("%d", r.TotalFee)
|
|
|
|
|
signStr = signStr + "&key=" + APIKey
|
|
|
|
|
r.Sign = strings.ToUpper(tools.MD5(signStr))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*=========================================================================================================
|
|
|
|
|
<xml>
|
|
|
|
|
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
|
|
|
|
|
<mch_id><![CDATA[10000100]]></mch_id>
|
|
|
|
|
<nonce_str><![CDATA[TeqClE3i0mvn3DrK]]></nonce_str>
|
|
|
|
|
<out_refund_no_0><![CDATA[1415701182]]></out_refund_no_0>
|
|
|
|
|
<out_trade_no><![CDATA[1415757673]]></out_trade_no>
|
|
|
|
|
<refund_count>1</refund_count>
|
|
|
|
|
<refund_fee_0>1</refund_fee_0>
|
|
|
|
|
<refund_id_0><![CDATA[2008450740201411110000174436]]></refund_id_0>
|
|
|
|
|
<refund_status_0><![CDATA[PROCESSING]]></refund_status_0>
|
|
|
|
|
<result_code><![CDATA[SUCCESS]]></result_code>
|
|
|
|
|
<return_code><![CDATA[SUCCESS]]></return_code>
|
|
|
|
|
<return_msg><![CDATA[OK]]></return_msg>
|
|
|
|
|
<sign><![CDATA[1F2841558E233C33ABA71A961D27561C]]></sign>
|
|
|
|
|
<transaction_id><![CDATA[1008450740201411110005820873]]></transaction_id>
|
|
|
|
|
</xml>
|
|
|
|
|
*/
|
|
|
|
|
type RefundQueryResult struct {
|
|
|
|
|
XMLName xml.Name `xml:"xml"`
|
|
|
|
|
ReturnCode string `xml:"return_code"`
|
|
|
|
|
ResultCode string `xml:"result_code"`
|
|
|
|
|
AppID string `xml:"appid"`
|
|
|
|
|
MchId string `xml:"mch_id"`
|
|
|
|
|
TransactionID string `xml:"transaction_id"` // 微信支付订单号
|
|
|
|
|
OutTradeNo string `xml:"out_trade_no" ` // 商户订单号
|
|
|
|
|
TotalRefundCount uint `xml:"total_refund_count"` // 订单总共已发生的部分退款次数,当请求参数传入offset后有返回
|
|
|
|
|
RefundCount uint `xml:"refund_count"` // 当前返回退款笔数
|
|
|
|
|
TotalFee uint `xml:"total_fee"`
|
|
|
|
|
RefundFee uint `xml:"refund_fee"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *RefundQueryResult) IsOk() bool {
|
|
|
|
|
return r.ReturnCode == "SUCCESS" && r.ResultCode == "SUCCESS"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Refund(asGoods *after.RecookAfterSalesGoodsModel) error {
|
|
|
|
|
param := RefundParam{
|
|
|
|
|
AppID: AppID,
|
|
|
|
|
MchId: MchID,
|
|
|
|
|
NonceStr: tools.Token(),
|
|
|
|
|
OutTradeNo: asGoods.TradeNo,
|
|
|
|
|
OutRefundNo: asGoods.RefundNo,
|
|
|
|
|
TotalFee: uint(asGoods.OrderTotalAmount.Mul(decimal.NewFromInt(100)).IntPart()),
|
|
|
|
|
RefundFee: uint(asGoods.RefundAmount.Mul(decimal.NewFromInt(100)).IntPart()),
|
|
|
|
|
NotifyUrl: RefundCallbackUrl,
|
|
|
|
|
}
|
|
|
|
|
param.GenerateSign()
|
|
|
|
|
return requestOrderRefund(¶m)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requestOrderRefund(param *RefundParam) error {
|
|
|
|
|
xmlStr, err := xml.MarshalIndent(param, "", "\t")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
config := &tls.Config{
|
|
|
|
|
Certificates: []tls.Certificate{pkcs12ToPem()},
|
|
|
|
|
}
|
|
|
|
|
transport := &http.Transport{
|
|
|
|
|
TLSClientConfig: config,
|
|
|
|
|
DisableCompression: true,
|
|
|
|
|
}
|
|
|
|
|
client := &http.Client{Transport: transport}
|
|
|
|
|
response, err := client.Post(RefundURL, "application/xml; charset=utf-8", strings.NewReader(string(xmlStr)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer func() { _ = response.Body.Close() }()
|
|
|
|
|
result, err := ioutil.ReadAll(response.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Println(string(result))
|
|
|
|
|
log.Println(err)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var r RefundQueryResult
|
|
|
|
|
err = xml.Unmarshal(result, &r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Println(string(result))
|
|
|
|
|
log.Println(err)
|
|
|
|
|
log.Println(r)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if r.IsOk() == false {
|
|
|
|
|
log.Println(string(xmlStr))
|
|
|
|
|
log.Println("========================================")
|
|
|
|
|
log.Println(string(result))
|
|
|
|
|
return errors.New("微信退款请求结果不对")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var once sync.Once
|
|
|
|
|
var wxRefundCert tls.Certificate
|
|
|
|
|
|
|
|
|
|
func pkcs12ToPem() tls.Certificate {
|
|
|
|
|
once.Do(func() {
|
|
|
|
|
path := "./credentials/wxpay/refund_apiclient_cert.p12"
|
|
|
|
|
certData, _ := ioutil.ReadFile(path)
|
|
|
|
|
|
|
|
|
|
blocks, err := pkcs12.ToPEM(certData, MchID)
|
|
|
|
|
|
|
|
|
|
var pemData []byte
|
|
|
|
|
for _, b := range blocks {
|
|
|
|
|
pemData = append(pemData, pem.EncodeToMemory(b)...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cert, err := tls.X509KeyPair(pemData, pemData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wxRefundCert = cert
|
|
|
|
|
})
|
|
|
|
|
return wxRefundCert
|
|
|
|
|
}
|