use std::collections::HashMap;
use crate::config::{Mode, WechatConfig};
use crate::errors::PayError;
use crate::utils::{gen_nonce, now_ts, rsa_sign_sha256_pem};
use crate::wechat::certs::PlatformCerts;
use reqwest::Client;
use serde_json::{json, Value};
use std::sync::Arc;
use url::Url;
use crate::wechat::notify::WechatNotify;
pub struct WechatClient {
cfg: Arc<WechatConfig>,
http: Client,
certs: Arc<PlatformCerts>,
base_url: String,
mode: Mode,
max_retries: usize,
}
impl WechatClient {
pub fn with_mode(cfg: Arc<WechatConfig>, mode: Mode) -> Self {
let http = Client::builder()
.user_agent("rust_pay_wf")
.build()
.expect("client");
let certs = Arc::new(PlatformCerts::new(cfg.clone()));
let base_url = match mode {
Mode::Sandbox => "https://api.mch.weixin.qq.com/sandboxnew".to_string(),
_ => "https://api.mch.weixin.qq.com".to_string(),
};
Self {
cfg,
http,
certs,
base_url,
mode,
max_retries: 3,
}
}
fn endpoint(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
fn get_service_url(&self, path: &str) -> String {
if let Mode::Service = self.mode {
if path.contains("/v3/pay/transactions/") {
let path=path.replace("/v3/pay/transactions/", "/v3/pay/partner/transactions/");
return self.endpoint(&path);
}
return self.endpoint(path);
} else {
self.endpoint(path)
}
}
fn build_service_params(&self, mut params: Value) -> Value {
if let Mode::Service = self.mode {
if !params.get("appid").is_some() && !params.get("sp_appid").is_some() {
if let Some(appid) = &self.cfg.appid {
params["sp_appid"] = json!(appid.clone());
}
}
if !params.get("sp_appid").is_some() {
if let Some(sp_appid) = &self.cfg.appid {
params["sp_appid"] = json!(sp_appid.clone());
} else if let Some(appid) = &self.cfg.appid_mp {
params["sp_appid"] = json!(appid.clone());
}
}
if !params.get("sp_mchid").is_some() {
params["sp_mchid"] = json!(self.cfg.mchid.clone());
}
if !params.get("sub_mchid").is_some() {
if let Some(sub_mchid) = &self.cfg.sub_mchid {
params["sub_mchid"] = json!(sub_mchid.clone());
}
}
let old_params=params.clone();
if let Some(payer) = params.get_mut("payer") {
if let Value::Object(payer_obj) = payer {
if old_params.get("sub_appid").is_some() {
if let Some(openid) = payer_obj.remove("openid") {
payer_obj.insert("sub_openid".to_string(), openid);
}
}else {
if let Some(openid) = payer_obj.remove("openid") {
payer_obj.insert("sp_openid".to_string(), openid);
}
}
}
}
}else {
params["mchid"] = json!(self.cfg.mchid.clone());
params["appid"] = json!(self.cfg.appid.clone());
}
if !params.get("notify_url").is_some() {
if let Some(notify_url) = &self.cfg.notify_url {
params["notify_url"] = json!(notify_url.clone());
}
}
params
}
pub async fn mp(&self, mut order: Value) -> Result<Value, PayError> {
if let Mode::Service = self.mode {
if !order.get("sub_appid").is_some() {
if let Some(appid) = &self.cfg.appid_mp {
order["sub_appid"] = json!(appid.clone());
}
}
}
order = self.build_service_params(order);
let url = self.get_service_url("/v3/pay/transactions/jsapi");
let resp = self.sign_and_post("POST", &url, &order).await?;
if let Some(prepay_id) = resp.get("prepay_id").and_then(|v| v.as_str()) {
let time_stamp = now_ts();
let nonce_str = gen_nonce(32);
let package = format!("prepay_id={}", prepay_id);
let appid = if let Mode::Service = self.mode {
order.get("sp_appid").and_then(|v| v.as_str()).unwrap_or("")
} else {
order.get("appid").and_then(|v| v.as_str()).unwrap_or("")
};
let sign_src = format!(
"{}\n{}\n{}\n{}\n",
appid,
time_stamp,
nonce_str,
package
);
let pay_sign = rsa_sign_sha256_pem(&self.cfg.private_key_pem, &sign_src)
.map_err(|e| PayError::Crypto(format!("{}", e)))?;
return Ok(
json!({
"appId": appid,
"timeStamp": time_stamp,
"nonceStr": nonce_str,
"package": package,
"signType": "RSA",
"paySign": pay_sign
}),
);
}
Ok(resp)
}
pub async fn miniapp(&self, mut order: Value) -> Result<Value, PayError> {
if let Mode::Service = self.mode {
if !order.get("sub_appid").is_some() {
if let Some(appid) = &self.cfg.appid_mini {
order["sub_appid"] = json!(appid.clone());
}
}
}
order = self.build_service_params(order);
let url = self.get_service_url("/v3/pay/transactions/jsapi");
let resp = self.sign_and_post("POST", &url, &order).await?;
if let Some(prepay_id) = resp.get("prepay_id").and_then(|v| v.as_str()) {
let time_stamp = now_ts();
let nonce_str = gen_nonce(32);
let package = format!("prepay_id={}", prepay_id);
let appid = if let Mode::Service = self.mode {
order.get("sp_appid").and_then(|v| v.as_str()).unwrap_or("")
} else {
order.get("appid").and_then(|v| v.as_str()).unwrap_or("")
};
let sign_src = format!(
"{}\n{}\n{}\n{}\n",
appid,
time_stamp,
nonce_str,
package
);
let pay_sign = rsa_sign_sha256_pem(&self.cfg.private_key_pem, &sign_src)
.map_err(|e| PayError::Crypto(format!("{}", e)))?;
return Ok(
json!({
"appId": appid,
"timeStamp": time_stamp,
"nonceStr": nonce_str,
"package": package,
"signType": "RSA",
"paySign": pay_sign
}),
);
}
Ok(resp)
}
pub async fn h5(&self, mut order: Value) -> Result<Value, PayError> {
if let Mode::Service = self.mode {
if !order.get("sub_appid").is_some() {
if let Some(appid) = &self.cfg.appid_mini {
order["sub_appid"] = json!(appid.clone());
}
}
}
order = self.build_service_params(order);
let url = self.get_service_url("/v3/pay/transactions/h5");
let resp = self.sign_and_post("POST", &url, &order).await?;
Ok(resp)
}
pub async fn app(&self, mut order: Value) -> Result<Value, PayError> {
if let Mode::Service = self.mode {
if !order.get("sub_appid").is_some() {
if let Some(appid) = &self.cfg.appid_app {
order["sub_appid"] = json!(appid.clone());
}
}
}
order = self.build_service_params(order);
let url = self.get_service_url("/v3/pay/transactions/app");
let resp = self.sign_and_post("POST", &url, &order).await?;
Ok(resp)
}
pub async fn native(&self, mut order: Value) -> Result<Value, PayError> {
order = self.build_service_params(order);
let url = self.get_service_url("/v3/pay/transactions/native");
let resp = self.sign_and_post("POST", &url, &order).await?;
Ok(resp)
}
pub async fn micropay(&self, mut order: Value) -> Result<Value, PayError> {
order = self.build_service_params(order);
let url = self.get_service_url("/v3/pay/transactions/micropay");
let resp = self.sign_and_post("POST", &url, &order).await?;
Ok(resp)
}
pub async fn query(&self, mut params: Value) -> Result<Value, PayError> {
params = self.build_service_params(params);
let url = if let Mode::Service = self.mode {
"/v3/pay/partner/transactions/id/{transaction_id}"
.replace("{transaction_id}",
params.get("transaction_id")
.and_then(|v| v.as_str())
.unwrap_or("")
)
} else {
"/v3/pay/transactions/id/{transaction_id}"
.replace("{transaction_id}",
params.get("transaction_id")
.and_then(|v| v.as_str())
.unwrap_or("")
)
};
let resp = self.sign_and_post("GET", &url, ¶ms).await?;
Ok(resp)
}
pub async fn close(&self, mut params: Value) -> Result<Value, PayError> {
params = self.build_service_params(params);
let url = if let Mode::Service = self.mode {
"/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/close"
.replace("{out_trade_no}",
params.get("out_trade_no")
.and_then(|v| v.as_str())
.unwrap_or("")
)
} else {
"/v3/pay/transactions/out-trade-no/{out_trade_no}/close"
.replace("{out_trade_no}",
params.get("out_trade_no")
.and_then(|v| v.as_str())
.unwrap_or("")
)
};
let resp = self.sign_and_post("POST", &url, ¶ms).await?;
Ok(resp)
}
pub async fn refund(&self, mut order: Value) -> Result<Value, PayError> {
order = self.build_service_params(order);
let url = if let Mode::Service = self.mode {
"/v3/refund/domestic/refunds"
} else {
"/v3/refund/domestic/refunds"
};
let resp = self.sign_and_post("POST", &url, &order).await?;
Ok(resp)
}
pub async fn query_refund(&self, mut params: Value) -> Result<Value, PayError> {
params = self.build_service_params(params);
let url = if let Mode::Service = self.mode {
"/v3/refund/domestic/refunds/{out_refund_no}"
.replace("{out_refund_no}",
params.get("out_refund_no")
.and_then(|v| v.as_str())
.unwrap_or("")
)
} else {
"/v3/refund/domestic/refunds/{out_refund_no}"
.replace("{out_refund_no}",
params.get("out_refund_no")
.and_then(|v| v.as_str())
.unwrap_or("")
)
};
let resp = self.sign_and_post("GET", &url, ¶ms).await?;
Ok(resp)
}
pub async fn transfer(&self, order: Value) -> Result<Value, PayError> {
let url = if let Mode::Service = self.mode {
"/v3/transfer/batches"
} else {
"/v3/transfer/batches"
};
let resp = self.sign_and_post("POST", &url, &order).await?;
Ok(resp)
}
pub async fn refresh_platform_certs(&self) -> Result<(), PayError> {
self.certs
.refresh()
.await
.map_err(|e| PayError::Other(format!("refresh platform certs: {}", e)))?;
Ok(())
}
pub async fn sign_and_post(
&self,
method: &str,
url: &str,
body: &Value,
) -> Result<Value, PayError> {
let body_str = if method == "GET" {
"".to_string()
} else {
body.to_string()
};
println!("sign_and_post: method={}, url={}, body={}", method, url, body_str);
let timestamp = now_ts();
let nonce = gen_nonce(32);
let parsed = Url::parse(url).map_err(|e| PayError::Other(format!("parse url: {}", e)))?;
let path = if let Some(query) = parsed.query() {
format!("{}?{}", parsed.path(), query)
} else {
parsed.path().to_string()
};
let sign_str = format!(
"{}\n{}\n{}\n{}\n{}\n",
method, path, timestamp, nonce, body_str
);
let signature = rsa_sign_sha256_pem(&self.cfg.private_key_pem, &sign_str)
.map_err(|e| PayError::Crypto(format!("{}", e)))?;
let mchid = self.cfg.mchid.clone();
let auth = format!(
r#"WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{ts}",serial_no="{serial}",signature="{sig}""#,
mchid = mchid,
nonce = nonce,
ts = timestamp,
serial = self.cfg.serial_no,
sig = signature
);
let client = &self.http;
let send_req = || async {
let mut req = match method {
"GET" => client.get(url),
"POST" => client.post(url),
_ => {
return Err(PayError::Other(format!("unsupported method: {}", method)));
}
};
req = req
.header("Authorization", auth.clone())
.header("Accept", "application/json")
.header("User-Agent", "rust_pay_wf");
if method == "POST" {
req = req
.header("Content-Type", "application/json")
.body(body_str.clone());
}
let resp = req.send().await?;
let status = resp.status();
let text = resp.text().await?;
if !status.is_success() {
return Err(PayError::Other(format!(
"HTTP request failed: {} - {}",
status, text
)));
}
let v: Value = serde_json::from_str(&text)?;
Ok(v)
};
let v = crate::utils::retry_async(self.max_retries, send_req)
.await
.map_err(|e| PayError::Other(format!(
"HTTP request failed:{}",
e
)))?;
Ok(v)
}
pub async fn handle_notify(&self, headers: HashMap<String,String>, body_str: &str) -> Result<Value, PayError> {
let notify = WechatNotify::new(self.cfg.clone(), self.certs.clone());
notify.verify_and_decrypt(&headers, body_str)
}
}