br_pay/
wechat.rs

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    /// 服务商APPID
13    pub appid: String,
14    /// 密钥
15    pub secret: String,
16    /// 服务商户号
17    pub sp_mchid: String,
18    /// 证书号
19    pub serial_no: String,
20    /// API证书私钥
21    pub app_private: String,
22    /// APIv3密钥
23    pub apikey: String,
24    /// APIv2密钥
25    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(); // 秒级时间戳
78        let random_string: String = rng().sample_iter(&Alphanumeric) // 生成随机字母+数字
79                                         .take(10) // 指定长度
80                                         .map(char::from).collect();
81
82        let sign_txt = format!("{method}\n{url}\n{timestamp}\n{random_string}\n{body}\n");
83        // 加载 RSA 私钥
84        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        // 创建签名器
97        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        // 输入待签名数据
104        match signer.update(sign_txt.as_bytes()) {
105            Ok(_) => {}
106            Err(e) => {
107                return Err(e.to_string())
108            }
109        };
110        // 生成签名
111        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(); // 秒级时间戳
128        let random_string: String = rng().sample_iter(&Alphanumeric) // 生成随机字母+数字
129                                         .take(10) // 指定长度
130                                         .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        // 加载 RSA 私钥
138        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        // 创建签名器
151        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        // 输入待签名数据
158        match signer.update(sign_txt.as_bytes()) {
159            Ok(_) => {}
160            Err(e) => {
161                return Err(e.to_string())
162            }
163        };
164        // 生成签名
165        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        // 组合 Payload(带 aad)
308        let payload = Payload {
309            msg: &data,
310            aad: associated_data.as_bytes(),
311        };
312
313        // 解密
314        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        // 组合 Payload(带 aad)
393        let payload = Payload {
394            msg: &data,
395            aad: associated_data.as_bytes(),
396        };
397
398        // 解密
399        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}