.Net后台实现微信小程序支付
最近一直再研究微信支付和支付宝支付,官方支付文档中一直在讲与第三方支付打交道的原理,却没有介绍我们自己项目中的APP与后台该怎么交互(哈哈,人家也没必要介绍这一块)。拜读了官方文档和前辈们的佳作,自己在这里做一些总结。
不管是微信支付还是支付宝支付,使用的是小程序、APP或网页都可以用以下示例图来说明。
支付流程:
① 支付端将订单号(小程序中还需要传递登录凭证)传递至后台商户。
②后台验证订单、统计订单总价,请求第三方获取下单参数。
③第三方返回下单参数。
④后台将从第三方返回的参数按需要返回至支付端。
⑤支付端拿着后台返回的参数下单。
⑥第三方返回支付结果。
⑦支付成功后,第三方发起支付回调通知商户后台,在这一步,商户可在回调中修改订单以及用户的相关支付状态。
微信小程序支付:
先放一张官方的图。
具体实现:
新建App.Pay项目,在新项目中新建Log类,记录操作过程中的日志。
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Text; 6 using System.Threading.Tasks; 7 using System.Web; 8 9 namespace App.Pay 10 { 11 public class Log 12 { 13 //在网站根目录下创建日志目录 14 public string path; 15 16 public Log(string path) 17 { 18 this.path = HttpContext.Current.Request.PhysicalApplicationPath + path; 19 } 20 /** 21 * 向日志文件写入调试信息 22 * @param className 类名 23 * @param content 写入内容 24 */ 25 public void Debug(string className, string content) 26 { 27 WriteLog("DEBUG", className, content); 28 } 29 30 /** 31 * 向日志文件写入运行时信息 32 * @param className 类名 33 * @param content 写入内容 34 */ 35 public void Info(string className, string content) 36 { 37 WriteLog("INFO", className, content); 38 } 39 40 /** 41 * 向日志文件写入出错信息 42 * @param className 类名 43 * @param content 写入内容 44 */ 45 public void Error(string className, string content) 46 { 47 WriteLog("ERROR", className, content); 48 } 49 50 /** 51 * 实际的写日志操作 52 * @param type 日志记录类型 53 * @param className 类名 54 * @param content 写入内容 55 */ 56 protected void WriteLog(string type, string className, string content) 57 { 58 if (!Directory.Exists(path))//如果日志目录不存在就创建 59 { 60 Directory.CreateDirectory(path); 61 } 62 63 string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");//获取当前系统时间 64 string filename = path + "/" + DateTime.Now.ToString("yyyy-MM-dd") + ".log";//用日期对日志文件命名 65 66 //创建或打开日志文件,向日志文件末尾追加记录 67 StreamWriter mySw = File.AppendText(filename); 68 69 //向日志文件写入内容 70 string write_content = time + " " + type + " " + className + ": " + content; 71 mySw.WriteLine(write_content); 72 73 //关闭日志文件 74 mySw.Close(); 75 } 76 } 77 }
新建WePay文件夹,新建Config基类,存放微信支付的公共配置参数。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace App.Pay.WePay 8 { 9 /** 10 * 配置账号信息 11 */ 12 public class WePayConfig 13 { 14 //=======【商户系统后台机器IP】===================================== 15 /* 此参数可手动配置也可在程序中自动获取 16 */ 17 public const string IP = "8.8.8.8"; 18 19 20 //=======【代理服务器设置】=================================== 21 /* 默认IP和端口号分别为0.0.0.0和0,此时不开启代理(如有需要才设置) 22 */ 23 public const string PROXY_URL = ""; 24 25 //=======【上报信息配置】=================================== 26 /* 测速上报等级,0.关闭上报; 1.仅错误时上报; 2.全量上报 27 */ 28 public const int REPORT_LEVENL = 1; 29 30 //=======【日志级别】=================================== 31 /* 日志等级,0.不输出日志;1.只输出错误信息; 2.输出错误和正常信息; 3.输出错误信息、正常信息和调试信息 32 */ 33 public const int LOG_LEVENL = 3; 34 } 35 }
新建Exception类,捕获微信支付过程中的异常。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace App.Pay.WePay 8 { 9 public class WePayException : Exception 10 { 11 public WePayException(string msg) : base(msg) 12 { 13 14 } 15 } 16 }
新建SafeXMLDocument类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Xml; 7 8 namespace App.Pay.WePay 9 { 10 public class SafeXmlDocument : XmlDocument 11 { 12 public SafeXmlDocument() 13 { 14 this.XmlResolver = null; 15 } 16 } 17 }
新建WeHelper类,目前只有一个方法,微信小程序支付中将登录凭证转换为openId。
1 using App.Common.Extension; 2 using Newtonsoft.Json.Linq; 3 using System; 4 using System.Collections.Generic; 5 using System.Configuration; 6 using System.Linq; 7 using System.Net; 8 using System.Security.Cryptography; 9 using System.Text; 10 using System.Threading.Tasks; 11 12 namespace App.Pay.WePay 13 { 14 public class WeHelper 15 { 16 // 小程序 17 private static string _appid = ConfigurationManager.AppSettings["wxAPPID"]; 18 // 小程序 19 private static string _appSecret = ConfigurationManager.AppSettings["wxAppSecret"]; 20 21 public static WxSession Code2Session(string code) 22 { 23 var url = $"https://api.weixin.qq.com/sns/jscode2session?appid={_appid}&secret={_appSecret}&js_code={code}&grant_type=authorization_code"; 24 try 25 { 26 var request = WebRequest.Create(url); 27 using (var response = request.GetResponse()) 28 { 29 using (var rs = response.GetResponseStream()) 30 { 31 using (var s = new System.IO.StreamReader(rs)) 32 { 33 return s.ReadToEnd().JsonTo<WxSession>(); 34 } 35 } 36 } 37 } 38 catch (Exception) 39 { 40 return null; 41 } 42 } 43 } 44 45 public class WxSession 46 { 47 public string openid { get; set; } 48 public string session_key { get; set; } 49 public string errcode { get; set; } 50 public string errMsg { get; set; } 51 public string unionid { get; set; } 52 } 53 54 }
以上四个类是微信支付通用的,因此统一放在了微信支付文件夹下。
新建XcxPay文件夹,用于存放微信小程序支付的文件,新建XcxPayConfig类,存放关于小程序支付参数,小程序APPID、账号Secert、商户号、商户支付密钥、支付回调地址。我把这些参数值都放在了解决方案的config配置文件中。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Web.Configuration; 7 8 namespace App.Pay.WePay.XcxPay 9 { 10 public class XcxPayConfig : WePayConfig 11 { 12 //=======【基本信息设置】===================================== 13 /* 微信公众号信息配置 14 * APPID:绑定支付的APPID(必须配置) 15 * MCHID:商户号(必须配置) 16 * KEY:商户支付密钥,参考开户邮件设置(必须配置) 17 * APPSECRET:公众帐号secert(仅JSAPI支付的时候需要配置) 18 */ 19 /// 小程序支付 20 public static string APPID = WebConfigurationManager.AppSettings["XcxAppID"].ToString(); 21 public static string MCHID = WebConfigurationManager.AppSettings["XcxMchID"].ToString(); 22 public static string KEY = WebConfigurationManager.AppSettings["XcxKey"].ToString(); 23 public static string APPSECRET = WebConfigurationManager.AppSettings["XcxAppSecret"].ToString(); 24 25 //=======【证书路径设置】===================================== 26 /* 证书路径,注意应该填写绝对路径(仅退款、撤销订单时需要) 27 */ 28 public const string SSLCERT_PATH = "cert/apiclient_cert.p12"; 29 public const string SSLCERT_PASSWORD = "1233410002"; 30 31 //=======【支付结果通知url】===================================== 32 /* 支付结果通知回调url,用于商户接收支付结果 33 */ 34 public static string NOTIFY_URL = WebConfigurationManager.AppSettings["XcxNotifyUrl"].ToString(); 35 36 // log记录 37 public static string LogPath = WebConfigurationManager.AppSettings["XcxLog"].ToString(); 38 } 39 }
<!--小程序支付--> <add key="XcxAppID" value="" /> <add key="XcxAppSecret" value="" /> <add key="XcxMchID" value="" /> <add key="XcxKey" value="" /> <!--回调通知--> <add key="XcxNotifyUrl" value="" />
新建WeXcxPayApi类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace App.Pay.WePay.XcxPay 8 { 9 public class XcxPayApi 10 { 11 public static Log Log = new Log(XcxPayConfig.LogPath); 12 13 /** 14 * 提交被扫支付API 15 * 收银员使用扫码设备读取微信用户刷卡授权码以后,二维码或条码信息传送至商户收银台, 16 * 由商户收银台或者商户后台调用该接口发起支付。 17 * @param WxPayData inputObj 提交给被扫支付API的参数 18 * @param int timeOut 超时时间 19 * @throws WePayException 20 * @return 成功时返回调用结果,其他抛异常 21 */ 22 public static XcxPayData Micropay(XcxPayData inputObj, int timeOut = 10) 23 { 24 string url = "https://api.mch.weixin.qq.com/pay/micropay"; 25 //检测必填参数 26 if (!inputObj.IsSet("body")) 27 { 28 throw new WePayException("提交被扫支付API接口中,缺少必填参数body!"); 29 } 30 else if (!inputObj.IsSet("out_trade_no")) 31 { 32 throw new WePayException("提交被扫支付API接口中,缺少必填参数out_trade_no!"); 33 } 34 else if (!inputObj.IsSet("total_fee")) 35 { 36 throw new WePayException("提交被扫支付API接口中,缺少必填参数total_fee!"); 37 } 38 else if (!inputObj.IsSet("auth_code")) 39 { 40 throw new WePayException("提交被扫支付API接口中,缺少必填参数auth_code!"); 41 } 42 43 inputObj.SetValue("spbill_create_ip", WePayConfig.IP);//终端ip 44 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 45 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 46 inputObj.SetValue("nonce_str", Guid.NewGuid().ToString().Replace("-", ""));//随机字符串 47 inputObj.SetValue("sign", inputObj.MakeSign());//签名 48 string xml = inputObj.ToXml(); 49 50 var start = DateTime.Now;//请求开始时间 51 52 Log.Info("XcxPayApi", "MicroPay request : " + xml); 53 string response = XcxPayHttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口以提交数据到API 54 Log.Info("XcxPayApi", "MicroPay response : " + response); 55 56 var end = DateTime.Now; 57 int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时 58 59 //将xml格式的结果转换为对象以返回 60 XcxPayData result = new XcxPayData(); 61 result.FromXml(response); 62 63 ReportCostTime(url, timeCost, result);//测速上报 64 65 return result; 66 } 67 68 69 /** 70 * 71 * 查询订单 72 * @param WxPayData inputObj 提交给查询订单API的参数 73 * @param int timeOut 超时时间 74 * @throws WePayException 75 * @return 成功时返回订单查询结果,其他抛异常 76 */ 77 public static XcxPayData OrderQuery(XcxPayData inputObj, int timeOut = 6) 78 { 79 string url = "https://api.mch.weixin.qq.com/pay/orderquery"; 80 //检测必填参数 81 if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id")) 82 { 83 throw new WePayException("订单查询接口中,out_trade_no、transaction_id至少填一个!"); 84 } 85 86 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 87 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 88 inputObj.SetValue("nonce_str", XcxPayApi.GenerateNonceStr());//随机字符串 89 inputObj.SetValue("sign", inputObj.MakeSign());//签名 90 91 string xml = inputObj.ToXml(); 92 93 var start = DateTime.Now; 94 95 Log.Info("XcxPayApi", "OrderQuery request : " + xml); 96 string response = XcxPayHttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口提交数据 97 Log.Info("XcxPayApi", "OrderQuery response : " + response); 98 99 var end = DateTime.Now; 100 int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时 101 102 //将xml格式的数据转化为对象以返回 103 XcxPayData result = new XcxPayData(); 104 result.FromXml(response); 105 106 ReportCostTime(url, timeCost, result);//测速上报 107 108 return result; 109 } 110 111 112 /** 113 * 114 * 撤销订单API接口 115 * @param WxPayData inputObj 提交给撤销订单API接口的参数,out_trade_no和transaction_id必填一个 116 * @param int timeOut 接口超时时间 117 * @throws WePayException 118 * @return 成功时返回API调用结果,其他抛异常 119 */ 120 public static XcxPayData Reverse(XcxPayData inputObj, int timeOut = 6) 121 { 122 string url = "https://api.mch.weixin.qq.com/secapi/pay/reverse"; 123 //检测必填参数 124 if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id")) 125 { 126 throw new WePayException("撤销订单API接口中,参数out_trade_no和transaction_id必须填写一个!"); 127 } 128 129 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 130 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 131 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 132 inputObj.SetValue("sign", inputObj.MakeSign());//签名 133 string xml = inputObj.ToXml(); 134 135 var start = DateTime.Now;//请求开始时间 136 137 Log.Info("XcxPayApi", "Reverse request : " + xml); 138 139 string response = XcxPayHttpService.Post(xml, url, true, timeOut); 140 141 Log.Info("XcxPayApi", "Reverse response : " + response); 142 143 var end = DateTime.Now; 144 int timeCost = (int)((end - start).TotalMilliseconds); 145 146 XcxPayData result = new XcxPayData(); 147 result.FromXml(response); 148 149 ReportCostTime(url, timeCost, result);//测速上报 150 151 return result; 152 } 153 154 155 /** 156 * 157 * 申请退款 158 * @param WxPayData inputObj 提交给申请退款API的参数 159 * @param int timeOut 超时时间 160 * @throws WePayException 161 * @return 成功时返回接口调用结果,其他抛异常 162 */ 163 public static XcxPayData Refund(XcxPayData inputObj, int timeOut = 6) 164 { 165 string url = "https://api.mch.weixin.qq.com/secapi/pay/refund"; 166 //检测必填参数 167 if (!inputObj.IsSet("out_trade_no") && !inputObj.IsSet("transaction_id")) 168 { 169 throw new WePayException("退款申请接口中,out_trade_no、transaction_id至少填一个!"); 170 } 171 else if (!inputObj.IsSet("out_refund_no")) 172 { 173 throw new WePayException("退款申请接口中,缺少必填参数out_refund_no!"); 174 } 175 else if (!inputObj.IsSet("total_fee")) 176 { 177 throw new WePayException("退款申请接口中,缺少必填参数total_fee!"); 178 } 179 else if (!inputObj.IsSet("refund_fee")) 180 { 181 throw new WePayException("退款申请接口中,缺少必填参数refund_fee!"); 182 } 183 else if (!inputObj.IsSet("op_user_id")) 184 { 185 throw new WePayException("退款申请接口中,缺少必填参数op_user_id!"); 186 } 187 188 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 189 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 190 inputObj.SetValue("nonce_str", Guid.NewGuid().ToString().Replace("-", ""));//随机字符串 191 inputObj.SetValue("sign", inputObj.MakeSign());//签名 192 193 string xml = inputObj.ToXml(); 194 var start = DateTime.Now; 195 196 Log.Info("XcxPayApi", "Refund request : " + xml); 197 string response = XcxPayHttpService.Post(xml, url, true, timeOut);//调用HTTP通信接口提交数据到API 198 Log.Info("XcxPayApi", "Refund response : " + response); 199 200 var end = DateTime.Now; 201 int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时 202 203 //将xml格式的结果转换为对象以返回 204 XcxPayData result = new XcxPayData(); 205 result.FromXml(response); 206 207 ReportCostTime(url, timeCost, result);//测速上报 208 209 return result; 210 } 211 212 213 /** 214 * 215 * 查询退款 216 * 提交退款申请后,通过该接口查询退款状态。退款有一定延时, 217 * 用零钱支付的退款20分钟内到账,银行卡支付的退款3个工作日后重新查询退款状态。 218 * out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个 219 * @param WxPayData inputObj 提交给查询退款API的参数 220 * @param int timeOut 接口超时时间 221 * @throws WePayException 222 * @return 成功时返回,其他抛异常 223 */ 224 public static XcxPayData RefundQuery(XcxPayData inputObj, int timeOut = 6) 225 { 226 string url = "https://api.mch.weixin.qq.com/pay/refundquery"; 227 //检测必填参数 228 if (!inputObj.IsSet("out_refund_no") && !inputObj.IsSet("out_trade_no") && 229 !inputObj.IsSet("transaction_id") && !inputObj.IsSet("refund_id")) 230 { 231 throw new WePayException("退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个!"); 232 } 233 234 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 235 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 236 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 237 inputObj.SetValue("sign", inputObj.MakeSign());//签名 238 239 string xml = inputObj.ToXml(); 240 241 var start = DateTime.Now;//请求开始时间 242 243 Log.Info("XcxPayApi", "RefundQuery request : " + xml); 244 string response = XcxPayHttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口以提交数据到API 245 Log.Info("XcxPayApi", "RefundQuery response : " + response); 246 247 var end = DateTime.Now; 248 int timeCost = (int)((end - start).TotalMilliseconds);//获得接口耗时 249 250 //将xml格式的结果转换为对象以返回 251 XcxPayData result = new XcxPayData(); 252 result.FromXml(response); 253 254 ReportCostTime(url, timeCost, result);//测速上报 255 256 return result; 257 } 258 259 260 /** 261 * 下载对账单 262 * @param WxPayData inputObj 提交给下载对账单API的参数 263 * @param int timeOut 接口超时时间 264 * @throws WePayException 265 * @return 成功时返回,其他抛异常 266 */ 267 public static XcxPayData DownloadBill(XcxPayData inputObj, int timeOut = 6) 268 { 269 string url = "https://api.mch.weixin.qq.com/pay/downloadbill"; 270 //检测必填参数 271 if (!inputObj.IsSet("bill_date")) 272 { 273 throw new WePayException("对账单接口中,缺少必填参数bill_date!"); 274 } 275 276 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 277 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 278 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 279 inputObj.SetValue("sign", inputObj.MakeSign());//签名 280 281 string xml = inputObj.ToXml(); 282 283 Log.Info("XcxPayApi", "DownloadBill request : " + xml); 284 string response = XcxPayHttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口以提交数据到API 285 Log.Info("XcxPayApi", "DownloadBill result : " + response); 286 287 XcxPayData result = new XcxPayData(); 288 //若接口调用失败会返回xml格式的结果 289 if (response.Substring(0, 5) == "<xml>") 290 { 291 result.FromXml(response); 292 } 293 //接口调用成功则返回非xml格式的数据 294 else 295 result.SetValue("result", response); 296 297 return result; 298 } 299 300 301 /** 302 * 303 * 转换短链接 304 * 该接口主要用于扫码原生支付模式一中的二维码链接转成短链接(weixin://wxpay/s/XXXXXX), 305 * 减小二维码数据量,提升扫描速度和精确度。 306 * @param WxPayData inputObj 提交给转换短连接API的参数 307 * @param int timeOut 接口超时时间 308 * @throws WePayException 309 * @return 成功时返回,其他抛异常 310 */ 311 public static XcxPayData ShortUrl(XcxPayData inputObj, int timeOut = 6) 312 { 313 string url = "https://api.mch.weixin.qq.com/tools/shorturl"; 314 //检测必填参数 315 if (!inputObj.IsSet("long_url")) 316 { 317 throw new WePayException("需要转换的URL,签名用原串,传输需URL encode!"); 318 } 319 320 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 321 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 322 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 323 inputObj.SetValue("sign", inputObj.MakeSign());//签名 324 inputObj.SetValue("device_info", "wxAPP");//设备名称 325 string xml = inputObj.ToXml(); 326 327 var start = DateTime.Now;//请求开始时间 328 329 Log.Info("XcxPayApi", "ShortUrl request : " + xml); 330 string response = XcxPayHttpService.Post(xml, url, false, timeOut); 331 Log.Info("XcxPayApi", "ShortUrl response : " + response); 332 333 var end = DateTime.Now; 334 int timeCost = (int)((end - start).TotalMilliseconds); 335 336 XcxPayData result = new XcxPayData(); 337 result.FromXml(response); 338 ReportCostTime(url, timeCost, result);//测速上报 339 340 return result; 341 } 342 343 344 /** 345 * 346 * 统一下单 347 * @param WxPaydata inputObj 提交给统一下单API的参数 348 * @param int timeOut 超时时间 349 * @throws WePayException 350 * @return 成功时返回,其他抛异常 351 */ 352 public static XcxPayData UnifiedOrder(XcxPayData inputObj, int timeOut = 6) 353 { 354 string url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; 355 //检测必填参数 356 if (!inputObj.IsSet("out_trade_no")) 357 { 358 throw new WePayException("缺少统一支付接口必填参数out_trade_no!"); 359 } 360 else if (!inputObj.IsSet("body")) 361 { 362 throw new WePayException("缺少统一支付接口必填参数body!"); 363 } 364 else if (!inputObj.IsSet("total_fee")) 365 { 366 throw new WePayException("缺少统一支付接口必填参数total_fee!"); 367 } 368 else if (!inputObj.IsSet("trade_type")) 369 { 370 throw new WePayException("缺少统一支付接口必填参数trade_type!"); 371 } 372 373 //关联参数 374 if (inputObj.GetValue("trade_type").ToString() == "JSAPI" && !inputObj.IsSet("openid")) 375 { 376 throw new WePayException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!"); 377 } 378 if (inputObj.GetValue("trade_type").ToString() == "NATIVE" && !inputObj.IsSet("product_id")) 379 { 380 throw new WePayException("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!"); 381 } 382 383 //异步通知url未设置,则使用配置文件中的url 384 if (!inputObj.IsSet("notify_url")) 385 { 386 inputObj.SetValue("notify_url", XcxPayConfig.NOTIFY_URL);//异步通知url 387 } 388 389 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 390 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 391 inputObj.SetValue("spbill_create_ip", WePayConfig.IP);//终端ip 392 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 393 394 //签名 395 inputObj.SetValue("sign", inputObj.MakeSign()); 396 string xml = inputObj.ToXml(); 397 398 var start = DateTime.Now; 399 400 Log.Info("XcxPayApi", "UnfiedOrder request : " + xml); 401 string response = XcxPayHttpService.Post(xml, url, false, timeOut); 402 Log.Info("XcxPayApi", "UnfiedOrder response : " + response); 403 404 var end = DateTime.Now; 405 int timeCost = (int)((end - start).TotalMilliseconds); 406 407 XcxPayData result = new XcxPayData(); 408 result.FromXml(response); 409 410 ReportCostTime(url, timeCost, result);//测速上报 411 412 return result; 413 } 414 415 /** 416 * 417 * 统一下单 418 * @param WxPaydata inputObj 提交给统一下单API的参数 419 * @param int timeOut 超时时间 420 * @throws WePayException 421 * @return 成功时返回,其他抛异常 422 */ 423 public static XcxPayData UnifiedOrderApp(XcxPayData inputObj, int timeOut = 6) 424 { 425 string url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; 426 //检测必填参数 427 if (!inputObj.IsSet("out_trade_no")) 428 { 429 throw new WePayException("缺少统一支付接口必填参数out_trade_no!"); 430 } 431 else if (!inputObj.IsSet("body")) 432 { 433 throw new WePayException("缺少统一支付接口必填参数body!"); 434 } 435 else if (!inputObj.IsSet("total_fee")) 436 { 437 throw new WePayException("缺少统一支付接口必填参数total_fee!"); 438 } 439 else if (!inputObj.IsSet("trade_type")) 440 { 441 throw new WePayException("缺少统一支付接口必填参数trade_type!"); 442 } 443 444 //关联参数 445 if (inputObj.GetValue("trade_type").ToString() == "JSAPI" && !inputObj.IsSet("openid")) 446 { 447 throw new WePayException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!"); 448 } 449 if (inputObj.GetValue("trade_type").ToString() == "NATIVE" && !inputObj.IsSet("product_id")) 450 { 451 throw new WePayException("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!"); 452 } 453 454 //异步通知url未设置,则使用配置文件中的url 455 if (!inputObj.IsSet("notify_url")) 456 { 457 inputObj.SetValue("notify_url", XcxPayConfig.NOTIFY_URL);//异步通知url 458 } 459 460 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 461 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 462 inputObj.SetValue("spbill_create_ip", WePayConfig.IP);//终端ip 463 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 464 465 //签名 466 inputObj.SetValue("sign", inputObj.MakeSign()); 467 string xml = inputObj.ToXml(); 468 469 var start = DateTime.Now; 470 471 Log.Info("XcxPayApi", "UnfiedOrder request : " + xml); 472 string response = XcxPayHttpService.Post(xml, url, false, timeOut); 473 Log.Info("XcxPayApi", "UnfiedOrder response : " + response); 474 475 var end = DateTime.Now; 476 int timeCost = (int)((end - start).TotalMilliseconds); 477 478 XcxPayData result = new XcxPayData(); 479 result.FromXml(response); 480 481 ReportCostTime(url, timeCost, result);//测速上报 482 483 return result; 484 } 485 486 487 /** 488 * 489 * 关闭订单 490 * @param WxPayData inputObj 提交给关闭订单API的参数 491 * @param int timeOut 接口超时时间 492 * @throws WePayException 493 * @return 成功时返回,其他抛异常 494 */ 495 public static XcxPayData CloseOrder(XcxPayData inputObj, int timeOut = 6) 496 { 497 string url = "https://api.mch.weixin.qq.com/pay/closeorder"; 498 //检测必填参数 499 if (!inputObj.IsSet("out_trade_no")) 500 { 501 throw new WePayException("关闭订单接口中,out_trade_no必填!"); 502 } 503 504 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 505 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 506 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 507 inputObj.SetValue("sign", inputObj.MakeSign());//签名 508 string xml = inputObj.ToXml(); 509 510 var start = DateTime.Now;//请求开始时间 511 512 string response = XcxPayHttpService.Post(xml, url, false, timeOut); 513 514 var end = DateTime.Now; 515 int timeCost = (int)((end - start).TotalMilliseconds); 516 517 XcxPayData result = new XcxPayData(); 518 result.FromXml(response); 519 520 ReportCostTime(url, timeCost, result);//测速上报 521 522 return result; 523 } 524 525 526 /** 527 * 528 * 测速上报 529 * @param string interface_url 接口URL 530 * @param int timeCost 接口耗时 531 * @param WxPayData inputObj参数数组 532 */ 533 private static void ReportCostTime(string interface_url, int timeCost, XcxPayData inputObj) 534 { 535 //如果不需要进行上报 536 if (WePayConfig.REPORT_LEVENL == 0) 537 { 538 return; 539 } 540 541 //如果仅失败上报 542 if (WePayConfig.REPORT_LEVENL == 1 && inputObj.IsSet("return_code") && inputObj.GetValue("return_code").ToString() == "SUCCESS" && 543 inputObj.IsSet("result_code") && inputObj.GetValue("result_code").ToString() == "SUCCESS") 544 { 545 return; 546 } 547 548 //上报逻辑 549 XcxPayData data = new XcxPayData(); 550 data.SetValue("interface_url", interface_url); 551 data.SetValue("execute_time_", timeCost); 552 //返回状态码 553 if (inputObj.IsSet("return_code")) 554 { 555 data.SetValue("return_code", inputObj.GetValue("return_code")); 556 } 557 //返回信息 558 if (inputObj.IsSet("return_msg")) 559 { 560 data.SetValue("return_msg", inputObj.GetValue("return_msg")); 561 } 562 //业务结果 563 if (inputObj.IsSet("result_code")) 564 { 565 data.SetValue("result_code", inputObj.GetValue("result_code")); 566 } 567 //错误代码 568 if (inputObj.IsSet("err_code")) 569 { 570 data.SetValue("err_code", inputObj.GetValue("err_code")); 571 } 572 //错误代码描述 573 if (inputObj.IsSet("err_code_des")) 574 { 575 data.SetValue("err_code_des", inputObj.GetValue("err_code_des")); 576 } 577 //商户订单号 578 if (inputObj.IsSet("out_trade_no")) 579 { 580 data.SetValue("out_trade_no", inputObj.GetValue("out_trade_no")); 581 } 582 //设备号 583 if (inputObj.IsSet("device_info")) 584 { 585 data.SetValue("device_info", inputObj.GetValue("device_info")); 586 } 587 588 try 589 { 590 Report(data); 591 } 592 catch (WePayException ex) 593 { 594 //不做任何处理 595 } 596 } 597 598 599 /** 600 * 601 * 测速上报接口实现 602 * @param WxPayData inputObj 提交给测速上报接口的参数 603 * @param int timeOut 测速上报接口超时时间 604 * @throws WePayException 605 * @return 成功时返回测速上报接口返回的结果,其他抛异常 606 */ 607 public static XcxPayData Report(XcxPayData inputObj, int timeOut = 1) 608 { 609 string url = "https://api.mch.weixin.qq.com/payitil/report"; 610 //检测必填参数 611 if (!inputObj.IsSet("interface_url")) 612 { 613 throw new WePayException("接口URL,缺少必填参数interface_url!"); 614 } 615 if (!inputObj.IsSet("return_code")) 616 { 617 throw new WePayException("返回状态码,缺少必填参数return_code!"); 618 } 619 if (!inputObj.IsSet("result_code")) 620 { 621 throw new WePayException("业务结果,缺少必填参数result_code!"); 622 } 623 if (!inputObj.IsSet("user_ip")) 624 { 625 throw new WePayException("访问接口IP,缺少必填参数user_ip!"); 626 } 627 if (!inputObj.IsSet("execute_time_")) 628 { 629 throw new WePayException("接口耗时,缺少必填参数execute_time_!"); 630 } 631 632 inputObj.SetValue("appid", XcxPayConfig.APPID);//公众账号ID 633 inputObj.SetValue("mch_id", XcxPayConfig.MCHID);//商户号 634 inputObj.SetValue("user_ip", WePayConfig.IP);//终端ip 635 inputObj.SetValue("time", DateTime.Now.ToString("yyyyMMddHHmmss"));//商户上报时间 636 inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 637 inputObj.SetValue("sign", inputObj.MakeSign());//签名 638 string xml = inputObj.ToXml(); 639 640 Log.Info("XcxPayApi", "Report request : " + xml); 641 642 string response = XcxPayHttpService.Post(xml, url, false, timeOut); 643 644 Log.Info("XcxPayApi", "Report response : " + response); 645 646 XcxPayData result = new XcxPayData(); 647 result.FromXml(response); 648 return result; 649 } 650 651 /** 652 * 根据当前系统时间加随机序列来生成订单号 653 * @return 订单号 654 */ 655 public static string GenerateOutTradeNo() 656 { 657 var ran = new Random(); 658 return string.Format("{0}{1}{2}", XcxPayConfig.MCHID, DateTime.Now.ToString("yyyyMMddHHmmss"), ran.Next(999)); 659 } 660 661 /** 662 * 生成时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数 663 * @return 时间戳 664 */ 665 public static string GenerateTimeStamp() 666 { 667 TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); 668 return Convert.ToInt64(ts.TotalSeconds).ToString(); 669 } 670 671 /** 672 * 生成随机串,随机串包含字母或数字 673 * @return 随机串 674 */ 675 public static string GenerateNonceStr() 676 { 677 return Guid.NewGuid().ToString().Replace("-", ""); 678 } 679 } 680 }
新建XcxPayData类
1 using LitJson; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Security.Cryptography; 6 using System.Text; 7 using System.Threading.Tasks; 8 using System.Xml; 9 10 namespace App.Pay.WePay.XcxPay 11 { 12 /// <summary> 13 /// 微信支付协议接口数据类,所有的API接口通信都依赖这个数据结构, 14 /// 在调用接口之前先填充各个字段的值,然后进行接口通信, 15 /// 这样设计的好处是可扩展性强,用户可随意对协议进行更改而不用重新设计数据结构, 16 /// 还可以随意组合出不同的协议数据包,不用为每个协议设计一个数据包结构 17 /// </summary> 18 public class XcxPayData 19 { 20 private Log Log = new Log(XcxPayConfig.LogPath); 21 22 public XcxPayData() 23 { 24 } 25 26 //采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序 27 private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>(); 28 29 /** 30 * 设置某个字段的值 31 * @param key 字段名 32 * @param value 字段值 33 */ 34 public void SetValue(string key, object value) 35 { 36 m_values[key] = value; 37 } 38 39 /** 40 * 根据字段名获取某个字段的值 41 * @param key 字段名 42 * @return key对应的字段值 43 */ 44 public object GetValue(string key) 45 { 46 object o = null; 47 m_values.TryGetValue(key, out o); 48 return o; 49 } 50 51 /** 52 * 判断某个字段是否已设置 53 * @param key 字段名 54 * @return 若字段key已被设置,则返回true,否则返回false 55 */ 56 public bool IsSet(string key) 57 { 58 object o = null; 59 m_values.TryGetValue(key, out o); 60 if (null != o) 61 return true; 62 else 63 return false; 64 } 65 66 /** 67 * @将Dictionary转成xml 68 * @return 经转换得到的xml串 69 * @throws WePayException 70 **/ 71 public string ToXml() 72 { 73 //数据为空时不能转化为xml格式 74 if (0 == m_values.Count) 75 { 76 Log.Error(this.GetType().ToString(), "WxPayData数据为空!"); 77 throw new WePayException("WxPayData数据为空!"); 78 } 79 80 string xml = "<xml>"; 81 foreach (KeyValuePair<string, object> pair in m_values) 82 { 83 //字段值不能为null,会影响后续流程 84 if (pair.Value == null) 85 { 86 Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!"); 87 throw new WePayException("WxPayData内部含有值为null的字段!"); 88 } 89 90 if (pair.Value.GetType() == typeof(int)) 91 { 92 xml += "<" + pair.Key + ">" + pair.Value + "</" + pair.Key + ">"; 93 } 94 else if (pair.Value.GetType() == typeof(string)) 95 { 96 xml += "<" + pair.Key + ">" + "<![CDATA[" + pair.Value + "]]></" + pair.Key + ">"; 97 } 98 else//除了string和int类型不能含有其他数据类型 99 { 100 Log.Error(this.GetType().ToString(), "WxPayData字段数据类型错误!"); 101 throw new WePayException("WxPayData字段数据类型错误!"); 102 } 103 } 104 xml += "</xml>"; 105 return xml; 106 } 107 108 /** 109 * @将xml转为WxPayData对象并返回对象内部的数据 110 * @param string 待转换的xml串 111 * @return 经转换得到的Dictionary 112 * @throws WePayException 113 */ 114 public SortedDictionary<string, object> FromXml(string xml) 115 { 116 if (string.IsNullOrEmpty(xml)) 117 { 118 Log.Error(this.GetType().ToString(), "将空的xml串转换为WxPayData不合法!"); 119 throw new WePayException("将空的xml串转换为WxPayData不合法!"); 120 } 121 122 SafeXmlDocument xmlDoc = new SafeXmlDocument(); 123 xmlDoc.LoadXml(xml); 124 XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml> 125 XmlNodeList nodes = xmlNode.ChildNodes; 126 foreach (XmlNode xn in nodes) 127 { 128 XmlElement xe = (XmlElement)xn; 129 m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中 130 } 131 132 try 133 { 134 //2015-06-29 错误是没有签名 135 if (m_values["return_code"] != "SUCCESS") 136 { 137 return m_values; 138 } 139 CheckSign();//验证签名,不通过会抛异常 140 } 141 catch (WePayException ex) 142 { 143 throw new WePayException(ex.Message); 144 } 145 146 return m_values; 147 } 148 149 /** 150 * @Dictionary格式转化成url参数格式 151 * @ return url格式串, 该串不包含sign字段值 152 */ 153 public string ToUrl() 154 { 155 string buff = ""; 156 foreach (KeyValuePair<string, object> pair in m_values) 157 { 158 if (pair.Value == null) 159 { 160 Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!"); 161 throw new WePayException("WxPayData内部含有值为null的字段!"); 162 } 163 164 if (pair.Key != "sign" && pair.Value.ToString() != "") 165 { 166 buff += pair.Key + "=" + pair.Value + "&"; 167 } 168 } 169 buff = buff.Trim('&'); 170 return buff; 171 } 172 173 174 /** 175 * @Dictionary格式化成Json 176 * @return json串数据 177 */ 178 public string ToJson() 179 { 180 string jsonStr = JsonMapper.ToJson(m_values); 181 return jsonStr; 182 } 183 184 /** 185 * @values格式化成能在Web页面上显示的结果(因为web页面上不能直接输出xml格式的字符串) 186 */ 187 public string ToPrintStr() 188 { 189 string str = ""; 190 foreach (KeyValuePair<string, object> pair in m_values) 191 { 192 if (pair.Value == null) 193 { 194 Log.Error(this.GetType().ToString(), "WxPayData内部含有值为null的字段!"); 195 throw new WePayException("WxPayData内部含有值为null的字段!"); 196 } 197 198 str += string.Format("{0}={1}<br>", pair.Key, pair.Value.ToString()); 199 } 200 Log.Info(this.GetType().ToString(), "Print in Web Page : " + str); 201 return str; 202 } 203 204 /** 205 * @生成签名,详见签名生成算法 206 * @return 签名, sign字段不参加签名 207 */ 208 public string MakeSign() 209 { 210 //转url格式 211 string str = ToUrl(); 212 //在string后加入API KEY 213 str += "&key=" + XcxPayConfig.KEY; 214 //MD5加密 215 var md5 = MD5.Create(); 216 var bs = md5.ComputeHash(Encoding.UTF8.GetBytes(str)); 217 var sb = new StringBuilder(); 218 foreach (byte b in bs) 219 { 220 sb.Append(b.ToString("x2")); 221 } 222 //所有字符转为大写 223 return sb.ToString().ToUpper(); 224 } 225 226 /** 227 * 228 * 检测签名是否正确 229 * 正确返回true,错误抛异常 230 */ 231 public bool CheckSign() 232 { 233 //如果没有设置签名,则跳过检测 234 if (!IsSet("sign")) 235 { 236 Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!"); 237 throw new WePayException("WxPayData签名存在但不合法!"); 238 } 239 //如果设置了签名但是签名为空,则抛异常 240 else if (GetValue("sign") == null || GetValue("sign").ToString() == "") 241 { 242 Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!"); 243 throw new WePayException("WxPayData签名存在但不合法!"); 244 } 245 246 //获取接收到的签名 247 string return_sign = GetValue("sign").ToString(); 248 249 //在本地计算新的签名 250 string cal_sign = MakeSign(); 251 252 if (cal_sign == return_sign) 253 { 254 return true; 255 } 256 257 Log.Error(this.GetType().ToString(), "WxPayData签名验证错误!"); 258 throw new WePayException("WxPayData签名验证错误!"); 259 } 260 261 /** 262 * @获取Dictionary 263 */ 264 public SortedDictionary<string, object> GetValues() 265 { 266 return m_values; 267 } 268 } 269 }
新建XcxPayHttpService类,封装了POST请求和Get请求,在这里,我们只使用了POST请求。
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Net; 6 using System.Net.Security; 7 using System.Security.Cryptography.X509Certificates; 8 using System.Text; 9 using System.Threading.Tasks; 10 using System.Web; 11 12 namespace App.Pay.WePay.XcxPay 13 { 14 public class XcxPayHttpService 15 { 16 private static Log Log = new Log(XcxPayConfig.LogPath); 17 18 public static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) 19 { 20 //直接确认,否则打不开 21 return true; 22 } 23 24 public static string Post(string xml, string url, bool isUseCert, int timeout) 25 { 26 System.GC.Collect();//垃圾回收,回收没有正常关闭的http连接 27 28 string result = "";//返回结果 29 30 HttpWebRequest request = null; 31 HttpWebResponse response = null; 32 Stream reqStream = null; 33 34 try 35 { 36 //设置最大连接数 37 ServicePointManager.DefaultConnectionLimit = 200; 38 //设置https验证方式 39 if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase)) 40 { 41 ServicePointManager.ServerCertificateValidationCallback = 42 new RemoteCertificateValidationCallback(CheckValidationResult); 43 } 44 45 /*************************************************************** 46 * 下面设置HttpWebRequest的相关属性 47 * ************************************************************/ 48 request = (HttpWebRequest)WebRequest.Create(url); 49 50 request.Method = "POST"; 51 request.Timeout = timeout * 1000; 52 53 //设置代理服务器 54 //WebProxy proxy = new WebProxy(); //定义一个网关对象 55 //proxy.Address = new Uri(WxPayConfig.PROXY_URL); //网关服务器端口:端口 56 //request.Proxy = proxy; 57 58 //设置POST的数据类型和长度 59 request.ContentType = "text/xml"; 60 byte[] data = System.Text.Encoding.UTF8.GetBytes(xml); 61 request.ContentLength = data.Length; 62 63 //是否使用证书 64 if (isUseCert) 65 { 66 string path = HttpContext.Current.Request.PhysicalApplicationPath; 67 X509Certificate2 cert = new X509Certificate2(path + XcxPayConfig.SSLCERT_PATH, XcxPayConfig.SSLCERT_PASSWORD); 68 request.ClientCertificates.Add(cert); 69 Log.Info("XcxPayHttpService", "PostXml used cert"); 70 } 71 72 //往服务器写入数据 73 reqStream = request.GetRequestStream(); 74 reqStream.Write(data, 0, data.Length); 75 reqStream.Close(); 76 77 //获取服务端返回 78 response = (HttpWebResponse)request.GetResponse(); 79 80 //获取服务端返回数据 81 StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8); 82 result = sr.ReadToEnd().Trim(); 83 sr.Close(); 84 } 85 catch (System.Threading.ThreadAbortException e) 86 { 87 Log.Error("XcxPayHttpService", "Thread - caught ThreadAbortException - resetting."); 88 Log.Error("Exception message: {0}", e.Message); 89 System.Threading.Thread.ResetAbort(); 90 } 91 catch (WebException e) 92 { 93 Log.Error("XcxPayHttpService", e.ToString()); 94 if (e.Status == WebExceptionStatus.ProtocolError) 95 { 96 Log.Error("XcxPayHttpService", "StatusCode : " + ((HttpWebResponse)e.Response).StatusCode); 97 Log.Error("XcxPayHttpService", "StatusDescription : " + ((HttpWebResponse)e.Response).StatusDescription); 98 } 99 throw new WePayException(e.ToString()); 100 } 101 catch (Exception e) 102 { 103 Log.Error("XcxPayHttpService", e.ToString()); 104 throw new WePayException(e.ToString()); 105 } 106 finally 107 { 108 //关闭连接和流 109 if (response != null) 110 { 111 response.Close(); 112 } 113 if (request != null) 114 { 115 request.Abort(); 116 } 117 } 118 return result; 119 } 120 121 /// <summary> 122 /// 处理http GET请求,返回数据 123 /// </summary> 124 /// <param name="url">请求的url地址</param> 125 /// <returns>http GET成功后返回的数据,失败抛WebException异常</returns> 126 public static string Get(string url) 127 { 128 System.GC.Collect(); 129 string result = ""; 130 131 HttpWebRequest request = null; 132 HttpWebResponse response = null; 133 134 //请求url以获取数据 135 try 136 { 137 //设置最大连接数 138 ServicePointManager.DefaultConnectionLimit = 200; 139 //设置https验证方式 140 if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase)) 141 { 142 ServicePointManager.ServerCertificateValidationCallback = 143 new RemoteCertificateValidationCallback(CheckValidationResult); 144 } 145 146 /*************************************************************** 147 * 下面设置HttpWebRequest的相关属性 148 * ************************************************************/ 149 request = (HttpWebRequest)WebRequest.Create(url); 150 151 request.Method = "GET"; 152 153 //设置代理 154 //WebProxy proxy = new WebProxy(); 155 //proxy.Address = new Uri(WxPayConfig.PROXY_URL); 156 //request.Proxy = proxy; 157 158 //获取服务器返回 159 response = (HttpWebResponse)request.GetResponse(); 160 161 //获取HTTP返回数据 162 StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8); 163 result = sr.ReadToEnd().Trim(); 164 sr.Close(); 165 } 166 catch (System.Threading.ThreadAbortException e) 167 { 168 Log.Error("XcxPayHttpService", "Thread - caught ThreadAbortException - resetting."); 169 Log.Error("Exception message: {0}", e.Message); 170 System.Threading.Thread.ResetAbort(); 171 } 172 catch (WebException e) 173 { 174 Log.Error("XcxPayHttpService", e.ToString()); 175 if (e.Status == WebExceptionStatus.ProtocolError) 176 { 177 Log.Error("XcxPayHttpService", "StatusCode : " + ((HttpWebResponse)e.Response).StatusCode); 178 Log.Error("XcxPayHttpService", "StatusDescription : " + ((HttpWebResponse)e.Response).StatusDescription); 179 } 180 throw new WePayException(e.ToString()); 181 } 182 catch (Exception e) 183 { 184 Log.Error("XcxPayHttpService", e.ToString()); 185 throw new WePayException(e.ToString()); 186 } 187 finally 188 { 189 //关闭连接和流 190 if (response != null) 191 { 192 response.Close(); 193 } 194 if (request != null) 195 { 196 request.Abort(); 197 } 198 } 199 return result; 200 } 201 } 202 }
新建XcxPayNotify类,回调处理基类,负责接收微信支付后台发送过来的数据,并对数据进行签名验证。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Web; 7 8 namespace App.Pay.WePay.XcxPay 9 { 10 /// <summary> 11 /// 回调处理基类 12 /// 主要负责接收微信支付后台发送过来的数据,对数据进行签名验证 13 /// 子类在此类基础上进行派生并重写自己的回调处理过程 14 /// </summary> 15 public class XcxPayNotify 16 { 17 public HttpContext context { get; set; } 18 19 public Log Log = new Log(XcxPayConfig.LogPath); 20 21 public XcxPayNotify(HttpContext context) 22 { 23 this.context = context; 24 } 25 26 /// <summary> 27 /// 接收从微信支付后台发送过来的数据并验证签名 28 /// </summary> 29 /// <returns>微信支付后台返回的数据</returns> 30 public XcxPayData GetNotifyData() 31 { 32 //接收从微信后台POST过来的数据 33 System.IO.Stream s = context.Request.InputStream; 34 int count = 0; 35 byte[] buffer = new byte[1024]; 36 StringBuilder builder = new StringBuilder(); 37 while ((count = s.Read(buffer, 0, 1024)) > 0) 38 { 39 builder.Append(Encoding.UTF8.GetString(buffer, 0, count)); 40 } 41 s.Flush(); 42 s.Close(); 43 s.Dispose(); 44 45 //转换数据格式并验证签名 46 XcxPayData data = new XcxPayData(); 47 try 48 { 49 data.FromXml(builder.ToString()); 50 } 51 catch (WePayException ex) 52 { 53 //若签名错误,则立即返回结果给微信支付后台 54 XcxPayData res = new XcxPayData(); 55 res.SetValue("return_code", "FAIL"); 56 res.SetValue("return_msg", ex.Message); 57 Log.Error(this.GetType().ToString(), "Sign check error : " + res.ToXml()); 58 context.Response.Write(res.ToXml()); 59 context.Response.End(); 60 } 61 62 Log.Info(this.GetType().ToString(), "Check sign success"); 63 return data; 64 } 65 66 //派生类需要重写这个方法,进行不同的回调处理 67 public virtual void ProcessNotify() 68 { 69 70 } 71 } 72 }
至此,小程序支付的架子我们已经搭建好了,接下来,就是在我们的业务中去使用这个架子。
1 using App.Pay.WePay; 2 using App.Pay.WePay.XcxPay; 3 using System; 4 using System.Collections.Generic; 5 using System.Linq; 6 using System.Web; 7 using System.Web.Configuration; 8 using System.Web.Mvc; 9 10 namespace App.WebTest.Controllers 11 { 12 /// <summary> 13 /// 微信小程序支付 14 /// </summary> 15 public class WeXcxPayController : BaseController 16 { 17 /// <summary> 18 /// 小程序下单 19 /// </summary> 20 /// <param name="oIds">订单Id</param> 21 /// <param name="code">临时登录凭证</param> 22 /// <returns></returns> 23 public ActionResult WeXcxPay(int[] oIds, string code) 24 { 25 #region 验证订单是否有效,并合计价格 26 27 //订单价格 28 decimal payPrice = 0; 29 30 //订单描述 31 string detail = ""; 32 33 //验证订单..... 34 35 36 #endregion 37 38 #region 统一下单 39 40 try 41 { 42 //支付回调通知地址 43 var address = WebConfigurationManager.AppSettings["WxXcxNotifyUrl"].ToString(); 44 XcxPayData data = new XcxPayData(); 45 data.SetValue("body", "商品购买"); 46 47 //可以将用户Id和订单Id同时封装在attach中 48 data.SetValue("attach", String.Join(",", oIds).ToString()); 49 Random rd = new Random(); 50 51 //外部商户订单号 52 var payNum = DateTime.Now.ToString("yyyyMMddHHmmss") + rd.Next(0, 1000).ToString().PadLeft(3, '0'); 53 data.SetValue("out_trade_no", payNum); 54 data.SetValue("detail", detail.Substring(0, detail.Length - 1)); 55 data.SetValue("total_fee", Convert.ToInt32(payPrice * 100)); 56 data.SetValue("time_start", DateTime.Now.ToString("yyyyMMddHHmmss")); 57 data.SetValue("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss")); 58 data.SetValue("notify_url", address); 59 //data.SetValue("goods_tag", "test"); 60 data.SetValue("trade_type", "JSAPI"); 61 data.SetValue("openid", WeHelper.Code2Session(code).openid); 62 63 XcxPayData result = XcxPayApi.UnifiedOrder(data); 64 var flag = true; 65 var msg = ""; 66 var nonceStr = ""; 67 var appId = ""; 68 var package = ""; 69 var mch_id = ""; 70 if (!result.IsSet("appid") || !result.IsSet("prepay_id") || result.GetValue("prepay_id").ToString() == "") 71 { 72 flag = false; 73 msg = "下单失败"; 74 return Json(new { Result = false, Msg = "下单失败!" }); 75 } 76 else 77 { 78 //统一下单 79 80 ///TO Do...... 81 /// 修改订单状态 82 83 nonceStr = result.GetValue("nonce_str").ToString(); 84 appId = result.GetValue("appid").ToString(); 85 mch_id = result.GetValue("mch_id").ToString(); 86 package = "prepay_id=" + result.GetValue("prepay_id").ToString(); 87 } 88 var signType = "MD5"; 89 var timeStamp = ((DateTime.Now.Ticks - TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)).Ticks) / 10000).ToString(); 90 XcxPayData applet = new XcxPayData(); 91 applet.SetValue("appId", appId); 92 applet.SetValue("nonceStr", nonceStr); 93 applet.SetValue("package", package); 94 applet.SetValue("signType", signType); 95 applet.SetValue("timeStamp", timeStamp); 96 var appletSign = applet.MakeSign(); 97 return Json(new { timeStamp, nonceStr, package, signType, paySign = appletSign, Result = flag, msg }); 98 } 99 catch (Exception ex) 100 { 101 return Json(new { Result = false, msg = "缺少参数" }); 102 } 103 #endregion 104 } 105 106 /// <summary> 107 /// 微信小程序支付回调通知 108 /// </summary> 109 /// <returns></returns> 110 public void WeXcxNotifyUrl() 111 { 112 Pay.Log Log = new Pay.Log(XcxPayConfig.LogPath); 113 Log.Info("WxXcxNotifyUrl", "支付回调"); 114 XcxPayNotify notify = new XcxPayNotify(System.Web.HttpContext.Current); 115 XcxPayData notifyData = notify.GetNotifyData(); 116 117 //检查支付结果中transaction_id是否存在 118 if (!notifyData.IsSet("transaction_id")) 119 { 120 //若transaction_id不存在,则立即返回结果给微信支付后台 121 XcxPayData res = new XcxPayData(); 122 res.SetValue("return_code", "FAIL"); 123 res.SetValue("return_msg", "支付结果中微信订单号不存在"); 124 Log.Error(this.GetType().ToString(), "The Pay result is error : " + res.ToXml()); 125 Response.Write(res.ToXml()); 126 Response.End(); 127 } 128 129 string transaction_id = notifyData.GetValue("transaction_id").ToString(); 130 131 //查询订单,判断订单真实性 132 if (!XcxQueryOrder(transaction_id)) 133 { 134 //若订单查询失败,则立即返回结果给微信支付后台 135 XcxPayData res = new XcxPayData(); 136 res.SetValue("return_code", "FAIL"); 137 res.SetValue("return_msg", "订单查询失败"); 138 Log.Error(this.GetType().ToString(), "Order query failure : " + res.ToXml()); 139 140 Response.Write(res.ToXml()); 141 Response.End(); 142 } 143 //查询订单成功 144 else 145 { 146 XcxPayData res = new XcxPayData(); 147 res.SetValue("return_code", "SUCCESS"); 148 res.SetValue("return_msg", "OK"); 149 Log.Info(this.GetType().ToString(), "Order query success : " + res.ToXml()); 150 Log.Info(this.GetType().ToString(), "Order query success,notifyData : " + notifyData.ToXml()); 151 var returnCode = notifyData.GetValue("return_code").ToString(); 152 var transactionNo = transaction_id;//微信订单号 153 var outTradeNo = notifyData.GetValue("out_trade_no").ToString();//自定义订单号 154 var attach = notifyData.GetValue("attach").ToString();//身份证 155 var endTime = notifyData.GetValue("time_end").ToString();//交易结束时间 156 //var body = notifyData.GetValue("body").ToString();//projectIdlist 157 var totalFee = notifyData.GetValue("total_fee").ToString(); ;//支付金额 158 159 int userId = Convert.ToInt32(attach.Split('|')[0]); 160 string msg; 161 try 162 { 163 //var result = OrderBll.Value.CompleteWePay(userId, totalFee, transactionNo, returnCode, outTradeNo, attach, endTime, out msg); 164 165 var result = true; 166 167 Log.Info(this.GetType().ToString(), "CompleteWePay:" + result); 168 } 169 catch (Exception e) 170 { 171 Log.Error(this.GetType().ToString(), "CompleteWePay:" + e.ToString()); 172 } 173 174 Response.Write(res.ToXml()); 175 Response.End(); 176 } 177 } 178 179 /// <summary> 180 /// 查询订单 181 /// </summary> 182 /// <param name="transaction_id">微信交易订单号</param> 183 /// <returns></returns> 184 private bool XcxQueryOrder(string transaction_id) 185 { 186 XcxPayData req = new XcxPayData(); 187 req.SetValue("transaction_id", transaction_id); 188 XcxPayData res = XcxPayApi.OrderQuery(req); 189 if (res.GetValue("return_code").ToString() == "SUCCESS" && res.GetValue("result_code").ToString() == "SUCCESS") 190 { 191 return true; 192 } 193 else 194 { 195 return false; 196 } 197 } 198 } 199 }
注意:扩展一个对象反序列化的方法(WeHelper类中将code转化为Session用到),如果不想添加扩展,也可以直接引用JsonConvert包的DeserializeObject反序列化方法即可。
1 public static class Serialize 2 { 3 public static string ToJson(this object obj) 4 { 5 return JsonConvert.SerializeObject(obj); 6 } 7 8 public static T JsonTo<T>(this string obj) 9 { 10 return (T)JsonConvert.DeserializeObject(obj, typeof(T)); 11 } 12 }
支付完成后,微信会把相关支付信息通知支付回调接口发送给商户,商户在回调接口中接收处理,并返回应答。注意,支付回调接口必须要在外网可以访问到、不能有身份验证(允许匿名访问)、接口无异常,此外如果微信收到商户的应答不是成功或超时,微信会认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率(通知频率15/15/30/180/1800/1800/1800/1800/3600,单位:秒)。