微信WAP和公众号支付 golang实现的几个关键点.

在一些支付项目中如果要外调公共服务微信WAP支付和公众号支付,官方的文档介绍里仅仅有java和php的相关样例code,特此补充go版本。之所以分WAP和公众号,是为了解决一个推广链接,分别在外设浏览器和微信内浏览器内打开的条件,嗯,微信内是无法打开wap支付的,所以需要公众号支付方式来解决微信内支付的情况。
VXWAP
详情参考文档:https://open.swiftpass.cn/openapi/wiki?index=5&chapter=1
WAP支付需要知道的关键参数:MchId xml:"mch_id",商户号,由银行下发给商户,在商户后台可以查询到,比如:https://citicmch.swiftpass.cn/
1. 首先是xml请求参数的准备

    //VXWAP支付 POST 模型
type VXXML struct {
    XMLName     xml.Name `xml:"xml"`
    Service     string   `xml:"service"`             //接口类型,pay.weixin.wappay
    Version     string   `xml:"version,omitempty"`   //版本, 2.0
    Charset     string   `xml:"charset,omitempty"`   //字符集 UTF-8
    SignType    string   `xml:"sign_type"`           //签名方式 RSA_1_256
    MchId       string   `xml:"mch_id"`              //商户号   待填
    OutTradeNo  string   `xml:"out_trade_no"`        //用户订单号
    Body        string   `xml:"body"`                //商品描述 待填
    Attach      string   `xml:"attach,omitempty"`    //附加信息,选填
    TotalFee    string   `xml:"total_fee"`           //总金额
    MchCreateIp string   `xml:"mch_create_ip"`       // 终端ip
    NotifyUrl   string   `xml:"notify_url"`          //通知接口
    CallBackUrl string   `xml:"callback_url"`        //回调界面
    GoodsTag    string   `xml:"goods_tag,omitempty"` //商品标记 选填
    DevInfo     string   `xml:"device_info"`         //应用类型 苹果-iOS_SDK,安卓-AND_SDK,其他iOS_WAP
    MchAppName  string   `xml:"mch_app_name"`        //应用名
    MchAppId    string   `xml:"mch_app_id"`          //应用标识
    NonceStr    string   `xml:"nonce_str"`           //随机字符串 32位以内
    Sign        string   `xml:"sign"`                //签名结果
}

//将该(任意)字符串结构体转换成a=x&b=y&c=z 格式,参数名按照asc递增排序,""值不排
func ToParam(vx interface{}) string {
    var result string
    var tagName_FieldValue = make(map[string]string)
    var SortedArr = make([]string, 0)
    var tagValueTemp string
    var valueStrTemp string

    vType := reflect.TypeOf(vx)
    vValue := reflect.ValueOf(vx)
    for i := 0; i < vType.NumField(); i++ {
        tagValueTemp = filtTag(vType.Field(i).Tag.Get("xml"))
        //设置过滤
        if tagValueTemp == "" || tagValueTemp == "xml" || tagValueTemp == "sign" {
            continue
        }
        valueStrTemp = vValue.Field(i).String()
        if valueStrTemp == "" {
            continue
        }
        tagName_FieldValue[tagValueTemp] = valueStrTemp
        SortedArr = append(SortedArr, tagValueTemp)
    }
    sort.Strings(SortedArr)

    for i, v := range SortedArr {
        if i == 0 {
            result = result + v + "=" + tagName_FieldValue[v]
            continue
        }
        result = result + "&" + v + "=" + tagName_FieldValue[v]
    }
    return result
}

//获取标签第一个值
func filtTag(tag string) string {
    if strings.Contains(tag, ",") {
        return strings.Split(tag, ",")[0]
    } else {
        return tag
    }
}

上述代码的使用条件:
xml参数的格式都是string
请求参数的排列不包括sign
参数排列过滤掉xmlName即首行以及sign以及空
2. 签名的生成
wap支付已经不支持md5,统一使用rsa_1_256
rsa/interface.go

package rsa
import "crypto"

type Cipher interface {
    Encrypt(plaintext []byte) ([]byte, error)
    Decrypt(ciphertext []byte) ([]byte, error)
    Sign(src []byte, hash crypto.Hash) ([]byte, error)
    Verify(src []byte, sign []byte, hash crypto.Hash,publicKey string) error
} 

rsa/Imple.go

package rsa


import (
"crypto"
"crypto/rand"
"crypto/rsa"
    "encoding/pem"
    "errors"
)
type Type int64

const (
    PKCS1 Type = iota
    PKCS8
)
type pkcsClient struct {
    privateKey *rsa.PrivateKey
    publicKey  *rsa.PublicKey
}

