br_pay/
ccbc.rs

1use std::collections::{HashMap};
2use chrono::Local;
3use json::{object, JsonValue};
4use log::{warn};
5use reqwest::header;
6use reqwest::tls::Version;
7use xmltree::Element;
8use crate::{PayMode, PayNotify, TradeState, TradeType, Types};
9
10/// 建设银行
11#[derive(Clone, Debug)]
12pub struct Ccbc {
13    /// 服务商APPID
14    pub appid: String,
15    /// 密钥
16    pub secret: String,
17    /// 登录密码
18    pub pass: String,
19    /// 银行商户号
20    pub mchid: String,
21    /// 微信商户号
22    pub sp_mchid: String,
23    /// 通知地址
24    pub notify_url: String,
25    /// 商户柜台代码
26    pub posid: String,
27    /// 分行代码
28    pub branchid: String,
29    /// 二级商户名称
30    pub smername: String,
31    /// 二级商户类别代码
32    pub smertypeid: String,
33    /// 二级商户类别名称
34    pub smertype: String,
35    /// 二级商户公钥
36    pub public_key: String,
37    pub client_ip: String,
38    /// 重试次
39    pub retry: usize,
40}
41
42impl Ccbc {
43    pub fn http(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
44        let mut mac = vec![];
45        let mut path = vec![];
46        let fields = ["MAC"];
47        for (key, value) in body.entries() {
48            if value.is_empty() && fields.contains(&key) {
49                continue;
50            }
51            if key != "PUB" {
52                path.push(format!("{key}={value}"));
53            }
54            mac.push(format!("{key}={value}"));
55        }
56
57
58        let mac_text = mac.join("&");
59        let path = path.join("&");
60        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
61        body.remove("PUB");
62        let mac = format!("{}&MAC={}", path, body["MAC"]);
63
64        let urls = format!("{url}&{mac}");
65
66
67        let mut map = HashMap::new();
68        for (key, value) in body.entries_mut() {
69            map.insert(key, value.to_string());
70        }
71
72        let http = match reqwest::blocking::Client::builder().build() {
73            Ok(e) => e,
74            Err(e) => return Err(e.to_string())
75        };
76
77        let res = match http.post(urls).json(&map).send() {
78            Ok(e) => e,
79            Err(e) => {
80                if self.retry > 2 {
81                    return Err(e.to_string());
82                }
83                self.retry += 1;
84                warn!("建行接口重试: {}", self.retry);
85                body.remove("MAC");
86                let res = self.http(url, body.clone())?;
87                return Ok(res);
88            }
89        };
90        let res = res.text().unwrap();
91        match json::parse(&res) {
92            Ok(e) => Ok(e),
93            Err(_) => Err(res)
94        }
95    }
96    fn escape_unicode(&mut self, s: &str) -> String {
97        s.chars().map(|c| {
98            if c.is_ascii() {
99                c.to_string()
100            } else {
101                format!("%u{:04X}", c as u32)
102            }
103        }).collect::<String>()
104    }
105    fn _unescape_unicode(&mut self, s: &str) -> String {
106        let mut output = String::new();
107        let mut chars = s.chars().peekable();
108        while let Some(c) = chars.next() {
109            if c == '%' && chars.peek() == Some(&'u') {
110                chars.next(); // consume 'u'
111                let codepoint: String = chars.by_ref().take(4).collect();
112                if let Ok(value) = u32::from_str_radix(&codepoint, 16) {
113                    if let Some(ch) = std::char::from_u32(value) {
114                        output.push(ch);
115                    }
116                }
117            } else {
118                output.push(c);
119            }
120        }
121        output
122    }
123    pub fn http_q(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
124        let mut path = vec![];
125        let fields = ["MAC"];
126        for (key, value) in body.entries() {
127            if value.is_empty() && fields.contains(&key) {
128                continue;
129            }
130            if key.contains("QUPWD") {
131                path.push(format!("{key}="));
132                continue;
133            }
134            path.push(format!("{key}={value}"));
135        }
136
137        let mac = path.join("&");
138        body["MAC"] = br_crypto::md5::encrypt_hex(mac.as_bytes()).into();
139
140        let mut map = vec![];
141        for (key, value) in body.entries() {
142            map.push((key, value.to_string()));
143        }
144        let mut headers = header::HeaderMap::new();
145        headers.insert("user-agent", header::HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"));
146
147        let http = match reqwest::blocking::Client::builder().default_headers(headers).build() {
148            Ok(e) => e,
149            Err(e) => return Err(e.to_string())
150        };
151
152        let res = match http.post(url).form(&map).send() {
153            Ok(e) => e,
154            Err(e) => {
155                if self.retry > 2 {
156                    return Err(e.to_string());
157                }
158                self.retry += 1;
159                warn!("建行查询接口重试: {}", self.retry);
160                body.remove("MAC");
161                let res = self.http_q(url, body)?;
162                return Ok(res);
163            }
164        };
165        let res = res.text().unwrap().trim().to_string();
166        match Element::parse(res.as_bytes()) {
167            Ok(e) => Ok(xml_element_to_json(&e)),
168            Err(e) => Err(e.to_string())
169        }
170    }
171}
172impl PayMode for Ccbc {
173    fn check(&mut self) -> Result<bool, String> {
174        todo!()
175    }
176
177    fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
178        todo!()
179    }
180
181    fn notify(&mut self, _data: JsonValue) -> Result<JsonValue, String> {
182        todo!()
183    }
184
185    fn config(&mut self) -> JsonValue {
186        todo!()
187    }
188
189
190    fn auth(&mut self, _code: &str) -> Result<JsonValue, String> {
191        todo!()
192    }
193
194    fn pay(&mut self, channel: &str, types: Types, _sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, sp_openid: &str) -> Result<JsonValue, String> {
195        if self.public_key.is_empty() {
196            return Err(String::from("Public key is empty"));
197        }
198        let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
199        let mut body = object! {
200            MERCHANTID:self.mchid.clone(),
201            POSID:self.posid.clone(),
202            BRANCHID:self.branchid.clone(),
203            ORDERID:out_trade_no,
204            PAYMENT:total_fee,
205            CURCODE:"01",
206            TXCODE:"530590",
207            REMARK1:"",
208            REMARK2:"",
209            TYPE:"1",
210            PUB:pubtext,
211            GATEWAY:"0",
212            CLIENTIP:self.client_ip.clone(),
213            REGINFO:self.escape_unicode(&self.smername.clone()),
214            PROINFO: self.escape_unicode(description),
215            REFERER:"",
216            TRADE_TYPE:"",
217            MAC:"",
218        };
219
220        let url = match channel {
221            "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
222            "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
223            _ => return Err(format!("Invalid channel: {channel}")),
224        };
225
226        match channel {
227            "wechat" => {
228                body["TRADE_TYPE"] = match types {
229                    Types::Jsapi => "JSAPI",
230                    Types::MiniJsapi => "MINIPRO",
231                    _ => return Err(format!("Invalid channel: {types:?}")),
232                }.into();
233                body["SUB_APPID"] = self.appid.clone().into();
234                body["SUB_OPENID"] = sp_openid.into();
235
236                //body["WX_CHANNELID"] = self.sp_mchid.clone().into();
237
238                //body["SMERID"] = sub_mchid.into();
239                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
240                //body["SMERTYPEID"] = self.smertypeid.clone().into();
241                //body["SMERTYPE"] = self.escape_unicode(self.smertype.clone().as_str()).into();
242
243                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
244                //body["SMERTYPEID"] = 1.into();
245                //body["SMERTYPE"] = self.escape_unicode("宾馆餐娱类").into();
246
247                //body["TRADECODE"] = "交易类型代码".into();
248                //body["TRADENAME"] = self.escape_unicode("消费").into();
249                //body["SMEPROTYPE"] = "商品类别代码".into();
250                //body["PRONAME"] = self.escape_unicode("商品").into();
251            }
252            "alipay" => {
253                body["TXCODE"] = "530591".into();
254                body["TRADE_TYPE"] = match types {
255                    Types::Jsapi => "JSAPI",
256                    Types::MiniJsapi => "JSAPI",
257                    _ => return Err(format!("Invalid channel: {types:?}")),
258                }.into();
259                body["USERID"] = sp_openid.into();
260            }
261            _ => return Err(format!("Invalid channel: {channel}")),
262        }
263        let res = self.http(url, body)?;
264        match types {
265            Types::Jsapi | Types::MiniJsapi => {
266                if res.has_key("PAYURL") {
267                    let url = res["PAYURL"].to_string();
268
269                    let http = match reqwest::blocking::Client::builder().min_tls_version(Version::TLS_1_1).max_tls_version(Version::TLS_1_1).danger_accept_invalid_certs(true).build() {
270                        Ok(e) => e,
271                        Err(e) => return Err(e.to_string())
272                    };
273                    let re = match http.post(url.as_str()).send() {
274                        Ok(e) => e,
275                        Err(e) => {
276                            return Err(e.to_string());
277                        }
278                    };
279                    let re = re.text().unwrap();
280                    let res = match json::parse(&re) {
281                        Ok(e) => e,
282                        Err(_) => return Err(re)
283                    };
284                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
285                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
286                    }
287                    Ok(res)
288                } else {
289                    Err(res.to_string())
290                }
291            }
292            _ => {
293                Ok(res)
294            }
295        }
296    }
297
298    fn micropay(&mut self, channel: &str, auth_code: &str, _sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, _org_openid: &str, _ip: &str) -> Result<JsonValue, String> {
299        let mut body = object! {
300            MERCHANTID:self.sp_mchid.clone(),
301            POSID:self.posid.clone(),
302            BRANCHID:self.branchid.clone(),
303            ccbParam:"",
304            TXCODE:"PAY100",
305            MERFLAG:"1",
306            ORDERID:out_trade_no,
307            QRCODE:auth_code,
308            AMOUNT:total_fee,
309            PROINFO:"商品名称",
310            REMARK1:description
311        };
312
313        let url = match channel {
314            "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
315            "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
316            _ => return Err(format!("Invalid channel: {channel}")),
317        };
318
319        match channel {
320            "wechat" => {
321                body["SUB_APPID"] = self.appid.clone().into();
322            }
323            "alipay" => {}
324            _ => return Err(format!("Invalid channel: {channel}")),
325        }
326
327        let res = self.http(url, body)?;
328        Ok(res)
329    }
330
331    fn close(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
332        Ok(true.into())
333    }
334
335    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
336        let today = Local::now().date_naive();
337        let date_str = today.format("%Y%m%d").to_string();
338        let body = object! {
339            MERCHANTID:self.mchid.clone(),
340            BRANCHID:self.branchid.clone(),
341            POSID:self.posid.clone(),
342            ORDERDATE:date_str,
343            BEGORDERTIME:"00:00:00",
344            ENDORDERTIME:"23:59:59",
345            ORDERID:out_trade_no,
346            QUPWD:self.pass.clone(),
347            TXCODE:"410408",
348            TYPE:"0",
349            KIND:"0",
350            STATUS:"1",
351            SEL_TYPE:"3",
352            PAGE:"1",
353            OPERATOR:"",
354            CHANNEL:"",
355            MAC:""
356        };
357        let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
358        if res["RETURN_CODE"] != "000000" {
359            if res["RETURN_MSG"].eq("流水记录不存在") {
360                let res = PayNotify {
361                    trade_type: TradeType::None,
362                    out_trade_no: "".to_string(),
363                    sp_mchid: "".to_string(),
364                    sub_mchid: "".to_string(),
365                    sp_appid: "".to_string(),
366                    transaction_id: "".to_string(),
367                    success_time: 0,
368                    sp_openid: "".to_string(),
369                    sub_openid: "".to_string(),
370                    total: 0.0,
371                    payer_total: 0.0,
372                    currency: "".to_string(),
373                    payer_currency: "".to_string(),
374                    trade_state: TradeState::NOTPAY,
375                };
376                return Ok(res.json());
377            }
378            return Err(res["RETURN_MSG"].to_string());
379        }
380        let data = res["QUERYORDER"].clone();
381        let res = PayNotify {
382            trade_type: TradeType::None,
383            out_trade_no: data["ORDERID"].to_string(),
384            sp_mchid: "".to_string(),
385            sub_mchid: sub_mchid.to_string(),
386            sp_appid: "".to_string(),
387            transaction_id: data["ORDERID"].to_string(),
388            success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
389            sp_openid: "".to_string(),
390            sub_openid: "".to_string(),
391            total: data["AMOUNT"].as_f64().unwrap_or(0.0),
392            currency: "CNY".to_string(),
393            payer_total: data["AMOUNT"].as_f64().unwrap_or(0.0),
394            payer_currency: "CNY".to_string(),
395            trade_state: TradeState::from(data["STATUS"].as_str().unwrap()),
396        };
397        Ok(res.json())
398    }
399
400    fn pay_micropay_query(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
401        todo!()
402    }
403
404    fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
405        todo!()
406    }
407
408    fn refund(&mut self, _sub_mchid: &str, _out_trade_no: &str, _transaction_id: &str, _out_refund_no: &str, _amount: f64, _total: f64, _currency: &str) -> Result<JsonValue, String> {
409        todo!()
410    }
411
412    fn micropay_refund(&mut self, _sub_mchid: &str, _out_trade_no: &str, _transaction_id: &str, _out_refund_no: &str, _amount: f64, _total: f64, _currency: &str, _refund_text: &str) -> Result<JsonValue, String> {
413        todo!()
414    }
415
416    fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
417        todo!()
418    }
419
420    fn refund_query(&mut self, _trade_no: &str, _out_refund_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
421        todo!()
422    }
423
424    fn incoming(&mut self, _business_code: &str, _contact_info: JsonValue, _subject_info: JsonValue, _business_info: JsonValue, _settlement_info: JsonValue, _bank_account_info: JsonValue) -> Result<JsonValue, String> {
425        todo!()
426    }
427}
428
429fn xml_element_to_json(elem: &Element) -> JsonValue {
430    let mut obj = object! {};
431
432    for child in &elem.children {
433        if let xmltree::XMLNode::Element(e) = child {
434            obj[e.name.clone()] = xml_element_to_json(e);
435        }
436    }
437
438    match elem.get_text() {
439        None => obj,
440        Some(text) => JsonValue::from(text.to_string()),
441    }
442}