Browse Source

微信支付 - 退款通知 (#359)

* 🆕 发起退款 - 添加notify_url参数

* 🆕 退款通知
LouGaZen 5 years ago
parent
commit
d5a67eaf29
4 changed files with 213 additions and 3 deletions
  1. 77 0
      pay/notify/refund.go
  2. 26 0
      pay/notify/refund_test.go
  3. 7 3
      pay/refund/refund.go
  4. 103 0
      util/crypto.go

+ 77 - 0
pay/notify/refund.go

@@ -0,0 +1,77 @@
+package notify
+
+import (
+	"crypto/md5"
+	"encoding/base64"
+	"encoding/hex"
+	"encoding/xml"
+	"errors"
+
+	"github.com/silenceper/wechat/v2/util"
+)
+
+// reference: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10
+
+// RefundedResult 退款回调
+type RefundedResult struct {
+	ReturnCode *string `xml:"return_code"`
+	ReturnMsg  *string `xml:"return_msg"`
+
+	AppID    *string `xml:"appid"`
+	MchID    *string `xml:"mch_id"`
+	NonceStr *string `xml:"nonce_str"`
+	ReqInfo  *string `xml:"req_info"`
+}
+
+// RefundedReqInfo 退款结果(明文)
+type RefundedReqInfo struct {
+	TransactionID       *string `xml:"transaction_id"`
+	OutTradeNO          *string `xml:"out_trade_no"`
+	RefundID            *string `xml:"refund_id"`
+	OutRefundNO         *string `xml:"out_refund_no"`
+	TotalFee            *int    `xml:"total_fee"`
+	SettlementTotalFee  *int    `xml:"settlement_total_fee"`
+	RefundFee           *int    `xml:"refund_fee"`
+	SettlementRefundFee *int    `xml:"settlement_refund_fee"`
+	RefundStatus        *string `xml:"refund_status"`
+	SuccessTime         *string `xml:"success_time"`
+	RefundRecvAccount   *string `xml:"refund_recv_account"`
+	RefundAccount       *string `xml:"refund_account"`
+	RefundRequestSource *string `xml:"refund_request_source"`
+}
+
+// RefundedResp 消息通知返回
+type RefundedResp struct {
+	ReturnCode string `xml:"return_code"`
+	ReturnMsg  string `xml:"return_msg"`
+}
+
+// DecryptReqInfo 对退款结果进行解密
+func (notify *Notify) DecryptReqInfo(result *RefundedResult) (*RefundedReqInfo, error) {
+	var err error
+	if result == nil || result.ReqInfo == nil {
+		return nil, errors.New("empty refunded_result or req_info")
+	}
+
+	base64Decode, err := base64.StdEncoding.DecodeString(*result.ReqInfo)
+	if err != nil {
+		return nil, err
+	}
+
+	hash := md5.New()
+	if _, err = hash.Write([]byte(notify.Key)); err != nil {
+		return nil, err
+	}
+	md5APIKey := hex.EncodeToString(hash.Sum(nil))
+
+	data, err := util.AesECBDecrypt(base64Decode, []byte(md5APIKey))
+	if err != nil {
+		return nil, err
+	}
+
+	res := &RefundedReqInfo{}
+	if err = xml.Unmarshal(data, res); err != nil {
+		return nil, err
+	}
+	return res, nil
+}

File diff suppressed because it is too large
+ 26 - 0
pay/notify/refund_test.go


+ 7 - 3
pay/refund/refund.go

@@ -29,6 +29,7 @@ type Params struct {
 	RefundFee     string
 	RefundDesc    string
 	RootCa        string //ca证书
+	NotifyURL     string
 }
 
 //request 接口请求参数
@@ -43,7 +44,7 @@ type request struct {
 	TotalFee      string `xml:"total_fee"`
 	RefundFee     string `xml:"refund_fee"`
 	RefundDesc    string `xml:"refund_desc,omitempty"`
-	//NotifyUrl     string `xml:"notify_url,omitempty"`
+	NotifyURL     string `xml:"notify_url,omitempty"`
 }
 
 //Response 接口返回
@@ -83,13 +84,16 @@ func (refund *Refund) Refund(p *Params) (rsp Response, err error) {
 	param["total_fee"] = p.TotalFee
 	param["sign_type"] = util.SignTypeMD5
 	param["transaction_id"] = p.TransactionID
+	if p.NotifyURL != "" {
+		param["notify_url"] = p.NotifyURL
+	}
 
 	sign, err := util.ParamSign(param, refund.Key)
 	if err != nil {
 		return
 	}
 
-	request := request{
+	req := request{
 		AppID:         refund.AppID,
 		MchID:         refund.MchID,
 		NonceStr:      nonceStr,
@@ -101,7 +105,7 @@ func (refund *Refund) Refund(p *Params) (rsp Response, err error) {
 		RefundFee:     p.RefundFee,
 		RefundDesc:    p.RefundDesc,
 	}
-	rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, refund.MchID)
+	rawRet, err := util.PostXMLWithTLS(refundGateway, req, p.RootCa, refund.MchID)
 	if err != nil {
 		return
 	}

+ 103 - 0
util/crypto.go

@@ -1,6 +1,7 @@
 package util
 
 import (
+	"bytes"
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/hmac"
@@ -227,3 +228,105 @@ func ParamSign(p map[string]string, key string) (string, error) {
 
 	return CalculateSign(str, signType, key)
 }
+
+// ECB provides confidentiality by assigning a fixed ciphertext block to each plaintext block.
+// See NIST SP 800-38A, pp 08-09
+// reference: https://codereview.appspot.com/7860047/patch/23001/24001
+type ecb struct {
+	b         cipher.Block
+	blockSize int
+}
+
+func newECB(b cipher.Block) *ecb {
+	return &ecb{
+		b:         b,
+		blockSize: b.BlockSize(),
+	}
+}
+
+// ECBEncryptor -
+type ECBEncryptor ecb
+
+// NewECBEncryptor returns a BlockMode which encrypts in electronic code book mode, using the given Block.
+func NewECBEncryptor(b cipher.Block) cipher.BlockMode {
+	return (*ECBEncryptor)(newECB(b))
+}
+
+// BlockSize implement BlockMode.BlockSize
+func (x *ECBEncryptor) BlockSize() int {
+	return x.blockSize
+}
+
+// CryptBlocks implement BlockMode.CryptBlocks
+func (x *ECBEncryptor) CryptBlocks(dst, src []byte) {
+	if len(src)%x.blockSize != 0 {
+		panic("crypto/cipher: input not full blocks")
+	}
+	if len(dst) < len(src) {
+		panic("crypto/cipher: output smaller than input")
+	}
+	for len(src) > 0 {
+		x.b.Encrypt(dst, src[:x.blockSize])
+		src = src[x.blockSize:]
+		dst = dst[x.blockSize:]
+	}
+}
+
+// ECBDecryptor -
+type ECBDecryptor ecb
+
+// NewECBDecryptor returns a BlockMode which decrypts in electronic code book mode, using the given Block.
+func NewECBDecryptor(b cipher.Block) cipher.BlockMode {
+	return (*ECBDecryptor)(newECB(b))
+}
+
+// BlockSize implement BlockMode.BlockSize
+func (x *ECBDecryptor) BlockSize() int {
+	return x.blockSize
+}
+
+// CryptBlocks implement BlockMode.CryptBlocks
+func (x *ECBDecryptor) CryptBlocks(dst, src []byte) {
+	if len(src)%x.blockSize != 0 {
+		panic("crypto/cipher: input not full blocks")
+	}
+	if len(dst) < len(src) {
+		panic("crypto/cipher: output smaller than input")
+	}
+	for len(src) > 0 {
+		x.b.Decrypt(dst, src[:x.blockSize])
+		src = src[x.blockSize:]
+		dst = dst[x.blockSize:]
+	}
+}
+
+// AesECBDecrypt will decrypt data with PKCS5Padding
+func AesECBDecrypt(ciphertext []byte, aesKey []byte) ([]byte, error) {
+	if len(ciphertext) < aes.BlockSize {
+		return nil, errors.New("ciphertext too short")
+	}
+	// ECB mode always works in whole blocks.
+	if len(ciphertext)%aes.BlockSize != 0 {
+		return nil, errors.New("ciphertext is not a multiple of the block size")
+	}
+	block, err := aes.NewCipher(aesKey)
+	if err != nil {
+		return nil, err
+	}
+	NewECBDecryptor(block).CryptBlocks(ciphertext, ciphertext)
+	return PKCS5UnPadding(ciphertext), nil
+}
+
+// PKCS5Padding -
+func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
+	padding := blockSize - len(ciphertext)%blockSize
+	padText := bytes.Repeat([]byte{byte(padding)}, padding)
+	return append(ciphertext, padText...)
+}
+
+// PKCS5UnPadding -
+func PKCS5UnPadding(origData []byte) []byte {
+	length := len(origData)
+	unPadding := int(origData[length-1])
+	return origData[:(length - unPadding)]
+}