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