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
154    pub fn http_ccb_param(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
155
156        println!("url: {url}");
157        println!("public_key: {}",self.public_key);
158        
159
160        let mut mac = vec![];
161        let mut path = vec![];
162        let fields = ["MAC", "ccbParam"];
163        for (key, value) in body.entries() {
164            if value.is_empty() && fields.contains(&key) {
165                continue;
166            }
167            if fields.contains(&key) {
168                continue;
169            }
170            if key != "PUB" {
171                path.push(format!("{key}={value}"));
172            }
173            mac.push(format!("{key}={value}"));
174        }
175
176
177        let mac_text = mac.join("&");
178        println!("mac: {mac_text}");
179        // MERCHANTID=105000373721227&POSID=091864103&BRANCHID=530000000&MERFLAG=1&TERMNO1=&TERMNO2=&ORDERID=202508041754271606105000373721227&QRCODE=131318016110834439&AMOUNT=0.01&TXCODE=PAY100&PROINFO=单人&REMARK1=&REMARK2=&SUB_APPID=wx2408d13eefae86
180        // MERCHANTID=105910100190000&POSID=000000000&BRANCHID=610000000&MERFLAG=1&TERMNO1=&TERMNO2=&ORDERID=202508041754271433105000373721227&QRCODE=134737690209713400&AMOUNT=0.01&TXCODE=PAY100&PROINFO=&REMARK1=&REMARK2=&SMERID=&SMERNAME=&SMERTYPEID=&SMERTYPE=&TRADECODE=&TRADENAME=&SMEPROTYPE=&PRONAME=
181        let path = path.join("&");
182        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
183        body.remove("PUB");
184        let mac = format!("{}&MAC={}", path, body["MAC"]);
185
186        let urls = format!("{url}&{mac}");
187
188        let mut http = br_reqwest::Client::new();
189
190        let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
191            Ok(e) => e,
192            Err(e) => {
193                if self.retry > 2 {
194                    return Err(e.to_string());
195                }
196                self.retry += 1;
197                warn!("建行接口重试: {}", self.retry);
198                body.remove("MAC");
199                let res = self.http(url, body.clone())?;
200                return Ok(res);
201            }
202        };
203        let res = res.body().to_string();
204        match json::parse(&res) {
205            Ok(e) => Ok(e),
206            Err(_) => Err(res)
207        }
208    }
209
210}
211impl PayMode for Ccbc {
212    fn check(&mut self) -> Result<bool, String> {
213        todo!()
214    }
215
216    fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
217        todo!()
218    }
219
220    fn config(&mut self) -> JsonValue {
221        todo!()
222    }
223
224
225    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> {
226        if self.public_key.is_empty() || self.public_key.len() < 30 {
227            return Err(String::from("Public key is empty"));
228        }
229        let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
230
231        let url = match channel {
232            "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
233            "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
234            _ => return Err(format!("Invalid channel: {channel}")),
235        };
236
237        let body = match channel {
238            "wechat" => {
239                let mut body = object! {
240                    MERCHANTID:sub_mchid,
241            POSID:self.posid.clone(),
242            BRANCHID:self.branchid.clone(),
243            ORDERID:out_trade_no,
244            PAYMENT:total_fee,
245            CURCODE:"01",
246            TXCODE:"530590",
247            REMARK1:"",
248            REMARK2:"",
249            TYPE:"1",
250            PUB:pubtext,
251            GATEWAY:"0",
252            CLIENTIP:self.client_ip.clone(),
253            REGINFO:"",
254            PROINFO: self.escape_unicode(description),
255            REFERER:"",
256            TRADE_TYPE:"",
257                    SUB_APPID: "",
258                    SUB_OPENID:sp_openid,
259            MAC:"",
260        };
261                body["TRADE_TYPE"] = match types {
262                    Types::Jsapi => {
263                        body["SUB_APPID"] = self.appid_subscribe.clone().into();
264                        "JSAPI"
265                    }
266                    Types::MiniJsapi => {
267                        body["SUB_APPID"] = self.appid.clone().into();
268                        "MINIPRO"
269                    }
270                    _ => return Err(format!("Invalid types: {types:?}")),
271                }.into();
272
273                //body["WX_CHANNELID"] = self.sp_mchid.clone().into();
274
275                //body["SMERID"] = sub_mchid.into();
276                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
277                //body["SMERTYPEID"] = self.smertypeid.clone().into();
278                //body["SMERTYPE"] = self.escape_unicode(self.smertype.clone().as_str()).into();
279
280                //body["SMERNAME"] = self.escape_unicode(self.smername.clone().as_str()).into();
281                //body["SMERTYPEID"] = 1.into();
282                //body["SMERTYPE"] = self.escape_unicode("宾馆餐娱类").into();
283
284                //body["TRADECODE"] = "交易类型代码".into();
285                //body["TRADENAME"] = self.escape_unicode("消费").into();
286                //body["SMEPROTYPE"] = "商品类别代码".into();
287                //body["PRONAME"] = self.escape_unicode("商品").into();
288                body
289            }
290            "alipay" => {
291                let body = match types {
292                    Types::Jsapi | Types::MiniJsapi => object! {
293                        MERCHANTID:sub_mchid,
294                        POSID:self.posid.clone(),
295                        BRANCHID:self.branchid.clone(),
296                        ORDERID:out_trade_no,
297                        PAYMENT:total_fee,
298                        CURCODE:"01",
299                        TXCODE:"530591",
300                        TRADE_TYPE:"JSAPI",
301                        USERID:sp_openid,
302                        PUB:pubtext,
303                        MAC:""
304                    },
305                    Types::H5 => object! {
306                        BRANCHID:self.branchid.clone(),
307                        MERCHANTID:sub_mchid,
308                        POSID:self.posid.clone(),
309                        TXCODE:"ZFBWAP",
310                        ORDERID:out_trade_no,
311                        AMOUNT:total_fee,
312                        TIMEOUT:"",
313                        REMARK1:"",
314                        REMARK2:"",
315                        PUB:pubtext,
316                        MAC:"",
317                        SUBJECT:description,
318                        AREA_INFO:""
319                    },
320                    _ => return Err(format!("Invalid types: {types:?}")),
321                };
322                body
323            }
324            _ => return Err(format!("Invalid channel: {channel}")),
325        };
326        let res = self.http(url, body)?;
327        match (channel, types) {
328            ("wechat", Types::Jsapi | Types::MiniJsapi) => {
329                if res.has_key("PAYURL") {
330                    let url = res["PAYURL"].to_string();
331                    let mut http = br_reqwest::Client::new();
332
333                    let re = match http.post(url.as_str()).send() {
334                        Ok(e) => e,
335                        Err(e) => {
336                            return Err(e.to_string());
337                        }
338                    };
339                    let re = re.body().to_string();
340                    let res = match json::parse(&re) {
341                        Ok(e) => e,
342                        Err(_) => return Err(re)
343                    };
344                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
345                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
346                    }
347                    Ok(res)
348                } else {
349                    Err(res.to_string())
350                }
351            }
352            ("alipay", Types::MiniJsapi) => {
353                if res.has_key("PAYURL") {
354                    let url = res["PAYURL"].to_string();
355                    let mut http = br_reqwest::Client::new();
356
357                    let re = match http.post(url.as_str()).send() {
358                        Ok(e) => e,
359                        Err(e) => {
360                            return Err(e.to_string());
361                        }
362                    };
363                    let re = re.body().to_string();
364                    let res = match json::parse(&re) {
365                        Ok(e) => e,
366                        Err(_) => return Err(re)
367                    };
368                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
369                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
370                    }
371                    Ok(res)
372                } else {
373                    Err(res.to_string())
374                }
375            }
376            ("alipay", Types::H5) => {
377                if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
378                    return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
379                }
380                Ok(res["form_data"].clone())
381            }
382            _ => {
383                Ok(res)
384            }
385        }
386    }
387
388    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> {
389        let mut body = object! {
390            MERCHANTID:sub_mchid,
391            POSID:self.posid.clone(),
392            BRANCHID:self.branchid.clone(),
393            ccbParam:"",
394            MERFLAG:"1",
395            TERMNO1:"",
396            TERMNO2:"",
397            ORDERID:out_trade_no,
398            QRCODE:auth_code,
399            AMOUNT:total_fee,
400            TXCODE:"PAY100",
401            PROINFO:description,
402            REMARK1:"",
403            REMARK2:"",
404            SUB_APPID:"",
405        };
406        let url = "https://ebanking2.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY";
407
408        match channel {
409            "wechat" => {
410                body["SUB_APPID"] = self.appid.clone().into();
411            }
412            "alipay" => {}
413            _ => return Err(format!("Invalid channel: {channel}")),
414        }
415        let res = self.http_ccb_param(url, body)?;
416        Ok(res)
417    }
418
419    fn close(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
420        Ok(true.into())
421    }
422
423    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
424        let today = Local::now().date_naive();
425        let date_str = today.format("%Y%m%d").to_string();
426        let body = object! {
427            MERCHANTID:sub_mchid,
428            BRANCHID:self.branchid.clone(),
429            POSID:self.posid.clone(),
430            ORDERDATE:date_str,
431            BEGORDERTIME:"00:00:00",
432            ENDORDERTIME:"23:59:59",
433            ORDERID:out_trade_no,
434            QUPWD:self.pass.clone(),
435            TXCODE:"410408",
436            TYPE:"0",
437            KIND:"0",
438            STATUS:"1",
439            SEL_TYPE:"3",
440            PAGE:"1",
441            OPERATOR:"",
442            CHANNEL:"",
443            MAC:""
444        };
445        let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
446        if res["RETURN_CODE"] != "000000" {
447            if res["RETURN_MSG"].eq("流水记录不存在") {
448                let res = PayNotify {
449                    trade_type: TradeType::None,
450                    out_trade_no: "".to_string(),
451                    sp_mchid: "".to_string(),
452                    sub_mchid: "".to_string(),
453                    sp_appid: "".to_string(),
454                    transaction_id: "".to_string(),
455                    success_time: 0,
456                    sp_openid: "".to_string(),
457                    sub_openid: "".to_string(),
458                    total: 0.0,
459                    payer_total: 0.0,
460                    currency: "".to_string(),
461                    payer_currency: "".to_string(),
462                    trade_state: TradeState::NOTPAY,
463                };
464                return Ok(res.json());
465            }
466            return Err(res["RETURN_MSG"].to_string());
467        }
468        let data = res["QUERYORDER"].clone();
469        let res = PayNotify {
470            trade_type: TradeType::None,
471            out_trade_no: data["ORDERID"].to_string(),
472            sp_mchid: "".to_string(),
473            sub_mchid: sub_mchid.to_string(),
474            sp_appid: "".to_string(),
475            transaction_id: data["ORDERID"].to_string(),
476            success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
477            sp_openid: "".to_string(),
478            sub_openid: "".to_string(),
479            total: data["AMOUNT"].as_f64().unwrap_or(0.0),
480            currency: "CNY".to_string(),
481            payer_total: data["AMOUNT"].as_f64().unwrap_or(0.0),
482            payer_currency: "CNY".to_string(),
483            trade_state: TradeState::from(data["STATUS"].as_str().unwrap()),
484        };
485        Ok(res.json())
486    }
487
488    fn pay_micropay_query(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
489        Err("暂未开通".to_string())
490    }
491
492    fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
493        Err("暂未开通".to_string())
494    }
495
496    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> {
497        //
498        //let mut http = br_reqwest::Client::new();
499        //http.set_cert_p12("br-pay/examples/1822131-1.pfx", "1822131");
500        //let txt = fs::read_to_string("br-pay/examples/jsyh/退款/reund.xml").unwrap();
501        //http.post("https://merchant.ccb.com").form_urlencoded(object! {
502        //    requestXml:txt
503        //});
504        //let res = http.send()?;
505        Err("暂未开通".to_string())
506    }
507
508    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> {
509        Err("暂未开通".to_string())
510    }
511
512    fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
513        Err("暂未开通".to_string())
514    }
515
516    fn refund_query(&mut self, trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
517        let today = Local::now().date_naive();
518        let date_str = today.format("%Y%m%d").to_string();
519        let body = object! {
520            MERCHANTID:sub_mchid,
521            BRANCHID:self.branchid.clone(),
522            POSID:self.posid.clone(),
523            ORDERDATE:date_str,
524            BEGORDERTIME:"00:00:00",
525            ENDORDERTIME:"23:59:59",
526            ORDERID:out_refund_no,
527            QUPWD:self.pass.clone(),
528            TXCODE:"410408",
529            TYPE:"1",
530            KIND:"0",
531            STATUS:"1",
532            SEL_TYPE:"3",
533            PAGE:"1",
534            OPERATOR:"",
535            CHANNEL:"",
536            MAC:""
537        };
538        let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
539        if res["RETURN_CODE"] != "000000" {
540            if res["RETURN_MSG"].eq("流水记录不存在") {
541                let res = PayNotify {
542                    trade_type: TradeType::None,
543                    out_trade_no: "".to_string(),
544                    sp_mchid: "".to_string(),
545                    sub_mchid: "".to_string(),
546                    sp_appid: "".to_string(),
547                    transaction_id: "".to_string(),
548                    success_time: 0,
549                    sp_openid: "".to_string(),
550                    sub_openid: "".to_string(),
551                    total: 0.0,
552                    payer_total: 0.0,
553                    currency: "".to_string(),
554                    payer_currency: "".to_string(),
555                    trade_state: TradeState::NOTPAY,
556                };
557                return Ok(res.json());
558            }
559            return Err(res["RETURN_MSG"].to_string());
560        }
561        println!("refund_query: {res:#}");
562        let data = res["QUERYORDER"].clone();
563
564        let res = RefundNotify {
565            out_trade_no: trade_no.to_string(),
566            refund_no: out_refund_no.to_string(),
567            sp_mchid: "".to_string(),
568            sub_mchid: sub_mchid.to_string(),
569            transaction_id: data["ORDERID"].to_string(),
570            refund_id: data["refund_id"].to_string(),
571            success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
572            total: data["AMOUNT"].as_f64().unwrap_or(0.0),
573            payer_total: data["amount"]["total"].to_string().parse::<f64>().unwrap(),
574            refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
575            payer_refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
576            status: RefundStatus::from(data["STATUS"].as_str().unwrap()),
577        };
578
579        Ok(res.json())
580    }
581
582    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> {
583        todo!()
584    }
585}
586
587fn xml_element_to_json(elem: &Element) -> JsonValue {
588    let mut obj = object! {};
589
590    for child in &elem.children {
591        if let xmltree::XMLNode::Element(e) = child {
592            obj[e.name.clone()] = xml_element_to_json(e);
593        }
594    }
595
596    match elem.get_text() {
597        None => obj,
598        Some(text) => JsonValue::from(text.to_string()),
599    }
600}