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