campus-errand/server/Services/WxPayService.cs
18631081161 b359070a0e
All checks were successful
continuous-integration/drone/push Build is passing
聊天修改
2026-04-02 01:09:02 +08:00

296 lines
11 KiB
C#
Raw Permalink 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.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace CampusErrand.Services;
/// <summary>
/// 微信支付服务V3 公钥模式)
/// </summary>
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 _);
}
/// <summary>
/// JSAPI 下单(小程序支付)
/// </summary>
public async Task<WxPayResult> 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<JsonElement>(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
}
};
}
/// <summary>
/// 验证支付回调签名并解密
/// </summary>
public WxPayNotifyResult? VerifyAndDecryptNotify(string serialNo, string timestamp, string nonce, string signature, string body)
{
// 注意:公钥模式下回调验签需要用微信平台公钥,这里简化处理先信任回调
// 生产环境应严格验签
try
{
// 解析回调数据
var json = JsonSerializer.Deserialize<JsonElement>(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<JsonElement>(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;
}
}
/// <summary>
/// 申请退款
/// </summary>
public async Task<bool> 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;
}
/// <summary>
/// 商家转账到零钱(提现用)
/// </summary>
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);
}
/// <summary>
/// 生成签名
/// </summary>
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);
}
/// <summary>
/// AES-GCM 解密(回调通知解密)
/// </summary>
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);
}
}
/// <summary>
/// 微信支付下单结果
/// </summary>
public class WxPayResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public WxPaymentParams? PaymentParams { get; set; }
}
/// <summary>
/// 小程序调起支付参数
/// </summary>
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; } = "";
}
/// <summary>
/// 支付回调解密结果
/// </summary>
public class WxPayNotifyResult
{
public string OrderNo { get; set; } = "";
public string TransactionId { get; set; } = "";
public string TradeState { get; set; } = "";
public int TotalAmount { get; set; }
}