小程序支付

一、背景

​ 最近一朋友咨询项目小程序需要调起微信支付,所以就研究了下,一开始拿到手的信息有:

  • 公众平台appId,appSecret 有了,服务器域名,业务域名(我配置打开的,可能用不到没管了)有了
  • 公众平台的微信支付模块中关联了商户号,并且微信支付已申请成功
  • 微信商户平台已获得商户秘钥,后面我加入了接口安全域名的配置(应该叫这名不太记得了)
    image-20200927224350579
    image-20200927224350579
    image-20200927224350579

二、实现

目前能找的最权威资料就是在官网开发平台,入口https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1

截取下来支付的主要动作:

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API

2、商户server调用支付统一下单,api参见公共api【统一下单API

3、商户server调用再次签名,api参见公共api【再次签名

4、商户server接收支付通知,api参见公共api【支付结果通知API

5、商户server查询支付结果,api参见公共api【查询订单API

这其中采坑最多的是字段大小写的问题,还有支付签名验证失败的问题,以及官网提供JSAPI的demo(.net 版本),采坑:

1、统一下单接口返回的数据字段大小写跟调起支付接口的字段大小写区别大,没注意坑惨了

2、如果遇到签名验证失败的情况,可参考平台提供的接口签名校验工具

3、demo中的签名算法HMAC-SHA256 但小程序要MD5才行

下面附上代码

小程序端