func (this *pkcsClient) Encrypt(plaintext []byte) ([]byte, error) {
    return rsa.EncryptPKCS1v15(rand.Reader, this.publicKey, plaintext)
}
func (this *pkcsClient) Decrypt(ciphertext []byte) ([]byte, error) {
    return rsa.DecryptPKCS1v15(rand.Reader, this.privateKey, ciphertext)
}

func (this *pkcsClient) Sign(src []byte, hash crypto.Hash) ([]byte, error) {
    h := hash.New()
    h.Write(src)
    hashed := h.Sum(nil)
    return rsa.SignPKCS1v15(rand.Reader, this.privateKey, hash, hashed)
}

func (this *pkcsClient) Verify(src []byte, sign []byte, hash crypto.Hash,publickKey string) error {
    h := hash.New()
    h.Write(src)
    hashed := h.Sum(nil)
    if publickKey ==""{
        return rsa.VerifyPKCS1v15(this.publicKey, hash, hashed, sign)
    }else{
        blockPub, _ := pem.Decode([]byte(publickKey))
        if blockPub == nil {
            return  errors.New("public key error")
        }
        pubKey, err := genPubKey(blockPub.Bytes)
        if err != nil {
            return err
        }
        return rsa.VerifyPKCS1v15(pubKey, hash, hashed, sign)
    }
}

rsa/client.go

package rsa

import (
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "errors"
)

//默认客户端,pkcs8私钥格式,pem编码
func NewDefault(privateKey, publicKey string) (Cipher, error) {
    blockPri, _ := pem.Decode([]byte(privateKey))
    if blockPri == nil {
        return nil, errors.New("private key error")
    }

    blockPub, _ := pem.Decode([]byte(publicKey))
    if blockPub == nil {
        return nil, errors.New("public key error")
    }

    return New(blockPri.Bytes, blockPub.Bytes,PKCS8)
}

func New(privateKey, publicKey []byte, privateKeyType Type) (Cipher, error) {

    priKey, err := genPriKey(privateKey, privateKeyType)
    if err != nil {
        return nil, err
    }
    pubKey, err := genPubKey(publicKey)
    if err != nil {
        return nil, err
    }
    return &pkcsClient{privateKey: priKey, publicKey: pubKey}, nil
}

func genPubKey(publicKey []byte) (*rsa.PublicKey, error) {
    pub, err := x509.ParsePKIXPublicKey(publicKey)
    if err != nil {
        return nil, err
    }
    return pub.(*rsa.PublicKey), nil
}

func genPriKey(privateKey []byte, privateKeyType Type) (*rsa.PrivateKey, error) {
    var priKey *rsa.PrivateKey
    var err error
    switch privateKeyType {
    case PKCS1:
        {
            priKey, err = x509.ParsePKCS1PrivateKey([]byte(privateKey))
            if err != nil {
                return nil, err
            }
        }
    case PKCS8:
        {
            prkI, err := x509.ParsePKCS8PrivateKey([]byte(privateKey))
            if err != nil {
                return nil, err
            }
            priKey = prkI.(*rsa.PrivateKey)
        }
    default:
        {
            return nil, errors.New("unsupport private key type")
        }
    }
    return priKey, nil
}

加密rsa包可以独立使用,使用者不用关注内部逻辑,下面是使用方式
beanFactory/beans.go

package beanFactory

import (
    "github.com/fwhezfwhez/BeanFactory"    //工厂
    "Pay/Common/rsa"                       //rsa加密工具
    "Pay/Common/utils"
    "fmt"
    "Pay/Common/consts"
)

//全局单例的工厂实例
var Factory *BeanFactory.BeanFactory

//工厂bean初始化
func init() {
    Factory = BeanFactory.GetFactory()
    //rsa设置好私钥和公钥,己方的rsa 私钥公钥
    rsaClient, err := rsa.NewDefault(string(consts.Private), string(consts.Public))
    if err != nil {
        fmt.Println(utils.Location() + err.Error())
        return
        }
    Factory.SetBean("rsaClient", rsaClient)
 }
...
//signData是上述结构体执行ToParam()方法后得到的query参数,比如:
//body=测试支付&mch_create_ip=127.0.0.1&mch_id=175510359638&nonce_str=1409196838notify_url=http://227.0.0.1:9001/javak/sds?123&23=3&out_trade_no=141903606228&service=pay.weixin.wappay&total_fee=1

sign, err := rsaEncrypt([]byte(signData))
...

//对数据进行签名
func rsaEncrypt(data []byte) (string, error) {
    rsClient := beanFactory.Factory.UnsafeGet("rsaClient").(rsa.Cipher)
    signBytes,err:=rsClient.Sign(data, crypto.SHA256)
    if err!=nil {
        orderControlFATALPrinter.Println("location:%s,err:%s",utils.Location(),err.Error())
        return "",err
    }
    rs:=base64.StdEncoding.EncodeToString(signBytes)   //这句话一定要加
    return rs,nil
}

