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