using Baidu.Aip.BodyAnalysis;
|
using Furion.DatabaseAccessor;
|
using Furion.DataEncryption;
|
using Furion.DependencyInjection;
|
using Furion.HttpRemote;
|
using Furion.Logging;
|
using log4net;
|
using Microsoft.Extensions.Options;
|
using Newtonsoft.Json;
|
using System;
|
using System.Collections.Generic;
|
using System.Diagnostics;
|
using System.IO;
|
using System.Linq;
|
using System.Net.Http;
|
using System.Reflection;
|
using System.Security.Cryptography;
|
using System.Security.Cryptography.X509Certificates;
|
using System.Text;
|
using System.Text.Encodings.Web;
|
using System.Threading.Tasks;
|
|
namespace ApiTools.Core
|
{
|
/// <summary>
|
/// 微信支付工具
|
/// </summary>
|
public class WeChatPayUtils(
|
IOptions<WeChatPayOptions> options,
|
IHttpRemoteService httpRemoteService,
|
IRepository<ThreeResourceLog, LogDbContextLocator> repThreeResourceLog
|
) : ITransient
|
{
|
private readonly IOptions<WeChatPayOptions> options = options;
|
private readonly IHttpRemoteService httpRemoteService = httpRemoteService;
|
private readonly IRepository<ThreeResourceLog, LogDbContextLocator> repThreeResourceLog = repThreeResourceLog;
|
|
public async Task<TResponse> Send<TPathParameters, TQueryParameters, TBodyParameters, TResponse>(
|
EnumResourceMethod method,
|
string path,
|
WeChatPayRequest<TPathParameters, TQueryParameters, TBodyParameters> request,
|
Func<HttpRequestBuilder, HttpRequestBuilder> build = null)
|
where TResponse : WeChatPayResponse, new()
|
{
|
var log = new ThreeResourceLog
|
{
|
Method = method,
|
Domain = "https://api.mch.weixin.qq.com",
|
Path = path,
|
Request = request.ToJson()
|
};
|
if (request.PathParameters != null)
|
{
|
var properties = typeof(TPathParameters).GetProperties();
|
foreach (var property in properties)
|
{
|
var jsonProperty = property.GetCustomAttribute<JsonPropertyAttribute>();
|
var name = jsonProperty != null ? jsonProperty.PropertyName : property.Name;
|
path = path.Replace($"{{{name}}}", property.GetValue(request.PathParameters).ToString());
|
}
|
}
|
if (request.QueryParamters != null)
|
{
|
var properties = typeof(TQueryParameters).GetProperties();
|
var queries = new Dictionary<string, string>();
|
foreach (var property in properties)
|
{
|
var jsonProperty = property.GetCustomAttribute<JsonPropertyAttribute>();
|
var name = jsonProperty != null ? jsonProperty.PropertyName : property.Name;
|
var value = property.GetValue(request.QueryParamters).ToString();
|
if (value.IsNotNull())
|
{
|
value = UrlEncoder.Default.Encode(value);
|
}
|
queries.Add(name, value);
|
}
|
var queriesString = queries.Select(it => $"{it.Key}={it.Value}").SplitJoin("&");
|
path = $"{path}?{queriesString}";
|
}
|
|
var methodString = method.ToString().ToUpper();
|
var builder = HttpRequestBuilder.Create(methodString, $"{log.Domain}{path}");
|
|
var body = "";
|
if (request.BodyParamters != null)
|
{
|
body = request.BodyParamters.ToJson(new JsonSerializerSettings
|
{
|
NullValueHandling = NullValueHandling.Ignore
|
});
|
builder = builder.SetJsonContent(body);
|
builder = builder.WithHeader("Content-Type", "application/json");
|
}
|
var authorization = GetAuthorization(methodString, path, body, options.Value.MchId, options.Value.SerialNo, options.Value.PrivateKey);
|
builder = builder.WithHeader("Authorization", authorization);
|
builder = builder.WithHeader("Accept", "application/json");
|
if (build != null)
|
{
|
builder = build(builder);
|
}
|
|
await repThreeResourceLog.InsertNowAsync(log);
|
var stopwatch = Stopwatch.StartNew();
|
log.Response = await httpRemoteService.SendAsStringAsync(builder);
|
log.UpdatedTime = DateTimeOffset.Now;
|
stopwatch.Stop();
|
log.ElapsedMilliseconds = stopwatch.ElapsedMilliseconds;
|
var result = log.Response.JsonTo<TResponse>();
|
log.IsSuccess = false;
|
await repThreeResourceLog.UpdateNowAsync(log);
|
return result;
|
}
|
|
/// <summary>
|
/// 发起批量转账
|
/// </summary>
|
/// <param name="request"></param>
|
/// <returns></returns>
|
public async Task<PartnerTransferBatchesResponse> PartnerTransferBatches(WeChatPayRequest<object, object, WeChatPayPartnerTransferBatchesBodyParameters> request)
|
{
|
foreach (var item in request.BodyParamters.TransferDetailList)
|
{
|
item.UserName = item.UserName.IsNotNull() ? Encrypt(options.Value.PublicKey, item.UserName) : null;
|
item.UserIdCard = item.UserIdCard.IsNotNull() ? Encrypt(options.Value.PublicKey, item.UserIdCard) : null;
|
}
|
return await Send<object, object, WeChatPayPartnerTransferBatchesBodyParameters, PartnerTransferBatchesResponse>(
|
EnumResourceMethod.Post,
|
"/v3/partner-transfer/batches",
|
request,
|
it => it.WithHeader("Wechatpay-Serial", options.Value.SerialNo));
|
}
|
|
/// <summary>
|
/// 查询特约商户账户实时余额
|
/// </summary>
|
/// <param name="request"></param>
|
/// <returns></returns>
|
public async Task<WeChatPayEcommerceFundBalanceResponse> EcommerceFundBalance(WeChatPayRequest<WeChatPayEcommerceFundBalancePathParamters, WeChatPayEcommerceFundBalanceQueryParamters, object> request)
|
{
|
return await Send<WeChatPayEcommerceFundBalancePathParamters, WeChatPayEcommerceFundBalanceQueryParamters, object, WeChatPayEcommerceFundBalanceResponse>(
|
EnumResourceMethod.Get,
|
"/v3/ecommerce/fund/balance/{sub_mchid}",
|
request,
|
it => it.WithHeader("Wechatpay-Serial", options.Value.SerialNo));
|
}
|
|
/// <summary>
|
/// 生成认证信息
|
/// </summary>
|
/// <param name="method"></param>
|
/// <param name="path"></param>
|
/// <param name="body"></param>
|
/// <param name="mchId"></param>
|
/// <param name="serialNo"></param>
|
/// <param name="privateKey"></param>
|
/// <returns></returns>
|
private string GetAuthorization(string method, string path, string body, string mchId, string serialNo, string privateKey)
|
{
|
var timestamp = DateTime.Now.ToTimeStamp(false);
|
var nonce_str = GetHexDumpRandom();
|
var signature = GetSignature(method, path, body, privateKey, timestamp, nonce_str);
|
return $"WECHATPAY2-SHA256-RSA2048 mchid=\"{mchId}\",nonce_str=\"{nonce_str}\",signature=\"{signature}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\"";
|
}
|
|
/// <summary>
|
/// 加密敏感字段
|
/// </summary>
|
/// <param name="publicKey"></param>
|
/// <param name="data"></param>
|
/// <returns></returns>
|
public string Encrypt(string publicKey, string data)
|
{
|
byte[] dataToEncrypt = Encoding.UTF8.GetBytes(data);
|
using (var rsa = new RSACryptoServiceProvider())
|
{
|
rsa.ImportFromPem(publicKey.ToCharArray());
|
byte[] encryptedBytes = rsa.Encrypt(dataToEncrypt, true);
|
return Convert.ToBase64String(encryptedBytes);
|
}
|
}
|
|
/// <summary>
|
/// 解密敏感字段
|
/// </summary>
|
/// <param name="privareKey"></param>
|
/// <param name="encryptedBase64"></param>
|
/// <returns></returns>
|
public string Decrypt(string privareKey, string encryptedBase64)
|
{
|
byte[] encryptedBytes = Convert.FromBase64String(encryptedBase64);
|
using (var rsa = new RSACryptoServiceProvider())
|
{
|
rsa.ImportFromPem(privareKey.ToCharArray());
|
byte[] decryptedBytes = rsa.Decrypt(encryptedBytes, true);
|
return Encoding.UTF8.GetString(decryptedBytes);
|
}
|
}
|
|
/// <summary>
|
/// 生成签名
|
/// </summary>
|
/// <param name="method"></param>
|
/// <param name="path"></param>
|
/// <param name="body"></param>
|
/// <param name="privateKey"></param>
|
/// <param name="timestamp"></param>
|
/// <param name="nonce_str"></param>
|
/// <returns></returns>
|
private string GetSignature(string method, string path, string body, string privateKey, long timestamp, string nonce_str)
|
{
|
string signString = $"{method}\n{path}\n{timestamp}\n{nonce_str}\n{body}\n";
|
using (var rsa = new RSACryptoServiceProvider())
|
{
|
rsa.ImportFromPem(privateKey.ToCharArray());
|
|
// 将字符串转换为字节数组(使用UTF8编码)
|
byte[] dataBytes = Encoding.UTF8.GetBytes(signString);
|
|
// 执行签名:SHA256哈希算法,PKCS#1 v1.5填充模式
|
byte[] signatureBytes = rsa.SignData(
|
dataBytes,
|
HashAlgorithmName.SHA256,
|
RSASignaturePadding.Pkcs1
|
);
|
|
// 将签名结果转换为Base64字符串
|
var base64 = Convert.ToBase64String(signatureBytes);
|
return base64;
|
}
|
}
|
|
/// <summary>
|
/// 生成一个请求随机串
|
/// </summary>
|
/// <returns></returns>
|
private string GetHexDumpRandom()
|
{
|
// 16字节缓冲区,与hexdump -n 16对应
|
byte[] randomBytes = new byte[16];
|
|
// 使用加密级随机数生成器,类似于/dev/random
|
using (var rng = RandomNumberGenerator.Create())
|
{
|
rng.GetBytes(randomBytes);
|
}
|
|
// 构建输出字符串
|
StringBuilder sb = new StringBuilder();
|
|
// 4/4 "%08X" 对应:将16字节分成4个4字节整数,每个以8位十六进制显示
|
for (int i = 0; i < 4; i++)
|
{
|
// 从字节数组中解析32位整数(大端字节序)
|
uint value = BitConverter.ToUInt32(randomBytes, i * 4);
|
// 如果系统是小端字节序,需要转换为大端
|
if (BitConverter.IsLittleEndian)
|
{
|
value = ReverseBytes(value);
|
}
|
// 格式化为8位十六进制大写形式
|
sb.AppendFormat("{0:X8}", value);
|
}
|
|
return sb.ToString();
|
}
|
|
// 反转32位整数的字节序(小端转大端)
|
private static uint ReverseBytes(uint value)
|
{
|
return (value << 24) | ((value & 0x0000FF00) << 8) |
|
((value & 0x00FF0000) >> 8) | (value >> 24);
|
}
|
}
|
}
|