br_pay/
wechat.rs

1use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType};
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}
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) -> String {
34        let timestamp = Utc::now().timestamp(); // 秒级时间戳
35        let random_string: String = rng().sample_iter(&Alphanumeric) // 生成随机字母+数字
36                                         .take(10) // 指定长度
37                                         .map(char::from).collect();
38
39        let sign_txt = format!("{method}\n{url}\n{timestamp}\n{random_string}\n{body}\n");
40        // 加载 RSA 私钥
41        let rsa = Rsa::private_key_from_pem(self.app_private.as_bytes()).expect("Failed to load private key");
42        let pkey = PKey::from_rsa(rsa).expect("Failed to create PKey");
43        // 创建签名器
44        let mut signer = Signer::new(MessageDigest::sha256(), &pkey).expect("Failed to create signer");
45        // 输入待签名数据
46        signer.update(sign_txt.as_bytes()).expect("Failed to update signer");
47        // 生成签名
48        let signature = signer.sign_to_vec().expect("Failed to sign");
49        let signature_b64 = STANDARD.encode(signature);
50        let sign = format!(
51            r#"WECHATPAY2-SHA256-RSA2048 mchid="{}",nonce_str="{random_string}",signature="{signature_b64}",timestamp="{timestamp}",serial_no="{}""#,
52            self.mchid.as_str(),
53            self.serial_no
54        );
55        sign
56    }
57
58    pub fn paysign(&mut self, prepay_id: &str) -> JsonValue {
59        let timestamp = Utc::now().timestamp(); // 秒级时间戳
60        let random_string: String = rng().sample_iter(&Alphanumeric) // 生成随机字母+数字
61                                         .take(10) // 指定长度
62                                         .map(char::from).collect();
63
64        let sign_txt = format!(
65            "{}\n{timestamp}\n{random_string}\n{prepay_id}\n",
66            self.appid
67        );
68        // 加载 RSA 私钥
69        let rsa = Rsa::private_key_from_pem(self.app_private.as_bytes()).expect("Failed to load private key");
70        let pkey = PKey::from_rsa(rsa).expect("Failed to create PKey");
71        // 创建签名器
72        let mut signer = Signer::new(MessageDigest::sha256(), &pkey).expect("Failed to create signer");
73        // 输入待签名数据
74        signer.update(sign_txt.as_bytes()).expect("Failed to update signer");
75        // 生成签名
76        let signature = signer.sign_to_vec().expect("Failed to sign");
77        let signature_b64 = STANDARD.encode(signature);
78        let sign = signature_b64;
79        object! {
80            timeStamp:timestamp,
81            nonceStr:random_string,
82            package:prepay_id,
83            signType:"RSA",
84            paySign:sign
85        }
86    }
87}
88impl PayMode for Wechat {
89    fn login(&mut self, code: &str) -> Result<JsonValue, String> {
90        let mut http = br_http::Http::new();
91        match http.get(
92            "https://api.weixin.qq.com/sns/jscode2session".to_string().as_str(),
93        ).header("Accept", "application/json").header("User-Agent", "api").header("Content-Type", "application/json").query(object! {
94                appid: self.appid.as_str(),
95                secret: self.secret.as_str(),
96                js_code:code,
97                grant_type:"authorization_code",
98            }).json() {
99            Ok(e) => Ok(e),
100            Err(e) => Err(e),
101        }
102    }
103
104    fn jsapi(
105        &mut self,
106        sub_mchid: &str,
107        out_trade_no: &str,
108        description: &str,
109        total_fee: f64,
110        notify_url: &str,
111        sp_openid: &str,
112    ) -> Result<JsonValue, String> {
113        let url = "/v3/pay/partner/transactions/jsapi";
114        let total = (total_fee * 100.0) as u64;
115        let body = object! {
116            "sp_appid" => self.appid.clone(),
117            "sp_mchid"=> self.mchid.clone(),
118            "sub_mchid"=> sub_mchid,
119            "description"=>description,
120            "out_trade_no"=>out_trade_no,
121            "notify_url"=>notify_url,
122            "support_fapiao"=>true,
123            "amount"=>object! {
124                total: total,
125                currency:"CNY"
126            },
127            "payer"=>object! {
128                sp_openid:sp_openid
129            }
130        };
131        let sign = self.sign("POST", url, body.to_string().as_str());
132        let mut http = br_http::Http::new();
133        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() {
134            Ok(e) => {
135                if e.has_key("prepay_id") {
136                    let signinfo = self.paysign(format!("prepay_id={}", e["prepay_id"]).as_str());
137                    Ok(signinfo)
138                } else {
139                    Err(e["message"].to_string())
140                }
141            }
142            Err(e) => Err(e),
143        }
144    }
145
146    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
147        let url = format!(
148            "/v3/pay/partner/transactions/out-trade-no/{}?sub_mchid={}&sp_mchid={}",
149            out_trade_no, sub_mchid, self.mchid
150        );
151        let sign = self.sign("GET", url.as_str(), "");
152        let mut http = br_http::Http::new();
153        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() {
154            Ok(e) => {
155                if e.has_key("message") {
156                    return Err(e["message"].to_string());
157                }
158
159                let mut pay_time = 0.0;
160                if e.has_key("success_time") {
161                    let success_time = e["success_time"].as_str().unwrap_or("").to_string();
162                    if !success_time.is_empty() {
163                        let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
164                        pay_time = datetime.timestamp() as f64;
165                    }
166                }
167
168                let mut info = object! {
169                    pay_time:pay_time,
170                    sp_appid:e["sp_appid"].clone(),
171                    transaction_id:e["transaction_id"].clone(),
172                    sp_mchid:e["sp_mchid"].clone(),
173                    sub_mchid:e["sub_mchid"].clone(),
174                    order_no:e["out_trade_no"].clone(),
175                    status:"",
176                    sp_openid:e["payer"]["sp_openid"].clone(),
177                    sub_openid:e["payer"]["sub_openid"].clone(),
178                    amount_currency:e["amount"]["currency"].clone(),
179                    amount_total:e["amount"]["total"].clone(),
180                    trade_state_desc:e["trade_state_desc"].clone(),
181                    trade_type:e["trade_type"].clone(),
182                };
183                info["status"] = match e["trade_state"].as_str().unwrap() {
184                    "SUCCESS" => "已支付",
185                    "REFUND" => "退款",
186                    "CLOSED" => "已关闭",
187                    _ => e["trade_state"].as_str().unwrap(),
188                }.into();
189                Ok(info)
190            }
191            Err(e) => Err(e),
192        }
193    }
194
195    fn refund(
196        &mut self,
197        sub_mchid: &str,
198        out_trade_no: &str,
199        transaction_id: &str,
200        out_refund_no: &str,
201        refund: f64,
202        total: f64,
203        currency: &str,
204    ) -> Result<JsonValue, String> {
205        let url = "/v3/refund/domestic/refunds";
206        let refund = (refund * 100.0) as u64;
207        let total = (total * 100.0) as u64;
208        let body = object! {
209            "sub_mchid"=> sub_mchid,
210            "transaction_id"=>transaction_id,
211            "out_trade_no"=>out_trade_no,
212            "out_refund_no"=>out_refund_no,
213            "amount"=>object! {
214                refund: refund,
215                total: total,
216                currency:currency
217            }
218        };
219        let sign = self.sign("POST", url, body.to_string().as_str());
220        let mut http = br_http::Http::new();
221        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() {
222            Ok(e) => {
223                if e.has_key("message") {
224                    return Err(e["message"].to_string());
225                }
226                let mut refund_time = 0.0;
227                if e.has_key("success_time") {
228                    let success_time = e["success_time"].as_str().unwrap_or("").to_string();
229                    if !success_time.is_empty() {
230                        let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
231                        refund_time = datetime.timestamp() as f64;
232                    }
233                }
234
235                let status = match e["status"].as_str().unwrap() {
236                    "PROCESSING" => "退款中",
237                    "SUCCESS" => "已退款",
238                    _ => "无退款",
239                };
240                let info = object! {
241                    refund_id: e["refund_id"].clone(),
242                    user_received_account:e["user_received_account"].clone(),
243                    status:status,
244                    refund_time:refund_time,
245                    out_refund_no: e["out_refund_no"].clone(),
246                };
247                Ok(info)
248            }
249            Err(e) => Err(e),
250        }
251    }
252
253    fn pay_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
254        if self.apikey.is_empty() {
255            return Err("apikey 不能为空".to_string());
256        }
257        let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
258        let cipher = Aes256Gcm::new(key);
259        let nonce = Nonce::from_slice(nonce.as_bytes());
260        let data = match STANDARD.decode(ciphertext) {
261            Ok(e) => e,
262            Err(e) => return Err(format!("Invalid data received from API :{}", e))
263        };
264        // 组合 Payload(带 aad)
265        let payload = Payload {
266            msg: &data,
267            aad: associated_data.as_bytes(),
268        };
269
270        // 解密
271        let plaintext = match cipher.decrypt(nonce, payload) {
272            Ok(e) => e,
273            Err(e) => {
274                return Err(format!("解密 API:{}", e));
275            }
276        };
277        let rr = match String::from_utf8(plaintext) {
278            Ok(d) => d,
279            Err(_) => return Err("utf8 error".to_string())
280        };
281        let json = match json::parse(rr.as_str()) {
282            Ok(e) => e,
283            Err(_) => return Err("json error".to_string())
284        };
285        let res = PayNotify {
286            trade_type: TradeType::from(json["trade_type"].as_str().unwrap()),
287            out_trade_no: json["out_trade_no"].as_str().unwrap().to_string(),
288            sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
289            sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
290            sp_appid: json["sp_appid"].as_str().unwrap().to_string(),
291            transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
292            success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
293            sp_openid: json["payer"]["sp_openid"].as_str().unwrap().to_string(),
294            sub_openid: json["payer"]["sub_openid"].as_str().unwrap().to_string(),
295            total: json["amount"]["total"].as_u64().unwrap() as usize,
296            payer_total: json["amount"]["payer_total"].as_u64().unwrap() as usize,
297            currency: json["amount"]["currency"].to_string(),
298            payer_currency: json["amount"]["payer_currency"].to_string(),
299            trade_state: TradeState::from(json["trade_state"].as_str().unwrap()),
300        };
301        Ok(res.json())
302    }
303
304    fn refund_query(&mut self, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
305        let url = format!("/v3/refund/domestic/refunds/{out_refund_no}?sub_mchid={}", sub_mchid);
306        let sign = self.sign("GET", url.as_str(), "");
307        let mut http = br_http::Http::new();
308        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() {
309            Ok(e) => {
310                if e.has_key("message") {
311                    return Err(e["message"].to_string());
312                }
313
314                let mut refund_time = 0.0;
315                if e.has_key("success_time") {
316                    let success_time = e["success_time"].as_str().unwrap_or("").to_string();
317                    if !success_time.is_empty() {
318                        let datetime = DateTime::parse_from_rfc3339(success_time.as_str()).unwrap();
319                        refund_time = datetime.timestamp() as f64;
320                    }
321                }
322                let mut info = object! {
323                    refund_time:refund_time,
324                    refund_id:e["refund_id"].clone(),
325                    refund_no: e["out_refund_no"].clone(),
326                    order_no:e["out_trade_no"].clone(),
327                    refund_amount: e["amount"]["refund"].clone(),
328                    status:"",
329                    transaction_id:e["transaction_id"].clone(),
330                    amount_currency:e["amount"]["currency"].clone(),
331                    amount_total:e["amount"]["total"].clone(),
332                    note:e["user_received_account"].clone(),
333                };
334                info["status"] = match e["status"].as_str().unwrap() {
335                    "SUCCESS" => "已退款",
336                    "PROCESSING" => "退款中",
337                    _ => e["status"].as_str().unwrap(),
338                }.into();
339                Ok(info)
340            }
341            Err(e) => Err(e),
342        }
343    }
344    fn refund_notify(&mut self, nonce: &str, ciphertext: &str, associated_data: &str) -> Result<JsonValue, String> {
345        if self.apikey.is_empty() {
346            return Err("apikey 不能为空".to_string());
347        }
348        let key = Key::<Aes256Gcm>::from_slice(self.apikey.as_bytes());
349        let cipher = Aes256Gcm::new(key);
350        let nonce = Nonce::from_slice(nonce.as_bytes());
351        let data = match STANDARD.decode(ciphertext) {
352            Ok(e) => e,
353            Err(e) => return Err(format!("Invalid data received from API :{}", e))
354        };
355        // 组合 Payload(带 aad)
356        let payload = Payload {
357            msg: &data,
358            aad: associated_data.as_bytes(),
359        };
360
361        // 解密
362        let plaintext = match cipher.decrypt(nonce, payload) {
363            Ok(e) => e,
364            Err(e) => {
365                return Err(format!("解密 API:{}", e));
366            }
367        };
368        let rr = match String::from_utf8(plaintext) {
369            Ok(d) => d,
370            Err(_) => return Err("utf8 error".to_string())
371        };
372        let json = match json::parse(rr.as_str()) {
373            Ok(e) => e,
374            Err(_) => return Err("json error".to_string())
375        };
376        let res = RefundNotify {
377            out_trade_no: json["out_trade_no"].to_string(),
378            refund_no: json["out_refund_no"].to_string(),
379            refund_id: json["refund_id"].to_string(),
380            sp_mchid: json["sp_mchid"].as_str().unwrap().to_string(),
381            sub_mchid: json["sub_mchid"].as_str().unwrap().to_string(),
382            transaction_id: json["transaction_id"].as_str().unwrap().to_string(),
383            success_time: PayNotify::success_time(json["success_time"].as_str().unwrap_or("")),
384            total: json["amount"]["total"].as_f64().unwrap_or(0.0) / 100.0,
385            refund: json["amount"]["refund"].as_f64().unwrap_or(0.0) / 100.0,
386            payer_total: json["amount"]["payer_total"].as_f64().unwrap() / 100.0,
387            payer_refund: json["amount"]["payer_refund"].as_f64().unwrap() / 100.0,
388            status: RefundStatus::from(json["refund_status"].as_str().unwrap()),
389        };
390        Ok(res.json())
391    }
392
393    fn auth(&mut self, _code: &str) -> Result<JsonValue, String> {
394        todo!()
395    }
396
397    fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
398        let url = format!("/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/close");
399        let body = object! {
400            "sp_mchid"=> self.mchid.clone(),
401            "sub_mchid"=> sub_mchid
402        };
403        let sign = self.sign("POST", url.as_str(), body.to_string().as_str());
404        let mut http = br_http::Http::new();
405        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() {
406            Ok(_) => Ok(true.into()),
407            Err(e) => Err(e)
408        }
409    }
410}