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