mi-assessment/server/MiAssessment/src/MiAssessment.Core/Services/WechatPayV3Service.cs
zpc 21e8ff5372 refactor: 清理遗留实体和无效代码
- 删除无数据库表的实体: UserDetail, UserAddress, PaymentOrder, Admin, AdminLoginLog, AdminOperationLog, Picture, Delivery
- 删除关联服务: AddressService, PaymentService, PaymentOrderService, PaymentRewardDispatcher, DefaultPaymentRewardHandler
- 删除关联接口: IAddressService, IPaymentService, IPaymentOrderService, IPaymentRewardHandler, IPaymentRewardDispatcher
- 删除关联控制器: AddressController
- 删除关联DTO: AddressModels, CreatePaymentOrderRequest, PaymentOrderDto, PaymentOrderQueryRequest
- 删除关联测试: PaymentOrderServicePropertyTests, PaymentRewardDispatcherPropertyTests
- 修复实体字段映射: User, UserLoginLog, UserRefreshToken, Config, OrderNotify
- 更新 NotifyController 移除 IPaymentOrderService 依赖
- 更新 ServiceModule 移除已删除服务的DI注册
- 更新 MiAssessmentDbContext 移除已删除实体的DbSet和OnModelCreating配置
2026-02-20 20:29:34 +08:00

923 lines
35 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 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;
/// <summary>
/// 微信支付 V3 服务实现
/// 提供基于 RSA-SHA256 签名和 AES-256-GCM 加密的 V3 版本支付功能
/// </summary>
public class WechatPayV3Service : IWechatPayV3Service
{
private readonly MiAssessmentDbContext _dbContext;
private readonly HttpClient _httpClient;
private readonly ILogger<WechatPayV3Service> _logger;
private readonly IWechatPayConfigService _configService;
/// <summary>
/// V3 JSAPI 下单 API 地址
/// </summary>
private const string V3_JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
/// <summary>
/// V3 订单查询 API 地址(商户订单号)
/// </summary>
private const string V3_QUERY_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{0}";
/// <summary>
/// V3 关闭订单 API 地址
/// </summary>
private const string V3_CLOSE_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{0}/close";
/// <summary>
/// V3 退款 API 地址
/// </summary>
private const string V3_REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
/// <summary>
/// 随机字符串字符集
/// </summary>
private const string NONCE_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
public WechatPayV3Service(
MiAssessmentDbContext dbContext,
HttpClient httpClient,
ILogger<WechatPayV3Service> logger,
IWechatPayConfigService configService)
{
_dbContext = dbContext;
_httpClient = httpClient;
_logger = logger;
_configService = configService;
}
#region
/// <inheritdoc />
public async Task<WechatPayResult> 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<WechatPayV3ErrorResponse>(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<WechatPayV3JsapiResponse>(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
/// <inheritdoc />
public async Task<WechatPayV3QueryResult> 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<WechatPayV3ErrorResponse>(responseContent);
return new WechatPayV3QueryResult
{
Success = false,
ErrorCode = errorResponse?.Code,
ErrorMessage = errorResponse?.Message
};
}
var queryResponse = JsonSerializer.Deserialize<WechatPayV3QueryResponse>(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
};
}
}
/// <inheritdoc />
public async Task<WechatPayV3CloseResult> 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<WechatPayV3ErrorResponse>(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 退
/// <inheritdoc />
public async Task<WechatPayV3RefundResult> 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<WechatPayV3ErrorResponse>(responseContent);
return new WechatPayV3RefundResult
{
Success = false,
ErrorCode = errorResponse?.Code,
ErrorMessage = errorResponse?.Message
};
}
var refundResponse = JsonSerializer.Deserialize<WechatPayV3RefundApiResponse>(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
/// <inheritdoc />
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);
}
/// <inheritdoc />
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;
}
}
/// <summary>
/// 使用指定的公钥验证回调签名
/// </summary>
/// <param name="timestamp">时间戳</param>
/// <param name="nonce">随机串</param>
/// <param name="body">请求体</param>
/// <param name="signature">签名</param>
/// <param name="publicKey">公钥 PEM 内容</param>
/// <returns>签名是否有效</returns>
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
/// <inheritdoc />
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<byte>()
: 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;
}
}
/// <summary>
/// 使用 AES-256-GCM 加密数据(用于测试)
/// </summary>
/// <param name="plaintext">明文</param>
/// <param name="nonce">随机串12 字节)</param>
/// <param name="associatedData">附加数据</param>
/// <param name="apiV3Key">APIv3 密钥32 字节)</param>
/// <returns>Base64 编码的密文(包含认证标签)</returns>
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<byte>()
: 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
/// <summary>
/// 检测回调数据是否为 V3 格式
/// V3 格式特征JSON 格式且包含 resource 字段
/// </summary>
/// <param name="notifyBody">回调请求体</param>
/// <returns>是否为 V3 格式</returns>
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;
}
}
/// <summary>
/// 检测回调数据是否为 V2 格式
/// V2 格式特征XML 格式,以 &lt;xml&gt; 开头
/// </summary>
/// <param name="notifyBody">回调请求体</param>
/// <returns>是否为 V2 格式</returns>
public bool IsV2NotifyFormat(string notifyBody)
{
if (string.IsNullOrWhiteSpace(notifyBody))
{
return false;
}
var trimmedBody = notifyBody.TrimStart();
// V2 格式是 XML以 < 开头
if (!trimmedBody.StartsWith('<'))
{
return false;
}
// 检查是否包含 <xml> 标签(不区分大小写)
return trimmedBody.Contains("<xml>", StringComparison.OrdinalIgnoreCase) ||
trimmedBody.Contains("<xml ", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 检测回调格式并返回版本
/// </summary>
/// <param name="notifyBody">回调请求体</param>
/// <returns>回调版本V3、V2 或 Unknown</returns>
public NotifyVersion DetectNotifyVersion(string notifyBody)
{
if (IsV3NotifyFormat(notifyBody))
{
return NotifyVersion.V3;
}
if (IsV2NotifyFormat(notifyBody))
{
return NotifyVersion.V2;
}
return NotifyVersion.Unknown;
}
#endregion
#region
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public string GetTimestamp()
{
return DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
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;
}
}
/// <summary>
/// 截断商品描述V3 限制最大 127 字符)
/// </summary>
private static string TruncateDescription(string description, int maxLength)
{
if (string.IsNullOrEmpty(description))
{
return "商品购买";
}
return description.Length <= maxLength ? description : description[..maxLength];
}
/// <summary>
/// 保存订单通知记录
/// </summary>
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);
}
/// <summary>
/// 获取 V3 错误消息
/// </summary>
private static string GetV3ErrorMessage(string code, string message)
{
var errorMessages = new Dictionary<string, string>
{
{ "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
}