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