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 }