请注意,使用rsClient.Sign()获得的signBytes一定还要经过一轮base64编码,不然通不过微信的检测
rs:=base64.StdEncoding.EncodeToString(signBytes)
3.将结构体进行xml格式转化以后post给微信方即可

...
vxXml = VXXML{
//赋值
//记得带上sign值
}
buf, err := xml.Marshal(vxXml)
//handle err
//http post
//handle response
...

示例:

 <xml>
      <service>pay.weixin.wappay</service>
      <version>2.0</version>
      <charset>UTF-8</charset>
      <sign_type>RSA_1_256</sign_type>
      <mch_id>xxxxxxxxxxx</mch_id><!-商户后台对应的mch_id-->
      <out_trade_no>D201805231527042043733482</out_trade_no>
      <body>测试数据</body>
      <total_fee>6600</total_fee>
      <mch_create_ip>199.95.193.25</mch_create_ip>
      <notify_url>https://xxx.xxx.com/notifyUrl</notify_url>
      <callback_url>https://xxx.xxx.com/</callback_url>
      <device_info>iOS_SDK</device_info>
      <mch_app_name>xxxxx</mch_app_name>
      <mch_app_id>xxxxxxx</mch_app_id>
      <nonce_str>Eq4PMmHW05JVhINmy4lcTtXSuJpfN8wv</nonce_str>
      <sign>ROa16mZncQp7q/DbYKa1kN5OkAaqxk31hzQT+w75opoANGouPjygUAwJfVObTnshwsW0/ZF5+nOBRp3v373uXKcQRVLynxtfsbXM44WveN4215cHJvCsRHyHMrPHqdbE+EPo1guCXYabtcZ+J+Z0N0wbWgY4yRCTzrC0VN9jyIwODbpSQsCoo4PIcbXECkJl6TNU4Tv308WDTqgdIRqfJrsOeB1H2IaHIeXoJ9YpaKudE6gE/eopHnKdGqE3snh86qd+YabZCiAhk6wkwzW0jm2Qkg/MXcaxNqdV527d5hudIuFx1E8DU5FOhwgr33+bBGKvl/+Pf9SkemBd2gvdRg==</sign>
  </xml>

4.处理响应支付结果notifyurl,该响应可以和公众号支付复用

