diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Config/IPayConfig.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Config/IPayConfig.cs new file mode 100644 index 00000000..7e9eb90d --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Config/IPayConfig.cs @@ -0,0 +1,66 @@ +using System; +namespace Yi.Framework.WeChatPay.Core +{ + /// + /// IConfig + /// + public interface IPayConfig + { + + //=======【基本信息设置】===================================== + /* 微信公众号信息配置 + * APPID:绑定支付的APPID(必须配置) + * MCHID:商户号(必须配置) + * KEY:商户支付密钥,参考开户邮件设置(必须配置),请妥善保管,避免密钥泄露 + * APPSECRET:公众帐号secert(仅JSAPI支付的时候需要配置),请妥善保管,避免密钥泄露 + */ + + string GetAppID(); + string GetMchID(); + string GetKey(); + string GetAppSecret(); + + + + //=======【证书路径设置】===================================== + /* 证书路径,注意应该填写绝对路径(仅退款、撤销订单时需要) + * 1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载; + * 2.建议将证书文件名改为复杂且不容易猜测的文件 + * 3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。 + */ + string GetSSlCertPath(); + string GetSSlCertPassword(); + + + + //=======【支付结果通知url】===================================== + /* 支付结果通知回调url,用于商户接收支付结果 + */ + string GetNotifyUrl(); + + //=======【商户系统后台机器IP】===================================== + /* 此参数可手动配置也可在程序中自动获取 + */ + string GetIp(); + + + //=======【代理服务器设置】=================================== + /* 默认IP和端口号分别为0.0.0.0和0,此时不开启代理(如有需要才设置) + */ + string GetProxyUrl(); + + + //=======【上报信息配置】=================================== + /* 测速上报等级,0.关闭上报; 1.仅错误时上报; 2.全量上报 + */ + int GetReportLevel(); + + + //=======【日志级别】=================================== + /* 日志等级,0.不输出日志;1.只输出错误信息; 2.输出错误和正常信息; 3.输出错误信息、正常信息和调试信息 + */ + int GetLogLevel(); + + + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Config/PayConfig.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Config/PayConfig.cs new file mode 100644 index 00000000..73600b69 --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Config/PayConfig.cs @@ -0,0 +1,108 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using Microsoft.Extensions.Configuration; +using Yi.Framework.WeChatPay.Options; + +namespace Yi.Framework.WeChatPay.Core +{ + public class PayConfig : IPayConfig + { + public static PayConfig Current = null; + + public PayConfig(PayOptions payOptions) + { + Current = this; + _BasicConfig = payOptions; + } + + private static PayOptions _BasicConfig = null; + + + //=======【基本信息设置】===================================== + /* 微信公众号信息配置 + * APPID:绑定支付的APPID(必须配置) + * MCHID:商户号(必须配置) + * KEY:商户支付密钥,参考开户邮件设置(必须配置),请妥善保管,避免密钥泄露 + * APPSECRET:公众帐号secert(仅JSAPI支付的时候需要配置),请妥善保管,避免密钥泄露 + */ + + public string GetAppID() + { + return _BasicConfig.AppID; + } + public string GetMchID() + { + return _BasicConfig.MchID; + } + public string GetKey() + { + return _BasicConfig.Key; + } + public string GetAppSecret() + { + return ""; + } + + + + //=======【证书路径设置】===================================== + /* 证书路径,注意应该填写绝对路径(仅退款、撤销订单时需要) + * 1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载; + * 2.建议将证书文件名改为复杂且不容易猜测的文件 + * 3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。 + */ + public string GetSSlCertPath() + { + return ""; + } + public string GetSSlCertPassword() + { + return ""; + } + + + + //=======【支付结果通知url】===================================== + /* 支付结果通知回调url,用于商户接收支付结果 + */ + public string GetNotifyUrl() + { + return _BasicConfig.NotifyUrl; + } + + //=======【商户系统后台机器IP】===================================== + /* 此参数可手动配置也可在程序中自动获取 + */ + public string GetIp() + { + return "0.0.0.0"; + } + + //=======【代理服务器设置】=================================== + /* 默认IP和端口号分别为0.0.0.0和0,此时不开启代理(如有需要才设置) + */ + public string GetProxyUrl() + { + return ""; + } + + + //=======【上报信息配置】=================================== + /* 测速上报等级,0.关闭上报; 1.仅错误时上报; 2.全量上报 + */ + public int GetReportLevel() + { + return 1; + } + + + //=======【日志级别】=================================== + /* 日志等级,0.不输出日志;1.只输出错误信息; 2.输出错误和正常信息; 3.输出错误信息、正常信息和调试信息 + */ + public int GetLogLevel() + { + return 1; + } + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayApi.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayApi.cs new file mode 100644 index 00000000..db6e41f3 --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayApi.cs @@ -0,0 +1,138 @@ +using FizzWare.NBuilder; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.WeChatPay.Exceptions; + +namespace Yi.Framework.WeChatPay.Core +{ + public class PayApi + { + private readonly IPayConfig _IPayConfig = null; + private readonly PayHttpService _PayHttpService = null; + public PayApi(IPayConfig payConfig, PayHttpService payHttpService) + { + this._IPayConfig = payConfig; + this._PayHttpService = payHttpService; + } + + /** + * + * 统一下单 + * @param WxPaydata inputObj 提交给统一下单API的参数 + * @param int timeOut 超时时间 + * @throws WxPayException + * @return 成功时返回,其他抛异常 + */ + public PayData UnifiedOrder(PayData inputObj, int timeOut = 6) + { + string url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; + //检测必填参数 + if (!inputObj.IsSet("out_trade_no")) + { + throw new PayException("缺少统一支付接口必填参数out_trade_no!"); + } + else if (!inputObj.IsSet("body")) + { + throw new PayException("缺少统一支付接口必填参数body!"); + } + else if (!inputObj.IsSet("total_fee")) + { + throw new PayException("缺少统一支付接口必填参数total_fee!"); + } + else if (!inputObj.IsSet("trade_type")) + { + throw new PayException("缺少统一支付接口必填参数trade_type!"); + } + + //关联参数 + if (inputObj.GetValue("trade_type").ToString() == "JSAPI" && !inputObj.IsSet("openid")) + { + throw new PayException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!"); + } + if (inputObj.GetValue("trade_type").ToString() == "NATIVE" && !inputObj.IsSet("product_id")) + { + throw new PayException("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!"); + } + + //异步通知url未设置,则使用配置文件中的url + /*if (!inputObj.IsSet("notify_url")) + { + inputObj.SetValue("notify_url", this._IWxPayConfig().GetNotifyUrl());//异步通知url + }*/ + + inputObj.SetValue("appid", this._IPayConfig.GetAppID());//公众账号ID + inputObj.SetValue("mch_id", this._IPayConfig.GetMchID());//商户号 + inputObj.SetValue("spbill_create_ip", this._IPayConfig.GetIp());//终端ip + inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 + inputObj.SetValue("sign_type", PayData.SIGN_TYPE_HMAC_SHA256);//签名类型 + + //签名 + inputObj.SetValue("sign", inputObj.MakeSign()); + string xml = inputObj.ToXml(); + // 发起http请求 + string response = this._PayHttpService.Post(xml, url, false, timeOut); + PayData result = new PayData(); + result.FromXml(response); + + return result; + } + + /** + * + * 查询订单 + * @param WxPayData inputObj 提交给查询订单API的参数 + * @param int timeOut 超时时间 + * @throws WxPayException + * @return 成功时返回订单查询结果,其他抛异常 + */ + public PayData OrderQuery(PayData inputObj, HttpContext httpContext, 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 PayException("订单查询接口中,out_trade_no、transaction_id至少填一个!"); + } + + inputObj.SetValue("appid", this._IPayConfig.GetAppID());//公众账号ID + inputObj.SetValue("mch_id", this._IPayConfig.GetMchID());//商户号 + inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串 + inputObj.SetValue("sign_type", PayData.SIGN_TYPE_HMAC_SHA256);//签名类型 + inputObj.SetValue("sign", inputObj.MakeSign());//签名 + string xml = inputObj.ToXml(); + //Log.Debug("WxPayApi", "OrderQuery request : " + xml); + string response = this._PayHttpService.Post(xml, url, false, timeOut);//调用HTTP通信接口提交数据 + //Log.Debug("WxPayApi", "OrderQuery response : " + response); + + //将xml格式的数据转化为对象以返回 + PayData result = new PayData(); + result.FromXml(response); + + return result; + } + + /** + * 生成时间戳,标准北京时间,时区为东八区,自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.Int().ToString(); + } + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayData.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayData.cs new file mode 100644 index 00000000..61dff76a --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayData.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.Web; +using System.Xml; +using System.Security.Cryptography; +using System.Text; +using System.Linq; +using Newtonsoft.Json; +using Yi.Framework.WeChatPay.Core; + +namespace Yi.Framework.WeChatPay.Core +{ + /// + /// 微信支付协议接口数据类,所有的API接口通信都依赖这个数据结构, + /// 在调用接口之前先填充各个字段的值,然后进行接口通信, + /// 这样设计的好处是可扩展性强,用户可随意对协议进行更改而不用重新设计数据结构, + /// 还可以随意组合出不同的协议数据包,不用为每个协议设计一个数据包结构 + /// + public class PayData + { + public const string SIGN_TYPE_MD5 = "MD5"; + public const string SIGN_TYPE_HMAC_SHA256 = "HMAC-SHA256"; + + + //采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序 + private SortedDictionary m_values = new SortedDictionary(); + + /** + * 设置某个字段的值 + * @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 && !o.Equals("")) + return true; + else + return false; + } + + /** + * @将Dictionary转成xml + * @return 经转换得到的xml串 + * @throws WxPayException + **/ + public string ToXml() + { + //数据为空时不能转化为xml格式 + if (0 == m_values.Count) + { + throw new Exception("WxPayData数据为空!"); + } + + string xml = ""; + foreach (KeyValuePair pair in m_values) + { + //字段值不能为null,会影响后续流程 + if (pair.Value == null) + { + throw new Exception("WxPayData内部含有值为null的字段!"); + } + + if (pair.Value.GetType() == typeof(int)) + { + xml += "<" + pair.Key + ">" + pair.Value + ""; + } + else if (pair.Value.GetType() == typeof(string)) + { + xml += "<" + pair.Key + ">" + ""; + } + else//除了string和int类型不能含有其他数据类型 + { + throw new Exception("WxPayData字段数据类型错误!"); + } + } + xml += ""; + return xml; + } + + /** + * @将xml转为WxPayData对象并返回对象内部的数据 + * @param string 待转换的xml串 + * @return 经转换得到的Dictionary + * @throws WxPayException + */ + public SortedDictionary FromXml(string xml) + { + if (string.IsNullOrEmpty(xml)) + { + throw new Exception("将空的xml串转换为WxPayData不合法!"); + } + + + SafeXmlDocument xmlDoc = new SafeXmlDocument(); + xmlDoc.LoadXml(xml); + XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点 + 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"].ToString() != "SUCCESS") + { + return m_values; + } + //验证签名,不通过会抛异常 + CheckSign(); + } + catch (Exception ex) + { + throw new Exception(ex.Message); + } + + return m_values; + } + + /** + * @Dictionary格式转化成url参数格式 + * @ return url格式串, 该串不包含sign字段值 + */ + public string ToUrl() + { + string buff = ""; + foreach (KeyValuePair pair in m_values) + { + if (pair.Value == null) + { + throw new Exception("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 = JsonConvert.SerializeObject(m_values); + return jsonStr; + + } + + /** + * @values格式化成能在Web页面上显示的结果(因为web页面上不能直接输出xml格式的字符串) + */ + public string ToPrintStr() + { + string str = ""; + foreach (KeyValuePair pair in m_values) + { + if (pair.Value == null) + { + throw new Exception("WxPayData内部含有值为null的字段!"); + } + + + str += string.Format("{0}={1}\n", pair.Key, pair.Value.ToString()); + } + str = HttpUtility.HtmlEncode(str); + return str; + } + + + /** + * @生成签名,详见签名生成算法 + * @return 签名, sign字段不参加签名 + */ + public string MakeSign(string signType) + { + //转url格式 + string str = ToUrl(); + //在string后加入API KEY + string apiKey = PayConfig.Current.GetKey(); + str += "&key=" + apiKey; + 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, apiKey); + } + else + { + throw new Exception("sign_type 不合法"); + } + } + + /** + * @生成签名,详见签名生成算法 + * @return 签名, sign字段不参加签名 SHA256 + */ + public string MakeSign() + { + return MakeSign(SIGN_TYPE_HMAC_SHA256); + } + + + + /** + * + * 检测签名是否正确 + * 正确返回true,错误抛异常 + */ + public bool CheckSign(string signType) + { + //如果没有设置签名,则跳过检测 + if (!IsSet("sign")) + { + throw new Exception("WxPayData签名存在但不合法!"); + } + //如果设置了签名但是签名为空,则抛异常 + else if (GetValue("sign") == null || GetValue("sign").ToString() == "") + { + throw new Exception("WxPayData签名存在但不合法!"); + } + + //获取接收到的签名 + string return_sign = GetValue("sign").ToString(); + + //在本地计算新的签名 + string cal_sign = MakeSign(signType); + + if (cal_sign == return_sign) + { + return true; + } + + throw new Exception("WxPayData签名验证错误!"); + } + + + + /** + * + * 检测签名是否正确 + * 正确返回true,错误抛异常 + */ + public bool CheckSign() + { + return CheckSign(SIGN_TYPE_HMAC_SHA256); + } + + /** + * @获取Dictionary + */ + public SortedDictionary GetValues() + { + return m_values; + } + + + private string CalcHMACSHA256Hash(string plaintext, string salt) + { + string result = ""; + var enc = Encoding.Default; + byte[] + baText2BeHashed = enc.GetBytes(plaintext), + baSalt = enc.GetBytes(salt); + HMACSHA256 hasher = new HMACSHA256(baSalt); + byte[] baHashedText = hasher.ComputeHash(baText2BeHashed); + result = string.Join("", baHashedText.ToList().Select(b => b.ToString("x2")).ToArray()); + return result.ToUpper(); + } + + + private class SafeXmlDocument : XmlDocument + { + public SafeXmlDocument() + { + this.XmlResolver = null; + } + } + } +} \ No newline at end of file diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayHelper.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayHelper.cs new file mode 100644 index 00000000..6eae84c2 --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayHelper.cs @@ -0,0 +1,178 @@ +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.WeChatPay.Options; + +namespace Yi.Framework.WeChatPay.Core +{ + public class PayHelper + { + public static readonly string KEY_PAY_PREFIX = "order:pay:url:"; + private readonly IPayConfig _IPayConfig = null; + private readonly PayApi _PayApi = null; + + private HttpContext _httpContext; + public PayHelper(IPayConfig PayConfig, PayApi PayApi, IHttpContextAccessor httpContextAccessor) + { + this._IPayConfig = PayConfig; + this._PayApi = PayApi; + this._httpContext = httpContextAccessor.HttpContext; + } + + /// + /// 创建支付连接 + /// + /// + /// + /// + /// + /// + public string CreatePayUrl(long orderId, string description, long totalPay) + { + // 定义返回的支付连接 + string url; + try + { + // 构建支付需要的参数对象 + PayData data = new PayData(); + //描述 + data.SetValue("body", description); + //订单号 + data.SetValue("out_trade_no", orderId.ToString()); + data.SetValue("product_id", orderId.ToString()); + //货币(默认就是人民币) + data.SetValue("fee_type", "CNY"); + //TODO 总金额 (模拟1分钱, 线上环境换成真实价格) + data.SetValue("total_fee", /*totalPay.ToString()*/ 1); // 单位是分 + //调用微信支付的终端ip + data.SetValue("spbill_create_ip", "0.0.0.0"); + //回调地址 + data.SetValue("notify_url", this._IPayConfig.GetNotifyUrl()); + //交易类型为扫码支付 + data.SetValue("trade_type", "NATIVE"); + + PayData result = this._PayApi.UnifiedOrder(data);//调用统一下单接口 + url = result.GetValue("code_url").ToString();//获得统一下单接口返回的二维码链接 + } + catch (Exception e) + { + throw new Exception("生成支付链接连接失败", e); + } + + return url; + } + + + /// + /// (调用微信API)根据订单ID查询订单信息,全部信息 + /// + /// + /// + public PayData QueryOrderById(long orderId) + { + PayData req = new PayData(); + req.SetValue("out_trade_no", orderId.ToString()); + PayData res = this._PayApi.OrderQuery(req, _httpContext); + return res;// 返回查询数据 + } + + + + + public static PayOptions GetPayOptions(string path) + { + string config = File.ReadAllText(path); + var option = JsonConvert.DeserializeObject(config); + Console.WriteLine($"configPath={path} AppID={option.AppID}"); + return option; + } + + /// + /// 生成二维码方法 + /// + /// 输入的字符串 + /// 二维码宽度 + /// 二维码高度 + /// + public static string QRcode(string text, int width = 360, int height = 360) + { + + //这里要感谢一下http://old.wwei.cn/ + Dictionary dic = new() + { + { "qrid", "0" }, + { "data[type]", "index" }, + { "data[text]", text }, + { "moban_id", "0" }, + { "size", "300" }, + { "level", "M" }, + { "moban_type", "qrcpu" }, + { "style_setting[protype]", "1" }, + { "style_setting[ptcolor]", "#000000" }, + { "style_setting[inptcolor]", "#000000" }, + { "style_setting[fcolor]", "#000000" }, + { "style_setting[bcolor]", "#ffffff" }, + { "style_setting[mbtype_hb]", "0" }, + { "style_setting[logo_id]", "" }, + { "style_setting[logo_width]", "46" }, + { "style_setting[logo_height]", "46" }, + { "style_setting[logo_border]", "0" } + + }; + + + StringBuilder builder = new StringBuilder(); + int i = 0; + if (dic.Count > 0) + { + foreach (var item in dic) + { + if (i > 0) + builder.Append("&"); + builder.AppendFormat("{0}={1}", item.Key, item.Value); + i++; + } + } + + string postDataStr = builder.ToString(); + + +#pragma warning disable SYSLIB0014 // 类型或成员已过时 + HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://old.wwei.cn/qrcode-wwei_create.html"); +#pragma warning restore SYSLIB0014 // 类型或成员已过时 + + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; + request.ContentLength = Encoding.UTF8.GetByteCount(postDataStr); + request.Headers.Add("Host", "old.wwei.cn"); + request.Headers.Add("User-Agent", "PostmanRuntime/6.66.6"); + request.Headers.Add("Origin", "http://old.wwei.cn"); + request.Headers.Add("Referer", "http://old.wwei.cn/"); + + Stream myRequestStream = request.GetRequestStream(); + StreamWriter myStreamWriter = new StreamWriter(myRequestStream, Encoding.GetEncoding("utf-8")); + myStreamWriter.Write(postDataStr); + myStreamWriter.Close(); + + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + + Stream myResponseStream = response.GetResponseStream(); + StreamReader myStreamReader = new StreamReader(myResponseStream, Encoding.GetEncoding("utf-8")); + string retString = myStreamReader.ReadToEnd(); + myStreamReader.Close(); + myResponseStream.Close(); + + var json = Newtonsoft.Json.JsonConvert.DeserializeObject(retString); + var data = json["data"].ToString(); + + return data; + + } + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayHttpService.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayHttpService.cs new file mode 100644 index 00000000..432bfd53 --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Helper/PayHttpService.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Web; +using System.Net; +using System.IO; +using System.Text; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Http; +using Yi.Framework.WeChatPay.Exceptions; + +namespace Yi.Framework.WeChatPay.Core +{ + /// + /// http连接基础类,负责底层的http通信 + /// + public class PayHttpService + { + private readonly IPayConfig _IPayConfig = null; + private readonly HttpContext _httpContext = null; + public PayHttpService(IPayConfig PayConfig, IHttpContextAccessor httpContextAccessor) + { + this._IPayConfig = PayConfig; + this._httpContext = httpContextAccessor.HttpContext; + } + + public static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) + { + //直接确认,否则打不开 + return true; + } + + public 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的相关属性 + * ************************************************************/ +#pragma warning disable SYSLIB0014 // 类型或成员已过时 + request = (HttpWebRequest)WebRequest.Create(url); +#pragma warning restore SYSLIB0014 // 类型或成员已过时 + request.UserAgent = string.Format("WXPaySDK/{3} ({0}) .net/{1} {2}", Environment.OSVersion, Environment.Version, this._IPayConfig.GetMchID(), typeof(PayHttpService).Assembly.GetName().Version); + request.Method = "POST"; + request.Timeout = timeout * 1000; + + //设置POST的数据类型和长度 + request.ContentType = "text/xml"; + byte[] data = Encoding.UTF8.GetBytes(xml); + request.ContentLength = data.Length; + + + if (isUseCert)//是否使用证书--没用证书 + { + string path = _httpContext.Request.Path; + X509Certificate2 cert = new X509Certificate2(path + this._IPayConfig.GetSSlCertPath(), this._IPayConfig.GetSSlCertPassword()); + request.ClientCertificates.Add(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) + { +#pragma warning disable SYSLIB0006 // 类型或成员已过时 + Thread.ResetAbort(); +#pragma warning restore SYSLIB0006 // 类型或成员已过时 + } + catch (WebException e) + { + if (e.Status == WebExceptionStatus.ProtocolError) + { + throw new PayException(e.ToString()); + } + + } + catch (Exception e) + { + throw new PayException(e.ToString()); + } + finally + { + //关闭连接和流 + if (response != null) + { + response.Close(); + } + if (request != null) + { + request.Abort(); + } + } + return result; + } + + /// + /// 处理http GET请求,返回数据 + /// + /// 请求的url地址 + /// http GET成功后返回的数据,失败抛WebException异常 + public string Get(string url) + { + System.GC.Collect(); + string result = ""; + + HttpWebRequest request = null; + HttpWebResponse response = null; + + //请求url以获取数据 + try + { + //设置最大连接数 + ServicePointManager.DefaultConnectionLimit = 200; + //设置https验证方式 + if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase)) + { + ServicePointManager.ServerCertificateValidationCallback = + new RemoteCertificateValidationCallback(CheckValidationResult); + } + + /*************************************************************** + * 下面设置HttpWebRequest的相关属性 + * ************************************************************/ +#pragma warning disable SYSLIB0014 // 类型或成员已过时 + request = (HttpWebRequest)WebRequest.Create(url); +#pragma warning restore SYSLIB0014 // 类型或成员已过时 + request.UserAgent = string.Format("WXPaySDK/{3} ({0}) .net/{1} {2}", Environment.OSVersion, Environment.Version, this._IPayConfig.GetMchID(), typeof(PayHttpService).Assembly.GetName().Version); + request.Method = "GET"; + + //获取服务器返回 + response = (HttpWebResponse)request.GetResponse(); + + //获取HTTP返回数据 + StreamReader sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8); + result = sr.ReadToEnd().Trim(); + sr.Close(); + } + catch (System.Threading.ThreadAbortException) + { +#pragma warning disable SYSLIB0006 // 类型或成员已过时 + Thread.ResetAbort(); +#pragma warning restore SYSLIB0006 // 类型或成员已过时 + } + catch (WebException e) + { + throw new PayException(e.ToString()); + } + catch (Exception e) + { + throw new PayException(e.ToString()); + } + finally + { + //关闭连接和流 + if (response != null) + { + response.Close(); + } + if (request != null) + { + request.Abort(); + } + } + return result; + } + } +} \ No newline at end of file diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/IPayInvoker.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/IPayInvoker.cs new file mode 100644 index 00000000..5d1fbe9b --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/IPayInvoker.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.WeChatPay.Core +{ + public interface IPayInvoker + { + /// + /// 获取WX支付链接的方法 + /// 然后发布定时同步状态任务 + /// + /// 订单ID + /// 用户信息 + /// 请求上下文 + /// 返回生成的支持链接 + string GenerateUrl(long orderId, long totalPay); + /// + /// 处理微信支付回调 + /// + /// + /// + PayData HandleNotify(); + + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Notify/AbstractNotify.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Notify/AbstractNotify.cs new file mode 100644 index 00000000..0034e46b --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Notify/AbstractNotify.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.WeChatPay.Exceptions; + +namespace Yi.Framework.WeChatPay.Core +{ + public abstract class AbstractNotify + { + protected HttpContext _httpContext; + + public AbstractNotify(IHttpContextAccessor httpContextAccessor) + { + this._httpContext = httpContextAccessor.HttpContext; + } + + /// + /// 从请求对象中获取信息 + /// + /// + /// + /// + private async Task GetRawBodyStringAsync(HttpRequest request, Encoding encoding = null) + { + if (encoding is null) + { + encoding = Encoding.UTF8; + } + var stream = new MemoryStream(); + await request.Body.CopyToAsync(stream); + stream.Seek(0, 0); + using (var reader = new StreamReader(stream, encoding)) + { + var result = await reader.ReadToEndAsync(); + return result; + } + } + + /// + /// 接收从微信支付后台发送过来的数据并验证签名 + /// + /// 微信支付后台返回的数据 + public PayData GetNotifyData() + { + //接收从微信后台POST过来的数据 + string content = GetRawBodyStringAsync(_httpContext.Request).Result; + Console.WriteLine(this.GetType().ToString(), "Receive data from WeChat : " + content); + + //转换数据格式并验证签名 + PayData data = new PayData(); + try + { + data.FromXml(content); + } + catch (PayException ex) + { + throw new Exception("验签失败", ex); + } + + Console.WriteLine(this.GetType().ToString(), "Check sign success"); + return data; + } + + //派生类需要重写这个方法,进行不同的回调处理 + public abstract PayData ProcessNotify(); + + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Notify/ResultNotify.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Notify/ResultNotify.cs new file mode 100644 index 00000000..562e04ca --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/Notify/ResultNotify.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Yi.Framework.WeChatPay.Core +{ + public class ResultNotify : AbstractNotify + { + private readonly PayApi _PayApi; + private readonly ILogger _logger; + public ResultNotify(PayApi PayApi,ILogger logger, IHttpContextAccessor httpContextAccessor) :base(httpContextAccessor) + { + this._PayApi = PayApi; + this._logger = logger; + } + + /// + /// 处理支付回调 + /// + /// + public override PayData ProcessNotify() + { + PayData notifyData = GetNotifyData(); + //解析数据 + string totalFee = notifyData.GetValue("total_fee").ToString(); //订单金额 + string outTradeNo = notifyData.GetValue("out_trade_no").ToString(); //订单编号 + string transactionId = notifyData.GetValue("transaction_id").ToString(); //商户订单号 + string bankType = notifyData.GetValue("bank_type").ToString(); //银行类型 + + _logger.LogInformation($"======支付回调参数:{totalFee}=={outTradeNo}=={transactionId}=={bankType}"); + if (totalFee.Equals("") || outTradeNo.Equals("") || transactionId.Equals("") || bankType.Equals("")) + { + PayData res = new(); + res.SetValue("return_code", "FAIL"); + res.SetValue("return_msg", "支付回调返回数据不正确"); + _logger.LogInformation("支付错误结果 : " + res.ToXml()); + return res; + } + + //检查支付结果中transaction_id是否存在--流水号 + if (!notifyData.IsSet("transaction_id")) + { + //若transaction_id不存在,则立即返回结果给微信支付后台 + PayData res = new(); + res.SetValue("return_code", "FAIL"); + res.SetValue("return_msg", "支付结果中微信订单号不存在"); + _logger.LogInformation("支付错误结果 : " + res.ToXml()); + + return res; + } + + //查询订单,判断订单真实性 + if (!QueryOrder(transactionId)) + { + //若订单查询失败,则立即返回结果给微信支付后台 + PayData res = new(); + res.SetValue("return_code", "FAIL"); + res.SetValue("return_msg", "订单查询失败"); + _logger.LogInformation("订单查询失败 : " + res.ToXml()); + + return res; + } + //查询订单成功 + else + { + // 打印结果 (仅打印) + PayData res = new PayData(); + res.SetValue("return_code", "SUCCESS"); + res.SetValue("return_msg", "OK"); + res.SetValue("transaction_id", transactionId); + _logger.LogInformation("订单查成功: " + res.ToXml()); + + // 设置返回响应结果 + notifyData.SetValue("return_code", "SUCCESS"); + notifyData.SetValue("return_msg", "OK"); + + return notifyData; + } + } + + /// + /// 根据流水号查询订单信息 + /// + /// + /// + private bool QueryOrder(string transaction_id) + { + PayData req = new PayData(); + req.SetValue("transaction_id", transaction_id); + PayData res = this._PayApi.OrderQuery(req, _httpContext); + if (res.GetValue("return_code").ToString() == "SUCCESS" && + res.GetValue("result_code").ToString() == "SUCCESS") + { + return true; + } + else + { + return false; + } + } + } +} \ No newline at end of file diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/PayInvoker.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/PayInvoker.cs new file mode 100644 index 00000000..e1817ef8 --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Core/PayInvoker.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.WeChatPay.Options; + +namespace Yi.Framework.WeChatPay.Core +{ + public class PayInvoker:IPayInvoker + { + private readonly PayHelper _payHelper; + private readonly ILogger _logger; + private readonly AbstractNotify _notify; + + public PayInvoker(PayHelper payHelper, ILogger logger, AbstractNotify notify) + { + this._payHelper = payHelper; + this._logger = logger; + this._notify = notify; + } + + /// + /// 获取WX支付链接的方法 + /// 然后发布定时同步状态任务 + /// + /// 订单ID + /// 用户信息 + /// 请求上下文 + /// 返回生成的支持链接 + public string GenerateUrl(long orderId, long totalPay) + { + string payUrl = null; + _logger.LogInformation("准备生成支付链接....."); + payUrl = _payHelper.CreatePayUrl(orderId, "微信支付链接", totalPay); + _logger.LogInformation("生成支付链接为:{payUrl}", payUrl); + return payUrl; + } + + /// + /// 处理微信支付回调 + /// + /// + /// + public PayData HandleNotify() + { + PayData wxPayData = this._notify.ProcessNotify(); + + return wxPayData; + } + + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Exceptions/PayException.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Exceptions/PayException.cs new file mode 100644 index 00000000..2aafe37d --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Exceptions/PayException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; + +namespace Yi.Framework.WeChatPay.Exceptions +{ + [Serializable] + public class PayException : Exception + { + public PayException() + { + } + + public PayException(string message) : base(message) + { + } + + public PayException(string message, Exception innerException) : base(message, innerException) + { + } + + protected PayException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Options/PayOptions.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Options/PayOptions.cs new file mode 100644 index 00000000..861266da --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Options/PayOptions.cs @@ -0,0 +1,14 @@ +namespace Yi.Framework.WeChatPay.Options +{ + public class PayOptions + { + public string AppID { get; set; } + public string MchID { get; set; } + public string Key { get; set; } + public string NotifyUrl { get; set; } + + public bool IsFileConfig { get; set; } = false; + + public string ConfigPath { get; set; } + } +} \ No newline at end of file diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/PayServiceExtensions.cs b/Yi.Framework.Net6/Yi.Framework.WeChatPay/PayServiceExtensions.cs new file mode 100644 index 00000000..883c4eb9 --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/PayServiceExtensions.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.WeChatPay.Core; +using Yi.Framework.WeChatPay.Exceptions; +using Yi.Framework.WeChatPay.Options; + +namespace Yi.Framework.WeChatPay.WebCore +{ + public static class PayServiceExtensions + { + public static IServiceCollection AddWeChatPayService(this IServiceCollection services,Action configure) + { + var option= new PayOptions(); + configure(option); + if (option.IsFileConfig) + { + option=PayHelper.GetPayOptions(option.ConfigPath); + } + if (option.AppID == null) + { + throw new PayException("AppId为空值"); + } + if (option.MchID == null) + { + throw new PayException("MchID为空值"); + } + if (option.Key == null) + { + throw new PayException("Key为空值"); + } + if (option.NotifyUrl == null) + { + throw new PayException("NotifyUrl为空值"); + } + services.AddSingleton(option); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddHttpContextAccessor(); + return services; + } + } +} diff --git a/Yi.Framework.Net6/Yi.Framework.WeChatPay/Yi.Framework.WeChatPay.csproj b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Yi.Framework.WeChatPay.csproj new file mode 100644 index 00000000..dc80f313 --- /dev/null +++ b/Yi.Framework.Net6/Yi.Framework.WeChatPay/Yi.Framework.WeChatPay.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + + diff --git a/Yi.Framework.Net6/Yi.Framework.sln b/Yi.Framework.Net6/Yi.Framework.sln index f169cc5a..47d08aa4 100644 --- a/Yi.Framework.Net6/Yi.Framework.sln +++ b/Yi.Framework.Net6/Yi.Framework.sln @@ -41,9 +41,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yi.Framework.Job", "Yi.Fram EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yi.Framework.SMSProcessor", "Yi.Framework.SMSProcessor\Yi.Framework.SMSProcessor.csproj", "{7C58FB7C-9AB5-47CF-ACEB-B784CF820E7E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.Repository", "Yi.Framework.Repository\Yi.Framework.Repository.csproj", "{DA96B62F-2D4C-4AFB-937C-5AEA1119A129}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yi.Framework.Repository", "Yi.Framework.Repository\Yi.Framework.Repository.csproj", "{DA96B62F-2D4C-4AFB-937C-5AEA1119A129}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yi.Framework.Language", "Yi.Framework.Language\Yi.Framework.Language.csproj", "{3047069B-4084-461F-BC9F-023BC60401D1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yi.Framework.Language", "Yi.Framework.Language\Yi.Framework.Language.csproj", "{3047069B-4084-461F-BC9F-023BC60401D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yi.Framework.WeChatPay", "Yi.Framework.WeChatPay\Yi.Framework.WeChatPay.csproj", "{C307189D-C42C-4C09-BB65-5A386C9F182B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -115,6 +117,10 @@ Global {3047069B-4084-461F-BC9F-023BC60401D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {3047069B-4084-461F-BC9F-023BC60401D1}.Release|Any CPU.ActiveCfg = Release|Any CPU {3047069B-4084-461F-BC9F-023BC60401D1}.Release|Any CPU.Build.0 = Release|Any CPU + {C307189D-C42C-4C09-BB65-5A386C9F182B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C307189D-C42C-4C09-BB65-5A386C9F182B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C307189D-C42C-4C09-BB65-5A386C9F182B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C307189D-C42C-4C09-BB65-5A386C9F182B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -136,6 +142,7 @@ Global {7C58FB7C-9AB5-47CF-ACEB-B784CF820E7E} = {D6B44435-EAFA-4D55-90D0-3AF80485FB83} {DA96B62F-2D4C-4AFB-937C-5AEA1119A129} = {DB2506F5-05FD-4E76-940E-41D7AA148550} {3047069B-4084-461F-BC9F-023BC60401D1} = {9ABAF6B1-6C02-498A-90A2-ABC1140CF89A} + {C307189D-C42C-4C09-BB65-5A386C9F182B} = {9ABAF6B1-6C02-498A-90A2-ABC1140CF89A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1ED77A6E-377F-4EEF-A3D0-D65C94657DAF}