uni-app + .net8 webapi 微信小程序调用微信支付全流程前后端详细代码分享,一步到位搞定小程序支付

微信支付官方没有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
    {
        //签名验证失败
    }
}

六、更新平台证书

在验证请求通知的签名时,所用到的微信平台公钥可能会定期更换,可以参考平台证书更换操作指引,我这里还没有研究,感觉不怕麻烦的话手动下一个新的换一下也不是不行,后续如果做了自动更换证书功能会把代码发上来。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值