2026-03-25 23:58:30 +08:00
|
|
|
|
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;
|
2026-04-02 18:06:01 +08:00
|
|
|
|
using Microsoft.AspNetCore.Hosting;
|
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
|
using Microsoft.Extensions.Hosting;
|
2026-03-25 23:58:30 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-04-02 18:06:01 +08:00
|
|
|
|
private readonly IWebHostEnvironment _webHostEnvironment;
|
|
|
|
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|
|
|
|
|
private const int DefaultNetworkTimeoutSeconds = 30;
|
2026-03-25 23:58:30 +08:00
|
|
|
|
|
2026-04-02 18:06:01 +08:00
|
|
|
|
public WeChatPayService(ILogger<WeChatPayService> logger, IWebHostEnvironment webHostEnvironment, IHttpClientFactory httpClientFactory)
|
2026-03-25 23:58:30 +08:00
|
|
|
|
{
|
|
|
|
|
|
_logger = logger;
|
2026-04-02 18:06:01 +08:00
|
|
|
|
_webHostEnvironment = webHostEnvironment;
|
|
|
|
|
|
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
2026-03-25 23:58:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建微信支付客户端
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="config">微信支付配置信息</param>
|
|
|
|
|
|
/// <returns>微信支付客户端实例</returns>
|
2026-04-02 18:06:01 +08:00
|
|
|
|
private async Task<WechatTenpayClient> CreateClientAsync(WeChatPayConfig config)
|
2026-03-25 23:58:30 +08:00
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var privateKey = await GetPrivateKeyAsync(config.MerchantCertificatePrivateKey);
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(privateKey))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException("无法获取微信支付商户私钥,请检查配置。");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-25 23:58:30 +08:00
|
|
|
|
var options = new WechatTenpayClientOptions()
|
|
|
|
|
|
{
|
|
|
|
|
|
MerchantId = config.MerchantIdValue,
|
|
|
|
|
|
MerchantV3Secret = config.MerchantV3Secret,
|
|
|
|
|
|
MerchantCertificateSerialNumber = config.MerchantCertificateSerialNumber,
|
2026-04-02 18:06:01 +08:00
|
|
|
|
MerchantCertificatePrivateKey = privateKey
|
2026-03-25 23:58:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
return new WechatTenpayClient(options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 18:06:01 +08:00
|
|
|
|
/// <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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-25 23:58:30 +08:00
|
|
|
|
/// <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
|
|
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var client = await CreateClientAsync(config);
|
2026-03-25 23:58:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var client = await CreateClientAsync(config);
|
2026-03-25 23:58:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var client = await CreateClientAsync(config);
|
2026-03-25 23:58:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-04-02 08:40:47 +08:00
|
|
|
|
_logger.LogInformation($"微信退款申请: 商户单号={request.OutTradeNo}, 退款单号={request.OutRefundNo}, 退款金额={request.RefundAmount}");
|
|
|
|
|
|
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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())
|
2026-03-25 23:58:30 +08:00
|
|
|
|
{
|
2026-04-03 09:42:10 +08:00
|
|
|
|
_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 = "网络请求失败,请稍后重试"
|
|
|
|
|
|
};
|
2026-03-25 23:58:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
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}");
|
2026-04-02 08:40:47 +08:00
|
|
|
|
|
2026-04-03 09:42:10 +08:00
|
|
|
|
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
|
2026-03-25 23:58:30 +08:00
|
|
|
|
{
|
2026-04-03 09:42:10 +08:00
|
|
|
|
_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 = "网络请求失败,请稍后重试"
|
|
|
|
|
|
};
|
2026-03-25 23:58:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var client = await CreateClientAsync(config);
|
2026-03-25 23:58:30 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var client = await CreateClientAsync(config);
|
2026-03-25 23:58:30 +08:00
|
|
|
|
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>
|
2026-04-02 18:06:01 +08:00
|
|
|
|
public async Task<WeChatPayNotifyData?> DecryptPayNotifyDataAsync(WeChatPayConfig config, WeChatPayNotifyRequest notifyRequest)
|
2026-03-25 23:58:30 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var client = await CreateClientAsync(config);
|
2026-03-25 23:58:30 +08:00
|
|
|
|
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>
|
2026-04-02 18:06:01 +08:00
|
|
|
|
public async Task<WeChatRefundNotifyData?> DecryptRefundNotifyDataAsync(WeChatPayConfig config, WeChatRefundNotifyRequest notifyRequest)
|
2026-03-25 23:58:30 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-04-02 18:06:01 +08:00
|
|
|
|
var client = await CreateClientAsync(config);
|
2026-03-25 23:58:30 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|