微信JS支付以及微信商户提现(一)

最近有小伙伴问我,微信js支付我应该怎么做,在网上搜了各种文档,结合了微信JSAPI支付文档依然无法实现微信客户支付至商户端的业务。最近刚好有类似业务,趁机总结一下。

其实我认为微信内H5调起支付的文档微信写的还算清晰明了。

按照要求我们新建一个Html页面。

打开JSAPI接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6

微信JS支付以及微信商户提现(一)

打开文档后,我们可以很清晰的看到,在H5中页面中发起支付我们需要6个参数,分别为:

  • 公众号id;
  • 时间戳;
  • 随机字符串;
  • 订单详情扩展字符串;
  • 签名方式;
  • 签名;

appId:微信商户端关联的微信公众号的AppId

timeStamp:获取timeStamp的代码如下:

 public static string GenerateTimeStamp()
 {
     TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
     return Convert.ToInt64(ts.TotalSeconds).ToString();
 }

nonceStr:获取nonceStr的代码如下:

  public static string GenerateNonceStr()
  {
     return Guid.NewGuid().ToString().Replace("-", "");
  }

package:prepay_id=u802345jgfjsdfgsdg888

我们在使用H5支付之前,需先调用统一下单接口。

微信在调用统一下单接口之后,如果return_code返回值为SUCCESS,并且return_msg为OK,则统一下单接口中将返回:

预支付交易会话标识,prepay_id(微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时)

下文中将会对统一下单接口进行详细说明。

signType:MD5

paySign:js支付的MD5签名,签名代码如下:

 public Dictionary<string, string> GetSignDic()
 {
      var dicSign = new Dictionary<string, string>();
      dicSign.Add("appId","888888888");
      dicSign.Add("timeStamp", "1414561699");
      dicSign.Add("nonceStr", "5K8264ILTKCH16CQ2502SI8ZNMTM67VS");
      dicSign.Add("package", "prepay_id=123456789");
      dicSign.Add("signType", "MD5");
      return dicSign;
  }

然后,我们需要对这个字典进行排序,然后拼接为Url之后,拼接微信商户断的Key(WxMchKey)之后,完成MD5加密,生成签名。

WxMchKey:微信商户端API**

public string MakeSign()
{
     SortedDictionary<string, string> sortDic = new SortedDictionary<string, string>(GetSignDic());    //我们对签名的字典进行排序
    //将签名的字典转换为Url
    string str = ToUrl(sortDic);
    //拼接微信公众号的商户端的AppKey
    str=string.concat(str,"&key="+WxMchKey);
     //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();
}


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

          if (pair.Key != "sign" && pair.Value.ToString() != "")
          {
              buff += pair.Key + "=" + pair.Value + "&";
          }
       }
      buff = buff.Trim('&');
      return buff;
 }

ok,下一步我们说明一下,如何在Html页面中发起微信支付。

我们只需要把我们的字典dicSign,转换为JSON返回给前端代码即可。

if (typeof WeixinJSBridge == "undefined"){
   if( document.addEventListener ){
       document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
   }else if (document.attachEvent){
       document.attachEvent('WeixinJSBridgeReady', onBridgeReady); 
       document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
   }
}else{
   onBridgeReady();
}
function onBridgeReady(){
   WeixinJSBridge.invoke(
      'getBrandWCPayRequest', {
         "appId":"wx2421b1c4370ec43b",     //公众号名称,由商户传入     
         "timeStamp":"1395712654",         //时间戳,自1970年以来的秒数     
         "nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串     
         "package":"prepay_id=u802345jgfjsdfgsdg888",     
         "signType":"MD5",         //微信签名方式:     
         "paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名 
      },
      function(res){
         switch (res.err_msg) {
             case "get_brand_wcpay_request:ok":
                   //支付成功
                   break;
                   case "get_brand_wcpay_request:cancel":
                   //支付取消
                   break;
                   default:
                   //支付失败
                   break;
          }
   }); 
}