//异步支付结果通知结果
//model包里
type NotifyResult struct {
    XMLName          xml.Name `xml:"xml"`
    Version          string   `xml:"version"`
    Charset          string   `xml:"charset"`
    SignType         string   `xml:"sign_type"`
    Status           string   `xml:"status"`
    Message          string   `xml:"message"`
    ResultCode       string   `xml:"result_code"`
    MchId            string   `xml:"mch_id"`
    DeviceInfo       string   `xml:"device_info"`
    NonceStr         string   `xml:"nonce_str"`
    ErrCode          string   `xml:"err_code"`
    ErrMsg           string   `xml:"err_msg"`
    Sign             string   `xml:"sign"`
    OpenId           string   `xml:"openid"`
    TradeType        string   `xml:"trade_type"`
    IsSubscribe      string   `xml:"is_subscribe"`
    PayResult        string   `xml:"pay_result"`
    PayInfo          string   `xml:"pay_info"`
    TransactionId    string   `xml:"transaction_id"`
    OutTransactionId string   `xml:"out_transaction_id"`
    SubIsSubscribe   string   `xml:"sub_is_subscribe"`
    SubAppId         string   `xml:"sub_appid"`
    SubOpenId        string   `xml:"sub_openid"`
    OutTradeNo       string   `xml:"out_trade_no"`
    TotalFee         string   `xml:"total_fee"`
    CashFee          string   `xml:"cash_fee"`
    CouponFee        string   `xml:"coupon_fee"`
    FeeType          string   `xml:"fee_type"`
    Attach           string   `xml:"attach"`
    BankType         string   `xml:"bank_type"`
    BankBillNo       string   `xml:"bank_billno"`
    TimeEnd          string   `xml:"time_end"`
}
//Control包里
func Notify(c *gin.Context) {
    //TODO 清空测试记录
    log.Println("支付信息成功进入notify,%s")
    controlPrinter.Println("缓存的orderId_oid列表是::%v", consts.OrderId_Oid)
    controlPrinter.Println("缓存的orderId_Money列表是::%v", consts.OrderId_Money)
    controlPrinter.Println("缓存的orderId_ProductId列表是::%v", consts.OrderId_ProductId)
    rs := model.NotifyResult{}
    c.Bind(&rs)
    //该资源用于判断是否合法
    src := model.ToParam(rs)
    controlPrinter.Println("location:%s,rsParam:%s", utils.Location(), src)

    controlPrinter.Println("location:%s,rs:%s", utils.Location(), rs.String())
    _, er := OrderDao.SaveNotifyResult(rs)
    if er != nil {
        controlFATALPrinter.Println("%s,存放notifyresult失败:%s", utils.Location(), er.Error())
    }
    if rs.Status != "0" {
        controlFATALPrinter.Println("location:%s,notify状态state非0失败", utils.Location())
        c.String(200, "fail")
        return
    }
    orderIdrs := rs.OutTradeNo
    var oid string
    var ok bool

    if _, ok = consts.OrderId_Money[orderIdrs]; !ok {
        controlPrinter.Println(utils.Location() + ",该oid不存在于orderid_oid表里,已处理完")
        //不存在,即已经处理完了
        c.String(200, "%s", "success")
        return
    } else {
        //更新ditribution order状态
        if doDecodeSign(rs.Sign, src) {
            //签名验证成功
            if localMoney, ok2 := consts.OrderId_Money[orderIdrs]; !ok2 {
                controlFATALPrinter.Println("Location:%s,order_money表没有该orderId的金额记录", utils.Location())
            } else {
                if rs.PayResult != "0" {
                    controlPrinter.Println("[%s],支付状态为非零,用户取消了,或者支付失败", utils.Location())
                    //支付失败,用户不想支付了
                    c.String(200, "%s", "success")
                    removeCache(orderIdrs)
                    return
                }

                controlPrinter.Println("金额缓存列表:%v,[%s]", consts.OrderId_Money, utils.Location())
                if localMoney == rs.TotalFee {
                    controlFATALPrinter.Println("金额匹配成功,[%s]", utils.Location())

                    //金额与订单匹配
                    //同步数据
                    //检查缓存里是否存在对应的oid,判断是否需要同步第三方服务
                    if oid, ok = consts.OrderId_Oid[orderIdrs]; !ok {
                        controlFATALPrinter.Println(utils.GenerateMsg("缓存里找不到orderiD,该条处理已经处理完了"))
                    } else if oid == "" {
                        //即本地支付成功可存,业务方订单出了问题
                        controlFATALPrinter.Println(utils.GenerateMsg("缓存里找不到oid,该条处理未被业务方记录,仅在本地记录了"))
                        //添加未被远程记录的订单入库
                        orderService.UpdateSyn(orderIdrs)
                    } else {
                        //对远程业务进行同步
                        switch consts.OrderId_ProductId[orderIdrs]{
                        case"1001":
                            ok1, errMsg := OrderService.CallDistributionText(oid)
                            if !ok1 {
                                controlFATALPrinter.Println(utils.GenerateErrMsg(errMsg + "调用短信服务失败"))
                            }
                        case"1002":{}
                        }

                    }
                    //同步本地
                    ok2 := orderService.UpdatePayStatus(orderIdrs)
                    if ok2 {
                        controlFATALPrinter.Println("成功同步业务pay和本地paystatus,%s", utils.Location())
                        c.String(200, "%s", "success")
                        //移除缓存
                        removeCache(orderIdrs)
                        log.Println("缓存移除成功orderid:%s,oid:%s,location:%s", orderIdrs, oid, utils.Location())
                        return
                    }
                }
            }
        } else {
            c.String(200, "%s", "fail")
            controlFATALPrinter.Println("签名不正确,数据可能丢失或者篡改,%s", utils.Location())
        }
    }
}


//移除缓存
func removeCache(orderId string){
    consts.RemoveOrderId_Money(orderId)
    consts.RemoveOrderId_Oid(orderId)
    consts.RemoveOrderId_ProductId(orderId)
}

//签名验证
func doDecodeSign(sign string, src string) bool {
    controlPrinter.Println("进入验证sign,%s", utils.Location())
    rsClient := beanFactory.Factory.UnsafeGet("rsaClient").(rsa.Cipher)
    signBytes, err := base64.StdEncoding.DecodeString(sign)
    if err != nil {
        controlFATALPrinter.Println("转换签名出错,%s", utils.Location())
        return false
    }
    if err != nil {
        controlFATALPrinter.Println("location:%s,err:%s", utils.Location(), err.Error())
        return false
    }
    errV := rsClient.Verify([]byte(src), signBytes, crypto.SHA256, consts.VXPublic)
    if errV != nil {
        controlFATALPrinter.Println("验证sign失败%s,location:%s", errV.Error(), utils.Location())
        return false
    }
    controlPrinter.Println("验证签名成功,%s", utils.Location())
    return true
}

公众号支付待更

猜你喜欢

转载自blog.csdn.net/fwhezfwhez/article/details/80417235
今日推荐