br_pay/
ccbc.rs

1use chrono::Local;
2use json::{object, JsonValue};
3use log::{warn};
4use xmltree::Element;
5use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
6
7/// 建设银行
8#[derive(Clone, Debug)]
9pub struct Ccbc {
10    /// 微信小程序APPID
11    pub appid: String,
12    /// 微信公众号APPID
13    pub appid_subscribe: String,
14    /// 登录密码
15    pub pass: String,
16    /// 银行服务商号
17    pub sp_mchid: String,
18    /// 通知地址
19    pub notify_url: String,
20    /// 商户柜台代码
21    pub posid: String,
22    /// 分行代码
23    pub branchid: String,
24    /// 二级商户公钥
25    pub public_key: String,
26    pub client_ip: String,
27    /// 微信服务商号
28    pub wechat_mchid: String,
29    /// 重试次
30    pub retry: usize,
31}
32
33impl Ccbc {
34    pub fn http(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
35        let mut mac = vec![];
36        let mut path = vec![];
37        let fields = ["MAC", "SUBJECT", "AREA_INFO"];
38        for (key, value) in body.entries() {
39            if value.is_empty() && fields.contains(&key) {
40                continue;
41            }
42            if fields.contains(&key) {
43                continue;
44            }
45            if key != "PUB" {
46                path.push(format!("{key}={value}"));
47            }
48            mac.push(format!("{key}={value}"));
49        }
50
51
52        let mac_text = mac.join("&");
53        let path = path.join("&");
54        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
55        body.remove("PUB");
56        let mac = format!("{}&MAC={}", path, body["MAC"]);
57
58        let urls = format!("{url}&{mac}");
59
60        let mut http = br_reqwest::Client::new();
61
62        let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
63            Ok(e) => e,
64            Err(e) => {
65                if self.retry > 2 {
66                    return Err(e.to_string());
67                }
68                self.retry += 1;
69                warn!("建行接口重试: {}", self.retry);
70                body.remove("MAC");
71                let res = self.http(url, body.clone())?;
72                return Ok(res);
73            }
74        };
75        let res = res.body().to_string();
76        match json::parse(&res) {
77            Ok(e) => Ok(e),
78            Err(_) => Err(res)
79        }
80    }
81    fn escape_unicode(&mut self, s: &str) -> String {
82        s.chars().map(|c| {
83            if c.is_ascii() {
84                c.to_string()
85            } else {
86                format!("%u{:04X}", c as u32)
87            }
88        }).collect::<String>()
89    }
90    fn _unescape_unicode(&mut self, s: &str) -> String {
91        let mut output = String::new();
92        let mut chars = s.chars().peekable();
93        while let Some(c) = chars.next() {
94            if c == '%' && chars.peek() == Some(&'u') {
95                chars.next(); // consume 'u'
96                let codepoint: String = chars.by_ref().take(4).collect();
97                if let Ok(value) = u32::from_str_radix(&codepoint, 16) {
98                    if let Some(ch) = std::char::from_u32(value) {
99                        output.push(ch);
100                    }
101                }
102            } else {
103                output.push(c);
104            }
105        }
106        output
107    }
108    pub fn http_q(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
109        let mut path = vec![];
110        let fields = ["MAC"];
111        for (key, value) in body.entries() {
112            if value.is_empty() && fields.contains(&key) {
113                continue;
114            }
115            if key.contains("QUPWD") {
116                path.push(format!("{key}="));
117                continue;
118            }
119            path.push(format!("{key}={value}"));
120        }
121
122        let mac = path.join("&");
123        body["MAC"] = br_crypto::md5::encrypt_hex(mac.as_bytes()).into();
124
125        let mut map = vec![];
126        for (key, value) in body.entries() {
127            map.push((key, value.to_string()));
128        }
129
130        let mut http = br_reqwest::Client::new();
131
132        http.header("user-agent", "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");
133
134        let res = match http.post(url).form_urlencoded(body.clone()).send() {
135            Ok(d) => d,
136            Err(e) => {
137                if self.retry > 2 {
138                    return Err(e.to_string());
139                }
140                self.retry += 1;
141                warn!("建行查询接口重试: {}", self.retry);
142                body.remove("MAC");
143                let res = self.http_q(url, body.clone())?;
144                return Ok(res);
145            }
146        };
147        let res = res.body().to_string().trim().to_string();
148        match Element::parse(res.as_bytes()) {
149            Ok(e) => Ok(xml_element_to_json(&e)),
150            Err(e) => Err(e.to_string())
151        }
152    }
153}
154impl PayMode for Ccbc {
155    fn check(&mut self) -> Result<bool, String> {
156        todo!()
157    }
158
159    fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
160        todo!()
161    }
162
163    fn config(&mut self) -> JsonValue {
164        todo!()
165    }
166
167
168    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> {
169        if self.public_key.is_empty() || self.public_key.len() < 30 {
170            return Err(String::from("Public key is empty"));
171        }
172        let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
173
174        let url = match channel {
175            "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
176            "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
177            _ => return Err(format!("Invalid channel: {channel}")),
178        };
179
180        let body = match channel {
181            "wechat" => {
182                let mut body = object! {
183                    MERCHANTID:sub_mchid,
184            POSID:self.posid.clone(),
185            BRANCHID:self.branchid.clone(),
186            ORDERID:out_trade_no,
187            PAYMENT:total_fee,
188            CURCODE:"01",
189            TXCODE:"530590",
190            REMARK1:"",
191            REMARK2:"",
192            TYPE:"1",
193            PUB:pubtext,
194            GATEWAY:"0",
195            CLIENTIP:self.client_ip.clone(),
196            REGINFO:"",
197            PROINFO: self.escape_unicode(description),
198            REFERER:"",
199            TRADE_TYPE:"",
200                    SUB_APPID: "",
201                    SUB_OPENID:sp_openid,
202            MAC:"",
203        };
204                body["TRADE_TYPE"] = match types {
205                    Types::Jsapi => {
206                        body["SUB_APPID"] = self.appid_subscribe.clone().into();
207                        "JSAPI"
208                    }
209                    Types::MiniJsapi => {
210                        body["SUB_APPID"] = self.appid.clone().into();
211                        "MINIPRO"
212                    }
213                    _ => return Err(format!("Invalid types: {types:?}")),
214                }.into();
215
216                //body["WX_CHANNELID"] = self.sp_mchid.clone().into();
217
218                //body["SMERID"] = sub_mchid.into();
219                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
220                //body["SMERTYPEID"] = self.smertypeid.clone().into();
221                //body["SMERTYPE"] = self.escape_unicode(self.smertype.clone().as_str()).into();
222
223                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
224                //body["SMERTYPEID"] = 1.into();
225                //body["SMERTYPE"] = self.escape_unicode("宾馆餐娱类").into();
226
227                //body["TRADECODE"] = "交易类型代码".into();
228                //body["TRADENAME"] = self.escape_unicode("消费").into();
229                //body["SMEPROTYPE"] = "商品类别代码".into();
230                //body["PRONAME"] = self.escape_unicode("商品").into();
231                body
232            }
233            "alipay" => {
234                let body = match types {
235                    Types::Jsapi | Types::MiniJsapi => object! {
236                        MERCHANTID:sub_mchid,
237                        POSID:self.posid.clone(),
238                        BRANCHID:self.branchid.clone(),
239                        ORDERID:out_trade_no,
240                        PAYMENT:total_fee,
241                        CURCODE:"01",
242                        TXCODE:"530591",
243                        TRADE_TYPE:"JSAPI",
244                        USERID:sp_openid,
245                        PUB:pubtext,
246                        MAC:""
247                    },
248                    Types::H5 => object! {
249                        BRANCHID:self.branchid.clone(),
250                        MERCHANTID:sub_mchid,
251                        POSID:self.posid.clone(),
252                        TXCODE:"ZFBWAP",
253                        ORDERID:out_trade_no,
254                        AMOUNT:total_fee,
255                        TIMEOUT:"",
256                        REMARK1:"",
257                        REMARK2:"",
258                        PUB:pubtext,
259                        MAC:"",
260                        SUBJECT:description,
261                        AREA_INFO:""
262                    },
263                    _ => return Err(format!("Invalid types: {types:?}")),
264                };
265                body
266            }
267            _ => return Err(format!("Invalid channel: {channel}")),
268        };
269        let res = self.http(url, body)?;
270        match (channel, types) {
271            ("wechat", Types::Jsapi | Types::MiniJsapi) => {
272                if res.has_key("PAYURL") {
273                    let url = res["PAYURL"].to_string();
274                    let mut http = br_reqwest::Client::new();
275
276                    let re = match http.post(url.as_str()).send() {
277                        Ok(e) => e,
278                        Err(e) => {
279                            return Err(e.to_string());
280                        }
281                    };
282                    let re = re.body().to_string();
283                    let res = match json::parse(&re) {
284                        Ok(e) => e,
285                        Err(_) => return Err(re)
286                    };
287                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
288                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
289                    }
290                    Ok(res)
291                } else {
292                    Err(res.to_string())
293                }
294            }
295            ("alipay", Types::MiniJsapi) => {
296                if res.has_key("PAYURL") {
297                    let url = res["PAYURL"].to_string();
298                    let mut http = br_reqwest::Client::new();
299
300                    let re = match http.post(url.as_str()).send() {
301                        Ok(e) => e,
302                        Err(e) => {
303                            return Err(e.to_string());
304                        }
305                    };
306                    let re = re.body().to_string();
307                    let res = match json::parse(&re) {
308                        Ok(e) => e,
309                        Err(_) => return Err(re)
310                    };
311                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
312                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
313                    }
314                    Ok(res)
315                } else {
316                    Err(res.to_string())
317                }
318            }
319            ("alipay", Types::H5) => {
320                if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
321                    return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
322                }
323                Ok(res["form_data"].clone())
324            }
325            _ => {
326                Ok(res)
327            }
328        }
329    }
330
331    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> {
332        let mut body = object! {
333            MERCHANTID:self.sp_mchid.clone(),
334            POSID:self.posid.clone(),
335            BRANCHID:self.branchid.clone(),
336            ccbParam:"",
337            TXCODE:"PAY100",
338            MERFLAG:"1",
339            ORDERID:out_trade_no,
340            QRCODE:auth_code,
341            AMOUNT:total_fee,
342            PROINFO:"商品名称",
343            REMARK1:description
344        };
345
346        let url = match channel {
347            "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
348            "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
349            _ => return Err(format!("Invalid channel: {channel}")),
350        };
351
352        match channel {
353            "wechat" => {
354                body["SUB_APPID"] = self.appid.clone().into();
355            }
356            "alipay" => {}
357            _ => return Err(format!("Invalid channel: {channel}")),
358        }
359
360        let res = self.http(url, body)?;
361        Ok(res)
362    }
363
364    fn close(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
365        Ok(true.into())
366    }
367
368    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
369        let today = Local::now().date_naive();
370        let date_str = today.format("%Y%m%d").to_string();
371        let body = object! {
372            MERCHANTID:sub_mchid,
373            BRANCHID:self.branchid.clone(),
374            POSID:self.posid.clone(),
375            ORDERDATE:date_str,
376            BEGORDERTIME:"00:00:00",
377            ENDORDERTIME:"23:59:59",
378            ORDERID:out_trade_no,
379            QUPWD:self.pass.clone(),
380            TXCODE:"410408",
381            TYPE:"0",
382            KIND:"0",
383            STATUS:"1",
384            SEL_TYPE:"3",
385            PAGE:"1",
386            OPERATOR:"",
387            CHANNEL:"",
388            MAC:""
389        };
390        let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
391        if res["RETURN_CODE"] != "000000" {
392            if res["RETURN_MSG"].eq("流水记录不存在") {
393                let res = PayNotify {
394                    trade_type: TradeType::None,
395                    out_trade_no: "".to_string(),
396                    sp_mchid: "".to_string(),
397                    sub_mchid: "".to_string(),
398                    sp_appid: "".to_string(),
399                    transaction_id: "".to_string(),
400                    success_time: 0,
401                    sp_openid: "".to_string(),
402                    sub_openid: "".to_string(),
403                    total: 0.0,
404                    payer_total: 0.0,
405                    currency: "".to_string(),
406                    payer_currency: "".to_string(),
407                    trade_state: TradeState::NOTPAY,
408                };
409                return Ok(res.json());
410            }
411            return Err(res["RETURN_MSG"].to_string());
412        }
413        let data = res["QUERYORDER"].clone();
414        let res = PayNotify {
415            trade_type: TradeType::None,
416            out_trade_no: data["ORDERID"].to_string(),
417            sp_mchid: "".to_string(),
418            sub_mchid: sub_mchid.to_string(),
419            sp_appid: "".to_string(),
420            transaction_id: data["ORDERID"].to_string(),
421            success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
422            sp_openid: "".to_string(),
423            sub_openid: "".to_string(),
424            total: data["AMOUNT"].as_f64().unwrap_or(0.0),
425            currency: "CNY".to_string(),
426            payer_total: data["AMOUNT"].as_f64().unwrap_or(0.0),
427            payer_currency: "CNY".to_string(),
428            trade_state: TradeState::from(data["STATUS"].as_str().unwrap()),
429        };
430        Ok(res.json())
431    }
432
433    fn pay_micropay_query(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
434        Err("暂未开通".to_string())
435    }
436
437    fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
438        Err("暂未开通".to_string())
439    }
440
441    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> {
442        //
443        //let mut http = br_reqwest::Client::new();
444        //http.set_cert_p12("br-pay/examples/1822131-1.pfx", "1822131");
445        //let txt = fs::read_to_string("br-pay/examples/jsyh/退款/reund.xml").unwrap();
446        //http.post("https://merchant.ccb.com").form_urlencoded(object! {
447        //    requestXml:txt
448        //});
449        //let res = http.send()?;
450        Err("暂未开通".to_string())
451    }
452
453    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> {
454        Err("暂未开通".to_string())
455    }
456
457    fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
458        Err("暂未开通".to_string())
459    }
460
461    fn refund_query(&mut self, trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
462        let today = Local::now().date_naive();
463        let date_str = today.format("%Y%m%d").to_string();
464        let body = object! {
465            MERCHANTID:sub_mchid,
466            BRANCHID:self.branchid.clone(),
467            POSID:self.posid.clone(),
468            ORDERDATE:date_str,
469            BEGORDERTIME:"00:00:00",
470            ENDORDERTIME:"23:59:59",
471            ORDERID:out_refund_no,
472            QUPWD:self.pass.clone(),
473            TXCODE:"410408",
474            TYPE:"1",
475            KIND:"0",
476            STATUS:"1",
477            SEL_TYPE:"3",
478            PAGE:"1",
479            OPERATOR:"",
480            CHANNEL:"",
481            MAC:""
482        };
483        let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
484        if res["RETURN_CODE"] != "000000" {
485            if res["RETURN_MSG"].eq("流水记录不存在") {
486                let res = PayNotify {
487                    trade_type: TradeType::None,
488                    out_trade_no: "".to_string(),
489                    sp_mchid: "".to_string(),
490                    sub_mchid: "".to_string(),
491                    sp_appid: "".to_string(),
492                    transaction_id: "".to_string(),
493                    success_time: 0,
494                    sp_openid: "".to_string(),
495                    sub_openid: "".to_string(),
496                    total: 0.0,
497                    payer_total: 0.0,
498                    currency: "".to_string(),
499                    payer_currency: "".to_string(),
500                    trade_state: TradeState::NOTPAY,
501                };
502                return Ok(res.json());
503            }
504            return Err(res["RETURN_MSG"].to_string());
505        }
506        println!("refund_query: {res:#}");
507        let data = res["QUERYORDER"].clone();
508
509        let res = RefundNotify {
510            out_trade_no: trade_no.to_string(),
511            refund_no: out_refund_no.to_string(),
512            sp_mchid: "".to_string(),
513            sub_mchid: sub_mchid.to_string(),
514            transaction_id: data["ORDERID"].to_string(),
515            refund_id: data["refund_id"].to_string(),
516            success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
517            total: data["AMOUNT"].as_f64().unwrap_or(0.0),
518            payer_total: data["amount"]["total"].to_string().parse::<f64>().unwrap(),
519            refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
520            payer_refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
521            status: RefundStatus::from(data["STATUS"].as_str().unwrap()),
522        };
523
524        Ok(res.json())
525    }
526
527    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> {
528        todo!()
529    }
530}
531
532fn xml_element_to_json(elem: &Element) -> JsonValue {
533    let mut obj = object! {};
534
535    for child in &elem.children {
536        if let xmltree::XMLNode::Element(e) = child {
537            obj[e.name.clone()] = xml_element_to_json(e);
538        }
539    }
540
541    match elem.get_text() {
542        None => obj,
543        Some(text) => JsonValue::from(text.to_string()),
544    }
545}