1use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType};
2use base64::engine::general_purpose::STANDARD;
3use base64::{Engine};
4use json::{object, JsonValue};
5use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
6use aes_gcm::aead::{Aead, Payload};
7
8#[derive(Clone)]
9pub struct Wechat {
10 pub appid: String,
12 pub secret: String,
14 pub mchid: String,
16 pub serial_no: String,
18 pub app_private: String,
20 pub apikey: String,
22}
23
24use chrono::{DateTime, Utc};
25use openssl::hash::MessageDigest;
26use openssl::pkey::PKey;
27use openssl::rsa::Rsa;
28use openssl::sign::Signer;
29use rand::distr::Alphanumeric;
30use rand::{rng, Rng};
31
32impl Wechat {
33 pub fn sign(&mut self, method: &str, url: &str, body: &str) -> String {
34 let timestamp = Utc::now().timestamp(); let random_string: String = rng().sample_iter(&Alphanumeric) .take(10) .map(char::from).collect();
38
39 let sign_txt = format!("{method}\n{url}\n{timestamp}\n{random_string}\n{body}\n");
40 let rsa = Rsa::private_key_from_pem(self.app_private.as_bytes()).expect("Failed to load private key");
42 let pkey = PKey::from_rsa(rsa).expect("Failed to create PKey");
43 let mut signer = Signer::new(MessageDigest::sha256(), &pkey).expect("Failed to create signer");
45 signer.update(sign_txt.as_bytes()).expect("Failed to update signer");
47 let signature = signer.sign_to_vec().expect("Failed to sign");
49 let signature_b64 = STANDARD.encode(signature);
50 let sign = format!(
51 r#"WECHATPAY2-SHA256-RSA2048 mchid="{}",nonce_str="{random_string}",signature="{signature_b64}",timestamp="{timestamp}",serial_no="{}""#,
52 self.mchid.as_str(),
53 self.serial_no
54 );
55 sign
56 }
57
58 pub fn paysign(&mut self, prepay_id: &str) -> JsonValue {
59 let timestamp = Utc::now().timestamp(); let random_string: String = rng().sample_iter(&Alphanumeric) .take(10) .map(char::from).collect();
63
64 let sign_txt = format!(
65 "{}\n{timestamp}\n{random_string}\n{prepay_id}\n",
66 self.appid
67 );
68 let rsa = Rsa::private_key_from_pem(self.app_private.as_bytes()).expect("Failed to load private key");
70 let pkey = PKey::from_rsa(rsa).expect("Failed to create PKey");
71 let mut signer = Signer::new(MessageDigest::sha256(), &pkey).expect("Failed to create signer");
73 signer.update(sign_txt.as_bytes()).expect("Failed to update signer");
75 let signature = signer.sign_to_vec().expect("Failed to sign");
77 let signature_b64 = STANDARD.encode(signature);
78 let sign = signature_b64;
79 object! {
80 timeStamp:timestamp,
81 nonceStr:random_string,
82 package:prepay_id,
83 signType:"RSA",
84 paySign:sign
85 }
86 }
87}
88impl PayMode for Wechat {
89 fn login(&mut self, code: &str) -> Result<JsonValue, String> {
90 let mut http = br_http::Http::new();
91 match http.get(
92 "https://api.weixin.qq.com/sns/jscode2session".to_string().as_str(),
93 ).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").query(object! {
94 appid: self.appid.as_str(),
95 secret: self.secret.as_str(),
96 js_code:code,
97 grant_type:"authorization_code",
98 }).json() {
99 Ok(e) => Ok(e),
100 Err(e) => Err(e),
101 }
102 }
103
104 fn jsapi(
105 &mut self,
106 sub_mchid: &str,
107 out_trade_no: &str,
108 description: &str,
109 total_fee: f64,
110 notify_url: &str,
111 sp_openid: &str,
112 ) -> Result<JsonValue, String> {
113 let url = "/v3/pay/partner/transactions/jsapi";
114 let total = (total_fee * 100.0) as u64;
115 let body = object! {
116 "sp_appid" => self.appid.clone(),
117 "sp_mchid"=> self.mchid.clone(),
118 "sub_mchid"=> sub_mchid,
119 "description"=>description,
120 "out_trade_no"=>out_trade_no,
121 "notify_url"=>notify_url,
122 "support_fapiao"=>true,
123 "amount"=>object! {
124 total: total,
125 currency:"CNY"
126 },
127 "payer"=>object! {
128 sp_openid:sp_openid
129 }
130 };
131 let sign = self.sign("POST", url, body.to_string().as_str());
132 let mut http = br_http::Http::new();
133 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() {
134 Ok(e) => {
135 if e.has_key("prepay_id") {
136 let signinfo = self.paysign(format!("prepay_id={}", e["prepay_id"]).as_str());
137 Ok(signinfo)
138 } else {
139 Err(e["message"].to_string())
140 }
141 }
142 Err(e) => Err(e),
143 }
144 }
145
146 fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
147 let url = format!(
148 "/v3/pay/partner/transactions/out-trade-no/{}?sub_mchid={}&sp_mchid={}",
149 out_trade_no, sub_mchid, self.mchid
150 );
151 let sign = self.sign("GET", url.as_str(), "");
152 let mut http = br_http::Http::new();
153 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() {
154 Ok(e) => {
155 if e.has_key("message") {
156 return Err(e["message"].to_string());
157 }
158
159 let mut pay_time = 0.0;
160 if e.has_key("success_time") {
161 let success_time = e["success_time"].as_str().unwrap_or("").to_string();
162 if !success_time.is_empty() {
163 let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
164 pay_time = datetime.timestamp() as f64;
165 }
166 }
167
168 let mut info = object! {
169 pay_time:pay_time,
170 sp_appid:e["sp_appid"].clone(),
171 transaction_id:e["transaction_id"].clone(),
172 sp_mchid:e["sp_mchid"].clone(),
173 sub_mchid:e["sub_mchid"].clone(),
174 order_no:e["out_trade_no"].clone(),
175 status:"",
176 sp_openid:e["payer"]["sp_openid"].clone(),
177 sub_openid:e["payer"]["sub_openid"].clone(),
178 amount_currency:e["amount"]["currency"].clone(),
179 amount_total:e["amount"]["total"].clone(),
180 trade_state_desc:e["trade_state_desc"].clone(),
181 trade_type:e["trade_type"].clone(),
182 };
183 info["status"] = match e["trade_state"].as_str().unwrap() {
184 "SUCCESS" => "已支付",
185 "REFUND" => "退款",
186 "CLOSED" => "已关闭",
187 _ => e["trade_state"].as_str().unwrap(),
188 }.into();
189 Ok(info)
190 }
191 Err(e) => Err(e),
192 }
193 }
194
195 fn refund(
196 &mut self,
197 sub_mchid: &str,
198 out_trade_no: &str,
199 transaction_id: &str,
200 out_refund_no: &str,
201 refund: f64,
202 total: f64,
203 currency: &str,
204 ) -> Result<JsonValue, String> {
205 let url = "/v3/refund/domestic/refunds";
206 let refund = (refund * 100.0) as u64;
207 let total = (total * 100.0) as u64;
208 let body = object! {
209 "sub_mchid"=> sub_mchid,
210 "transaction_id"=>transaction_id,
211 "out_trade_no"=>out_trade_no,
212 "out_refund_no"=>out_refund_no,
213 "amount"=>object! {
214 refund: refund,
215 total: total,
216 currency:currency
217 }
218 };
219 let sign = self.sign("POST", url, body.to_string().as_str());
220 let mut http = br_http::Http::new();
221 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() {
222 Ok(e) => {
223 if e.has_key("message") {
224 return Err(e["message"].to_string());
225 }
226 let mut refund_time = 0.0;
227 if e.has_key("success_time") {
228 let success_time = e["success_time"].as_str().unwrap_or("").to_string();
229 if !success_time.is_empty() {
230 let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
231 refund_time = datetime.timestamp() as f64;
232 }
233 }
234
235 let status = match e["status"].as_str().unwrap() {
236 "PROCESSING" => "退款中",
237 "SUCCESS" => "已退款",
238 _ => "无退款",
239 };
240 let info = object! {
241 refund_id: e["refund_id"].clone(),
242 user_received_account:e["user_received_account"].clone(),
243 status:status,
244 refund_time:refund_time,
245 out_refund_no: e["out_refund_no"].clone(),
246 };
247 Ok(info)
248 }
249 Err(e) => Err(e),
250 }
251 }
252
253 fn pay_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
254 if self.apikey.is_empty() {
255 return Err("apikey 不能为空".to_string());
256 }
257 let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
258 let cipher = Aes256Gcm::new(key);
259 let nonce = Nonce::from_slice(nonce.as_bytes());
260 let data = match STANDARD.decode(ciphertext) {
261 Ok(e) => e,
262 Err(e) => return Err(format!("Invalid data received from API :{}", e))
263 };
264 let payload = Payload {
266 msg: &data,
267 aad: associated_data.as_bytes(),
268 };
269
270 let plaintext = match cipher.decrypt(nonce, payload) {
272 Ok(e) => e,
273 Err(e) => {
274 return Err(format!("解密 API:{}", e));
275 }
276 };
277 let rr = match String::from_utf8(plaintext) {
278 Ok(d) => d,
279 Err(_) => return Err("utf8 error".to_string())
280 };
281 let json = match json::parse(rr.as_str()) {
282 Ok(e) => e,
283 Err(_) => return Err("json error".to_string())
284 };
285 let res = PayNotify {
286 trade_type: TradeType::from(json["trade_type"].as_str().unwrap()),
287 out_trade_no: json["out_trade_no"].as_str().unwrap().to_string(),
288 sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
289 sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
290 sp_appid: json["sp_appid"].as_str().unwrap().to_string(),
291 transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
292 success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
293 sp_openid: json["payer"]["sp_openid"].as_str().unwrap().to_string(),
294 sub_openid: json["payer"]["sub_openid"].as_str().unwrap().to_string(),
295 total: json["amount"]["total"].as_u64().unwrap() as usize,
296 payer_total: json["amount"]["payer_total"].as_u64().unwrap() as usize,
297 currency: json["amount"]["currency"].to_string(),
298 payer_currency: json["amount"]["payer_currency"].to_string(),
299 trade_state: TradeState::from(json["trade_state"].as_str().unwrap()),
300 };
301 Ok(res.json())
302 }
303
304 fn refund_query(&mut self, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
305 let url = format!("/v3/refund/domestic/refunds/{out_refund_no}?sub_mchid={}", sub_mchid);
306 let sign = self.sign("GET", url.as_str(), "");
307 let mut http = br_http::Http::new();
308 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() {
309 Ok(e) => {
310 if e.has_key("message") {
311 return Err(e["message"].to_string());
312 }
313
314 let mut refund_time = 0.0;
315 if e.has_key("success_time") {
316 let success_time = e["success_time"].as_str().unwrap_or("").to_string();
317 if !success_time.is_empty() {
318 let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
319 refund_time = datetime.timestamp() as f64;
320 }
321 }
322 let mut info = object! {
323 refund_time:refund_time,
324 refund_id:e["refund_id"].clone(),
325 refund_no: e["out_refund_no"].clone(),
326 order_no:e["out_trade_no"].clone(),
327 refund_amount: e["amount"]["refund"].clone(),
328 status:"",
329 transaction_id:e["transaction_id"].clone(),
330 amount_currency:e["amount"]["currency"].clone(),
331 amount_total:e["amount"]["total"].clone(),
332 note:e["user_received_account"].clone(),
333 };
334 info["status"] = match e["status"].as_str().unwrap() {
335 "SUCCESS" => "已退款",
336 "PROCESSING" => "退款中",
337 _ => e["status"].as_str().unwrap(),
338 }.into();
339 Ok(info)
340 }
341 Err(e) => Err(e),
342 }
343 }
344 fn refund_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
345 if self.apikey.is_empty() {
346 return Err("apikey 不能为空".to_string());
347 }
348 let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
349 let cipher = Aes256Gcm::new(key);
350 let nonce = Nonce::from_slice(nonce.as_bytes());
351 let data = match STANDARD.decode(ciphertext) {
352 Ok(e) => e,
353 Err(e) => return Err(format!("Invalid data received from API :{}", e))
354 };
355 let payload = Payload {
357 msg: &data,
358 aad: associated_data.as_bytes(),
359 };
360
361 let plaintext = match cipher.decrypt(nonce, payload) {
363 Ok(e) => e,
364 Err(e) => {
365 return Err(format!("解密 API:{}", e));
366 }
367 };
368 let rr = match String::from_utf8(plaintext) {
369 Ok(d) => d,
370 Err(_) => return Err("utf8 error".to_string())
371 };
372 let json = match json::parse(rr.as_str()) {
373 Ok(e) => e,
374 Err(_) => return Err("json error".to_string())
375 };
376 let res = RefundNotify {
377 out_trade_no: json["out_trade_no"].to_string(),
378 refund_no: json["out_refund_no"].to_string(),
379 refund_id: json["refund_id"].to_string(),
380 sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
381 sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
382 transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
383 success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
384 total: json["amount"]["total"].as_f64().unwrap_or(0.0) / 100.0,
385 refund: json["amount"]["refund"].as_f64().unwrap_or(0.0) / 100.0,
386 payer_total: json["amount"]["payer_total"].as_f64().unwrap() / 100.0,
387 payer_refund: json["amount"]["payer_refund"].as_f64().unwrap() / 100.0,
388 status: RefundStatus::from(json["refund_status"].as_str().unwrap()),
389 };
390 Ok(res.json())
391 }
392
393 fn auth(&mut self, _code: &str) -> Result<JsonValue, String> {
394 todo!()
395 }
396
397 fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
398 let url = format!("/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/close");
399 let body = object! {
400 "sp_mchid"=> self.mchid.clone(),
401 "sub_mchid"=> sub_mchid
402 };
403 let sign = self.sign("POST", url.as_str(), body.to_string().as_str());
404 let mut http = br_http::Http::new();
405 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() {
406 Ok(_) => Ok(true.into()),
407 Err(e) => Err(e)
408 }
409 }
410}