rust对接微信支付V3接口,代码无限接近于精简。

浏览数(1217)

rust对接微信支付V3接口,代码无限接近于精简。

0

前段时间,给一个客户的微信小程序对接微信支付的接口,后端是用rust写的,这是照着官方文档单独写的模块,基本上可以实现两个文件就能发起预支付了,现在分享出来。


(此接口为发起预支付联调代码,拿到了接口返回的数据之后,还要进行相关的处理,然后通过app、网站或者小程序之类的,在用户前端发起支付


Cargo.toml

[package]
name = "rustwechatpayapi"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

# LCSAY TENCENT WECHAT PAY V3 预支付发起
# 建议使用 CLion 来运行,这样调用的类,如果本身没有,就会自动下载。
# WECHAT AND QQ : 516519782

[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
md5 = "0.7.0"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
chrono = "0.4.19"

openssl = { version = "0.10", features = ["vendored", ] }
serde_json = { version = "1.0.81" }
rand = "0.8"
base64 = "0.13"
rsa = "0.4"
tokio = { version = "1", features = ["full"] }


main.rs

use reqwest::Client;
use openssl::pkcs12::Pkcs12;
use openssl::pkey::PKey;
use openssl::sign::Signer;
use openssl::hash::MessageDigest;
use serde_json::json;
use rand::Rng;
use openssl::error::ErrorStack;
use openssl::asn1::Asn1IntegerRef;

/**
 * LCSAY TENCENT WECHAT PAY V3 预支付发起
 * 建议使用 CLion 来运行,这样前面调用的类,如果本身没有,就会自动下载。
 * WECHAT AND QQ : 516519782
 */

fn main() {
    let merchant_id = "";    //微信商户id,这个在微信支付的商户平台可以获取
    let app_id = "";    //公众号或者小程序的appid
    let cert_path= include_bytes!("D:\\wwwroot\\wechatpayrustapi\\src\\apiclient_cert.p12");    //微信支付V3证书,这个在微信支付平台按照指引获取
    let cert_password = "";    //微信支付V3证书密码
    let api_url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
    let request_data = json!({
        "mchid": merchant_id,
        "appid": app_id,
        "payer": {
            "openid": ""    //发起微信支付的openid,如果是公众号,就用公众号的openid,如果是小程序,就用小程序的openid
        },
        "description": "test",
        "out_trade_no": format!("{}{}", chrono::Utc::now().format("%Y%m%d%H%M%S"), rand::random::<u32>() % 10000),
        "notify_url": "",    //异步回调的地址(用户支付成功或者支付异常的情况下,都会给这个地址发送支付成功或者异常数据,异步回调的数据需要验签,如果验签成功要返回SUCCESS给微信支付的接口,告诉微信,不然会一直发数据过来,微信会检测异常和风控。)
        "amount": {
            "total": 100    //金额以分为单位,这里表示1元
        }
    });

    let json_data = serde_json::to_string(&request_data).unwrap();

    let client = Client::new();

    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(reqwest::header::CONTENT_TYPE, "application/json".parse().unwrap());
    headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap());
    headers.insert(reqwest::header::USER_AGENT, "*/*".parse().unwrap());

    let authorization = generate_authorization_header(merchant_id, api_url, &json_data, cert_path, cert_password);
    headers.insert(reqwest::header::AUTHORIZATION, authorization.parse().unwrap());
    println!("{:?}", headers);

    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async {
            let response = client
                .post(api_url)
                .headers(headers)
                .body(json_data)
                .send()
                .await; // 使用await等待异步操作完成

            // 处理响应
            match response {
                Ok(res) => {
                    let body = res.text().await.unwrap();
                    println!("微信支付统一下单接口返回结果:{}", body);
                    // 在这里根据返回结果进行后续处理,如解析XML、获取prepay_id等
                }
                Err(e) => {
                    println!("请求失败:{}", e);
                }
            }
        });

}

fn generate_authorization_header(merchant_id: &str, url: &str, body: &str, cert_path: &[u8], cert_password: &str) -> String {
    let nonce_str = generate_nonce_string(32);
    let timestamp = chrono::Utc::now().timestamp();
    let message = format!("POST\n{}\n{}\n{}\n{}\n", url.parse::<reqwest::Url>().unwrap().path(), timestamp, nonce_str, body);
    let signature = generate_signature(&message, &cert_path, cert_password);
    let serial_no = get_certificate_serial_number(cert_path, cert_password);

    format!("WECHATPAY2-SHA256-RSA2048 mchid=\"{}\",nonce_str=\"{}\",timestamp=\"{}\",serial_no=\"{}\",signature=\"{}\"",
            merchant_id, nonce_str, timestamp, serial_no, signature)
}

fn generate_nonce_string(length: usize) -> String {
    const CHARACTERS: &str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    let mut rng = rand::thread_rng();
    (0..length)
        .map(|_| CHARACTERS.chars().nth(rng.gen_range(0..CHARACTERS.len())).unwrap())
        .collect()
}

fn generate_signature(message: &str, cert_path: &[u8], cert_password: &str) -> String {
    let pkcs12 = Pkcs12::from_der(&cert_path).unwrap();
    let parsed = pkcs12.parse2(cert_password).unwrap();
    if let Some(stack) = parsed.ca {
        assert_eq!(stack.len(), 0);
    }
    //从p12证书文件提取公钥  -----BEGIN CERTIFICATE-----\n.....\n-----END CERTIFICATE-----
    //let certificate_pem = parsed.cert.expect("REASON").to_pem().expect("Failed to convert to PEM");
    //println!("Private Key:\n{}", String::from_utf8_lossy(&certificate_pem));
    //从p12证书文件提取私钥  -----BEGIN PRIVATE KEY-----\n.....\n-----END PRIVATE KEY-----
    let private_key_pem = parsed.pkey.expect("REASON").private_key_to_pem_pkcs8().expect("Failed to convert to PEM");
    //println!("Private Key:\n{}", String::from_utf8_lossy(&private_key_pem));

    let pkey = PKey::private_key_from_pem(&private_key_pem).unwrap();
    let signature = {
        let mut signer = Signer::new(MessageDigest::sha256(), &pkey).unwrap();
        signer.update(message.as_bytes()).unwrap();
        signer.sign_to_vec().unwrap()
    };
    let signature_base64 = base64::encode(&signature);
    signature_base64
}

// 辅助函数:将 Asn1IntegerRef 转换为字符串表示形式
fn asn1_integer_to_string(asn1_integer: &Asn1IntegerRef) -> Result<String, ErrorStack> {
    let bn = asn1_integer.to_bn()?;
    let string = bn.to_hex_str()?;
    Ok(string.to_string())
}

fn get_certificate_serial_number(cert_path: &[u8], cert_password: &str) -> String {
    let pkcs12 = Pkcs12::from_der(cert_path).unwrap();
    let parsed = pkcs12.parse2(cert_password).unwrap();
    if let Some(stack) = parsed.ca {
        assert_eq!(stack.len(), 0);
    }
    // 获取证书序列号
    let binding = parsed.cert.expect("REASON");
    let serial_number = binding.serial_number();
    let serial_number_string = asn1_integer_to_string(&serial_number).expect("Failed to convert serial number to string");
    serial_number_string.to_string()
}

如果顺利运行,返回的是json格式的数据,如下图(通过返回的 prepay_id 参数,就可以在前端JavaScript或者小程序,app拉起支付了):

微信截图_20240721164016.jpg

注:本文由www.lcsay.com发表,如需转载或已侵权,请联系我。

✎﹏𝓁𝒸𝓈𝒶𝓎﹍﹍·

评论0