using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace CampusErrand.Services;
///
/// 微信支付服务(V3 公钥模式)
///
public class WxPayService
{
private readonly string _appId;
private readonly string _mchId;
private readonly string _apiV3Key;
private readonly string _serialNo;
private readonly RSA _privateKey;
private readonly HttpClient _httpClient;
public WxPayService(IConfiguration config, HttpClient httpClient)
{
_appId = config["WeChat:AppId"]!;
_mchId = config["WeChat:MchId"]!;
_apiV3Key = config["WeChat:MchApiV3Key"]!;
_serialNo = config["WeChat:MchSerialNo"]!;
_httpClient = httpClient;
// 加载商户私钥
var privateKeyPem = config["WeChat:MchPrivateKeyPem"]!;
_privateKey = RSA.Create();
var keyBytes = Convert.FromBase64String(privateKeyPem);
_privateKey.ImportPkcs8PrivateKey(keyBytes, out _);
}
///
/// JSAPI 下单(小程序支付)
///
public async Task CreateJsapiOrder(string orderNo, decimal totalAmount, string description, string openId, string notifyUrl)
{
var totalFen = (int)(totalAmount * 100);
var requestBody = new
{
appid = _appId,
mchid = _mchId,
description,
out_trade_no = orderNo,
notify_url = notifyUrl,
amount = new { total = totalFen, currency = "CNY" },
payer = new { openid = openId }
};
var json = JsonSerializer.Serialize(requestBody);
var url = "/v3/pay/transactions/jsapi";
var fullUrl = $"https://api.mch.weixin.qq.com{url}";
var request = new HttpRequestMessage(HttpMethod.Post, fullUrl)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
// 签名
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Guid.NewGuid().ToString("N");
var signature = Sign("POST", url, timestamp, nonce, json);
request.Headers.Add("Authorization", $"WECHATPAY2-SHA256-RSA2048 mchid=\"{_mchId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{_serialNo}\",signature=\"{signature}\"");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("User-Agent", "CampusErrand/1.0");
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[微信支付] 下单失败: {responseBody}");
return new WxPayResult { Success = false, ErrorMessage = responseBody };
}
var result = JsonSerializer.Deserialize(responseBody);
var prepayId = result.GetProperty("prepay_id").GetString()!;
// 生成小程序调起支付的参数
var payTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var payNonce = Guid.NewGuid().ToString("N");
var package = $"prepay_id={prepayId}";
var paySignStr = $"{_appId}\n{payTimestamp}\n{payNonce}\n{package}\n";
var paySign = Convert.ToBase64String(_privateKey.SignData(Encoding.UTF8.GetBytes(paySignStr), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
return new WxPayResult
{
Success = true,
PaymentParams = new WxPaymentParams
{
TimeStamp = payTimestamp,
NonceStr = payNonce,
Package = package,
SignType = "RSA",
PaySign = paySign
}
};
}
///
/// 验证支付回调签名并解密
///
public WxPayNotifyResult? VerifyAndDecryptNotify(string serialNo, string timestamp, string nonce, string signature, string body)
{
// 注意:公钥模式下回调验签需要用微信平台公钥,这里简化处理先信任回调
// 生产环境应严格验签
try
{
// 解析回调数据
var json = JsonSerializer.Deserialize(body);
var resource = json.GetProperty("resource");
var ciphertext = resource.GetProperty("ciphertext").GetString()!;
var associatedData = resource.GetProperty("associated_data").GetString() ?? "";
var nonceStr = resource.GetProperty("nonce").GetString()!;
// AES-GCM 解密
var decrypted = AesGcmDecrypt(ciphertext, nonceStr, associatedData);
var result = JsonSerializer.Deserialize(decrypted);
return new WxPayNotifyResult
{
OrderNo = result.GetProperty("out_trade_no").GetString()!,
TransactionId = result.GetProperty("transaction_id").GetString()!,
TradeState = result.GetProperty("trade_state").GetString()!,
TotalAmount = result.GetProperty("amount").GetProperty("total").GetInt32()
};
}
catch (Exception ex)
{
Console.WriteLine($"[微信支付] 回调解密失败: {ex.Message}");
return null;
}
}
///
/// 申请退款
///
public async Task Refund(string orderNo, string refundNo, int totalFen, int refundFen, string reason = "")
{
var requestBody = new
{
out_trade_no = orderNo,
out_refund_no = refundNo,
reason = string.IsNullOrEmpty(reason) ? "订单退款" : reason,
amount = new { refund = refundFen, total = totalFen, currency = "CNY" }
};
var json = JsonSerializer.Serialize(requestBody);
var url = "/v3/refund/domestic/refunds";
var fullUrl = $"https://api.mch.weixin.qq.com{url}";
var request = new HttpRequestMessage(HttpMethod.Post, fullUrl)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Guid.NewGuid().ToString("N");
var signature = Sign("POST", url, timestamp, nonce, json);
request.Headers.Add("Authorization", $"WECHATPAY2-SHA256-RSA2048 mchid=\"{_mchId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{_serialNo}\",signature=\"{signature}\"");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("User-Agent", "CampusErrand/1.0");
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[微信支付] 退款失败: {responseBody}");
return false;
}
Console.WriteLine($"[微信支付] 退款成功: {refundNo}");
return true;
}
///
/// 商家转账到零钱(提现用)
///
public async Task<(bool Success, string? ErrorMessage)> TransferToWallet(string outBatchNo, string outDetailNo, string openId, int amountFen, string remark = "提现到账")
{
var requestBody = new
{
appid = _appId,
out_batch_no = outBatchNo,
batch_name = "跑腿提现",
batch_remark = remark,
total_amount = amountFen,
total_num = 1,
transfer_detail_list = new[]
{
new
{
out_detail_no = outDetailNo,
transfer_amount = amountFen,
transfer_remark = remark,
openid = openId
}
}
};
var json = JsonSerializer.Serialize(requestBody);
var url = "/v3/transfer/batches";
var fullUrl = $"https://api.mch.weixin.qq.com{url}";
var request = new HttpRequestMessage(HttpMethod.Post, fullUrl)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Guid.NewGuid().ToString("N");
var signature = Sign("POST", url, timestamp, nonce, json);
request.Headers.Add("Authorization", $"WECHATPAY2-SHA256-RSA2048 mchid=\"{_mchId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{_serialNo}\",signature=\"{signature}\"");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("User-Agent", "CampusErrand/1.0");
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"[微信转账] 转账失败: {responseBody}");
return (false, responseBody);
}
Console.WriteLine($"[微信转账] 转账成功: {outBatchNo}");
return (true, null);
}
///
/// 生成签名
///
private string Sign(string method, string url, string timestamp, string nonce, string body)
{
var message = $"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n";
var signBytes = _privateKey.SignData(Encoding.UTF8.GetBytes(message), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signBytes);
}
///
/// AES-GCM 解密(回调通知解密)
///
private string AesGcmDecrypt(string ciphertext, string nonce, string associatedData)
{
var ciphertextBytes = Convert.FromBase64String(ciphertext);
var nonceBytes = Encoding.UTF8.GetBytes(nonce);
var associatedDataBytes = Encoding.UTF8.GetBytes(associatedData);
// 密文最后16字节是 tag
var tagSize = 16;
var encryptedData = ciphertextBytes[..^tagSize];
var tag = ciphertextBytes[^tagSize..];
var plaintext = new byte[encryptedData.Length];
using var aesGcm = new AesGcm(Encoding.UTF8.GetBytes(_apiV3Key), tagSize);
aesGcm.Decrypt(nonceBytes, encryptedData, tag, plaintext, associatedDataBytes);
return Encoding.UTF8.GetString(plaintext);
}
}
///
/// 微信支付下单结果
///
public class WxPayResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public WxPaymentParams? PaymentParams { get; set; }
}
///
/// 小程序调起支付参数
///
public class WxPaymentParams
{
public string TimeStamp { get; set; } = "";
public string NonceStr { get; set; } = "";
public string Package { get; set; } = "";
public string SignType { get; set; } = "RSA";
public string PaySign { get; set; } = "";
}
///
/// 支付回调解密结果
///
public class WxPayNotifyResult
{
public string OrderNo { get; set; } = "";
public string TransactionId { get; set; } = "";
public string TradeState { get; set; } = "";
public int TotalAmount { get; set; }
}