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; } }