微信支付官方没有C# .net的SDK,虽然社区里也有集成好的,但是总感觉过于臃肿,只想要一个单纯的小程序支付,于是决定自己写,网上的代码比较零散,踩了不少坑,结合了许多代码最后终于实现了功能,把全流程写出来记录分享一下,希望对大家有所帮助,第一次写文章,有问题请留言指正
在开始编写之前,请先对照官方文档,完成接入前准备工作,获取所需的密钥等,本文的代码完全参照微信官网文档流程按顺序编写,可能有些繁琐,但方便一步一步的测试,大家可以自行优化。
一、获取Openid
发送微信支付请求时,首先需要获取用户的openid
前端代码
注:本文所有前端代码都是放在同一个异步function里,为了方便阅读所以拆开展示,支付按钮绑定这个方法即可
let code = await wx.login();
let openId = await uni.request(
{
url:你的后端api地址+"GetOpenId",
method: 'POST',
data:
{
UserCode: code.code,
},
header:
{
'content-type': 'application/json'
},
})
if(openId.statusCode != 200)
{
console.log(openId);
return;
}
后端代码
[Route("api/GetOpenId"),HttpPost]
public async Task<IActionResult> GetOpenId([FromBody] Data data)
{
if (string.IsNullOrEmpty(data.UserCode))
return BadRequest("Code is required.");
string appId = "你的小程序appid";
string secret = "你的小程序secret";
var url = $"https://api.weixin.qq.com/sns/jscode2session?appid={appId}&secret={secret}&js_code={data.UserCode}&grant_type=authorization_code";
var httpClient = _httpClientFactory.CreateClient();
var response = await httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
return StatusCode((int)response.StatusCode, "Failed to get session key and openid from WeChat server.");
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<WeChatLoginResponse>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (result == null)
return BadRequest("resule = null");
if (string.IsNullOrEmpty(result.SessionKey))
return BadRequest("result.SessionKey = null");
if (string.IsNullOrEmpty(result.OpenId))
return BadRequest("result.OpenId = null");
return Ok(result.OpenId);
}
Data类
public string? OpenId { get; set; }//用户的OpenID
public string? RandomStr { get; set; }//订单随机字符串
public string? Description { get; set; }//订单商品描述
二、发起预支付请求
这一步理论上可以和上一步整合到一起,如果全部逻辑在后台执行的话,只需要传一个wx.login获取到的code,剩下的操作可以写在一个后端的api里,我这里拆开了,逻辑上更清楚一些但是代码更多。
前端代码
let orderRes = await uni.request(
{
url:你的后端api地址+"CreateOrder",
method: 'POST',
header:
{
'content-type': 'application/json'
},
data:
{
RandomStr:RandomString(32),//自己写的随机字符串生成方法,也可以放到后端
OpenId: openId.data,
Description:"商品描述,会显示在微信支付详情的‘商品’那一栏",
},
})
if(orderRes.statusCode != 200)
{
console.log(orderRes);
return;
}
function RandomString(length)
{
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
let result = '';
const charLength = chars.length;
for (let i = 0; i < length; i++)
{
const randomIndex = Math.floor(Math.random() * charLength);
result += chars[randomIndex];
}
return result;
}
后端代码
[Route("api/CreateOrder"), HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] Data data)
{
//计算总价
float totalprice = 商品总价格
//一般这一步也会把商品传过来 在数据库里创建订单 不展示具体代码了 这里根据你的商品算;
//生成商品编号 这里用的写好的雪花算法 改用自己的编号生成逻辑
long orderId = idworker.nextId();
long timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds();//获取当前时间戳
HttpRequestMessage httpRequest = new(HttpMethod.Post,new Uri("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"));//创建请求
httpRequest.Headers.Add("Accept", "application/json");
httpRequest.Headers.Add("User-Agent", "dotnet/8.0");
WeiXinPayOrderRequestDTO request = new WeiXinPayOrderRequestDTO();
request.description = data.Description;
request.out_trade_no = orderId.ToString();
//request.time_expire = 订单失效时间 有点麻烦 我这里暂时没写 需要yyyy-MM-DDTHH:mm:ss+TIMEZONE格式 不是必填项
request.attach = orderId.ToString();
request.amount.total = (int)(totalprice * 100); //微信支付的单位为分 如果你的单位已经是分了 这里要改
request.payer.openid = data.OpenId;
string json = JsonSerializer.Serialize(request);
httpRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
string orderTosignData = PayOptions.BuildSignature("POST", "/v3/pay/transactions/jsapi", timeStamp.ToString(), data.RandomStr, json);
string signature = "";
// 创建RSA加密服务提供程序实例
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// 加载私钥
rsa.ImportFromPem(PayOptions.ApiClientKey);
// 将待签名的数据转换为字节数组
byte[] dataBytes = Encoding.UTF8.GetBytes(orderTosignData);
// 计算SHA256 with RSA签名
byte[] signatureBytes = rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// 将签名结果进行Base64编码
signature = Convert.ToBase64String(signatureBytes);
// 输出签名值
// Console.WriteLine("签名值: " + signature);
}
string authrizationValue = $"mchid=\"{request.mchid}\",nonce_str=\"{data.RandomStr}\",signature=\"{signature}\",timestamp=\"{timeStamp}\",serial_no=\"{PayOptions.Serial_No}\"";
httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("WECHATPAY2-SHA256-RSA2048", authrizationValue);
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.SendAsync(httpRequest);
string responseString = await response.Content.ReadAsStringAsync();
return Ok(responseString);
}
WeiXinPayOrderRequestDTO类
/// <summary>
/// 下单对应的请求DTO
/// </summary>
public class WeiXinPayOrderRequestDTO
{
/// <summary>
/// 应用id
/// </summary>
public string appid { get; } = PayOptions.AppId;
/// <summary>
/// 商户id
/// </summary>
public string mchid { get; } = PayOptions.MchId;
/// <summary>
/// 商品描述
/// </summary>
public string description { get; set; }
/// <summary>
/// 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。
/// </summary>
public string out_trade_no { get; set; }
/// <summary>
/// 订单失效时间
/// </summary>
//public string time_expire { get; set; }
/// <summary>
/// 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。
/// 本系统存放的是订单id
/// </summary>
public string attach { get; set; }
/// <summary>
/// 通知URL必须为直接可访问的URL,不允许携带查询串,要求必须为https地址。
/// </summary>
public string notify_url { get; } = "你的后端api地址/PayNotify";
/// <summary>
/// 是否开通发票
/// </summary>
public bool support_fapiao { get; private set; } = false;
/// <summary>
/// 订单金额
/// </summary>
public WeiXinPayOrderAmout amount { get; set; } = new WeiXinPayOrderAmout();
/// <summary>
/// 支付人
/// </summary>
public WeiXinPayPayer payer { get; set; } = new WeiXinPayPayer();
}
public class WeiXinPayOrderAmout
{
/// <summary>
///总金额
/// </summary>
public int total { get; set; }
/// <summary>
/// CNY:人民币,境内商户号仅支持人民币。
/// </summary>
public string currency { get; private set; } = "CNY";
}
public class WeiXinPayPayer
{
/// <summary>
/// 小程序对应的用户openid
/// </summary>
public string openid { get; set; }
}
PayOptions类
这里包含后续要用到的代码,如果复制粘贴遇到解决不了的报红先继续往后看。
/// <summary>
/// 支付用相关参数
/// </summary>
public class PayOptions
{
/// <summary>
/// 小程序id
/// </summary>
public static readonly string AppId = "你的微信小程序appid";
/// <summary>
/// 商户号
/// </summary>
public static readonly string MchId = "你的商户号MchId";
/// <summary>
/// 证书编号
/// </summary>
public static readonly string Serial_No = "商户微信支付api私钥的证书编号";
/// <summary>
/// API私钥
/// </summary>
public static readonly string ApiClientKey = File.ReadAllText("你的api私钥路径");
/// <summary>
/// APIv3密钥
/// </summary>
public static readonly string ApiV3Key = "你的apiv3密钥";
//构造签名体
public static string BuildSignature(string method, string url, string timeStamp, string nonceStr, string requestbody)
{
return method + "\n" + url +"\n" + timeStamp + "\n" + nonceStr + "\n" + requestbody + "\n";
}
public static bool VerifyWeiXinSign(long timestamp, string nonce, string body, string signature)
{
try
{
//将Unix时间戳转换为DateTime
DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime();
//获取当前时间
DateTime currentTime = DateTime.Now;
//计算时间差
TimeSpan timeDifference = currentTime.Subtract(dateTime);
if (timeDifference.Minutes > 20)
{
throw new Exception("当前请求已过期");
}
//构造应答的验签名串
string dataToSign = $"{timestamp}\n{nonce}\n{body}\n";
//待验签的数据
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
//微信支付公钥
X509Certificate2 wechatCert = new X509Certificate2("你的微信支付公钥文件路径");
var rsaPar = wechatCert.GetRSAPublicKey().ExportParameters(false);
var rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaPar);
return rsa.VerifyData(dataBytes, CryptoConfig.MapNameToOID("SHA256"), Convert.FromBase64String(signature));
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return false;
throw;
}
}
}
关于证书编号,在官网文档里貌似没说,可以去这个网站查询
https://myssl.com/cert_decode.html
三、提交订单
前端代码
//获取支付请求参数
let requestPayRes = await uni.request(
{
url:你的后端api地址+"RequestPay",
method: 'POST',
header:
{
'content-type': 'application/json'
},
data:
{
nonceStr:RandomString(32),
package:`prepay_id=${orderRes.data.prepay_id}`,
}
})
if(requestPayRes.statusCode != 200)
{
console.log(requestPayRes);
return;
}
后端代码
[Route("api/RequestPay"), HttpPost]
public IActionResult RequestPay([FromBody] WeiXinPayrequestPayment requset)
{
string timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
requset.timeStamp = timeStamp;
string dataToSign = PayOptions.AppId + "\n" + timeStamp + "\n" + requset.nonceStr + "\n" + requset.package + "\n";
string paySign = "";
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// 加载私钥
rsa.ImportFromPem(PayOptions.ApiClientKey);
// 将待签名的数据转换为字节数组
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
// 计算SHA256 with RSA签名
byte[] signatureBytes = rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
// 将签名结果进行Base64编码
paySign = Convert.ToBase64String(signatureBytes);
}
requset.paySign = paySign;
return Ok(requset);
}
四、发起支付请求
前端代码
//发起支付请求
wx.requestPayment(
{
"timeStamp": requestPayRes.data.timeStamp,
"nonceStr": requestPayRes.data.nonceStr,
"package": requestPayRes.data.package,
"signType": "RSA",
"paySign": requestPayRes.data.paySign,
"success":function(res)
{
//后续操作
},
"fail":function(err)
{
console.log("支付请求失败"+err);
},
})
五、后端处理支付通知
支付完成后,微信会发送一个通知到服务器,地址就是之前填的notify_url ,必须是https链接,不允许携带查询串,但可以用ip地址的形式,所以测试只需要弄好SSL证书就行了,这里不做介绍。
这一步调用的VerifyWeiXinSign方法中,需要用到微信平台证书的公钥,目前只能通过API下载,我是用官方提供的jar包下载的,大家可以自行选择。
后端代码
[Route("api/PayNotify"), HttpPost]
public async void PayNotify
(
[FromHeader(Name = "Wechatpay-Timestamp")] long timestamp,
[FromHeader(Name = "Wechatpay-Nonce")] string nonce,
[FromHeader(Name = "Wechatpay-Signature")] string signature,
[FromBody] object payNotifyJson
)
{
if(PayOptions.VerifyWeiXinSign(timestamp,nonce,payNotifyJson.ToString(),signature))
{
//解密
PayNotifyDto payNotify = JsonSerializer.Deserialize<PayNotifyDto>(payNotifyJson.ToString());
GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
AeadParameters aeadParameters = new AeadParameters
(
new KeyParameter(Encoding.UTF8.GetBytes(PayOptions.ApiV3Key)),
128,
Encoding.UTF8.GetBytes(payNotify.resource.nonce),
Encoding.UTF8.GetBytes(payNotify.resource.associated_data)
);
gcmBlockCipher.Init(false, aeadParameters);
byte[] data = Convert.FromBase64String(payNotify.resource.ciphertext);
byte[] plainText = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plainText, 0);
gcmBlockCipher.DoFinal(plainText, length);
string resourceJson = Encoding.UTF8.GetString(plainText);
PlainResource resource = JsonSerializer.Deserialize<PlainResource>(resourceJson);
//...
//进行后续操作
//...
}
else
{
//签名验证失败
}
}
六、更新平台证书
在验证请求通知的签名时,所用到的微信平台公钥可能会定期更换,可以参考平台证书更换操作指引,我这里还没有研究,感觉不怕麻烦的话手动下一个新的换一下也不是不行,后续如果做了自动更换证书功能会把代码发上来。