using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using MiAssessment.Model.Models.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
///
/// 微信支付 V3 服务实现
/// 提供基于 RSA-SHA256 签名和 AES-256-GCM 加密的 V3 版本支付功能
///
public class WechatPayV3Service : IWechatPayV3Service
{
private readonly MiAssessmentDbContext _dbContext;
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private readonly IWechatPayConfigService _configService;
///
/// V3 JSAPI 下单 API 地址
///
private const string V3_JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
///
/// V3 订单查询 API 地址(商户订单号)
///
private const string V3_QUERY_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{0}";
///
/// V3 关闭订单 API 地址
///
private const string V3_CLOSE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{0}/close";
///
/// V3 退款 API 地址
///
private const string V3_REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
///
/// 随机字符串字符集
///
private const string NONCE_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
public WechatPayV3Service(
MiAssessmentDbContext dbContext,
HttpClient httpClient,
ILogger logger,
IWechatPayConfigService configService)
{
_dbContext = dbContext;
_httpClient = httpClient;
_logger = logger;
_configService = configService;
}
#region 下单接口
///
public async Task CreateJsapiOrderAsync(WechatPayRequest request)
{
try
{
_logger.LogInformation("开始创建 V3 JSAPI 支付订单: OrderNo={OrderNo}, UserId={UserId}, Amount={Amount}",
request.OrderNo, request.UserId, request.Amount);
// 1. 获取用户信息和 OpenId
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == request.UserId);
if (user == null)
{
_logger.LogWarning("用户不存在: UserId={UserId}", request.UserId);
return new WechatPayResult { Status = 0, Msg = "用户不存在" };
}
var openId = string.IsNullOrEmpty(request.OpenId) ? user.OpenId : request.OpenId;
if (string.IsNullOrEmpty(openId))
{
_logger.LogWarning("用户 OpenId 为空: UserId={UserId}", request.UserId);
return new WechatPayResult { Status = 0, Msg = "用户 OpenId 不存在" };
}
// 2. 获取商户配置
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
// 验证 V3 配置
if (string.IsNullOrEmpty(merchantConfig.ApiV3Key) ||
string.IsNullOrEmpty(merchantConfig.CertSerialNo) ||
string.IsNullOrEmpty(merchantConfig.PrivateKeyPath))
{
_logger.LogError("V3 配置不完整: MchId={MchId}", merchantConfig.MchId);
return new WechatPayResult { Status = 0, Msg = "V3 支付配置不完整" };
}
_logger.LogDebug("使用 V3 商户配置: MchId={MchId}, AppId={AppId}", merchantConfig.MchId, merchantConfig.AppId);
// 3. 读取私钥
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath);
if (string.IsNullOrEmpty(privateKey))
{
_logger.LogError("读取私钥失败: Path={Path}", merchantConfig.PrivateKeyPath);
return new WechatPayResult { Status = 0, Msg = "读取商户私钥失败" };
}
// 4. 构建 V3 请求
var totalFee = (int)Math.Round(request.Amount * 100); // 转换为分
var v3Request = new WechatPayV3JsapiRequest
{
AppId = merchantConfig.AppId,
MchId = merchantConfig.MchId,
Description = TruncateDescription(request.Body, 127),
OutTradeNo = request.OrderNo,
NotifyUrl = merchantConfig.NotifyUrl,
Amount = new WechatPayV3Amount { Total = totalFee, Currency = "CNY" },
Payer = new WechatPayV3Payer { OpenId = openId },
Attach = request.Attach
};
var requestBody = JsonSerializer.Serialize(v3Request, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
});
// 5. 生成签名并发送请求
var timestamp = GetTimestamp();
var nonceStr = GenerateNonceStr();
var url = "/v3/pay/transactions/jsapi";
var signature = GenerateSignature("POST", url, timestamp, nonceStr, requestBody, privateKey);
// 6. 构建 Authorization 头
var authorization = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{merchantConfig.MchId}\",nonce_str=\"{nonceStr}\",timestamp=\"{timestamp}\",serial_no=\"{merchantConfig.CertSerialNo}\",signature=\"{signature}\"";
// 7. 发送请求
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, V3_JSAPI_URL);
httpRequest.Headers.Add("Authorization", authorization);
httpRequest.Headers.Add("Accept", "application/json");
httpRequest.Headers.Add("User-Agent", "MiAssessment/1.0");
httpRequest.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
_logger.LogDebug("V3 下单请求: URL={Url}, Body={Body}", V3_JSAPI_URL, requestBody);
var response = await _httpClient.SendAsync(httpRequest);
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogDebug("V3 下单响应: StatusCode={StatusCode}, Body={Body}", response.StatusCode, responseContent);
// 8. 处理响应
if (!response.IsSuccessStatusCode)
{
var errorResponse = JsonSerializer.Deserialize(responseContent);
var errorMsg = GetV3ErrorMessage(errorResponse?.Code ?? "UNKNOWN", errorResponse?.Message ?? "未知错误");
_logger.LogWarning("V3 下单失败: Code={Code}, Message={Message}", errorResponse?.Code, errorResponse?.Message);
return new WechatPayResult { Status = 0, Msg = $"支付失败({errorMsg})" };
}
var v3Response = JsonSerializer.Deserialize(responseContent);
if (v3Response == null || string.IsNullOrEmpty(v3Response.PrepayId))
{
_logger.LogError("V3 下单成功但 prepay_id 为空");
return new WechatPayResult { Status = 0, Msg = "网络故障,请稍后重试(prepay_id为空)" };
}
// 9. 保存订单通知记录
await SaveOrderNotifyAsync(request.OrderNo, merchantConfig.NotifyUrl, nonceStr, request.Amount, request.Attach, openId);
// 10. 构建返回给前端的支付参数(V3 使用 RSA 签名)
var payTimestamp = GetTimestamp();
var payNonceStr = GenerateNonceStr();
var packageStr = $"prepay_id={v3Response.PrepayId}";
var paySign = GeneratePaySign(merchantConfig.AppId, payTimestamp, payNonceStr, v3Response.PrepayId, privateKey);
_logger.LogInformation("V3 支付订单创建成功: OrderNo={OrderNo}, PrepayId={PrepayId}", request.OrderNo, v3Response.PrepayId);
return new WechatPayResult
{
Status = 1,
Msg = "success",
Data = new WechatPayData
{
AppId = merchantConfig.AppId,
TimeStamp = payTimestamp,
NonceStr = payNonceStr,
Package = packageStr,
SignType = "RSA",
PaySign = paySign,
IsWeixin = 1
}
};
}
catch (Exception ex)
{
_logger.LogError(ex, "创建 V3 支付订单异常: OrderNo={OrderNo}", request.OrderNo);
return new WechatPayResult { Status = 0, Msg = "系统错误,请稍后重试" };
}
}
#endregion
#region 订单管理接口
///
public async Task QueryOrderAsync(string orderNo)
{
try
{
_logger.LogInformation("开始查询 V3 订单: OrderNo={OrderNo}", orderNo);
var merchantConfig = _configService.GetMerchantByOrderNo(orderNo);
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath!);
var url = $"/v3/pay/transactions/out-trade-no/{orderNo}?mchid={merchantConfig.MchId}";
var fullUrl = string.Format(V3_QUERY_URL, orderNo) + $"?mchid={merchantConfig.MchId}";
var timestamp = GetTimestamp();
var nonceStr = GenerateNonceStr();
var signature = GenerateSignature("GET", url, timestamp, nonceStr, "", privateKey);
var authorization = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{merchantConfig.MchId}\",nonce_str=\"{nonceStr}\",timestamp=\"{timestamp}\",serial_no=\"{merchantConfig.CertSerialNo}\",signature=\"{signature}\"";
using var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl);
httpRequest.Headers.Add("Authorization", authorization);
httpRequest.Headers.Add("Accept", "application/json");
var response = await _httpClient.SendAsync(httpRequest);
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogDebug("V3 订单查询响应: StatusCode={StatusCode}, Body={Body}", response.StatusCode, responseContent);
if (!response.IsSuccessStatusCode)
{
var errorResponse = JsonSerializer.Deserialize(responseContent);
return new WechatPayV3QueryResult
{
Success = false,
ErrorCode = errorResponse?.Code,
ErrorMessage = errorResponse?.Message
};
}
var queryResponse = JsonSerializer.Deserialize(responseContent);
return new WechatPayV3QueryResult
{
Success = true,
TradeState = queryResponse?.TradeState ?? "",
TradeStateDesc = queryResponse?.TradeStateDesc ?? "",
TransactionId = queryResponse?.TransactionId,
OutTradeNo = queryResponse?.OutTradeNo,
TotalAmount = queryResponse?.Amount?.Total,
SuccessTime = queryResponse?.SuccessTime
};
}
catch (Exception ex)
{
_logger.LogError(ex, "查询 V3 订单异常: OrderNo={OrderNo}", orderNo);
return new WechatPayV3QueryResult
{
Success = false,
ErrorCode = "SYSTEM_ERROR",
ErrorMessage = ex.Message
};
}
}
///
public async Task CloseOrderAsync(string orderNo)
{
try
{
_logger.LogInformation("开始关闭 V3 订单: OrderNo={OrderNo}", orderNo);
var merchantConfig = _configService.GetMerchantByOrderNo(orderNo);
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath!);
var url = $"/v3/pay/transactions/out-trade-no/{orderNo}/close";
var fullUrl = string.Format(V3_CLOSE_URL, orderNo);
var requestBody = JsonSerializer.Serialize(new WechatPayV3CloseRequest { MchId = merchantConfig.MchId });
var timestamp = GetTimestamp();
var nonceStr = GenerateNonceStr();
var signature = GenerateSignature("POST", url, timestamp, nonceStr, requestBody, privateKey);
var authorization = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{merchantConfig.MchId}\",nonce_str=\"{nonceStr}\",timestamp=\"{timestamp}\",serial_no=\"{merchantConfig.CertSerialNo}\",signature=\"{signature}\"";
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl);
httpRequest.Headers.Add("Authorization", authorization);
httpRequest.Headers.Add("Accept", "application/json");
httpRequest.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(httpRequest);
_logger.LogDebug("V3 关闭订单响应: StatusCode={StatusCode}", response.StatusCode);
// HTTP 204 表示成功
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
{
_logger.LogInformation("V3 订单关闭成功: OrderNo={OrderNo}", orderNo);
return new WechatPayV3CloseResult { Success = true };
}
var responseContent = await response.Content.ReadAsStringAsync();
var errorResponse = JsonSerializer.Deserialize(responseContent);
return new WechatPayV3CloseResult
{
Success = false,
ErrorCode = errorResponse?.Code,
ErrorMessage = errorResponse?.Message
};
}
catch (Exception ex)
{
_logger.LogError(ex, "关闭 V3 订单异常: OrderNo={OrderNo}", orderNo);
return new WechatPayV3CloseResult
{
Success = false,
ErrorCode = "SYSTEM_ERROR",
ErrorMessage = ex.Message
};
}
}
#endregion
#region 退款接口
///
public async Task RefundAsync(WechatPayV3RefundRequest request)
{
try
{
_logger.LogInformation("开始 V3 退款: OrderNo={OrderNo}, RefundNo={RefundNo}, RefundAmount={RefundAmount}",
request.OrderNo, request.RefundNo, request.RefundAmount);
var merchantConfig = _configService.GetMerchantByOrderNo(request.OrderNo);
var privateKey = ReadPrivateKey(merchantConfig.PrivateKeyPath!);
var apiRequest = new WechatPayV3RefundApiRequest
{
OutTradeNo = request.OrderNo,
TransactionId = request.TransactionId,
OutRefundNo = request.RefundNo,
Reason = request.Reason,
NotifyUrl = request.NotifyUrl,
Amount = new WechatPayV3RefundAmount
{
Refund = request.RefundAmount,
Total = request.TotalAmount,
Currency = "CNY"
}
};
var requestBody = JsonSerializer.Serialize(apiRequest, new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
});
var url = "/v3/refund/domestic/refunds";
var timestamp = GetTimestamp();
var nonceStr = GenerateNonceStr();
var signature = GenerateSignature("POST", url, timestamp, nonceStr, requestBody, privateKey);
var authorization = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{merchantConfig.MchId}\",nonce_str=\"{nonceStr}\",timestamp=\"{timestamp}\",serial_no=\"{merchantConfig.CertSerialNo}\",signature=\"{signature}\"";
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, V3_REFUND_URL);
httpRequest.Headers.Add("Authorization", authorization);
httpRequest.Headers.Add("Accept", "application/json");
httpRequest.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(httpRequest);
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogDebug("V3 退款响应: StatusCode={StatusCode}, Body={Body}", response.StatusCode, responseContent);
if (!response.IsSuccessStatusCode)
{
var errorResponse = JsonSerializer.Deserialize(responseContent);
return new WechatPayV3RefundResult
{
Success = false,
ErrorCode = errorResponse?.Code,
ErrorMessage = errorResponse?.Message
};
}
var refundResponse = JsonSerializer.Deserialize(responseContent);
return new WechatPayV3RefundResult
{
Success = true,
RefundId = refundResponse?.RefundId,
OutRefundNo = refundResponse?.OutRefundNo,
Status = refundResponse?.Status,
RefundAmount = refundResponse?.Amount?.Refund
};
}
catch (Exception ex)
{
_logger.LogError(ex, "V3 退款异常: OrderNo={OrderNo}", request.OrderNo);
return new WechatPayV3RefundResult
{
Success = false,
ErrorCode = "SYSTEM_ERROR",
ErrorMessage = ex.Message
};
}
}
#endregion
#region 签名与验签
///
public string GenerateSignature(string method, string url, string timestamp, string nonce, string body, string privateKey)
{
// 构建签名字符串
// 格式:HTTP方法\nURL\n时间戳\n随机串\n请求体\n
var signatureString = $"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n";
// 使用 RSA-SHA256 签名
using var rsa = RSA.Create();
rsa.ImportFromPem(privateKey);
var signatureBytes = rsa.SignData(
Encoding.UTF8.GetBytes(signatureString),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signatureBytes);
}
///
public bool VerifyNotifySignature(string timestamp, string nonce, string body, string signature, string serialNo)
{
try
{
_logger.LogDebug("开始验证 V3 回调签名: Timestamp={Timestamp}, Nonce={Nonce}, SerialNo={SerialNo}",
timestamp, nonce, serialNo);
// 参数验证
if (string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) ||
string.IsNullOrEmpty(body) || string.IsNullOrEmpty(signature))
{
_logger.LogWarning("V3 回调签名验证参数不完整");
return false;
}
// 获取商户配置
var merchantConfig = _configService.GetDefaultConfig();
if (string.IsNullOrEmpty(merchantConfig.WechatPublicKeyPath))
{
_logger.LogError("微信支付公钥路径未配置");
return false;
}
// 验证公钥ID是否匹配(如果配置了)
if (!string.IsNullOrEmpty(merchantConfig.WechatPublicKeyId) &&
!string.IsNullOrEmpty(serialNo) &&
merchantConfig.WechatPublicKeyId != serialNo)
{
_logger.LogWarning("微信支付公钥ID不匹配: Expected={Expected}, Actual={Actual}",
merchantConfig.WechatPublicKeyId, serialNo);
// 继续验证,因为可能是微信更换了公钥
}
var publicKey = ReadPublicKey(merchantConfig.WechatPublicKeyPath);
if (string.IsNullOrEmpty(publicKey))
{
_logger.LogError("读取微信支付公钥失败: Path={Path}", merchantConfig.WechatPublicKeyPath);
return false;
}
// 构建验签字符串
// 格式:时间戳\n随机串\n请求体\n
var verifyString = $"{timestamp}\n{nonce}\n{body}\n";
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
var signatureBytes = Convert.FromBase64String(signature);
var isValid = rsa.VerifyData(
Encoding.UTF8.GetBytes(verifyString),
signatureBytes,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
if (isValid)
{
_logger.LogDebug("V3 回调签名验证成功");
}
else
{
_logger.LogWarning("V3 回调签名验证失败");
}
return isValid;
}
catch (FormatException ex)
{
_logger.LogError(ex, "V3 回调签名 Base64 解码失败");
return false;
}
catch (CryptographicException ex)
{
_logger.LogError(ex, "V3 回调签名验证加密异常");
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "验证回调签名异常");
return false;
}
}
///
/// 使用指定的公钥验证回调签名
///
/// 时间戳
/// 随机串
/// 请求体
/// 签名
/// 公钥 PEM 内容
/// 签名是否有效
public bool VerifyNotifySignatureWithPublicKey(string timestamp, string nonce, string body, string signature, string publicKey)
{
try
{
if (string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(nonce) ||
string.IsNullOrEmpty(body) || string.IsNullOrEmpty(signature) ||
string.IsNullOrEmpty(publicKey))
{
return false;
}
// 构建验签字符串
var verifyString = $"{timestamp}\n{nonce}\n{body}\n";
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKey);
var signatureBytes = Convert.FromBase64String(signature);
return rsa.VerifyData(
Encoding.UTF8.GetBytes(verifyString),
signatureBytes,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
}
catch (Exception ex)
{
_logger.LogError(ex, "使用指定公钥验证签名异常");
return false;
}
}
#endregion
#region 加解密
///
public string DecryptNotifyResource(string ciphertext, string nonce, string associatedData, string apiV3Key)
{
try
{
_logger.LogDebug("开始解密 V3 回调数据: Nonce={Nonce}, AssociatedData={AssociatedData}",
nonce, associatedData);
// 参数验证
if (string.IsNullOrEmpty(ciphertext))
{
throw new ArgumentException("密文不能为空", nameof(ciphertext));
}
if (string.IsNullOrEmpty(nonce))
{
throw new ArgumentException("随机串不能为空", nameof(nonce));
}
if (string.IsNullOrEmpty(apiV3Key))
{
throw new ArgumentException("APIv3 密钥不能为空", nameof(apiV3Key));
}
if (apiV3Key.Length != 32)
{
throw new ArgumentException("APIv3 密钥长度必须为 32 字节", nameof(apiV3Key));
}
var ciphertextBytes = Convert.FromBase64String(ciphertext);
var nonceBytes = Encoding.UTF8.GetBytes(nonce);
var associatedDataBytes = string.IsNullOrEmpty(associatedData)
? Array.Empty()
: Encoding.UTF8.GetBytes(associatedData);
var keyBytes = Encoding.UTF8.GetBytes(apiV3Key);
// AES-256-GCM 解密
// 密文最后 16 字节是 authentication tag
const int tagLength = 16;
if (ciphertextBytes.Length < tagLength)
{
throw new ArgumentException("密文长度不足,无法提取认证标签", nameof(ciphertext));
}
var actualCiphertext = ciphertextBytes[..^tagLength];
var tag = ciphertextBytes[^tagLength..];
using var aesGcm = new AesGcm(keyBytes, tagLength);
var plaintext = new byte[actualCiphertext.Length];
aesGcm.Decrypt(nonceBytes, actualCiphertext, tag, plaintext, associatedDataBytes);
var result = Encoding.UTF8.GetString(plaintext);
_logger.LogDebug("V3 回调数据解密成功");
return result;
}
catch (FormatException ex)
{
_logger.LogError(ex, "V3 回调数据 Base64 解码失败");
throw new InvalidOperationException("密文 Base64 解码失败", ex);
}
catch (CryptographicException ex)
{
_logger.LogError(ex, "V3 回调数据解密失败(可能是密钥错误或数据被篡改)");
throw new InvalidOperationException("解密失败,可能是密钥错误或数据被篡改", ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "解密回调数据失败");
throw;
}
}
///
/// 使用 AES-256-GCM 加密数据(用于测试)
///
/// 明文
/// 随机串(12 字节)
/// 附加数据
/// APIv3 密钥(32 字节)
/// Base64 编码的密文(包含认证标签)
public string EncryptNotifyResource(string plaintext, string nonce, string associatedData, string apiV3Key)
{
if (string.IsNullOrEmpty(plaintext))
{
throw new ArgumentException("明文不能为空", nameof(plaintext));
}
if (string.IsNullOrEmpty(nonce))
{
throw new ArgumentException("随机串不能为空", nameof(nonce));
}
if (string.IsNullOrEmpty(apiV3Key) || apiV3Key.Length != 32)
{
throw new ArgumentException("APIv3 密钥长度必须为 32 字节", nameof(apiV3Key));
}
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
var nonceBytes = Encoding.UTF8.GetBytes(nonce);
var associatedDataBytes = string.IsNullOrEmpty(associatedData)
? Array.Empty()
: Encoding.UTF8.GetBytes(associatedData);
var keyBytes = Encoding.UTF8.GetBytes(apiV3Key);
const int tagLength = 16;
var ciphertext = new byte[plaintextBytes.Length];
var tag = new byte[tagLength];
using var aesGcm = new AesGcm(keyBytes, tagLength);
aesGcm.Encrypt(nonceBytes, plaintextBytes, ciphertext, tag, associatedDataBytes);
// 将密文和标签合并
var result = new byte[ciphertext.Length + tag.Length];
ciphertext.CopyTo(result, 0);
tag.CopyTo(result, ciphertext.Length);
return Convert.ToBase64String(result);
}
#endregion
#region 回调格式识别
///
/// 检测回调数据是否为 V3 格式
/// V3 格式特征:JSON 格式且包含 resource 字段
///
/// 回调请求体
/// 是否为 V3 格式
public bool IsV3NotifyFormat(string notifyBody)
{
if (string.IsNullOrWhiteSpace(notifyBody))
{
return false;
}
var trimmedBody = notifyBody.TrimStart();
// V3 格式是 JSON,以 { 开头
if (!trimmedBody.StartsWith('{'))
{
return false;
}
try
{
// 尝试解析为 JSON 并检查是否包含 resource 字段
using var doc = JsonDocument.Parse(notifyBody);
var root = doc.RootElement;
// V3 回调必须包含 resource 字段
return root.TryGetProperty("resource", out _);
}
catch (JsonException)
{
// JSON 解析失败,不是 V3 格式
return false;
}
}
///
/// 检测回调数据是否为 V2 格式
/// V2 格式特征:XML 格式,以 <xml> 开头
///
/// 回调请求体
/// 是否为 V2 格式
public bool IsV2NotifyFormat(string notifyBody)
{
if (string.IsNullOrWhiteSpace(notifyBody))
{
return false;
}
var trimmedBody = notifyBody.TrimStart();
// V2 格式是 XML,以 < 开头
if (!trimmedBody.StartsWith('<'))
{
return false;
}
// 检查是否包含 标签(不区分大小写)
return trimmedBody.Contains("", StringComparison.OrdinalIgnoreCase) ||
trimmedBody.Contains("
/// 检测回调格式并返回版本
///
/// 回调请求体
/// 回调版本:V3、V2 或 Unknown
public NotifyVersion DetectNotifyVersion(string notifyBody)
{
if (IsV3NotifyFormat(notifyBody))
{
return NotifyVersion.V3;
}
if (IsV2NotifyFormat(notifyBody))
{
return NotifyVersion.V2;
}
return NotifyVersion.Unknown;
}
#endregion
#region 辅助方法
///
public string GeneratePaySign(string appId, string timestamp, string nonceStr, string prepayId, string privateKey)
{
// 小程序调起支付签名字符串
// 格式:appId\n时间戳\n随机串\nprepay_id=xxx\n
var signatureString = $"{appId}\n{timestamp}\n{nonceStr}\nprepay_id={prepayId}\n";
using var rsa = RSA.Create();
rsa.ImportFromPem(privateKey);
var signatureBytes = rsa.SignData(
Encoding.UTF8.GetBytes(signatureString),
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signatureBytes);
}
///
public string GenerateNonceStr(int length = 32)
{
var random = new Random();
var result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = NONCE_CHARS[random.Next(NONCE_CHARS.Length)];
}
return new string(result);
}
///
public string GetTimestamp()
{
return DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
}
///
public string ReadPrivateKey(string privateKeyPath)
{
try
{
// 支持相对路径和绝对路径
var fullPath = Path.IsPathRooted(privateKeyPath)
? privateKeyPath
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, privateKeyPath);
if (!File.Exists(fullPath))
{
_logger.LogError("私钥文件不存在: {Path}", fullPath);
return string.Empty;
}
return File.ReadAllText(fullPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "读取私钥文件失败: {Path}", privateKeyPath);
return string.Empty;
}
}
///
public string ReadPublicKey(string publicKeyPath)
{
try
{
var fullPath = Path.IsPathRooted(publicKeyPath)
? publicKeyPath
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, publicKeyPath);
if (!File.Exists(fullPath))
{
_logger.LogError("公钥文件不存在: {Path}", fullPath);
return string.Empty;
}
return File.ReadAllText(fullPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "读取公钥文件失败: {Path}", publicKeyPath);
return string.Empty;
}
}
///
/// 截断商品描述(V3 限制最大 127 字符)
///
private static string TruncateDescription(string description, int maxLength)
{
if (string.IsNullOrEmpty(description))
{
return "商品购买";
}
return description.Length <= maxLength ? description : description[..maxLength];
}
///
/// 保存订单通知记录
///
private async Task SaveOrderNotifyAsync(string orderNo, string notifyUrl, string nonceStr, decimal amount, string attach, string openId)
{
var orderNotify = new OrderNotify
{
OrderNo = orderNo,
NotifyUrl = notifyUrl,
NonceStr = nonceStr,
PayTime = DateTime.Now,
PayAmount = amount,
Status = 0,
RetryCount = 0,
Attach = attach,
OpenId = openId,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
_dbContext.OrderNotifies.Add(orderNotify);
await _dbContext.SaveChangesAsync();
_logger.LogDebug("保存订单通知记录: OrderNo={OrderNo}, NotifyUrl={NotifyUrl}", orderNo, notifyUrl);
}
///
/// 获取 V3 错误消息
///
private static string GetV3ErrorMessage(string code, string message)
{
var errorMessages = new Dictionary
{
{ "PARAM_ERROR", "参数错误" },
{ "OUT_TRADE_NO_USED", "订单号已使用" },
{ "ORDER_NOT_EXIST", "订单不存在" },
{ "ORDER_CLOSED", "订单已关闭" },
{ "SIGN_ERROR", "签名错误" },
{ "MCH_NOT_EXISTS", "商户号不存在" },
{ "APPID_MCHID_NOT_MATCH", "AppID和商户号不匹配" },
{ "FREQUENCY_LIMITED", "请求频率超限" },
{ "SYSTEM_ERROR", "系统错误" },
{ "INVALID_REQUEST", "请求参数无效" },
{ "OPENID_MISMATCH", "OpenID不匹配" },
{ "NOAUTH", "商户未开通此接口权限" },
{ "NOT_ENOUGH", "用户账户余额不足" },
{ "TRADE_ERROR", "交易错误" }
};
return errorMessages.TryGetValue(code, out var msg) ? msg : message;
}
#endregion
}