676 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.Extensions.Logging;
using SKIT.FlurlHttpClient.Wechat.TenpayV3;
using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models;
using YD_Event.Core.Entities;
using YD_Event.Core.Infrastructure.WeChat.Dto;
using Furion.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using System.Net.Http;
using Microsoft.Extensions.Hosting;
namespace YD_Event.Core.Infrastructure.WeChat;
/// <summary>
/// 微信支付服务
/// </summary>
/// <remarks>
/// 提供微信支付相关的核心功能,包括:
/// 1. JSAPI支付下单
/// 2. Native支付下单
/// 3. 订单状态查询
/// 4. 退款申请
/// 5. 退款查询
/// 6. 支付通知验证和解密
/// 7. 退款通知验证和解密
/// </remarks>
public class WeChatPayService : ITransient
{
private readonly ILogger<WeChatPayService> _logger;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpClientFactory _httpClientFactory;
private const int DefaultNetworkTimeoutSeconds = 30;
public WeChatPayService(ILogger<WeChatPayService> logger, IWebHostEnvironment webHostEnvironment, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_webHostEnvironment = webHostEnvironment;
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
}
/// <summary>
/// 创建微信支付客户端
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <returns>微信支付客户端实例</returns>
private async Task<WechatTenpayClient> CreateClientAsync(WeChatPayConfig config)
{
var privateKey = await GetPrivateKeyAsync(config.MerchantCertificatePrivateKey);
if (string.IsNullOrWhiteSpace(privateKey))
{
throw new InvalidOperationException("无法获取微信支付商户私钥,请检查配置。");
}
var options = new WechatTenpayClientOptions()
{
MerchantId = config.MerchantIdValue,
MerchantV3Secret = config.MerchantV3Secret,
MerchantCertificateSerialNumber = config.MerchantCertificateSerialNumber,
MerchantCertificatePrivateKey = privateKey
};
return new WechatTenpayClient(options);
}
/// <summary>
/// 获取微信支付商户私钥
/// </summary>
/// <param name="source">私钥来源,可以是:直接的密钥内容、本地文件路径、网络URL</param>
/// <returns>私钥内容字符串</returns>
private async Task<string> GetPrivateKeyAsync(string source)
{
if (string.IsNullOrWhiteSpace(source))
{
_logger.LogError("私钥来源为空");
throw new ArgumentException("私钥来源不能为空", nameof(source));
}
try
{
var trimmedSource = source.Trim();
if (IsPrivateKeyContent(trimmedSource))
{
_logger.LogDebug("使用直接提供的私钥内容");
return trimmedSource;
}
if (Uri.TryCreate(trimmedSource, UriKind.Absolute, out var uri))
{
if (uri.IsFile)
{
_logger.LogDebug("从本地文件路径加载私钥: {Path}", uri.LocalPath);
return await ReadPrivateKeyFromFileAsync(uri.LocalPath);
}
else if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
_logger.LogDebug("从网络URL加载私钥: {Url}", trimmedSource);
return await ReadPrivateKeyFromNetworkAsync(trimmedSource);
}
}
var localPath = Path.Combine(_webHostEnvironment.WebRootPath, trimmedSource.TrimStart('/', '\\'));
if (File.Exists(localPath))
{
_logger.LogDebug("从 wwwroot 相对路径加载私钥: {Path}", localPath);
return await ReadPrivateKeyFromFileAsync(localPath);
}
if (File.Exists(trimmedSource))
{
_logger.LogDebug("从绝对路径加载私钥: {Path}", trimmedSource);
return await ReadPrivateKeyFromFileAsync(trimmedSource);
}
_logger.LogError("无法从任何来源加载私钥: {Source}", source);
throw new FileNotFoundException($"无法找到私钥文件或解析私钥内容: {source}");
}
catch (Exception ex) when (ex is not ArgumentException && ex is not FileNotFoundException)
{
_logger.LogError(ex, "获取私钥过程中发生异常: {Source}", source);
throw;
}
}
/// <summary>
/// 检查输入是否为有效的私钥内容
/// </summary>
private bool IsPrivateKeyContent(string content)
{
return content.StartsWith("-----BEGIN", StringComparison.OrdinalIgnoreCase) ||
content.StartsWith("MII", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 从本地文件读取私钥
/// </summary>
private async Task<string> ReadPrivateKeyFromFileAsync(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"私钥文件不存在: {filePath}");
}
var content = await File.ReadAllTextAsync(filePath);
if (string.IsNullOrWhiteSpace(content))
{
throw new InvalidOperationException($"私钥文件内容为空: {filePath}");
}
_logger.LogInformation("成功从文件加载私钥: {Path}", filePath);
return content;
}
/// <summary>
/// 从网络URL读取私钥
/// </summary>
private async Task<string> ReadPrivateKeyFromNetworkAsync(string url)
{
var client = _httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(DefaultNetworkTimeoutSeconds);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(DefaultNetworkTimeoutSeconds + 5));
try
{
var response = await client.GetAsync(url, cts.Token);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cts.Token);
if (string.IsNullOrWhiteSpace(content))
{
throw new InvalidOperationException($"从网络获取的私钥内容为空: {url}");
}
_logger.LogInformation("成功从网络加载私钥: {Url}", url);
return content;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "网络请求失败,无法获取私钥: {Url}", url);
throw new InvalidOperationException($"网络请求失败,无法获取私钥: {url}", ex);
}
catch (OperationCanceledException ex)
{
_logger.LogError(ex, "获取私钥请求超时: {Url}", url);
throw new TimeoutException($"获取私钥请求超时: {url}", ex);
}
}
/// <summary>
/// 创建 JSAPI 支付
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="request">JSAPI 支付请求参数</param>
/// <returns>支付结果,包含 prepay_id 和支付参数</returns>
/// <remarks>
/// JSAPI 支付适用于小程序、公众号等场景,需要用户的 OpenId
/// </remarks>
public async Task<WeChatPayResult> CreateJsApiPayAsync(WeChatPayConfig config, CreateJsApiPayRequest request)
{
try
{
var client = await CreateClientAsync(config);
var tenpayRequest = new CreatePayTransactionJsapiRequest()
{
OutTradeNumber = request.OutTradeNo,
AppId = request.AppId,
Description = request.Description,
NotifyUrl = request.NotifyUrl,
Amount = new CreatePayTransactionJsapiRequest.Types.Amount()
{
Total = request.TotalAmount,
Currency = request.Currency
},
Payer = new CreatePayTransactionJsapiRequest.Types.Payer()
{
OpenId = request.OpenId
}
};
if (request.ExpireTime.HasValue)
{
tenpayRequest.ExpireTime = request.ExpireTime.Value;
}
var response = await client.ExecuteCreatePayTransactionJsapiAsync(tenpayRequest);
if (response.IsSuccessful())
{
var result = new WeChatPayResult
{
Success = true,
TradeType = "JSAPI",
PrepayId = response.PrepayId
};
var parameters = client.GenerateParametersForJsapiPayRequest(request.AppId, response.PrepayId);
result.JsApiParameters = new JsApiPayParameters
{
AppId = parameters["appId"],
TimeStamp = parameters["timeStamp"],
NonceStr = parameters["nonceStr"],
Package = parameters["package"],
SignType = parameters["signType"],
PaySign = parameters["paySign"]
};
return result;
}
else
{
_logger.LogError($"微信 JSAPI 支付下单失败: {response.ErrorMessage}");
return new WeChatPayResult
{
Success = false,
Message = response.ErrorMessage
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "微信 JSAPI 支付下单异常");
return new WeChatPayResult
{
Success = false,
Message = ex.Message
};
}
}
/// <summary>
/// 创建 Native 支付
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="request">Native 支付请求参数</param>
/// <returns>支付结果,包含二维码链接</returns>
/// <remarks>
/// Native 支付适用于生成二维码供用户扫描支付的场景
/// </remarks>
public async Task<WeChatPayResult> CreateNativePayAsync(WeChatPayConfig config, CreateNativePayRequest request)
{
try
{
var client = await CreateClientAsync(config);
var tenpayRequest = new CreatePayTransactionNativeRequest()
{
OutTradeNumber = request.OutTradeNo,
AppId = request.AppId,
Description = request.Description,
NotifyUrl = request.NotifyUrl,
Amount = new CreatePayTransactionNativeRequest.Types.Amount()
{
Total = request.TotalAmount,
Currency = request.Currency
}
};
if (request.ExpireTime.HasValue)
{
tenpayRequest.ExpireTime = request.ExpireTime.Value;
}
var response = await client.ExecuteCreatePayTransactionNativeAsync(tenpayRequest);
if (response.IsSuccessful())
{
return new WeChatPayResult
{
Success = true,
TradeType = "NATIVE",
CodeUrl = response.QrcodeUrl,
QrcodeUrl = response.QrcodeUrl
};
}
else
{
_logger.LogError($"微信 Native 支付下单失败: {response.ErrorMessage}");
return new WeChatPayResult
{
Success = false,
Message = response.ErrorMessage
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "微信 Native 支付下单异常");
return new WeChatPayResult
{
Success = false,
Message = ex.Message
};
}
}
/// <summary>
/// 查询订单状态
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="outTradeNo">商户订单号</param>
/// <returns>订单查询结果</returns>
/// <remarks>
/// 根据商户订单号查询微信支付订单的状态
/// </remarks>
public async Task<WeChatPayQueryResult> QueryOrderAsync(WeChatPayConfig config, string outTradeNo)
{
try
{
var client = await CreateClientAsync(config);
var request = new GetPayTransactionByOutTradeNumberRequest()
{
OutTradeNumber = outTradeNo,
MerchantId = config.MerchantIdValue
};
var response = await client.ExecuteGetPayTransactionByOutTradeNumberAsync(request);
if (response.IsSuccessful())
{
return new WeChatPayQueryResult
{
Success = true,
TradeState = response.TradeState,
TradeStateDesc = response.TradeStateDescription,
TransactionId = response.TransactionId,
OutTradeNo = response.OutTradeNumber,
TotalAmount = response.Amount?.Total,
PayerAmount = response.Amount?.PayerTotal,
SuccessTime = response.SuccessTime
};
}
else
{
_logger.LogError($"查询订单失败: {response.ErrorMessage}");
return new WeChatPayQueryResult
{
Success = false,
Message = response.ErrorMessage
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "查询订单异常");
return new WeChatPayQueryResult
{
Success = false,
Message = ex.Message
};
}
}
/// <summary>
/// 创建退款申请
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="request">退款请求参数</param>
/// <returns>退款结果</returns>
public async Task<WeChatRefundResult> CreateRefundAsync(WeChatPayConfig config, CreateRefundRequest request)
{
try
{
_logger.LogInformation($"微信退款申请: 商户单号={request.OutTradeNo}, 退款单号={request.OutRefundNo}, 退款金额={request.RefundAmount}");
var client = await CreateClientAsync(config);
var tenpayRequest = new CreateRefundDomesticRefundRequest
{
OutTradeNumber = request.OutTradeNo,
OutRefundNumber = request.OutRefundNo,
Reason = request.Reason ?? "用户取消报名",
NotifyUrl = request.NotifyUrl,
Amount = new CreateRefundDomesticRefundRequest.Types.Amount()
{
Refund = request.RefundAmount,
Total = request.TotalAmount,
Currency = request.Currency ?? "CNY"
}
};
var response = await client.ExecuteCreateRefundDomesticRefundAsync(tenpayRequest);
_logger.LogDebug($"微信退款响应: {System.Text.Json.JsonSerializer.Serialize(response)}");
if (response.IsSuccessful())
{
_logger.LogInformation($"微信退款申请成功: 退款单号={response.OutRefundNumber}, 微信退款ID={response.RefundId}");
return new WeChatRefundResult
{
Success = true,
RefundId = response.RefundId,
OutRefundNo = response.OutRefundNumber,
OutTradeNo = response.OutTradeNumber,
RefundStatus = response.Status,
Message = "退款申请已提交",
WeChatResponse = System.Text.Json.JsonSerializer.Serialize(response)
};
}
else
{
_logger.LogError($"微信退款申请失败: Code={response.ErrorCode}, Message={response.ErrorMessage}");
return new WeChatRefundResult
{
Success = false,
Message = response.ErrorMessage,
ErrorCode = response.ErrorCode,
WeChatResponse = System.Text.Json.JsonSerializer.Serialize(response)
};
}
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "微信退款网络请求异常");
return new WeChatRefundResult
{
Success = false,
Message = "网络请求失败,请稍后重试"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "微信退款申请异常");
return new WeChatRefundResult
{
Success = false,
Message = ex.Message
};
}
}
/// <summary>
/// 查询退款状态
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="outRefundNo">商户退款单号</param>
/// <returns>退款查询结果</returns>
public async Task<WeChatRefundQueryResult> QueryRefundAsync(WeChatPayConfig config, string outRefundNo)
{
try
{
_logger.LogInformation($"查询微信退款: 退款单号={outRefundNo}");
var client = await CreateClientAsync(config);
var tenpayRequest = new GetRefundDomesticRefundByOutRefundNumberRequest
{
OutRefundNumber = outRefundNo
};
var response = await client.ExecuteGetRefundDomesticRefundByOutRefundNumberAsync(tenpayRequest);
_logger.LogDebug($"微信退款查询响应: {System.Text.Json.JsonSerializer.Serialize(response)}");
if (response.IsSuccessful())
{
_logger.LogInformation($"查询微信退款成功: 退款单号={response.OutRefundNumber}, 状态={response.Status}");
return new WeChatRefundQueryResult
{
Success = true,
RefundId = response.RefundId,
OutRefundNo = response.OutRefundNumber,
OutTradeNo = response.OutTradeNumber,
RefundStatus = response.Status,
RefundAmount = response.Amount?.Refund,
PayerRefundAmount = response.Amount?.PayerRefund,
SuccessTime = response.SuccessTime,
Message = "查询成功",
WeChatResponse = System.Text.Json.JsonSerializer.Serialize(response)
};
}
else
{
_logger.LogError($"查询微信退款失败: Code={response.ErrorCode}, Message={response.ErrorMessage}");
return new WeChatRefundQueryResult
{
Success = false,
Message = response.ErrorMessage,
WeChatResponse = System.Text.Json.JsonSerializer.Serialize(response)
};
}
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "查询微信退款网络请求异常");
return new WeChatRefundQueryResult
{
Success = false,
Message = "网络请求失败,请稍后重试"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "查询退款异常");
return new WeChatRefundQueryResult
{
Success = false,
Message = ex.Message
};
}
}
/// <summary>
/// 验证支付通知签名
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="notifyRequest">支付通知请求参数</param>
/// <returns>签名是否有效</returns>
/// <remarks>
/// 验证微信支付通知的签名是否有效
/// </remarks>
public async Task<bool> VerifyPayNotifyAsync(WeChatPayConfig config, WeChatPayNotifyRequest notifyRequest)
{
try
{
var client = await CreateClientAsync(config);
bool valid = client.VerifyEventSignature(
notifyRequest.Timestamp,
notifyRequest.Nonce,
notifyRequest.Body,
notifyRequest.Signature,
notifyRequest.SerialNumber
);
return await Task.FromResult(valid);
}
catch (Exception ex)
{
_logger.LogError(ex, "验证支付通知签名异常");
return false;
}
}
/// <summary>
/// 验证退款通知签名
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="notifyRequest">退款通知请求参数</param>
/// <returns>签名是否有效</returns>
/// <remarks>
/// 验证微信退款通知的签名是否有效
/// </remarks>
public async Task<bool> VerifyRefundNotifyAsync(WeChatPayConfig config, WeChatRefundNotifyRequest notifyRequest)
{
try
{
var client = await CreateClientAsync(config);
bool valid = client.VerifyEventSignature(
notifyRequest.Timestamp,
notifyRequest.Nonce,
notifyRequest.Body,
notifyRequest.Signature,
notifyRequest.SerialNumber
);
return await Task.FromResult(valid);
}
catch (Exception ex)
{
_logger.LogError(ex, "验证退款通知签名异常");
return false;
}
}
/// <summary>
/// 解密支付通知数据
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="notifyRequest">支付通知请求参数</param>
/// <returns>解密后的支付通知数据,失败返回 null</returns>
/// <remarks>
/// 解密微信支付通知的加密数据,仅处理 TRANSACTION.SUCCESS 事件
/// </remarks>
public async Task<WeChatPayNotifyData?> DecryptPayNotifyDataAsync(WeChatPayConfig config, WeChatPayNotifyRequest notifyRequest)
{
try
{
var client = await CreateClientAsync(config);
var callbackModel = client.DeserializeEvent(notifyRequest.Body);
if (callbackModel.EventType == "TRANSACTION.SUCCESS")
{
var eventData = client.DecryptEventResource<SKIT.FlurlHttpClient.Wechat.TenpayV3.Events.TransactionResource>(callbackModel);
return new WeChatPayNotifyData
{
EventType = callbackModel.EventType,
OutTradeNo = eventData.OutTradeNumber,
TransactionId = eventData.TransactionId,
TradeState = eventData.TradeState,
TotalAmount = eventData.Amount?.Total,
PayerAmount = eventData.Amount?.PayerTotal,
SuccessTime = eventData.SuccessTime
};
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "解密支付通知数据异常");
return null;
}
}
/// <summary>
/// 解密退款通知数据
/// </summary>
/// <param name="config">微信支付配置信息</param>
/// <param name="notifyRequest">退款通知请求参数</param>
/// <returns>解密后的退款通知数据,失败返回 null</returns>
/// <remarks>
/// 解密微信退款通知的加密数据,处理 REFUND.SUCCESS、REFUND.ABNORMAL、REFUND.CLOSED 事件
/// </remarks>
public async Task<WeChatRefundNotifyData?> DecryptRefundNotifyDataAsync(WeChatPayConfig config, WeChatRefundNotifyRequest notifyRequest)
{
try
{
var client = await CreateClientAsync(config);
var callbackModel = client.DeserializeEvent(notifyRequest.Body);
if (callbackModel.EventType == "REFUND.SUCCESS" || callbackModel.EventType == "REFUND.ABNORMAL" || callbackModel.EventType == "REFUND.CLOSED")
{
var eventData = client.DecryptEventResource<SKIT.FlurlHttpClient.Wechat.TenpayV3.Events.RefundResource>(callbackModel);
return new WeChatRefundNotifyData
{
EventType = callbackModel.EventType,
OutTradeNo = eventData.OutTradeNumber,
OutRefundNo = eventData.OutRefundNumber,
RefundId = eventData.RefundId,
RefundStatus = eventData.RefundStatus,
RefundAmount = eventData.Amount?.Refund,
PayerRefundAmount = eventData.Amount?.PayerRefund,
SuccessTime = eventData.SuccessTime
};
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "解密退款通知数据异常");
return null;
}
}
}