br_pay/
ccbc.rs

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