js返回支付成功之后,我们即可处理一些业务逻辑(注意:所有的js代码的返回结果,我们都认为是不可信的,故,对于关键业务数据,我们建议在微信统一下单,结果消息回调通知中进行处理)

针对package参数,我们需要先获取prepay_id,而获取prepay_id,我们需要先调用微信统一下单。

微信统一下单接口请求地址:https://api.mch.weixin.qq.com/pay/unifiedorder

统一下单接口是不需要商户端证书的。

appid:微信公众号的AppId

mch_id:微信支付分配的商户号

device_info:Web

nonce_str:随机字符串

 public static string GenerateNonceStr()
 {
    return Guid.NewGuid().ToString().Replace("-", "");
 }

sign_type:MD5

body:"测试微信支付",

detail:"测试微信支付"

attach:"这个参数很关键,我们可以带上我们的自定义参数,以便我们在支付完成之后,完成对业务数据的处理。

out_trade_no:商户订单号

 public static string GenerateOutTradeNo()
 {
     var ran = new Random();
     return string.Format("{0}{1}{2}", WxPayConfig.MCHID, DateTime.Now.ToString("yyyyMMddHHmmss"), ran.Next(999));
  }

fee_type:标价币种:CNY

total_fee:标价金额,请注意单位为分

spbill_create_ip:终端IP,(该IP地址必须为微信公众号的白名单中的IP地址,否则会发生IP地址不合法的错误)

time_start:交易起始时间 

DateTime.Now.ToString("yyyyMMddHHmmss")

time_expire:交易结束时间 

DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));

goods_tag:订单优惠标记,使用代金券或立减优惠功能时需要的参数

notify_url:异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。微信在完成支付,并支付成功之后,微信端将会Post通知消息给该地址。

trade_type:JSAPI

openid:向商户端支付的用户的Openid

至此,我们对参数的解释即完成了,现在我们进行统一下单操作。

 public Dictionary<string, string> GetOrderDic()
 {
      var dicOrder = new Dictionary<string, string>();
      dicOrder.Add("appid","888888888");
      dicOrder.Add("mch_id","888888888");
      dicOrder.Add("device_info", "WEB");
      dicOrder.Add("nonceStr", "5K8264ILTKCH16CQ2502SI8ZNMTM67VS");
      dicOrder.Add("signType", "MD5");
      dicOrder.Add("body", "微信支付测试");
      dicOrder.Add("detail", "微信支付测试");
      dicOrder.Add("attach", "OrderId");
      dicOrder.Add("out_trade_no", "2019040217390001");
      dicOrder.Add("fee_type", "CNY");
      dicOrder.Add("total_fee", 100);
      dicOrder.Add("spbill_create_ip", "192.168.1.1");
      dicOrder.Add("time_start", "20190401174132");
      dicOrder.Add("time_expire", "20190401175132");
      dicOrder.Add("notify_url", "https://Order/OrderPayNotity");
      dicOrder.Add("trade_type", "JSAPI");
      return dicOrder;
  }

OK,同js支付中的签名方式,我们需要先对字典进行排序,后我们转换为Url之后,进行MD5加密以完成签名

public string MakeSign()
{
     SortedDictionary<string, string> sortDic = new SortedDictionary<string, string>(GetOrderDic());    //我们对签名的字典进行排序
    //将签名的字典转换为Url
    string str = ToUrl(sortDic);
    //拼接微信公众号的商户端的AppKey
    str=string.concat(str,"&key="+WxMchKey);
     //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();
}


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

          if (pair.Key != "sign" && pair.Value.ToString() != "")
          {
              buff += pair.Key + "=" + pair.Value + "&";
          }
       }
      buff = buff.Trim('&');
      return buff;
 }

完成签名之后,我们需要把Sign增加至Dictionary中

var dic=GetOrderDic();
dic.Add("sign",MakeSign())

完成这一步之后,我们就需要将Dictionary转换为XML,并提交Post请求,将Dictionary字典转换为Xml的代码如下:

public string ToXml(Dictionary<string, string> dicOrder)
        {
            //数据为空时不能转化为xml格式
            if (0 == dicOrder.Count)
            {
                throw new WxPayException("WxPayData数据为空!");
            }

            string xml = "<xml>";
            foreach (KeyValuePair<string, string> pair in dicOrder)
            {
                //字段值不能为null,会影响后续流程
                if (pair.Value == 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类型不能含有其他数据类型
                {
                    throw new WxPayException("WxPayData字段数据类型错误!");
                }
            }
            xml += "</xml>";
            return xml;
        }

转换成功之后,我们进行Post请求,Post方法代码如下,仅供参考:

 public static string Post(string xml, string url, bool isUseCert, int timeout)
        {
            System.GC.Collect();//垃圾回收,回收没有正常关闭的http连接

            string result = "";//返回结果

            HttpWebRequest request = null;
            HttpWebResponse response = null;
            Stream reqStream = null;

            try
            {
                //设置最大连接数
                ServicePointManager.DefaultConnectionLimit = 200;
                //设置https验证方式
                if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
                {
                    ServicePointManager.ServerCertificateValidationCallback =
                            new RemoteCertificateValidationCallback(CheckValidationResult);
                }

                /***************************************************************
                * 下面设置HttpWebRequest的相关属性
                * ************************************************************/
                request = (HttpWebRequest)WebRequest.Create(url);

                request.Method = "POST";
                request.Timeout = timeout * 1000;

                //设置代理服务器
                //WebProxy proxy = new WebProxy();                          //定义一个网关对象
                //proxy.Address = new Uri(WxPayConfig.PROXY_URL);              //网关服务器端口:端口
                //request.Proxy = proxy;

                //设置POST的数据类型和长度
                request.ContentType = "text/xml";
                byte[] data = System.Text.Encoding.UTF8.GetBytes(xml);
                request.ContentLength = data.Length;

                //是否使用证书
                if (isUseCert)
                {
                    string path = HttpContext.Current.Request.PhysicalApplicationPath;
                    X509Certificate2 cert = new X509Certificate2(path + WxPayConfig.SSLCERT_PATH, WxPayConfig.SSLCERT_PASSWORD);
                    request.ClientCertificates.Add(cert);
                    Log.Debug("WxPayApi", "PostXml used cert");
                }

                //往服务器写入数据
                reqStream = request.GetRequestStream();
                reqStream.Write(data, 0, data.Length);
                reqStream.Close();

                //获取服务端返回
                response = (HttpWebResponse)request.GetResponse();

                //获取服务端返回数据
                StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
                result = sr.ReadToEnd().Trim();
                sr.Close();
            }
            catch (System.Threading.ThreadAbortException e)
            {
                Log.Error("HttpService", "Thread - caught ThreadAbortException - resetting.");
                Log.Error("Exception message: {0}", e.Message);
                System.Threading.Thread.ResetAbort();
            }
            catch (WebException e)
            {
                Log.Error("HttpService", e.ToString());
                if (e.Status == WebExceptionStatus.ProtocolError)
                {
                   
                }
                throw new WxPayException(e.ToString());
            }
            catch (Exception e)
            {
                Log.Error("HttpService", e.ToString());
                throw new WxPayException(e.ToString());
            }
            finally
            {
                //关闭连接和流
                if (response != null)
                {
                    response.Close();
                }
                if(request != null)
                {
                    request.Abort();
                }
            }
            return result;
        }

ok,我们提交我们的统一下单请求

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

等待返回结果,如果返回状态码return_code为SUCCESS,并且return_msg为Ok,则我们会得到以下返回结果:

<xml>
   <return_code><![CDATA[SUCCESS]]></return_code>
   <return_msg><![CDATA[OK]]></return_msg>
   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
   <mch_id><![CDATA[10000100]]></mch_id>
   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
   <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
   <result_code><![CDATA[SUCCESS]]></result_code>
   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
   <trade_type><![CDATA[JSAPI]]></trade_type>
</xml>

如果返回的xml中的result_code为SUCCESS,则我们可以得到prepay_id;

该prepay_id则为js支付请求中的package字段需要填写的字段。

以上代码仅供参考,没有对代码进行测试,有什么疑问可以随时评论