getOpenId:function(){
    wx.login({
      success: function (res) {
        var d = {
          appid: that.data.appId, //AppID
          secret: that.data.appSecret,//secret密钥
          grant_type: 'authorization_code',
          js_code: res.code
        };
        if (res.code) {
          wx.request({//getOpenid
            url: 'https://api.weixin.qq.com/sns/jscode2session',
            data: d,
            header: { 'content-type': 'application/json' },
            success: function (res) {
              console.dir(res)
              var openid = res.data.openid; //登录之后返回的openid
              console.log(openid + '我的openid')
              wx.setStorageSync('openid', openid) //储存openid
              if (openid != null & openid != undefined) {
                wx.getUserInfo({
                  success: function (res) {

                  },
                  fail: function (res) {
                    //console.info('用户拒绝授权');
                  }
                });
              } else {
                console.info('获取用户openid失败');
              }
            },
            fail: function (res) {
              console.info('获取用户openid失败');
              console.log(error);
            }
          })
        }
      }
    })
  },
  wxpay(evt){
    this.getOpenId();
    var d = {
      gfee: '1',
      gname: '999',
      openId: wx.getStorageSync('openid')
    };
    var url = this.data.url +'wxPay/api/order';
      wx.request({
        url: url,
        method: "POST",
        data: d,
        success: function (res){
           //发起支付
           wx.requestPayment({
             'timeStamp': res.data.timeStamp,
             'nonceStr': res.data.nonce_str,
             'package': res.data.package,
             'signType': 'MD5',
             'paySign': res.data.paySign,
             success: function (res) {
               console.info(res)
             },
             fail: function (res) {
               console.info(res)
             },
             complete: function (res) {
               console.info(res)
             }
           })
         },
         fail:function(ret){
            console.dir(ret)
         },
         complete:function(ret){
           console.dir(ret)
         }
      })

服务端主要完成预下单和回调通知这两个动作:

这里要注意的就是在预下单后返回给小程序的字段中package字段由预下单返回的prepay_id组成,类型下面这种:

字段名:package 字段值:prepay_id=wx261422495332917f0574b9a23c5ca40000

这里借用了官网提供的demo中的一个帮助类来完成,这里一定要把里边的签名方式改成MD5

帮助类:

public class WxPayApi
{
    public static WxPayData Micropay(WxPayData inputObj, int timeOut = 10)
    {
        string url = "https://api.mch.weixin.qq.com/pay/micropay";
        //检测必填参数
        if (!inputObj.IsSet("body"))
        {
        ​    throw new WxPayException("提交被扫支付API接口中,缺少必填参数body!");
        }
        else if (!inputObj.IsSet("out_trade_no"))
        {
        ​    throw new WxPayException("提交被扫支付API接口中,缺少必填参数out_trade_no!");
        }
        else if (!inputObj.IsSet("total_fee"))
        {
        ​    throw new WxPayException("提交被扫支付API接口中,缺少必填参数total_fee!");
        }
        else if (!inputObj.IsSet("auth_code"))
        {
        ​    throw new WxPayException("提交被扫支付API接口中,缺少必填参数auth_code!");
        }
        
        inputObj.SetValue("spbill_create_ip", WxPayConfig.GetConfig().GetIp());//终端ip
        inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());//公众账号ID
        inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商户号
        inputObj.SetValue("nonce_str", Guid.NewGuid().ToString().Replace("-", ""));//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名
        string xml = inputObj.ToXml();

        var start = DateTime.Now;//请求开始时间

        Log.Debug("WxPayApi", "MicroPay request : " + xml);
        string response = HttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口以提交数据到API
        Log.Debug("WxPayApi", "MicroPay response : " + response);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时

        //将xml格式的结果转换为对象以返回
        WxPayData result = new WxPayData();
        result.FromXml(response);

        ReportCostTime(url, timeCost, result);//测速上报

        return result;
    }

    /**
    *    
    * 查询订单
    * @param WxPayData inputObj 提交给查询订单API的参数
    * @param int timeOut 超时时间
    * @throws WxPayException
    * @return 成功时返回订单查询结果,其他抛异常
    */
    public static WxPayData OrderQuery(WxPayData inputObj, int timeOut = 6)
    {
        string url = "https://api.mch.weixin.qq.com/pay/orderquery";
        //检测必填参数
        if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id"))
        {
            throw new WxPayException("订单查询接口中,out_trade_no、transaction_id至少填一个!");
        }

        inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());//公众账号ID
        inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商户号
        inputObj.SetValue("nonce_str", WxPayApi.GenerateNonceStr());//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名
        string xml = inputObj.ToXml();

        var start = DateTime.Now;

        Log.Debug("WxPayApi", "OrderQuery request : " + xml);
        string response = HttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口提交数据
        Log.Debug("WxPayApi", "OrderQuery response : " + response);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时

        //将xml格式的数据转化为对象以返回
        WxPayData result = new WxPayData();
        result.FromXml(response);

        ReportCostTime(url, timeCost, result);//测速上报

        return result;
    }
    /**
    * 
    * 撤销订单API接口
    * @param WxPayData inputObj 提交给撤销订单API接口的参数,out_trade_no和transaction_id必填一个
    * @param int timeOut 接口超时时间
    * @throws WxPayException
    * @return 成功时返回API调用结果,其他抛异常
    */
    public static WxPayData Reverse(WxPayData inputObj, int timeOut = 6)
    {
        string url = "https://api.mch.weixin.qq.com/secapi/pay/reverse";
        //检测必填参数
        if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id"))
        {
            throw new WxPayException("撤销订单API接口中,参数out_trade_no和transaction_id必须填写一个!");
        }

        inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());//公众账号ID
        inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商户号
        inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名
        string xml = inputObj.ToXml();

        var start = DateTime.Now;//请求开始时间

        Log.Debug("WxPayApi", "Reverse request : " + xml);

        string response = HttpService.Post(xml, url, true, timeOut);

        Log.Debug("WxPayApi", "Reverse response : " + response);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);

        WxPayData result = new WxPayData();
        result.FromXml(response);

        ReportCostTime(url, timeCost, result);//测速上报

        return result;
    } 

    /**
    * 
    * 申请退款
    * @param WxPayData inputObj 提交给申请退款API的参数
    * @param int timeOut 超时时间
    * @throws WxPayException
    * @return 成功时返回接口调用结果,其他抛异常
    */
    public static WxPayData Refund(WxPayData inputObj, int timeOut = 6)
    {
        string url = "https://api.mch.weixin.qq.com/secapi/pay/refund";
        //检测必填参数
        if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id"))
        {
            throw new WxPayException("退款申请接口中,out_trade_no、transaction_id至少填一个!");
        }
        else if (!inputObj.IsSet("out_refund_no"))
        {
            throw new WxPayException("退款申请接口中,缺少必填参数out_refund_no!");
        }
        else if (!inputObj.IsSet("total_fee"))
        {
            throw new WxPayException("退款申请接口中,缺少必填参数total_fee!");
        }
        else if (!inputObj.IsSet("refund_fee"))
        {
            throw new WxPayException("退款申请接口中,缺少必填参数refund_fee!");
        }
        else if (!inputObj.IsSet("op_user_id"))
        {
            throw new WxPayException("退款申请接口中,缺少必填参数op_user_id!");
        }

        inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());//公众账号ID
        inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商户号
        inputObj.SetValue("nonce_str", Guid.NewGuid().ToString().Replace("-", ""));//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名
        
        string xml = inputObj.ToXml();
        var start = DateTime.Now;

        Log.Debug("WxPayApi", "Refund request : " + xml);
        string response = HttpService.Post(xml, url, true, timeOut);//调用HTTP通信接口提交数据到API
        Log.Debug("WxPayApi", "Refund response : " + response);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时

        //将xml格式的结果转换为对象以返回
        WxPayData result = new WxPayData();
        result.FromXml(response);

        ReportCostTime(url, timeCost, result);//测速上报

        return result;
    }

    /**
    * 
    * 查询退款
    * 提交退款申请后,通过该接口查询退款状态。退款有一定延时,
    * 用零钱支付的退款20分钟内到账,银行卡支付的退款3个工作日后重新查询退款状态。
    * out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个
    * @param WxPayData inputObj 提交给查询退款API的参数
    * @param int timeOut 接口超时时间
    * @throws WxPayException
    * @return 成功时返回,其他抛异常
    */
    public static WxPayData RefundQuery(WxPayData inputObj, int timeOut = 6)
    {
	    string url = "https://api.mch.weixin.qq.com/pay/refundquery";
	    //检测必填参数
	    if(!inputObj.IsSet("out_refund_no") && !inputObj.IsSet("out_trade_no") &&
		    !inputObj.IsSet("transaction_id") && !inputObj.IsSet("refund_id"))
        {
		    throw new WxPayException("退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个!");
	    }

	    inputObj.SetValue("appid",WxPayConfig.GetConfig().GetAppID());//公众账号ID
	    inputObj.SetValue("mch_id",WxPayConfig.GetConfig().GetMchID());//商户号
	    inputObj.SetValue("nonce_str",GenerateNonceStr());//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名

	    string xml = inputObj.ToXml();
	
	    var start = DateTime.Now;//请求开始时间

        Log.Debug("WxPayApi", "RefundQuery request : " + xml);
        string response = HttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口以提交数据到API
        Log.Debug("WxPayApi", "RefundQuery response : " + response);

        var end = DateTime.Now;
        int timeCost = (int)((end-start).TotalMilliseconds);//获得接口耗时

        //将xml格式的结果转换为对象以返回
	    WxPayData result = new WxPayData();
        result.FromXml(response);

	    ReportCostTime(url, timeCost, result);//测速上报
	
	    return result;
    }

    /**
    * 下载对账单
    * @param WxPayData inputObj 提交给下载对账单API的参数
    * @param int timeOut 接口超时时间
    * @throws WxPayException
    * @return 成功时返回,其他抛异常
    */
    public static WxPayData DownloadBill(WxPayData inputObj, int timeOut = 6)
    {
        string url = "https://api.mch.weixin.qq.com/pay/downloadbill";
        //检测必填参数
        if (!inputObj.IsSet("bill_date"))
        {
            throw new WxPayException("对账单接口中,缺少必填参数bill_date!");
        }

        inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());//公众账号ID
        inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商户号
        inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名

        string xml = inputObj.ToXml();

        Log.Debug("WxPayApi", "DownloadBill request : " + xml);
        string response = HttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口以提交数据到API
        Log.Debug("WxPayApi", "DownloadBill result : " + response);

        WxPayData result = new WxPayData();
        //若接口调用失败会返回xml格式的结果
        if (response.Substring(0, 5) == "<xml>")
        {
            result.FromXml(response);
        }
        //接口调用成功则返回非xml格式的数据
        else
            result.SetValue("result", response);

        return result;
    }

    /**
    * 
    * 转换短链接
    * 该接口主要用于扫码原生支付模式一中的二维码链接转成短链接(weixin://wxpay/s/XXXXXX),
    * 减小二维码数据量,提升扫描速度和精确度。
    * @param WxPayData inputObj 提交给转换短连接API的参数
    * @param int timeOut 接口超时时间
    * @throws WxPayException
    * @return 成功时返回,其他抛异常
    */
    public static WxPayData ShortUrl(WxPayData inputObj, int timeOut = 6)
    {
	    string url = "https://api.mch.weixin.qq.com/tools/shorturl";
	    //检测必填参数
	    if(!inputObj.IsSet("long_url"))
        {
		    throw new WxPayException("需要转换的URL,签名用原串,传输需URL encode!");
	    }

	    inputObj.SetValue("appid",WxPayConfig.GetConfig().GetAppID());//公众账号ID
	    inputObj.SetValue("mch_id",WxPayConfig.GetConfig().GetMchID());//商户号
	    inputObj.SetValue("nonce_str",GenerateNonceStr());//随机字符串	
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名
	    string xml = inputObj.ToXml();
	
	    var start = DateTime.Now;//请求开始时间

        Log.Debug("WxPayApi", "ShortUrl request : " + xml);
        string response = HttpService.Post(xml, url, false, timeOut);
        Log.Debug("WxPayApi", "ShortUrl response : " + response);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);

        WxPayData result = new WxPayData();
        result.FromXml(response);
		ReportCostTime(url, timeCost, result);//测速上报
	
	    return result;
    }

    /**
    * 
    * 统一下单
    * @param WxPaydata inputObj 提交给统一下单API的参数
    * @param int timeOut 超时时间
    * @throws WxPayException
    * @return 成功时返回,其他抛异常
    */
    public static WxPayData UnifiedOrder(WxPayData inputObj, int timeOut = 6)
    {
        string url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        //检测必填参数
        if (!inputObj.IsSet("out_trade_no"))
        {
            throw new WxPayException("缺少统一支付接口必填参数out_trade_no!");
        }
        else if (!inputObj.IsSet("body"))
        {
            throw new WxPayException("缺少统一支付接口必填参数body!");
        }
        else if (!inputObj.IsSet("total_fee"))
        {
            throw new WxPayException("缺少统一支付接口必填参数total_fee!");
        }
        else if (!inputObj.IsSet("trade_type"))
        {
            throw new WxPayException("缺少统一支付接口必填参数trade_type!");
        }

        //关联参数
        if (inputObj.GetValue("trade_type").ToString() == "JSAPI" && !inputObj.IsSet("openid"))
        {
            throw new WxPayException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!");
        }
        if (inputObj.GetValue("trade_type").ToString() == "NATIVE" && !inputObj.IsSet("product_id"))
        {
            throw new WxPayException("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!");
        }

        //异步通知url未设置,则使用配置文件中的url
        if (!inputObj.IsSet("notify_url"))
        {
            inputObj.SetValue("notify_url", WxPayConfig.GetConfig().GetNotifyUrl());//异步通知url
        }

        inputObj.SetValue("appid", WxPayConfig.GetConfig().GetAppID());//公众账号ID
        inputObj.SetValue("mch_id", WxPayConfig.GetConfig().GetMchID());//商户号
        inputObj.SetValue("spbill_create_ip", WxPayConfig.GetConfig().GetIp());//终端ip	  	    
        inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_MD5);//签名类型

        //签名
        inputObj.SetValue("sign", inputObj.MakeSign(WxPayData.SIGN_TYPE_MD5));
        string xml = inputObj.ToXml();

        var start = DateTime.Now;

        Log.Debug("WxPayApi", "UnfiedOrder request : " + xml);
        string response = HttpService.Post(xml, url, false, timeOut);
        Log.Debug("WxPayApi", "UnfiedOrder response : " + response);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);

        WxPayData result = new WxPayData();
        result.FromXmlNoCheckSign(response);

        //ReportCostTime(url, timeCost, result);//测速上报

        return result;
    }

    /**
    * 
    * 关闭订单
    * @param WxPayData inputObj 提交给关闭订单API的参数
    * @param int timeOut 接口超时时间
    * @throws WxPayException
    * @return 成功时返回,其他抛异常
    */
    public static WxPayData CloseOrder(WxPayData inputObj, int timeOut = 6)
    {
	    string url = "https://api.mch.weixin.qq.com/pay/closeorder";
	    //检测必填参数
	    if(!inputObj.IsSet("out_trade_no"))
        {
		    throw new WxPayException("关闭订单接口中,out_trade_no必填!");
	    }

	    inputObj.SetValue("appid",WxPayConfig.GetConfig().GetAppID());//公众账号ID
	    inputObj.SetValue("mch_id",WxPayConfig.GetConfig().GetMchID());//商户号
	    inputObj.SetValue("nonce_str",GenerateNonceStr());//随机字符串		
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名
	    string xml = inputObj.ToXml();
	
	    var start = DateTime.Now;//请求开始时间

        string response = HttpService.Post(xml, url, false, timeOut);

        var end = DateTime.Now;
        int timeCost = (int)((end - start).TotalMilliseconds);

        WxPayData result = new WxPayData();
        result.FromXml(response);

	    ReportCostTime(url, timeCost, result);//测速上报
	
	    return result;
    }    

    /**
    * 
    * 测速上报
    * @param string interface_url 接口URL
    * @param int timeCost 接口耗时
    * @param WxPayData inputObj参数数组
    */
    private static void ReportCostTime(string interface_url, int timeCost, WxPayData inputObj)
    {
	    //如果不需要进行上报
	    if(WxPayConfig.GetConfig().GetReportLevel() == 0)
        {
		    return;
	    } 

	    //如果仅失败上报
	    if(WxPayConfig.GetConfig().GetReportLevel() == 1 && inputObj.IsSet("return_code") && inputObj.GetValue("return_code").ToString() == "SUCCESS" &&
		 inputObj.IsSet("result_code") && inputObj.GetValue("result_code").ToString() == "SUCCESS")
        {
	 	    return;
	    }
	 
	    //上报逻辑
	    WxPayData data = new WxPayData();
        data.SetValue("interface_url",interface_url);
	    data.SetValue("execute_time_",timeCost);
	    //返回状态码
	    if(inputObj.IsSet("return_code"))
        {
		    data.SetValue("return_code",inputObj.GetValue("return_code"));
	    }
	    //返回信息
        if(inputObj.IsSet("return_msg"))
        {
		    data.SetValue("return_msg",inputObj.GetValue("return_msg"));
	    }
	    //业务结果
        if(inputObj.IsSet("result_code"))
        {
		    data.SetValue("result_code",inputObj.GetValue("result_code"));
	    }
	    //错误代码
        if(inputObj.IsSet("err_code"))
        {
		    data.SetValue("err_code",inputObj.GetValue("err_code"));
	    }
	    //错误代码描述
        if(inputObj.IsSet("err_code_des"))
        {
		    data.SetValue("err_code_des",inputObj.GetValue("err_code_des"));
	    }
	    //商户订单号
        if(inputObj.IsSet("out_trade_no"))
        {
		    data.SetValue("out_trade_no",inputObj.GetValue("out_trade_no"));
	    }
	    //设备号
        if(inputObj.IsSet("device_info"))
        {
		    data.SetValue("device_info",inputObj.GetValue("device_info"));
	    }
	
	    try
        {
		    Report(data);
	    }
        catch (WxPayException ex)
        {
		    //不做任何处理
	    }
    }
    /**
    * 
    * 测速上报接口实现
    * @param WxPayData inputObj 提交给测速上报接口的参数
    * @param int timeOut 测速上报接口超时时间
    * @throws WxPayException
    * @return 成功时返回测速上报接口返回的结果,其他抛异常
    */
    public static WxPayData Report(WxPayData inputObj, int timeOut = 1)
    {
	    string url = "https://api.mch.weixin.qq.com/payitil/report";
	    //检测必填参数
	    if(!inputObj.IsSet("interface_url"))
        {
		    throw new WxPayException("接口URL,缺少必填参数interface_url!");
	    } 
        if(!inputObj.IsSet("return_code"))
        {
		    throw new WxPayException("返回状态码,缺少必填参数return_code!");
	    } 
        if(!inputObj.IsSet("result_code"))
        {
		    throw new WxPayException("业务结果,缺少必填参数result_code!");
	    } 
        if(!inputObj.IsSet("user_ip"))
        {
		    throw new WxPayException("访问接口IP,缺少必填参数user_ip!");
	    } 
        if(!inputObj.IsSet("execute_time_"))
        {
		    throw new WxPayException("接口耗时,缺少必填参数execute_time_!");
	    }

	    inputObj.SetValue("appid",WxPayConfig.GetConfig().GetAppID());//公众账号ID
	    inputObj.SetValue("mch_id",WxPayConfig.GetConfig().GetMchID());//商户号
        inputObj.SetValue("user_ip",WxPayConfig.GetConfig().GetIp());//终端ip
	    inputObj.SetValue("time",DateTime.Now.ToString("yyyyMMddHHmmss"));//商户上报时间	 
	    inputObj.SetValue("nonce_str",GenerateNonceStr());//随机字符串
        inputObj.SetValue("sign_type", WxPayData.SIGN_TYPE_HMAC_SHA256);//签名类型
        inputObj.SetValue("sign", inputObj.MakeSign());//签名
	    string xml = inputObj.ToXml();

        Log.Info("WxPayApi", "Report request : " + xml);

        string response = HttpService.Post(xml, url, false, timeOut);

        Log.Info("WxPayApi", "Report response : " + response);

        WxPayData result = new WxPayData();
        result.FromXml(response);
	    return result;
    }

    /**
    * 根据当前系统时间加随机序列来生成订单号
     * @return 订单号
    */
    public static string GenerateOutTradeNo()
    {
        var ran = new Random();
        return string.Format("{0}{1}{2}", WxPayConfig.GetConfig().GetMchID(), DateTime.Now.ToString("yyyyMMddHHmmss"), ran.Next(999));
    }

    /**
    * 生成时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数
     * @return 时间戳
    */
    public static string GenerateTimeStamp()
    {
        TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
        return Convert.ToInt64(ts.TotalSeconds).ToString();
    }

    /**
    * 生成随机串,随机串包含字母或数字
    * @return 随机串
    */
    public static string GenerateNonceStr()
    {
        RandomGenerator randomGenerator = new RandomGenerator();
        return randomGenerator.GetRandomUInt().ToString();
    }
}
    /// <summary>
    /// 微信支付协议接口数据类,所有的API接口通信都依赖这个数据结构,
    /// 在调用接口之前先填充各个字段的值,然后进行接口通信,
    /// 这样设计的好处是可扩展性强,用户可随意对协议进行更改而不用重新设计数据结构,
    /// 还可以随意组合出不同的协议数据包,不用为每个协议设计一个数据包结构
    /// </summary>
    public class WxPayData
    {
        public  const string SIGN_TYPE_MD5 = "MD5";
        public  const string SIGN_TYPE_HMAC_SHA256 = "HMAC-SHA256";
        public WxPayData()
        {
    
    }

    //采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序
    private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>();

    /**
    * 设置某个字段的值
    * @param key 字段名
     * @param value 字段值
    */
    public void SetValue(string key, object value)
    {
        m_values[key] = value;
    }

    /**
    * 根据字段名获取某个字段的值
    * @param key 字段名
     * @return key对应的字段值
    */
    public object GetValue(string key)
    {
        object o = null;
        m_values.TryGetValue(key, out o);
        return o;
    }

    /**
     * 判断某个字段是否已设置
     * @param key 字段名
     * @return 若字段key已被设置,则返回true,否则返回false
     */
    public bool IsSet(string key)
    {
        object o = null;
        m_values.TryGetValue(key, out o);
        if (null != o)
            return true;
        else
            return false;
    }

    /**
    * @将Dictionary转成xml
    * @return 经转换得到的xml串
    * @throws WxPayException
    **/
    public string ToXml()
    {
        //数据为空时不能转化为xml格式
        if (0 == m_values.Count)
        {
            Log.Error(this.GetType().ToString(), "WxPayData数据为空!");
            throw new WxPayException("WxPayData数据为空!");
        }

        string xml = "<xml>";
        foreach (KeyValuePair<string, object> pair in m_values)
        {
            //字段值不能为null,会影响后续流程
            if (pair.Value == null)
            {
                Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
                throw new WxPayException("WxPayData内部含有值为null的字段!");
            }

            if (pair.Value.GetType() == typeof(int))
            {
                xml += "<" + pair.Key + ">" + pair.Value + "</" + pair.Key + ">";
            }
            else if (pair.Value.GetType() == typeof(string))
            {
                xml += "<" + pair.Key + ">" + "<![CDATA[" + pair.Value + "]]></" + pair.Key + ">";
            }
            else//除了string和int类型不能含有其他数据类型
            {
                Log.Error(this.GetType().ToString(), "WxPayData字段数据类型错误!");
                throw new WxPayException("WxPayData字段数据类型错误!");
            }
        }
        xml += "</xml>";
        return xml;
    }

    /**
    * @将xml转为WxPayData对象并返回对象内部的数据
    * @param string 待转换的xml串
    * @return 经转换得到的Dictionary
    * @throws WxPayException
    */
    public SortedDictionary<string, object> FromXml(string xml)
    {
        if (string.IsNullOrEmpty(xml))
        {
            Log.Error(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!");
            throw new WxPayException("将空的xml串转换为WxPayData不合法!");
        }
        SafeXmlDocument xmlDoc = new SafeXmlDocument();
        xmlDoc.LoadXml(xml);
        XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml>
        XmlNodeList nodes = xmlNode.ChildNodes;
        foreach (XmlNode xn in nodes)
        {
            XmlElement xe = (XmlElement)xn;
            m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中
        }
        try
        {
			//2015-06-29 错误是没有签名
			if(m_values["return_code"] != "SUCCESS")
			{
				return m_values;
			}
            CheckSign(SIGN_TYPE_MD5);//验证签名,不通过会抛异常
        }
        catch(WxPayException ex)
        {
            throw new WxPayException(ex.Message);
        }

        return m_values;
    }
    /**
   * @将xml转为WxPayData对象并返回对象内部的数据
   * @param string 待转换的xml串
   * @return 经转换得到的Dictionary
   * @throws WxPayException
   */
    public SortedDictionary<string, object> FromXmlNoCheckSign(string xml)
    {
        if (string.IsNullOrEmpty(xml))
        {
            Log.Error(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!");
            throw new WxPayException("将空的xml串转换为WxPayData不合法!");
        }
        SafeXmlDocument xmlDoc = new SafeXmlDocument();
        xmlDoc.LoadXml(xml);
        XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml>
        XmlNodeList nodes = xmlNode.ChildNodes;
        foreach (XmlNode xn in nodes)
        {
            XmlElement xe = (XmlElement)xn;
            m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中
        }

        try
        {
            //2015-06-29 错误是没有签名
            if (m_values["return_code"] != "SUCCESS")
            {
                return m_values;
            }
        }
        catch (WxPayException ex)
        {
            throw new WxPayException(ex.Message);
        }

        return m_values;
    }
    /**
    * @Dictionary格式转化成url参数格式
    * @ return url格式串, 该串不包含sign字段值
    */
    public string ToUrl()
    {
        string buff = "";
        foreach (KeyValuePair<string, object> pair in m_values)
        {
            if (pair.Value == null)
            {
                Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
                throw new WxPayException("WxPayData内部含有值为null的字段!");
            }

            if (pair.Key != "sign" && pair.Value.ToString() != "")
            {
                buff += pair.Key + "=" + pair.Value + "&";
            }
        }
        buff = buff.Trim('&');
        return buff;
    }
    /**
    * @Dictionary格式化成Json
     * @return json串数据
    */
    public string ToJson()
    {
        string jsonStr = JsonMapper.ToJson(m_values);
        return jsonStr;

    }

    /**
    * @values格式化成能在Web页面上显示的结果(因为web页面上不能直接输出xml格式的字符串)
    */
    public string ToPrintStr()
    {
        string str = "";
        foreach (KeyValuePair<string, object> pair in m_values)
        {
            if (pair.Value == null)
            {
                Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!");
                throw new WxPayException("WxPayData内部含有值为null的字段!");
            }
            str += string.Format("{0}={1}\n", pair.Key, pair.Value.ToString());
        }
        str = HttpUtility.HtmlEncode(str);
        Log.Debug(this.GetType().ToString(), "Print in Web Page : " + str);
        return str;
    }
    /**
    * @生成签名,详见签名生成算法
    * @return 签名, sign字段不参加签名
    */
    public string MakeSign(string signType){
        //转url格式
        string str = ToUrl();
        //在string后加入API KEY
        str += "&key=" + WxPayConfig.GetConfig().GetKey();
        Log.Error("签名前", str);
        if (signType == SIGN_TYPE_MD5)
        {
            var md5 = MD5.Create();
            var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
            var sb = new StringBuilder();
            foreach (byte b in bs)
            {
                sb.Append(b.ToString("x2"));
            }
            //所有字符转为大写
            return sb.ToString().ToUpper();
        }
        else if(signType==SIGN_TYPE_HMAC_SHA256)
        {
            return CalcHMACSHA256Hash(str, WxPayConfig.GetConfig().GetKey());
        }else{
            throw new WxPayException("sign_type 不合法");
        }
    }

    /**
    * @生成签名,详见签名生成算法
    * @return 签名, sign字段不参加签名 SHA256
    */
    public string MakeSign()
    {
        return MakeSign(SIGN_TYPE_HMAC_SHA256);
    }
    /**
    * 
    * 检测签名是否正确
    * 正确返回true,错误抛异常
    */
    public bool CheckSign(string signType)
    {
        //如果没有设置签名,则跳过检测
        if (!IsSet("sign"))
        {
            Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
            throw new WxPayException("WxPayData签名存在但不合法!");
        }
        //如果设置了签名但是签名为空,则抛异常
        else if (GetValue("sign") == null || GetValue("sign").ToString() == "")
        {
            Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
            throw new WxPayException("WxPayData签名存在但不合法!");
        }

        //获取接收到的签名
        string return_sign = GetValue("sign").ToString();

        //在本地计算新的签名
        string cal_sign = MakeSign(signType);

        if (cal_sign == return_sign)
        {
            return true;
        }

        Log.Error(this.GetType().ToString(), "WxPayData签名验证错误!");
        throw new WxPayException("WxPayData签名验证错误!");
    }
    /**
    * 
    * 检测签名是否正确
    * 正确返回true,错误抛异常
    */
    public bool CheckSign()
    {
        return CheckSign(SIGN_TYPE_HMAC_SHA256);
    }

    /**
    * @获取Dictionary
    */
    public SortedDictionary<string, object> GetValues()
    {
        return m_values;
    }

    public void Remove(string key)
    {
        if (m_values.Keys.Contains(key))
        {
            m_values.Remove(key);
        }
    }

    private  string CalcHMACSHA256Hash(string plaintext, string salt)
    {
        string result = "";
        var enc = Encoding.Default;
        byte[]
        baText2BeHashed = enc.GetBytes(plaintext),
        baSalt = enc.GetBytes(salt);
        System.Security.Cryptography.HMACSHA256 hasher = new HMACSHA256(baSalt);
        byte[] baHashedText = hasher.ComputeHash(baText2BeHashed);
        result = string.Join("", baHashedText.ToList().Select(b => b.ToString("x2")).ToArray());
        return result;
    }
  }

预下单接口关键代码:

            WxPayData payData = new WxPayData();
            try
            {
                var orderno = WxPayApi.GenerateOutTradeNo();
                payData.SetValue("out_trade_no", orderno);//商户订单号 32个字符
                payData.SetValue("body", "999感冒灵");//商品描述
                payData.SetValue("total_fee", 1);//总金额 1 分钱
                payData.SetValue("trade_type", "JSAPI");//支付类型 小程序支付
                payData.SetValue("openid", openId);
            
            
            var result = WxPayApi.UnifiedOrder(payData);
            string strCode = result.GetValue("return_code").ToString();
            string strMsg = result.GetValue("return_msg").ToString();
            //响应字段
            //appid--------
            //mch_id--------
            //nonce_str--------
            //prepay_id--------
            //result_code--------SUCCESS
            //return_code--------SUCCESS
            //return_msg--------OK
            //sign--------A5D5BFEB0E4C3648DD61672CFA76DCA9
            //trade_type--------JSAPI
            var paySign = "";
            TimeSpan timeSpan = (DateTime.UtcNow - new DateTime(1970, 1, 1));
            string timeStamp = ((int)timeSpan.TotalSeconds).ToString();
            //if (strCode == "SUCCESS")
            //{
            //去掉非签名字段
            result.Remove("result_code");
            result.Remove("return_code");
            result.Remove("return_msg");
            result.Remove("return_msg");
            result.Remove("sign");

            //一定要注意返回的数据和签名的参数名大小写区别 巨坑
            var appid = result.GetValue("appid");
            result.Remove("appid");
            result.SetValue("appId", appid);

            var nonceStr = result.GetValue("nonce_str");
            result.Remove("nonce_str");
            result.SetValue("nonceStr", nonceStr);

            var trade_type = result.GetValue("trade_type");
            result.Remove("trade_type");

            var mch_id = result.GetValue("mch_id");
            result.Remove("mch_id");

            var prepay_id = result.GetValue("prepay_id");
            result.Remove("prepay_id");

            result.SetValue("signType", "MD5");
            result.SetValue("package", "prepay_id=" + prepay_id);
            result.SetValue("timeStamp", timeStamp);

            //需签名字段
            //appId,timeStamp,nonceStr,package,signType
            paySign = result.MakeSign(WxPayData.SIGN_TYPE_MD5);
            //}
            var strJson = "{\"package\":\"" + result.GetValue("package") + "\",\"timeStamp\":\"" + timeStamp + "\",\"transaction_id\":\"" + orderno + "\",\"appid\":\"" + result.GetValue("appId") + "\",\"mch_id\":\"" + mch_id + "\",\"nonce_str\":\"" + nonceStr + "\",\"trade_type\":\"" + result.GetValue("trade_type") + "\",\"paySign\":\"" + paySign + "\"}";
            return JObject.Parse(strJson);
posted @ 2020-09-28 00:11  这是个坑  阅读(356)  评论(0编辑  收藏  举报