396 lines
14 KiB
C#
396 lines
14 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Net.Http;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using System.Xml;
|
||
|
||
namespace PaymentSDK
|
||
{
|
||
/// <summary>
|
||
/// 微信支付协议接口数据类,所有的API接口通信都依赖这个数据结构,
|
||
/// 在调用接口之前先填充各个字段的值,然后进行接口通信,
|
||
/// 这样设计的好处是可扩展性强,用户可随意对协议进行更改而不用重新设计数据结构,
|
||
/// 还可以随意组合出不同的协议数据包,不用为每个协议设计一个数据包结构
|
||
/// </summary>
|
||
public class WxPayData
|
||
{
|
||
|
||
public WxPayData()
|
||
{
|
||
}
|
||
|
||
//采用排序的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)
|
||
return true;
|
||
else
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @将Dictionary转成xml
|
||
* @return 经转换得到的xml串
|
||
* @throws WePayException
|
||
**/
|
||
public string ToXml()
|
||
{
|
||
//数据为空时不能转化为xml格式
|
||
if (0 == m_values.Count)
|
||
{
|
||
|
||
//NLogHelper.WriteError("WxPayData数据为空!",DevAuthorNameEnum.Michael,false);
|
||
throw new Exception("WxPayData数据为空!");
|
||
}
|
||
|
||
string xml = "<xml>";
|
||
foreach (KeyValuePair<string, object> pair in m_values)
|
||
{
|
||
//字段值不能为null,会影响后续流程
|
||
if (pair.Value == null)
|
||
{
|
||
// NLogHelper.WriteError("WxPayData内部含有值为null的字段!", DevAuthorNameEnum.Michael, false);
|
||
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类型不能含有其他数据类型
|
||
{
|
||
// NLogHelper.WriteError("WxPayData字段数据类型错误!", DevAuthorNameEnum.Michael, false);
|
||
throw new Exception("WxPayData字段数据类型错误!");
|
||
}
|
||
}
|
||
xml += "</xml>";
|
||
return xml;
|
||
}
|
||
|
||
/**
|
||
* @将xml转为WxPayData对象并返回对象内部的数据
|
||
* @param string 待转换的xml串
|
||
* @return 经转换得到的Dictionary
|
||
* @throws WePayException
|
||
*/
|
||
public SortedDictionary<string, object> FromXml(string xml)
|
||
{
|
||
if (string.IsNullOrEmpty(xml))
|
||
{
|
||
// NLogHelper.WriteError("将空的xml串转换为WxPayData不合法!", DevAuthorNameEnum.Michael, false);
|
||
throw new Exception("将空的xml串转换为WxPayData不合法!");
|
||
}
|
||
|
||
XmlDocument xmlDoc = new XmlDocument();
|
||
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"] != "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)
|
||
{
|
||
// NLogHelper.WriteError("WxPayData内部含有值为null的字段!", DevAuthorNameEnum.Michael, false);
|
||
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 = Newtonsoft.Json.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)
|
||
{
|
||
// NLogHelper.WriteError("WxPayData内部含有值为null的字段!", DevAuthorNameEnum.Michael, false);
|
||
throw new Exception("WxPayData内部含有值为null的字段!");
|
||
}
|
||
|
||
str += string.Format("{0}={1}<br>", pair.Key, pair.Value.ToString());
|
||
}
|
||
// NLogHelper.WriteInfo("Print in Web Page : " + str, DevAuthorNameEnum.Michael, false);
|
||
return str;
|
||
}
|
||
|
||
/**
|
||
* @生成签名,详见签名生成算法
|
||
* @return 签名, sign字段不参加签名
|
||
*/
|
||
public string MakeSign()
|
||
{
|
||
//转url格式
|
||
string str = ToUrl();
|
||
//在string后加入API KEY
|
||
str += "&key=" + "{key}";
|
||
// NLogHelper.WriteInfo("Signurl" + str,DevAuthorNameEnum.Michael,false);
|
||
//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();
|
||
}
|
||
|
||
/**
|
||
*
|
||
* 检测签名是否正确
|
||
* 正确返回true,错误抛异常
|
||
*/
|
||
public bool CheckSign()
|
||
{
|
||
//如果没有设置签名,则跳过检测
|
||
if (!IsSet("sign"))
|
||
{
|
||
// Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
|
||
throw new Exception("WxPayData签名存在但不合法!");
|
||
}
|
||
//如果设置了签名但是签名为空,则抛异常
|
||
else if (GetValue("sign") == null || GetValue("sign").ToString() == "")
|
||
{
|
||
//Log.Error(this.GetType().ToString(), "WxPayData签名存在但不合法!");
|
||
throw new Exception("WxPayData签名存在但不合法!");
|
||
}
|
||
|
||
//获取接收到的签名
|
||
string return_sign = GetValue("sign").ToString();
|
||
|
||
//在本地计算新的签名
|
||
string cal_sign = MakeSign();
|
||
|
||
if (cal_sign == return_sign)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
//Log.Error(this.GetType().ToString(), "WxPayData签名验证错误!");
|
||
throw new Exception("WxPayData签名验证错误!");
|
||
}
|
||
|
||
/**
|
||
* @获取Dictionary
|
||
*/
|
||
public SortedDictionary<string, object> GetValues()
|
||
{
|
||
return m_values;
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
*
|
||
* 统一下单
|
||
* @param WxPaydata inputObj 提交给统一下单API的参数
|
||
* @param int timeOut 超时时间
|
||
* @throws WePayException
|
||
* @return 成功时返回,其他抛异常
|
||
*/
|
||
public static WxPayData UnifiedOrder(WxPayData inputObj, int timeOut = 60)
|
||
{
|
||
string url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
|
||
//检测必填参数
|
||
if (!inputObj.IsSet("out_trade_no"))
|
||
{
|
||
throw new Exception("缺少统一支付接口必填参数out_trade_no!");
|
||
}
|
||
else if (!inputObj.IsSet("body"))
|
||
{
|
||
throw new Exception("缺少统一支付接口必填参数body!");
|
||
}
|
||
else if (!inputObj.IsSet("total_fee"))
|
||
{
|
||
throw new Exception("缺少统一支付接口必填参数total_fee!");
|
||
}
|
||
else if (!inputObj.IsSet("trade_type"))
|
||
{
|
||
throw new Exception("缺少统一支付接口必填参数trade_type!");
|
||
}
|
||
|
||
//关联参数
|
||
if (inputObj.GetValue("trade_type").ToString() == "JSAPI" && !inputObj.IsSet("openid"))
|
||
{
|
||
throw new Exception("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!");
|
||
}
|
||
if (inputObj.GetValue("trade_type").ToString() == "NATIVE" && !inputObj.IsSet("product_id"))
|
||
{
|
||
throw new Exception("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!");
|
||
}
|
||
|
||
//异步通知url未设置,则使用配置文件中的url
|
||
if (!inputObj.IsSet("notify_url"))
|
||
{
|
||
inputObj.SetValue("notify_url", WeiXinPayConfig.NotifyUrl);//异步通知url
|
||
}
|
||
|
||
inputObj.SetValue("appid", WeiXinPayConfig.APP_ID);//公众账号ID
|
||
inputObj.SetValue("mch_id", WeiXinPayConfig.MCHID);//商户号
|
||
inputObj.SetValue("spbill_create_ip", WeiXinPayConfig.IP);//终端ip
|
||
inputObj.SetValue("nonce_str", GenerateNonceStr());//随机字符串
|
||
|
||
//签名
|
||
inputObj.SetValue("sign", inputObj.MakeSign());
|
||
string xml = inputObj.ToXml();
|
||
|
||
var start = DateTime.Now;
|
||
|
||
// Log.Info("XcxPayApi", "UnfiedOrder request : " + xml);
|
||
string response = HttpPost(xml, url, "application/xml", timeOut);
|
||
//Log.Info("XcxPayApi", "UnfiedOrder response : " + response);
|
||
// WebHookHelper.WebHookmarkdownSend(response);
|
||
|
||
var end = DateTime.Now;
|
||
int timeCost = (int)((end - start).TotalMilliseconds);
|
||
|
||
WxPayData result = new WxPayData();
|
||
result.FromXml(response);
|
||
// ReportCostTime(url, timeCost, result);//测速上报网络不好时使用
|
||
return result;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 生成随机数
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public static string GenerateNonceStr()
|
||
{
|
||
return Guid.NewGuid().ToString().Replace("-", "");
|
||
}
|
||
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// POST请求
|
||
/// </summary>
|
||
/// <param name="postData"></param>
|
||
/// <param name="url"></param>
|
||
/// <param name="contentType">application/xml、application/json、application/text、application/x-www-form-urlencoded</param>
|
||
/// <param name="timeOut"></param>
|
||
/// <param name="headers"></param>
|
||
/// <returns></returns>
|
||
public static string HttpPost(string postData, string url, string contentType = null, int timeOut = 30, Dictionary<string, string> headers = null)
|
||
{
|
||
postData = postData ?? "";
|
||
|
||
var httpClientHandler = new HttpClientHandler
|
||
{
|
||
ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true
|
||
};
|
||
using (HttpClient httpClient = new HttpClient(httpClientHandler))
|
||
{
|
||
if (headers != null)
|
||
{
|
||
foreach (var header in headers)
|
||
httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
|
||
}
|
||
using (HttpContent client = new StringContent(postData, Encoding.UTF8))
|
||
{
|
||
if (contentType != null)
|
||
client.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
|
||
|
||
HttpResponseMessage response = httpClient.PostAsync(url, client).Result;
|
||
return response.Content.ReadAsStringAsync().Result;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
}
|
||
} |