486 lines
20 KiB
C#
486 lines
20 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Net;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using Newtonsoft.Json;
|
||
|
||
namespace Mtxfw.Utility
|
||
{
|
||
public class HuifuPaymentService
|
||
{
|
||
private string _sysId;
|
||
private string _productId;
|
||
private string _huifuId;
|
||
private string _privateKey;
|
||
private string _apiUrl;
|
||
|
||
/// <summary>
|
||
/// 构造函数
|
||
/// </summary>
|
||
/// <param name="sysId">系统号</param>
|
||
/// <param name="productId">产品号</param>
|
||
/// <param name="huifuId">商户号</param>
|
||
/// <param name="privateKey">私钥</param>
|
||
/// <param name="apiUrl">API地址</param>
|
||
public HuifuPaymentService(string sysId, string productId, string huifuId, string privateKey, string apiUrl = "https://api.huifu.com/v3/trade/payment/jspay")
|
||
{
|
||
_sysId = sysId;
|
||
_productId = productId;
|
||
_huifuId = huifuId;
|
||
_privateKey = privateKey;
|
||
_apiUrl = apiUrl;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 记录异常到文件
|
||
/// </summary>
|
||
/// <param name="ex">异常对象</param>
|
||
/// <param name="methodName">方法名</param>
|
||
private void LogException(Exception ex, string methodName)
|
||
{
|
||
try
|
||
{
|
||
string logFileName = "huifu_pay_error_log_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".txt";
|
||
string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", logFileName);
|
||
|
||
// 确保logs目录存在
|
||
Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"));
|
||
|
||
// 记录异常信息
|
||
File.WriteAllText(logPath, "===== 异常时间: " + DateTime.Now.ToString() + " =====\n");
|
||
File.AppendAllText(logPath, "===== 方法名: " + methodName + " =====\n");
|
||
File.AppendAllText(logPath, "===== 异常类型: " + ex.GetType().FullName + " =====\n");
|
||
File.AppendAllText(logPath, "===== 异常消息: =====\n" + ex.Message + "\n");
|
||
File.AppendAllText(logPath, "===== 堆栈跟踪: =====\n" + ex.StackTrace + "\n");
|
||
|
||
if (ex.InnerException != null)
|
||
{
|
||
File.AppendAllText(logPath, "===== 内部异常: =====\n" + ex.InnerException.Message + "\n");
|
||
File.AppendAllText(logPath, "===== 内部异常堆栈: =====\n" + ex.InnerException.StackTrace + "\n");
|
||
}
|
||
}
|
||
catch (Exception logEx)
|
||
{
|
||
// 日志记录失败时,静默处理
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发起支付请求
|
||
/// </summary>
|
||
/// <param name="reqSeqId">请求流水号</param>
|
||
/// <param name="goodsDesc">商品描述</param>
|
||
/// <param name="tradeType">交易类型</param>
|
||
/// <param name="transAmt">交易金额</param>
|
||
/// <param name="wxData">微信参数集合</param>
|
||
/// <param name="timeExpire">交易有效期</param>
|
||
/// <param name="notifyUrl">异步通知地址</param>
|
||
/// <returns>支付响应</returns>
|
||
public HuifuPayResponse Pay(string reqSeqId, string goodsDesc, string tradeType, string transAmt, string wxData, string timeExpire = null, string notifyUrl = null)
|
||
{
|
||
try
|
||
{
|
||
// 构建请求数据
|
||
var requestData = new HuifuPayRequest
|
||
{
|
||
SysId = _sysId,
|
||
ProductId = _productId,
|
||
Data = new HuifuPayRequestData
|
||
{
|
||
ReqDate = DateTime.Now.ToString("yyyyMMdd"),
|
||
ReqSeqId = reqSeqId,
|
||
HuifuId = _huifuId,
|
||
GoodsDesc = goodsDesc,
|
||
TradeType = tradeType,
|
||
TransAmt = transAmt,
|
||
TimeExpire = timeExpire ?? DateTime.Now.AddMinutes(30).ToString("yyyyMMddHHmmss"),
|
||
WxData = wxData,
|
||
NotifyUrl = notifyUrl
|
||
}
|
||
};
|
||
|
||
// 生成签名
|
||
string dataJson = JsonConvert.SerializeObject(requestData.Data);
|
||
requestData.Sign = GenerateSign(dataJson, _privateKey);
|
||
|
||
// 序列化请求
|
||
string requestJson = JsonConvert.SerializeObject(requestData);
|
||
|
||
// 发送请求
|
||
string responseJson = SendRequest(_apiUrl, requestJson);
|
||
|
||
// 反序列化响应
|
||
return JsonConvert.DeserializeObject<HuifuPayResponse>(responseJson);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogException(ex, "Pay");
|
||
throw new Exception("支付请求失败: " + ex.Message, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成签名
|
||
/// </summary>
|
||
/// <param name="dataJson">数据JSON字符串</param>
|
||
/// <param name="privateKey">密钥</param>
|
||
/// <returns>签名结果</returns>
|
||
private string GenerateSign(string dataJson, string privateKey)
|
||
{
|
||
try
|
||
{
|
||
|
||
// 移除私钥中的换行符和空格
|
||
privateKey = privateKey.Replace("\r", "").Replace("\n", "").Replace(" ", "");
|
||
|
||
// 转换私钥格式
|
||
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
|
||
rsa.FromXmlString(ConvertPrivateKeyToXml(privateKey));
|
||
|
||
// 生成签名
|
||
byte[] dataBytes = Encoding.UTF8.GetBytes(dataJson);
|
||
byte[] signatureBytes = rsa.SignData(dataBytes, new SHA256CryptoServiceProvider());
|
||
|
||
return Convert.ToBase64String(signatureBytes);
|
||
|
||
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogException(ex, "GenerateSign");
|
||
throw new Exception("生成签名失败: " + ex.Message, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将Java格式的私钥转换为XML格式
|
||
/// </summary>
|
||
/// <param name="privateKey">Java格式私钥</param>
|
||
/// <returns>XML格式私钥</returns>
|
||
private string ConvertPrivateKeyToXml(string privateKey)
|
||
{
|
||
try
|
||
{
|
||
// 移除私钥中的头部和尾部
|
||
privateKey = privateKey.Replace("-----BEGIN PRIVATE KEY-----", "");
|
||
privateKey = privateKey.Replace("-----END PRIVATE KEY-----", "");
|
||
privateKey = privateKey.Trim();
|
||
|
||
// 解码Base64字符串
|
||
byte[] keyBytes = Convert.FromBase64String(privateKey);
|
||
|
||
// 使用BouncyCastle解析私钥
|
||
Org.BouncyCastle.Asn1.Asn1Sequence sequence = Org.BouncyCastle.Asn1.Asn1Sequence.GetInstance(keyBytes);
|
||
|
||
// 提取RSA私钥参数
|
||
Org.BouncyCastle.Asn1.DerInteger modulus, publicExponent, privateExponent, prime1, prime2, exponent1, exponent2, coefficient;
|
||
|
||
// 检查私钥格式
|
||
if (sequence.Count == 3) // PKCS#8格式
|
||
{
|
||
// 版本号
|
||
Org.BouncyCastle.Asn1.DerInteger version = (Org.BouncyCastle.Asn1.DerInteger)sequence[0];
|
||
// 算法标识符
|
||
Org.BouncyCastle.Asn1.Asn1Sequence algorithmId = (Org.BouncyCastle.Asn1.Asn1Sequence)sequence[1];
|
||
// 私钥数据
|
||
Org.BouncyCastle.Asn1.DerOctetString privateKeyOctet = (Org.BouncyCastle.Asn1.DerOctetString)sequence[2];
|
||
|
||
// 解析私钥数据
|
||
Org.BouncyCastle.Asn1.Asn1Sequence privateKeySequence = Org.BouncyCastle.Asn1.Asn1Sequence.GetInstance(privateKeyOctet.GetOctets());
|
||
|
||
// 提取RSA参数
|
||
modulus = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[1];
|
||
publicExponent = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[2];
|
||
privateExponent = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[3];
|
||
prime1 = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[4];
|
||
prime2 = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[5];
|
||
exponent1 = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[6];
|
||
exponent2 = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[7];
|
||
coefficient = (Org.BouncyCastle.Asn1.DerInteger)privateKeySequence[8];
|
||
}
|
||
else if (sequence.Count == 9) // 直接的RSA私钥格式
|
||
{
|
||
// 提取RSA参数
|
||
modulus = (Org.BouncyCastle.Asn1.DerInteger)sequence[1];
|
||
publicExponent = (Org.BouncyCastle.Asn1.DerInteger)sequence[2];
|
||
privateExponent = (Org.BouncyCastle.Asn1.DerInteger)sequence[3];
|
||
prime1 = (Org.BouncyCastle.Asn1.DerInteger)sequence[4];
|
||
prime2 = (Org.BouncyCastle.Asn1.DerInteger)sequence[5];
|
||
exponent1 = (Org.BouncyCastle.Asn1.DerInteger)sequence[6];
|
||
exponent2 = (Org.BouncyCastle.Asn1.DerInteger)sequence[7];
|
||
coefficient = (Org.BouncyCastle.Asn1.DerInteger)sequence[8];
|
||
}
|
||
else
|
||
{
|
||
throw new Exception("不支持的私钥格式,序列长度: " + sequence.Count);
|
||
}
|
||
|
||
// 构建XML格式私钥
|
||
string xmlPrivateKey = $@"<RSAKeyValue>
|
||
<Modulus>{Convert.ToBase64String(modulus.Value.ToByteArrayUnsigned())}</Modulus>
|
||
<Exponent>{Convert.ToBase64String(publicExponent.Value.ToByteArrayUnsigned())}</Exponent>
|
||
<P>{Convert.ToBase64String(prime1.Value.ToByteArrayUnsigned())}</P>
|
||
<Q>{Convert.ToBase64String(prime2.Value.ToByteArrayUnsigned())}</Q>
|
||
<DP>{Convert.ToBase64String(exponent1.Value.ToByteArrayUnsigned())}</DP>
|
||
<DQ>{Convert.ToBase64String(exponent2.Value.ToByteArrayUnsigned())}</DQ>
|
||
<InverseQ>{Convert.ToBase64String(coefficient.Value.ToByteArrayUnsigned())}</InverseQ>
|
||
<D>{Convert.ToBase64String(privateExponent.Value.ToByteArrayUnsigned())}</D>
|
||
</RSAKeyValue>";
|
||
|
||
return xmlPrivateKey;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogException(ex, "ConvertPrivateKeyToXml");
|
||
throw new Exception("私钥格式转换失败: " + ex.Message, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将Java格式的公钥转换为XML格式
|
||
/// </summary>
|
||
/// <param name="publicKey">Java格式公钥</param>
|
||
/// <returns>XML格式公钥</returns>
|
||
private string ConvertPublicKeyToXml(string publicKey)
|
||
{
|
||
try
|
||
{
|
||
// 移除公钥中的头部和尾部
|
||
publicKey = publicKey.Replace("-----BEGIN PUBLIC KEY-----", "");
|
||
publicKey = publicKey.Replace("-----END PUBLIC KEY-----", "");
|
||
publicKey = publicKey.Trim();
|
||
|
||
// 解码Base64字符串
|
||
byte[] keyBytes = Convert.FromBase64String(publicKey);
|
||
|
||
// 使用BouncyCastle解析公钥
|
||
Org.BouncyCastle.Asn1.Asn1Sequence sequence = Org.BouncyCastle.Asn1.Asn1Sequence.GetInstance(keyBytes);
|
||
|
||
// 提取RSA公钥参数
|
||
Org.BouncyCastle.Asn1.DerInteger modulus, publicExponent;
|
||
|
||
if (sequence.Count == 2) // X.509格式
|
||
{
|
||
// 算法标识符
|
||
Org.BouncyCastle.Asn1.Asn1Sequence algorithmId = (Org.BouncyCastle.Asn1.Asn1Sequence)sequence[0];
|
||
// 公钥数据
|
||
Org.BouncyCastle.Asn1.DerBitString publicKeyBitString = (Org.BouncyCastle.Asn1.DerBitString)sequence[1];
|
||
|
||
// 解析公钥数据
|
||
Org.BouncyCastle.Asn1.Asn1Sequence publicKeySequence = Org.BouncyCastle.Asn1.Asn1Sequence.GetInstance(publicKeyBitString.GetBytes());
|
||
|
||
// 提取RSA参数
|
||
modulus = (Org.BouncyCastle.Asn1.DerInteger)publicKeySequence[0];
|
||
publicExponent = (Org.BouncyCastle.Asn1.DerInteger)publicKeySequence[1];
|
||
}
|
||
else
|
||
{
|
||
throw new Exception("不支持的公钥格式,序列长度: " + sequence.Count);
|
||
}
|
||
|
||
// 构建XML格式公钥
|
||
string xmlPublicKey = $@"<RSAKeyValue>
|
||
<Modulus>{Convert.ToBase64String(modulus.Value.ToByteArrayUnsigned())}</Modulus>
|
||
<Exponent>{Convert.ToBase64String(publicExponent.Value.ToByteArrayUnsigned())}</Exponent>
|
||
</RSAKeyValue>";
|
||
|
||
return xmlPublicKey;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogException(ex, "ConvertPublicKeyToXml");
|
||
throw new Exception("公钥格式转换失败: " + ex.Message, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证签名
|
||
/// </summary>
|
||
/// <param name="dataJson">数据JSON字符串</param>
|
||
/// <param name="signature">签名</param>
|
||
/// <param name="publicKey">公钥</param>
|
||
/// <returns>签名是否有效</returns>
|
||
public bool VerifySign(string dataJson, string signature, string publicKey)
|
||
{
|
||
try
|
||
{
|
||
// 移除公钥中的换行符和空格
|
||
publicKey = publicKey.Replace("\r", "").Replace("\n", "").Replace(" ", "");
|
||
|
||
// 转换公钥格式
|
||
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
|
||
rsa.FromXmlString(ConvertPublicKeyToXml(publicKey));
|
||
|
||
// 验证签名
|
||
byte[] dataBytes = Encoding.UTF8.GetBytes(dataJson);
|
||
byte[] signatureBytes = Convert.FromBase64String(signature);
|
||
|
||
return rsa.VerifyData(dataBytes, new SHA256CryptoServiceProvider(), signatureBytes);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogException(ex, "VerifySign");
|
||
throw new Exception("验证签名失败: " + ex.Message, ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发送HTTP请求
|
||
/// </summary>
|
||
/// <param name="url">请求地址</param>
|
||
/// <param name="requestJson">请求JSON</param>
|
||
/// <returns>响应结果</returns>
|
||
private string SendRequest(string url, string requestJson)
|
||
{
|
||
string responseJson = "";
|
||
string logFileName = "huifu_pay_log_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".txt";
|
||
string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", logFileName);
|
||
|
||
try
|
||
{
|
||
// 确保logs目录存在
|
||
Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"));
|
||
|
||
// 记录请求数据
|
||
File.WriteAllText(logPath, "===== 请求时间: " + DateTime.Now.ToString() + " =====\n");
|
||
File.AppendAllText(logPath, "===== 请求URL: " + url + " =====\n");
|
||
File.AppendAllText(logPath, "===== 请求数据: =====\n" + requestJson + "\n\n");
|
||
|
||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||
request.Method = "POST";
|
||
request.ContentType = "application/json";
|
||
request.Timeout = 30000; // 30秒超时
|
||
|
||
// 写入请求数据
|
||
using (StreamWriter writer = new StreamWriter(request.GetRequestStream()))
|
||
{
|
||
writer.Write(requestJson);
|
||
}
|
||
|
||
// 获取响应
|
||
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
|
||
using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
|
||
{
|
||
responseJson = reader.ReadToEnd();
|
||
// 记录响应数据
|
||
File.AppendAllText(logPath, "===== 响应时间: " + DateTime.Now.ToString() + " =====\n");
|
||
File.AppendAllText(logPath, "===== 响应数据: =====\n" + responseJson + "\n");
|
||
return responseJson;
|
||
}
|
||
}
|
||
catch (WebException ex)
|
||
{
|
||
if (ex.Response != null)
|
||
{
|
||
using (StreamReader reader = new StreamReader(ex.Response.GetResponseStream(), Encoding.UTF8))
|
||
{
|
||
string errorResponse = reader.ReadToEnd();
|
||
// 记录错误响应
|
||
File.AppendAllText(logPath, "===== 错误时间: " + DateTime.Now.ToString() + " =====\n");
|
||
File.AppendAllText(logPath, "===== 错误响应: =====\n" + errorResponse + "\n");
|
||
LogException(ex, "SendRequest");
|
||
throw new Exception("HTTP请求失败: " + errorResponse, ex);
|
||
}
|
||
}
|
||
// 记录异常
|
||
File.AppendAllText(logPath, "===== 异常时间: " + DateTime.Now.ToString() + " =====\n");
|
||
File.AppendAllText(logPath, "===== 异常信息: =====\n" + ex.ToString() + "\n");
|
||
LogException(ex, "SendRequest");
|
||
throw new Exception("HTTP请求失败: " + ex.Message, ex);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 记录其他异常
|
||
File.AppendAllText(logPath, "===== 异常时间: " + DateTime.Now.ToString() + " =====\n");
|
||
File.AppendAllText(logPath, "===== 异常信息: =====\n" + ex.ToString() + "\n");
|
||
LogException(ex, "SendRequest");
|
||
throw new Exception("发送请求失败: " + ex.Message, ex);
|
||
}
|
||
}
|
||
}
|
||
|
||
#region 请求响应模型
|
||
|
||
public class HuifuPayRequest
|
||
{
|
||
[JsonProperty("sys_id")]
|
||
public string SysId { get; set; }
|
||
|
||
[JsonProperty("product_id")]
|
||
public string ProductId { get; set; }
|
||
|
||
[JsonProperty("sign")]
|
||
public string Sign { get; set; }
|
||
|
||
[JsonProperty("data")]
|
||
public HuifuPayRequestData Data { get; set; }
|
||
}
|
||
|
||
public class HuifuPayRequestData
|
||
{
|
||
[JsonProperty("req_date")]
|
||
public string ReqDate { get; set; }
|
||
|
||
[JsonProperty("req_seq_id")]
|
||
public string ReqSeqId { get; set; }
|
||
|
||
[JsonProperty("huifu_id")]
|
||
public string HuifuId { get; set; }
|
||
|
||
[JsonProperty("goods_desc")]
|
||
public string GoodsDesc { get; set; }
|
||
|
||
[JsonProperty("trade_type")]
|
||
public string TradeType { get; set; }
|
||
|
||
[JsonProperty("trans_amt")]
|
||
public string TransAmt { get; set; }
|
||
|
||
[JsonProperty("time_expire")]
|
||
public string TimeExpire { get; set; }
|
||
|
||
[JsonProperty("wx_data")]
|
||
public string WxData { get; set; }
|
||
|
||
[JsonProperty("notify_url")]
|
||
public string NotifyUrl { get; set; }
|
||
}
|
||
|
||
public class HuifuPayResponse
|
||
{
|
||
[JsonProperty("code")]
|
||
public string Code { get; set; }
|
||
|
||
[JsonProperty("msg")]
|
||
public string Msg { get; set; }
|
||
|
||
[JsonProperty("data")]
|
||
public HuifuPayResponseData Data { get; set; }
|
||
|
||
[JsonProperty("sign")]
|
||
public string Sign { get; set; }
|
||
}
|
||
|
||
public class HuifuPayResponseData
|
||
{
|
||
[JsonProperty("resp_code")]
|
||
public string RespCode { get; set; }
|
||
|
||
[JsonProperty("resp_desc")]
|
||
public string RespDesc { get; set; }
|
||
|
||
[JsonProperty("pay_info")]
|
||
public string PayInfo { get; set; }
|
||
|
||
[JsonProperty("out_trans_id")]
|
||
public string OutTransId { get; set; }
|
||
|
||
[JsonProperty("party_order_id")]
|
||
public string PartyOrderId { get; set; }
|
||
}
|
||
|
||
#endregion
|
||
} |