微信JS支付以及微信商户提现(一)
最近有小伙伴问我,微信js支付我应该怎么做,在网上搜了各种文档,结合了微信JSAPI支付文档依然无法实现微信客户支付至商户端的业务。最近刚好有类似业务,趁机总结一下。
其实我认为微信内H5调起支付的文档微信写的还算清晰明了。
按照要求我们新建一个Html页面。
打开JSAPI接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
打开文档后,我们可以很清晰的看到,在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字段需要填写的字段。
以上代码仅供参考,没有对代码进行测试,有什么疑问可以随时评论