添加微信支付模块

This commit is contained in:
橙子
2022-05-14 17:45:17 +08:00
parent 93180faa23
commit 208c93bc8f
15 changed files with 1382 additions and 2 deletions

View File

@@ -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("统一支付接口中缺少必填参数openidtrade_type为JSAPI时openid为必填参数");
}
if (inputObj.GetValue("trade_type").ToString() == "NATIVE" && !inputObj.IsSet("product_id"))
{
throw new PayException("统一支付接口中缺少必填参数product_idtrade_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();
}
}
}

View File

@@ -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
{
/// <summary>
/// 微信支付协议接口数据类所有的API接口通信都依赖这个数据结构
/// 在调用接口之前先填充各个字段的值,然后进行接口通信,
/// 这样设计的好处是可扩展性强,用户可随意对协议进行更改而不用重新设计数据结构,
/// 还可以随意组合出不同的协议数据包,不用为每个协议设计一个数据包结构
/// </summary>
public class PayData
{
public const string SIGN_TYPE_MD5 = "MD5";
public const string SIGN_TYPE_HMAC_SHA256 = "HMAC-SHA256";
//采用排序的Dictionary的好处是方便对数据包进行签名不用再签名之前再做一次排序
private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>();
/**
* 设置某个字段的值
* @param key 字段名
* @param value 字段值
*/
public void SetValue(string key, object value)
{
m_values[key] = value;
}
/**
* 根据字段名获取某个字段的值
* @param key 字段名
* @return key对应的字段值
*/
public object GetValue(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
return o;
}
/**
* 判断某个字段是否已设置
* @param key 字段名
* @return 若字段key已被设置则返回true否则返回false
*/
public bool IsSet(string key)
{
object o = null;
m_values.TryGetValue(key, out o);
if (null != o && !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 = "<xml>";
foreach (KeyValuePair<string, object> 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 + "</" + pair.Key + ">";
}
else if (pair.Value.GetType() == typeof(string))
{
xml += "<" + pair.Key + ">" + "<![CDATA[" + pair.Value + "]]></" + pair.Key + ">";
}
else//除了string和int类型不能含有其他数据类型
{
throw new Exception("WxPayData字段数据类型错误!");
}
}
xml += "</xml>";
return xml;
}
/**
* @将xml转为WxPayData对象并返回对象内部的数据
* @param string 待转换的xml串
* @return 经转换得到的Dictionary
* @throws WxPayException
*/
public SortedDictionary<string, object> FromXml(string xml)
{
if (string.IsNullOrEmpty(xml))
{
throw new Exception("将空的xml串转换为WxPayData不合法!");
}
SafeXmlDocument xmlDoc = new SafeXmlDocument();
xmlDoc.LoadXml(xml);
XmlNode xmlNode = xmlDoc.FirstChild;//获取到根节点<xml>
XmlNodeList nodes = xmlNode.ChildNodes;
foreach (XmlNode xn in nodes)
{
XmlElement xe = (XmlElement)xn;
m_values[xe.Name] = xe.InnerText;//获取xml的键值对到WxPayData内部的数据中
}
try
{
//2015-06-29 错误是没有签名
if (m_values["return_code"].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<string, object> 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<string, object> 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<string, object> 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;
}
}
}
}

View File

@@ -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;
}
/// <summary>
/// 创建支付连接
/// </summary>
/// <param name="orderId"></param>
/// <param name="description"></param>
/// <param name="totalPay"></param>
/// <param name="httpContext"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 调用微信API根据订单ID查询订单信息,全部信息
/// </summary>
/// <param name="transaction_id"></param>
/// <returns></returns>
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<PayOptions>(config);
Console.WriteLine($"configPath={path} AppID={option.AppID}");
return option;
}
/// <summary>
/// 生成二维码方法
/// </summary>
/// <param name="text">输入的字符串</param>
/// <param name="width">二维码宽度</param>
/// <param name="height">二维码高度</param>
/// <returns></returns>
public static string QRcode(string text, int width = 360, int height = 360)
{
//这里要感谢一下http://old.wwei.cn/
Dictionary<string, string> 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<JObject>(retString);
var data = json["data"].ToString();
return data;
}
}
}

View File

@@ -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
{
/// <summary>
/// http连接基础类负责底层的http通信
/// </summary>
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;
}
/// <summary>
/// 处理http GET请求返回数据
/// </summary>
/// <param name="url">请求的url地址</param>
/// <returns>http GET成功后返回的数据失败抛WebException异常</returns>
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;
}
}
}