use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
use base64::engine::general_purpose::STANDARD;
use base64::{Engine};
use json::{object, JsonValue};
use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
use aes_gcm::aead::{Aead, Payload};
#[derive(Clone)]
pub struct Wechat {
pub appid: String,
pub secret: String,
pub mchid: String,
pub serial_no: String,
pub app_private: String,
pub apikey: String,
pub notify_url: String,
}
use chrono::{DateTime, Utc};
use openssl::hash::MessageDigest;
use openssl::pkey::{PKey};
use openssl::rsa::Rsa;
use openssl::sign::Signer;
use rand::distr::Alphanumeric;
use rand::{rng, Rng};
impl Wechat {
pub fn sign(&mut self, method: &str, url: &str, body: &str) -> Result<String, String> {
let timestamp = Utc::now().timestamp(); let random_string: String = rng().sample_iter(&Alphanumeric) .take(10) .map(char::from).collect();
let sign_txt = format!("{method}\n{url}\n{timestamp}\n{random_string}\n{body}\n");
let rsa = match Rsa::private_key_from_pem(self.app_private.as_bytes()) {
Ok(e) => e,
Err(e) => {
return Err(e.to_string())
}
};
let pkey = match PKey::from_rsa(rsa) {
Ok(e) => e,
Err(e) => {
return Err(format!("Failed to create PKey: {}", e))
}
};
let mut signer = match Signer::new(MessageDigest::sha256(), &pkey) {
Ok(e) => e,
Err(e) => {
return Err(format!("Failed to create signer:{}", e));
}
};
match signer.update(sign_txt.as_bytes()) {
Ok(_) => {}
Err(e) => {
return Err(e.to_string())
}
};
let signature = match signer.sign_to_vec() {
Ok(e) => e,
Err(e) => {
return Err(format!("Failed to sign: {}", e));
}
};
let signature_b64 = STANDARD.encode(signature);
let sign = format!(
r#"WECHATPAY2-SHA256-RSA2048 mchid="{}",nonce_str="{random_string}",signature="{signature_b64}",timestamp="{timestamp}",serial_no="{}""#,
self.mchid.as_str(),
self.serial_no
);
Ok(sign)
}
pub fn paysign(&mut self, prepay_id: &str) -> Result<JsonValue, String> {
let timestamp = Utc::now().timestamp(); let random_string: String = rng().sample_iter(&Alphanumeric) .take(10) .map(char::from).collect();
let sign_txt = format!(
"{}\n{timestamp}\n{random_string}\n{prepay_id}\n",
self.appid
);
let rsa = match Rsa::private_key_from_pem(self.app_private.as_bytes()) {
Ok(e) => e,
Err(e) => {
return Err(e.to_string())
}
};
let pkey = match PKey::from_rsa(rsa) {
Ok(e) => e,
Err(e) => {
return Err(format!("Failed to create PKey: {}", e))
}
};
let mut signer = match Signer::new(MessageDigest::sha256(), &pkey) {
Ok(e) => e,
Err(e) => {
return Err(format!("Failed to create signer:{}", e));
}
};
match signer.update(sign_txt.as_bytes()) {
Ok(_) => {}
Err(e) => {
return Err(e.to_string())
}
};
let signature = match signer.sign_to_vec() {
Ok(e) => e,
Err(e) => {
return Err(format!("Failed to sign: {}", e));
}
};
let signature_b64 = STANDARD.encode(signature);
let sign = signature_b64;
Ok(object! {
timeStamp:timestamp,
nonceStr:random_string,
package:prepay_id,
signType:"RSA",
paySign:sign
})
}
}
impl PayMode for Wechat {
fn login(&mut self, code: &str) -> Result<JsonValue, String> {
let mut http = br_http::Http::new();
match http.get(
"https://api.weixin.qq.com/sns/jscode2session".to_string().as_str(),
).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").query(object! {
appid: self.appid.as_str(),
secret: self.secret.as_str(),
js_code:code,
grant_type:"authorization_code",
}).json() {
Ok(e) => Ok(e),
Err(e) => Err(e),
}
}
fn jsapi(
&mut self,
sub_mchid: &str,
out_trade_no: &str,
description: &str,
total_fee: f64,
notify_url: &str,
sp_openid: &str,
) -> Result<JsonValue, String> {
let url = "/v3/pay/partner/transactions/jsapi";
let total = format!("{:.0}", total_fee * 100.0);
let body = object! {
"sp_appid" => self.appid.clone(),
"sp_mchid"=> self.mchid.clone(),
"sub_mchid"=> sub_mchid,
"description"=>description,
"out_trade_no"=>out_trade_no,
"notify_url"=>notify_url,
"support_fapiao"=>true,
"amount"=>object! {
total: total.parse::<i64>().unwrap(),
currency:"CNY"
},
"payer"=>object! {
sp_openid:sp_openid
}
};
let sign = self.sign("POST", url, body.to_string().as_str())?;
let mut http = br_http::Http::new();
match http.post(format!("https://api.mch.weixin.qq.com{}", url).as_str()).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").header("Authorization", &sign).raw_json(body).json() {
Ok(e) => {
if e.has_key("prepay_id") {
let signinfo = self.paysign(format!("prepay_id={}", e["prepay_id"]).as_str())?;
Ok(signinfo)
} else {
Err(e["message"].to_string())
}
}
Err(e) => Err(e),
}
}
fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
let url = format!(
"/v3/pay/partner/transactions/out-trade-no/{}?sub_mchid={}&sp_mchid={}",
out_trade_no, sub_mchid, self.mchid
);
let sign = self.sign("GET", url.as_str(), "")?;
let mut http = br_http::Http::new();
match http.get(format!("https://api.mch.weixin.qq.com{}", url.clone()).as_str()).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").header("Authorization", sign.as_str()).json() {
Ok(e) => {
if e.has_key("message") {
return Err(e["message"].to_string());
}
let mut pay_time = 0.0;
if e.has_key("success_time") {
let success_time = e["success_time"].as_str().unwrap_or("").to_string();
if !success_time.is_empty() {
let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
pay_time = datetime.timestamp() as f64;
}
}
let mut info = object! {
pay_time:pay_time,
sp_appid:e["sp_appid"].clone(),
transaction_id:e["transaction_id"].clone(),
sp_mchid:e["sp_mchid"].clone(),
sub_mchid:e["sub_mchid"].clone(),
order_no:e["out_trade_no"].clone(),
status:"",
sp_openid:e["payer"]["sp_openid"].clone(),
sub_openid:e["payer"]["sub_openid"].clone(),
amount_currency:e["amount"]["currency"].clone(),
amount_total:e["amount"]["total"].clone(),
trade_state_desc:e["trade_state_desc"].clone(),
trade_type:e["trade_type"].clone(),
};
info["status"] = match e["trade_state"].as_str().unwrap() {
"SUCCESS" => "已支付",
"REFUND" => "退款",
"CLOSED" => "已关闭",
_ => e["trade_state"].as_str().unwrap(),
}.into();
Ok(info)
}
Err(e) => Err(e),
}
}
fn refund(
&mut self,
sub_mchid: &str,
out_trade_no: &str,
transaction_id: &str,
out_refund_no: &str,
refund: f64,
total: f64,
currency: &str,
) -> Result<JsonValue, String> {
let url = "/v3/refund/domestic/refunds";
let refund = format!("{:.0}", refund * 100.0);
let total = format!("{:.0}", total * 100.0);
let body = object! {
"sub_mchid"=> sub_mchid,
"transaction_id"=>transaction_id,
"out_trade_no"=>out_trade_no,
"out_refund_no"=>out_refund_no,
"amount"=>object! {
refund: refund.parse::<i64>().unwrap(),
total: total.parse::<i64>().unwrap(),
currency:currency
}
};
let sign = self.sign("POST", url, body.to_string().as_str())?;
let mut http = br_http::Http::new();
match http.post(format!("https://api.mch.weixin.qq.com{}", url).as_str()).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").header("Authorization", sign.as_str()).raw_json(body).json() {
Ok(e) => {
if e.is_empty() {
return Err("已执行".to_string());
}
if e.has_key("message") {
return Err(e["message"].to_string());
}
let mut refund_time = 0.0;
if e.has_key("success_time") {
let success_time = e["success_time"].as_str().unwrap_or("").to_string();
if !success_time.is_empty() {
let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
refund_time = datetime.timestamp() as f64;
}
}
let status = match e["status"].as_str().unwrap() {
"PROCESSING" => "退款中",
"SUCCESS" => "已退款",
_ => "无退款",
};
let info = object! {
refund_id: e["refund_id"].clone(),
user_received_account:e["user_received_account"].clone(),
status:status,
refund_time:refund_time,
out_refund_no: e["out_refund_no"].clone(),
};
Ok(info)
}
Err(e) => Err(e)
}
}
fn pay_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
if self.apikey.is_empty() {
return Err("apikey 不能为空".to_string());
}
let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
let cipher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(nonce.as_bytes());
let data = match STANDARD.decode(ciphertext) {
Ok(e) => e,
Err(e) => return Err(format!("Invalid data received from API :{}", e))
};
let payload = Payload {
msg: &data,
aad: associated_data.as_bytes(),
};
let plaintext = match cipher.decrypt(nonce, payload) {
Ok(e) => e,
Err(e) => {
return Err(format!("解密 API:{}", e));
}
};
let rr = match String::from_utf8(plaintext) {
Ok(d) => d,
Err(_) => return Err("utf8 error".to_string())
};
let json = match json::parse(rr.as_str()) {
Ok(e) => e,
Err(_) => return Err("json error".to_string())
};
let res = PayNotify {
trade_type: TradeType::from(json["trade_type"].as_str().unwrap()),
out_trade_no: json["out_trade_no"].as_str().unwrap().to_string(),
sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
sp_appid: json["sp_appid"].as_str().unwrap().to_string(),
transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
sp_openid: json["payer"]["sp_openid"].as_str().unwrap().to_string(),
sub_openid: json["payer"]["sub_openid"].as_str().unwrap().to_string(),
total: json["amount"]["total"].as_u64().unwrap() as usize,
payer_total: json["amount"]["payer_total"].as_u64().unwrap() as usize,
currency: json["amount"]["currency"].to_string(),
payer_currency: json["amount"]["payer_currency"].to_string(),
trade_state: TradeState::from(json["trade_state"].as_str().unwrap()),
};
Ok(res.json())
}
fn refund_query(&mut self, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
let url = format!("/v3/refund/domestic/refunds/{out_refund_no}?sub_mchid={}", sub_mchid);
let sign = self.sign("GET", url.as_str(), "")?;
let mut http = br_http::Http::new();
match http.get(format!("https://api.mch.weixin.qq.com{}", url.clone()).as_str()).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").header("Authorization", sign.as_str()).json() {
Ok(e) => {
if e.is_empty() {
return Err("已执行".to_string());
}
if e.has_key("message") {
return Err(e["message"].to_string());
}
let mut refund_time = 0.0;
if e.has_key("success_time") {
let success_time = e["success_time"].as_str().unwrap_or("").to_string();
if !success_time.is_empty() {
let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
refund_time = datetime.timestamp() as f64;
}
}
let mut info = object! {
refund_time:refund_time,
refund_id:e["refund_id"].clone(),
refund_no: e["out_refund_no"].clone(),
order_no:e["out_trade_no"].clone(),
refund_amount: e["amount"]["refund"].clone(),
status:"",
transaction_id:e["transaction_id"].clone(),
amount_currency:e["amount"]["currency"].clone(),
amount_total:e["amount"]["total"].clone(),
note:e["user_received_account"].clone(),
};
info["status"] = match e["status"].as_str().unwrap() {
"SUCCESS" => "已退款",
"PROCESSING" => "退款中",
_ => e["status"].as_str().unwrap(),
}.into();
Ok(info)
}
Err(e) => Err(e),
}
}
fn refund_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
if self.apikey.is_empty() {
return Err("apikey 不能为空".to_string());
}
let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
let cipher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(nonce.as_bytes());
let data = match STANDARD.decode(ciphertext) {
Ok(e) => e,
Err(e) => return Err(format!("Invalid data received from API :{}", e))
};
let payload = Payload {
msg: &data,
aad: associated_data.as_bytes(),
};
let plaintext = match cipher.decrypt(nonce, payload) {
Ok(e) => e,
Err(e) => {
return Err(format!("解密 API:{}", e));
}
};
let rr = match String::from_utf8(plaintext) {
Ok(d) => d,
Err(_) => return Err("utf8 error".to_string())
};
let json = match json::parse(rr.as_str()) {
Ok(e) => e,
Err(_) => return Err("json error".to_string())
};
let res = RefundNotify {
out_trade_no: json["out_trade_no"].to_string(),
refund_no: json["out_refund_no"].to_string(),
refund_id: json["refund_id"].to_string(),
sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
total: json["amount"]["total"].as_f64().unwrap_or(0.0) / 100.0,
refund: json["amount"]["refund"].as_f64().unwrap_or(0.0) / 100.0,
payer_total: json["amount"]["payer_total"].as_f64().unwrap() / 100.0,
payer_refund: json["amount"]["payer_refund"].as_f64().unwrap() / 100.0,
status: RefundStatus::from(json["refund_status"].as_str().unwrap()),
};
Ok(res.json())
}
fn auth(&mut self, _code: &str) -> Result<JsonValue, String> {
todo!()
}
fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
let url = format!("/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/close");
let body = object! {
"sp_mchid"=> self.mchid.clone(),
"sub_mchid"=> sub_mchid
};
let sign = self.sign("POST", url.as_str(), body.to_string().as_str())?;
let mut http = br_http::Http::new();
match http.post(format!("https://api.mch.weixin.qq.com{}", url).as_str()).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").header("Authorization", sign.as_str()).raw_json(body).json() {
Ok(_) => Ok(true.into()),
Err(e) => Err(e)
}
}
fn config(&mut self) -> JsonValue {
todo!()
}
fn pay(&mut self, types: Types, sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, sp_openid: &str) -> Result<JsonValue, String> {
let url = match types {
Types::Jsapi => "/v3/pay/partner/transactions/jsapi",
Types::Native => "/v3/pay/partner/transactions/native",
Types::H5 => "/v3/pay/partner/transactions/h5",
Types::MiniJsapi => "/v3/pay/partner/transactions/jsapi",
Types::App => "/v3/pay/partner/transactions/app",
Types::Micropay => "/pay/micropay"
};
let total = format!("{:.0}", total_fee * 100.0);
let mut body = object! {
"sp_appid" => self.appid.clone(),
"sp_mchid"=> self.mchid.clone(),
"sub_mchid"=> sub_mchid,
"description"=>description,
"out_trade_no"=>out_trade_no,
"notify_url"=>self.notify_url.clone(),
"support_fapiao"=>true,
"amount"=>object! {
total: total.parse::<i64>().unwrap(),
currency:"CNY"
}
};
match types {
Types::Native => {}
_ => {
body["payer"] = object! {
sp_openid:sp_openid
};
}
};
let sign = self.sign("POST", url, body.to_string().as_str())?;
let mut http = br_http::Http::new();
match http.post(format!("https://api.mch.weixin.qq.com{}", url).as_str()).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").header("Authorization", &sign).raw_json(body).json() {
Ok(e) => {
match types {
Types::Native => {
if e.has_key("code_url") {
Ok(e["code_url"].clone())
} else {
Err(e["message"].to_string())
}
}
Types::Jsapi | Types::MiniJsapi => {
if e.has_key("prepay_id") {
let signinfo = self.paysign(format!("prepay_id={}", e["prepay_id"]).as_str())?;
Ok(signinfo)
} else {
Err(e["message"].to_string())
}
}
_ => {
Ok(e)
}
}
}
Err(e) => Err(e),
}
}
}