1use std::collections::HashMap;
2use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
3use base64::engine::general_purpose::STANDARD;
4use base64::{Engine};
5use json::{object, JsonValue};
6use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
7use aes_gcm::aead::{Aead, Payload};
8use br_reqwest::Method;
9
10#[derive(Clone)]
11pub struct Wechat {
12 pub appid: String,
14 pub secret: String,
16 pub sp_mchid: String,
18 pub serial_no: String,
20 pub app_private: String,
22 pub apikey: String,
24 pub apiv2: String,
26 pub notify_url: String,
27}
28
29use chrono::{DateTime, Utc};
30use openssl::hash::MessageDigest;
31use openssl::pkey::{PKey};
32use openssl::rsa::Rsa;
33use openssl::sign::Signer;
34use rand::distr::Alphanumeric;
35use rand::{rng, Rng};
36
37impl Wechat {
38 pub fn http(&mut self, url: &str, method: Method, body: JsonValue) -> Result<JsonValue, String> {
39 let sign = self.sign(method.to_str().to_uppercase().as_str(), url, body.to_string().as_str())?;
40 let mut http = br_reqwest::Client::new();
41 let url = format!("https://api.mch.weixin.qq.com{}", url);
42 let send = match method {
43 Method::GET => http.get(url.as_str()),
44 Method::POST => http.post(url.as_str()).raw_json(body),
45 _ => http.post(url.as_str()),
46 };
47 match send.header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").header("Authorization", sign.as_str()).send()?.json() {
48 Ok(e) => Ok(e),
49 Err(e) => Err(e)
50 }
51 }
52
53 pub fn sign_v2(&mut self, body: JsonValue) -> Result<String, String> {
54 let mut map = HashMap::new();
55 for (key, value) in body.entries() {
56 if key == "sign" {
57 continue;
58 }
59 if value.is_empty() {
60 continue;
61 }
62 map.insert(key, value);
63 }
64 let mut keys: Vec<_> = map.keys().cloned().collect();
65 keys.sort();
66 let mut txt = vec![];
67 for key in keys {
68 txt.push(format!("{}={}", key, map.get(&key).unwrap()));
69 }
70 let txt = txt.join("&");
71 let string_sign_temp = format!("{}&key={}", txt, self.apiv2);
72 let sign = format!("{:x}", md5::compute(string_sign_temp.as_bytes())).to_uppercase();
73 Ok(sign)
74 }
75
76 pub fn sign(&mut self, method: &str, url: &str, body: &str) -> Result<String, String> {
77 let timestamp = Utc::now().timestamp(); let random_string: String = rng().sample_iter(&Alphanumeric) .take(10) .map(char::from).collect();
81
82 let sign_txt = format!("{method}\n{url}\n{timestamp}\n{random_string}\n{body}\n");
83 let rsa = match Rsa::private_key_from_pem(self.app_private.as_bytes()) {
85 Ok(e) => e,
86 Err(e) => {
87 return Err(e.to_string())
88 }
89 };
90 let pkey = match PKey::from_rsa(rsa) {
91 Ok(e) => e,
92 Err(e) => {
93 return Err(format!("Failed to create PKey: {}", e))
94 }
95 };
96 let mut signer = match Signer::new(MessageDigest::sha256(), &pkey) {
98 Ok(e) => e,
99 Err(e) => {
100 return Err(format!("Failed to create signer:{}", e));
101 }
102 };
103 match signer.update(sign_txt.as_bytes()) {
105 Ok(_) => {}
106 Err(e) => {
107 return Err(e.to_string())
108 }
109 };
110 let signature = match signer.sign_to_vec() {
112 Ok(e) => e,
113 Err(e) => {
114 return Err(format!("Failed to sign: {}", e));
115 }
116 };
117 let signature_b64 = STANDARD.encode(signature);
118 let sign = format!(
119 r#"WECHATPAY2-SHA256-RSA2048 mchid="{}",nonce_str="{random_string}",signature="{signature_b64}",timestamp="{timestamp}",serial_no="{}""#,
120 self.sp_mchid.as_str(),
121 self.serial_no
122 );
123 Ok(sign)
124 }
125
126 pub fn paysign(&mut self, prepay_id: &str) -> Result<JsonValue, String> {
127 let timestamp = Utc::now().timestamp(); let random_string: String = rng().sample_iter(&Alphanumeric) .take(10) .map(char::from).collect();
131
132 let sign_txt = format!(
133 "{}\n{timestamp}\n{random_string}\n{prepay_id}\n",
134 self.appid
135 );
136
137 let rsa = match Rsa::private_key_from_pem(self.app_private.as_bytes()) {
139 Ok(e) => e,
140 Err(e) => {
141 return Err(e.to_string())
142 }
143 };
144 let pkey = match PKey::from_rsa(rsa) {
145 Ok(e) => e,
146 Err(e) => {
147 return Err(format!("Failed to create PKey: {}", e))
148 }
149 };
150 let mut signer = match Signer::new(MessageDigest::sha256(), &pkey) {
152 Ok(e) => e,
153 Err(e) => {
154 return Err(format!("Failed to create signer:{}", e));
155 }
156 };
157 match signer.update(sign_txt.as_bytes()) {
159 Ok(_) => {}
160 Err(e) => {
161 return Err(e.to_string())
162 }
163 };
164 let signature = match signer.sign_to_vec() {
166 Ok(e) => e,
167 Err(e) => {
168 return Err(format!("Failed to sign: {}", e));
169 }
170 };
171 let signature_b64 = STANDARD.encode(signature);
172 let sign = signature_b64;
173 Ok(object! {
174 timeStamp:timestamp,
175 nonceStr:random_string,
176 package:prepay_id,
177 signType:"RSA",
178 paySign:sign
179 })
180 }
181}
182impl PayMode for Wechat {
183 fn login(&mut self, code: &str) -> Result<JsonValue, String> {
184 let mut http = br_reqwest::Client::new();
185 match http.get(
186 "https://api.weixin.qq.com/sns/jscode2session".to_string().as_str(),
187 ).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").query(object! {
188 appid: self.appid.as_str(),
189 secret: self.secret.as_str(),
190 js_code:code,
191 grant_type:"authorization_code",
192 }).send()?.json() {
193 Ok(e) => Ok(e),
194 Err(e) => Err(e),
195 }
196 }
197
198 fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
199 let url = format!(
200 "/v3/pay/partner/transactions/out-trade-no/{}?sub_mchid={}&sp_mchid={}",
201 out_trade_no, sub_mchid, self.sp_mchid
202 );
203 let sign = self.sign("GET", url.as_str(), "")?;
204 let mut http = br_reqwest::Client::new();
205 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()).send()?.json() {
206 Ok(e) => {
207 if e.has_key("message") {
208 return Err(e["message"].to_string());
209 }
210 let res = PayNotify {
211 trade_type: TradeType::from(e["trade_type"].to_string().as_str()),
212 out_trade_no: e["out_trade_no"].as_str().unwrap().to_string(),
213 sp_mchid: e["sp_mchid"].as_str().unwrap().to_string(),
214 sub_mchid: e["sub_mchid"].as_str().unwrap().to_string(),
215 sp_appid: e["sp_appid"].as_str().unwrap().to_string(),
216 transaction_id: e["transaction_id"].to_string(),
217 success_time: PayNotify::success_time(e["success_time"].as_str().unwrap_or("")),
218 sp_openid: e["payer"]["sp_openid"].to_string(),
219 sub_openid: e["payer"]["sub_openid"].to_string(),
220 total: e["amount"]["total"].as_u64().unwrap() as usize,
221 currency: e["amount"]["currency"].to_string(),
222 payer_total: e["amount"]["payer_total"].as_u64().unwrap_or(0) as usize,
223 payer_currency: e["amount"]["payer_currency"].to_string(),
224 trade_state: TradeState::from(e["trade_state"].as_str().unwrap()),
225 };
226
227 Ok(res.json())
228 }
229 Err(e) => Err(e),
230 }
231 }
232
233 fn refund(
234 &mut self,
235 sub_mchid: &str,
236 out_trade_no: &str,
237 transaction_id: &str,
238 out_refund_no: &str,
239 amount: f64,
240 total: f64,
241 currency: &str,
242 ) -> Result<JsonValue, String> {
243 let url = "/v3/refund/domestic/refunds";
244
245 let refund = format!("{:.0}", amount * 100.0);
246 let total = format!("{:.0}", total * 100.0);
247
248 let body = object! {
249 "sub_mchid"=> sub_mchid,
250 "transaction_id"=>transaction_id,
251 "out_trade_no"=>out_trade_no,
252 "out_refund_no"=>out_refund_no,
253 "amount"=>object! {
254 refund: refund.parse::<i64>().unwrap(),
255 total: total.parse::<i64>().unwrap(),
256 currency:currency
257 }
258 };
259 let sign = self.sign("POST", url, body.to_string().as_str())?;
260 let mut http = br_reqwest::Client::new();
261 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).send()?.json() {
262 Ok(e) => {
263 if e.is_empty() {
264 return Err("已执行".to_string());
265 }
266 if e.has_key("message") {
267 return Err(e["message"].to_string());
268 }
269 let mut refund_time = 0.0;
270 if e.has_key("success_time") {
271 let success_time = e["success_time"].as_str().unwrap_or("").to_string();
272 if !success_time.is_empty() {
273 let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
274 refund_time = datetime.timestamp() as f64;
275 }
276 }
277
278 let status = match e["status"].as_str().unwrap() {
279 "PROCESSING" => "退款中",
280 "SUCCESS" => "已退款",
281 _ => "无退款",
282 };
283 let info = object! {
284 refund_id: e["refund_id"].clone(),
285 user_received_account:e["user_received_account"].clone(),
286 status:status,
287 refund_time:refund_time,
288 out_refund_no: e["out_refund_no"].clone(),
289 };
290 Ok(info)
291 }
292 Err(e) => Err(e)
293 }
294 }
295
296 fn pay_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
297 if self.apikey.is_empty() {
298 return Err("apikey 不能为空".to_string());
299 }
300 let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
301 let cipher = Aes256Gcm::new(key);
302 let nonce = Nonce::from_slice(nonce.as_bytes());
303 let data = match STANDARD.decode(ciphertext) {
304 Ok(e) => e,
305 Err(e) => return Err(format!("Invalid data received from API :{}", e))
306 };
307 let payload = Payload {
309 msg: &data,
310 aad: associated_data.as_bytes(),
311 };
312
313 let plaintext = match cipher.decrypt(nonce, payload) {
315 Ok(e) => e,
316 Err(e) => {
317 return Err(format!("解密 API:{}", e));
318 }
319 };
320 let rr = match String::from_utf8(plaintext) {
321 Ok(d) => d,
322 Err(_) => return Err("utf8 error".to_string())
323 };
324 let json = match json::parse(rr.as_str()) {
325 Ok(e) => e,
326 Err(_) => return Err("json error".to_string())
327 };
328 let res = PayNotify {
329 trade_type: TradeType::from(json["trade_type"].as_str().unwrap()),
330 out_trade_no: json["out_trade_no"].as_str().unwrap().to_string(),
331 sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
332 sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
333 sp_appid: json["sp_appid"].as_str().unwrap().to_string(),
334 transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
335 success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
336 sp_openid: json["payer"]["sp_openid"].as_str().unwrap().to_string(),
337 sub_openid: json["payer"]["sub_openid"].as_str().unwrap().to_string(),
338 total: json["amount"]["total"].as_u64().unwrap() as usize,
339 payer_total: json["amount"]["payer_total"].as_u64().unwrap() as usize,
340 currency: json["amount"]["currency"].to_string(),
341 payer_currency: json["amount"]["payer_currency"].to_string(),
342 trade_state: TradeState::from(json["trade_state"].as_str().unwrap()),
343 };
344 Ok(res.json())
345 }
346
347 fn refund_query(&mut self, _trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
348 let url = format!("/v3/refund/domestic/refunds/{out_refund_no}?sub_mchid={}", sub_mchid);
349 let sign = self.sign("GET", url.as_str(), "")?;
350 let mut http = br_reqwest::Client::new();
351 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()).send()?.json() {
352 Ok(e) => {
353 if e.is_empty() {
354 return Err("已执行".to_string());
355 }
356 if e.has_key("message") {
357 return Err(e["message"].to_string());
358 }
359
360
361 let res = RefundNotify {
362 out_trade_no: e["out_trade_no"].to_string(),
363 refund_no: e["out_refund_no"].to_string(),
364 sp_mchid: "".to_string(),
365 sub_mchid: sub_mchid.to_string(),
366 transaction_id: e["transaction_id"].to_string(),
367 refund_id: e["refund_id"].to_string(),
368 success_time: PayNotify::success_time(e["success_time"].as_str().unwrap_or("")),
369 total: e["amount"]["total"].to_string().parse::<f64>().unwrap(),
370 payer_total: e["amount"]["total"].to_string().parse::<f64>().unwrap(),
371 refund: e["amount"]["refund"].to_string().parse::<f64>().unwrap(),
372 payer_refund: e["amount"]["refund"].to_string().parse::<f64>().unwrap(),
373 status: RefundStatus::from(e["status"].as_str().unwrap()),
374 };
375
376 Ok(res.json())
377 }
378 Err(e) => Err(e),
379 }
380 }
381 fn refund_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
382 if self.apikey.is_empty() {
383 return Err("apikey 不能为空".to_string());
384 }
385 let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
386 let cipher = Aes256Gcm::new(key);
387 let nonce = Nonce::from_slice(nonce.as_bytes());
388 let data = match STANDARD.decode(ciphertext) {
389 Ok(e) => e,
390 Err(e) => return Err(format!("Invalid data received from API :{}", e))
391 };
392 let payload = Payload {
394 msg: &data,
395 aad: associated_data.as_bytes(),
396 };
397
398 let plaintext = match cipher.decrypt(nonce, payload) {
400 Ok(e) => e,
401 Err(e) => {
402 return Err(format!("解密 API:{}", e));
403 }
404 };
405 let rr = match String::from_utf8(plaintext) {
406 Ok(d) => d,
407 Err(_) => return Err("utf8 error".to_string())
408 };
409 let json = match json::parse(rr.as_str()) {
410 Ok(e) => e,
411 Err(_) => return Err("json error".to_string())
412 };
413 let res = RefundNotify {
414 out_trade_no: json["out_trade_no"].to_string(),
415 refund_no: json["out_refund_no"].to_string(),
416 refund_id: json["refund_id"].to_string(),
417 sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
418 sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
419 transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
420 success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
421 total: json["amount"]["total"].as_f64().unwrap_or(0.0) / 100.0,
422 refund: json["amount"]["refund"].as_f64().unwrap_or(0.0) / 100.0,
423 payer_total: json["amount"]["payer_total"].as_f64().unwrap() / 100.0,
424 payer_refund: json["amount"]["payer_refund"].as_f64().unwrap() / 100.0,
425 status: RefundStatus::from(json["refund_status"].as_str().unwrap()),
426 };
427 Ok(res.json())
428 }
429
430 fn auth(&mut self, _code: &str) -> Result<JsonValue, String> {
431 todo!()
432 }
433
434 fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
435 let url = format!("/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/close");
436 let body = object! {
437 "sp_mchid"=> self.sp_mchid.clone(),
438 "sub_mchid"=> sub_mchid
439 };
440 let sign = self.sign("POST", url.as_str(), body.to_string().as_str())?;
441 let mut http = br_reqwest::Client::new();
442 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).send()?.json() {
443 Ok(_) => Ok(true.into()),
444 Err(e) => Err(e)
445 }
446 }
447
448 fn config(&mut self) -> JsonValue {
449 todo!()
450 }
451
452 fn pay(&mut self, types: Types, sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, sp_openid: &str) -> Result<JsonValue, String> {
453 let url = match types {
454 Types::Jsapi => "/v3/pay/partner/transactions/jsapi",
455 Types::Native => "/v3/pay/partner/transactions/native",
456 Types::H5 => "/v3/pay/partner/transactions/h5",
457 Types::MiniJsapi => "/v3/pay/partner/transactions/jsapi",
458 Types::App => "/v3/pay/partner/transactions/app",
459 Types::Micropay => "/pay/micropay"
460 };
461 let total = format!("{:.0}", total_fee * 100.0);
462 let mut body = object! {
463 "sp_appid" => self.appid.clone(),
464 "sp_mchid"=> self.sp_mchid.clone(),
465 "sub_mchid"=> sub_mchid,
466 "description"=>description,
467 "out_trade_no"=>out_trade_no,
468 "notify_url"=>self.notify_url.clone(),
469 "support_fapiao"=>true,
470 "amount"=>object! {
471 total: total.parse::<i64>().unwrap(),
472 currency:"CNY"
473 }
474 };
475 match types {
476 Types::Native => {}
477 _ => {
478 body["payer"] = object! {
479 sp_openid:sp_openid
480 };
481 }
482 };
483 let sign = self.sign("POST", url, body.to_string().as_str())?;
484 let mut http = br_reqwest::Client::new();
485 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).send()?.json() {
486 Ok(e) => {
487 match types {
488 Types::Native => {
489 if e.has_key("code_url") {
490 Ok(e["code_url"].clone())
491 } else {
492 Err(e["message"].to_string())
493 }
494 }
495 Types::Jsapi | Types::MiniJsapi => {
496 if e.has_key("prepay_id") {
497 let signinfo = self.paysign(format!("prepay_id={}", e["prepay_id"]).as_str())?;
498 Ok(signinfo)
499 } else {
500 Err(e["message"].to_string())
501 }
502 }
503 _ => {
504 Ok(e)
505 }
506 }
507 }
508 Err(e) => Err(e),
509 }
510 }
511 fn micropay(&mut self, auth_code: &str, sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, org_openid: &str, ip: &str) -> Result<JsonValue, String> {
512 let url = "/pay/micropay";
513 let total = format!("{:.0}", total_fee * 100.0);
514
515 let nonce_str: String = rand::rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect();
516
517 let mut body = object! {
518 "appid": self.appid.clone(),
519 "mch_id"=> self.sp_mchid.clone(),
520 "sub_mch_id"=> sub_mchid,
521 "nonce_str"=>nonce_str,
522 "body"=> description,
523 "out_trade_no"=>out_trade_no,
524 "total_fee"=>total.parse::<i64>().unwrap(),
525 "fee_type":"CNY",
526 "spbill_create_ip":ip,
527 "device_info":org_openid,
528 "auth_code":auth_code
529 };
530 body["sign"] = self.sign_v2(body.clone())?.into();
531 let mut xml = vec!["<xml>".to_owned()];
532 for (key, value) in body.entries() {
533 let t = format!("<{}>{}</{00}>", key, value.clone().clone());
534 xml.push(t);
535 }
536 xml.push("</xml>".to_owned());
537 let xml = xml.join("");
538 let mut http = br_reqwest::Client::new();
539 match http.post(format!("https://api.mch.weixin.qq.com{}", url).as_str()).header("Content-Type", "application/xml").raw_xml(xml.into()).send()?.xml() {
540 Ok(e) => Ok(e),
541 Err(e) => Err(e),
542 }
543 }
544
545 fn notify(&mut self, _data: JsonValue) -> Result<JsonValue, String> {
546 todo!()
547 }
548
549 fn get_sub_mchid(&mut self, sub_mchid: &str) -> Result<JsonValue, String> {
550 let url = format!("/v3/apply4sub/sub_merchants/{sub_mchid}/settlement");
551 let res = self.http(url.as_str(), Method::GET, "".into())?;
552 if res.has_key("verify_result") && res["verify_result"] == "VERIFY_SUCCESS" {
553 return Ok(true.into());
554 }
555 Err(res.to_string())
556 }
557}