br_pay/
wechat.rs

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    /// 服务商APPID
11    pub appid: String,
12    /// 密钥
13    pub secret: String,
14    /// 服务商户号
15    pub mchid: String,
16    /// 证书号
17    pub serial_no: String,
18    /// API证书私钥
19    pub app_private: String,
20    /// APIv3密钥
21    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(); // 秒级时间戳
36        let random_string: String = rng().sample_iter(&Alphanumeric) // 生成随机字母+数字
37                                         .take(10) // 指定长度
38                                         .map(char::from).collect();
39
40        let sign_txt = format!("{method}\n{url}\n{timestamp}\n{random_string}\n{body}\n");
41        // 加载 RSA 私钥
42        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        // 创建签名器
55        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        // 输入待签名数据
62        match signer.update(sign_txt.as_bytes()) {
63            Ok(_) => {}
64            Err(e) => {
65                return Err(e.to_string())
66            }
67        };
68        // 生成签名
69        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(); // 秒级时间戳
86        let random_string: String = rng().sample_iter(&Alphanumeric) // 生成随机字母+数字
87                                         .take(10) // 指定长度
88                                         .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        // 加载 RSA 私钥
96        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        // 创建签名器
109        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        // 输入待签名数据
116        match signer.update(sign_txt.as_bytes()) {
117            Ok(_) => {}
118            Err(e) => {
119                return Err(e.to_string())
120            }
121        };
122        // 生成签名
123        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        // 组合 Payload(带 aad)
322        let payload = Payload {
323            msg: &data,
324            aad: associated_data.as_bytes(),
325        };
326
327        // 解密
328        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        // 组合 Payload(带 aad)
415        let payload = Payload {
416            msg: &data,
417            aad: associated_data.as_bytes(),
418        };
419
420        // 解密
421        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}