br_pay/
ccbc.rs

1use base64::Engine;
2use base64::engine::general_purpose;
3use chrono::Local;
4use json::{object, JsonValue};
5use log::{debug, info, warn};
6use xmltree::Element;
7use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
8use cipher::{BlockEncryptMut, KeyInit};
9use cipher::block_padding::Pkcs7;
10use des::Des;
11use rand::{rng, Rng};
12
13
14/// 建设银行
15#[derive(Clone, Debug)]
16pub struct Ccbc {
17    /// 调试
18    pub debug: bool,
19    /// 微信小程序APPID
20    pub appid: String,
21    /// 微信公众号APPID
22    pub appid_subscribe: String,
23    /// 服务商号
24    pub sp_mchid: String,
25    /// 服务商账号ID
26    pub sp_user_id: String,
27    /// 服务商登录密码
28    pub sp_pass: String,
29    /// 通知地址
30    /// 服务商柜台代码
31    pub sp_posid: String,
32    pub notify_url: String,
33    /// 商户商柜台代码
34    pub sub_posid: String,
35    /// 分行代码
36    pub branchid: String,
37    /// 二级商户公钥
38    pub public_key: String,
39    pub client_ip: String,
40    /// 重试次
41    pub retry: usize,
42    /// 查询请求地址
43    pub query_url: String,
44}
45
46impl Ccbc {
47    pub fn http(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
48        let mut mac = vec![];
49        let mut path = vec![];
50        let fields = ["MAC"];
51        for (key, value) in body.entries() {
52            if value.is_empty() && fields.contains(&key) {
53                continue;
54            }
55            if key != "PUB" {
56                path.push(format!("{key}={value}"));
57            }
58            mac.push(format!("{key}={value}"));
59        }
60
61
62        let mac_text = mac.join("&");
63        let path = path.join("&");
64        if self.debug {
65            debug!("MERCHANTID=105000373721227&POSID=091864103&BRANCHID=530000000&ORDERID=96398&PAYMENT=0.01&CURCODE=01&TXCODE=530550&REMARK1=&REMARK2=&RETURNTYPE=3&TIMEOUT=&PUB=93bd9affe92c29dea12e5d79020111");
66            debug!("{mac_text:#}");
67        }
68        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
69        if self.debug {
70            debug!("{body:#}");
71        }
72        body.remove("PUB");
73        let mac = format!("{}&MAC={}", path, body["MAC"]);
74        if self.debug {
75            debug!("{mac:#}");
76        }
77        let urls = format!("{url}&{mac}");
78        if self.debug {
79            debug!("{urls:#}");
80        }
81        let mut http = br_reqwest::Client::new();
82
83        let res = http.post(urls.as_str()).raw_json(body.clone()).set_retry(3).send()?;
84        let res = res.body().to_string();
85        match json::parse(&res) {
86            Ok(e) => Ok(e),
87            Err(e) => Err(e.to_string())
88        }
89    }
90    pub fn http_alipay(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
91        let mut mac = vec![];
92        let mut path = vec![];
93        let fields = ["MAC", "SUBJECT", "AREA_INFO"];
94        for (key, value) in body.entries() {
95            if value.is_empty() && fields.contains(&key) {
96                continue;
97            }
98            if fields.contains(&key) {
99                continue;
100            }
101            if key != "PUB" {
102                path.push(format!("{key}={value}"));
103            }
104            mac.push(format!("{key}={value}"));
105        }
106
107
108        let mac_text = mac.join("&");
109        let path = path.join("&");
110        body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
111        body.remove("PUB");
112        let mac = format!("{}&MAC={}", path, body["MAC"]);
113
114        let urls = format!("{url}&{mac}");
115
116        let mut http = br_reqwest::Client::new();
117        let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
118            Ok(e) => e,
119            Err(e) => {
120                if self.retry > 2 {
121                    return Err(e.to_string());
122                }
123                self.retry += 1;
124                warn!("建行接口重试: {}", self.retry);
125                body.remove("MAC");
126                let res = self.http_alipay(url, body.clone())?;
127                return Ok(res);
128            }
129        };
130        let res = res.body().to_string();
131        match json::parse(&res) {
132            Ok(e) => Ok(e),
133            Err(_) => Err(res)
134        }
135    }
136    fn escape_unicode(&mut self, s: &str) -> String {
137        s.chars().map(|c| {
138            if c.is_ascii() {
139                c.to_string()
140            } else {
141                format!("%u{:04X}", c as u32)
142            }
143        }).collect::<String>()
144    }
145    fn _unescape_unicode(&mut self, s: &str) -> String {
146        let mut output = String::new();
147        let mut chars = s.chars().peekable();
148        while let Some(c) = chars.next() {
149            if c == '%' && chars.peek() == Some(&'u') {
150                chars.next(); // consume 'u'
151                let codepoint: String = chars.by_ref().take(4).collect();
152                if let Ok(value) = u32::from_str_radix(&codepoint, 16) {
153                    if let Some(ch) = std::char::from_u32(value) {
154                        output.push(ch);
155                    }
156                }
157            } else {
158                output.push(c);
159            }
160        }
161        output
162    }
163    pub fn http_q(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
164        let mut path = vec![];
165        let fields = ["MAC"];
166        for (key, value) in body.entries() {
167            if value.is_empty() && fields.contains(&key) {
168                continue;
169            }
170            if key.contains("QUPWD") {
171                path.push(format!("{key}="));
172                continue;
173            }
174            path.push(format!("{key}={value}"));
175        }
176
177        let mac = path.join("&");
178        body["MAC"] = br_crypto::md5::encrypt_hex(mac.as_bytes()).into();
179
180        let mut map = vec![];
181        for (key, value) in body.entries() {
182            map.push((key, value.to_string()));
183        }
184
185        let mut http = br_reqwest::Client::new();
186
187        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");
188
189        let res = match http.post(url).form_urlencoded(body.clone()).send() {
190            Ok(d) => d,
191            Err(e) => {
192                if self.retry > 2 {
193                    return Err(e.to_string());
194                }
195                self.retry += 1;
196                warn!("建行查询接口重试: {}", self.retry);
197                body.remove("MAC");
198                let res = self.http_q(url, body.clone())?;
199                return Ok(res);
200            }
201        };
202        let res = res.body().to_string().trim().to_string();
203        match Element::parse(res.as_bytes()) {
204            Ok(e) => Ok(xml_element_to_json(&e)),
205            Err(e) => Err(e.to_string())
206        }
207    }
208
209    pub fn http_ccb_param(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
210        let mer_info = format!("MERCHANTID={}&POSID={}&BRANCHID={}", body["MERCHANTID"], body["POSID"], body["BRANCHID"]);
211        let ccb_param = self.make_ccb_param(body.clone())?;
212        let urls = format!("{url}?{mer_info}&ccbParam={ccb_param}");
213        let mut http = br_reqwest::Client::new();
214        let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
215            Ok(e) => e,
216            Err(e) => {
217                if self.retry > 2 {
218                    return Err(e.to_string());
219                }
220                self.retry += 1;
221                warn!("建行接口重试: {}", self.retry);
222                body.remove("MAC");
223                let res = self.http_ccb_param(url, body.clone())?;
224                return Ok(res);
225            }
226        };
227        let res = res.body().to_string();
228        match json::parse(&res) {
229            Ok(e) => Ok(e),
230            Err(_) => Err(res)
231        }
232    }
233
234    pub fn make_ccb_param(&mut self, body: JsonValue) -> Result<String, String> {
235        if self.public_key.is_empty() || self.public_key.len() < 30 {
236            return Err(String::from("Public key is empty"));
237        }
238        let pubkey = self.public_key[self.public_key.len() - 30..].to_string();
239        if pubkey.len() < 8 {
240            return Err(String::from("Public key len 8"));
241        }
242        let pubkey = &pubkey[..8];
243        let mut mac = vec![];
244        let mut arr = vec![];
245        let mut params = vec![];
246        for (key, value) in body.entries() {
247            arr.push(key);
248            params.push(format!("{key}={value}"));
249        }
250        arr.sort();
251        for key in arr.iter() {
252            if body.has_key(key) && !body[key.to_string()].is_empty() {
253                mac.push(format!("{key}={}", body[key.to_string()]));
254            }
255        }
256        let ccb_param = mac.join("&");
257        let ccb_param = format!("{}20120315201809041004", ccb_param);
258        let ccb_param = br_crypto::md5::encrypt_hex(ccb_param.as_bytes());
259        let params = params.join("&");
260        let params = format!("{params}&SIGN={ccb_param}");
261        let bytes = self.utf16_bytes(params.as_str());
262        let encrypt = self.des_ecb_pkcs5_base64(&bytes, pubkey)?;
263        let encrypt = encrypt.replace("+", ",");
264        let url = br_crypto::encoding::urlencoding_encode(encrypt.as_str());
265        Ok(url)
266    }
267    fn utf16_bytes(&mut self, s: &str) -> Vec<u8> {
268        let mut out = Vec::with_capacity(2 + s.len() * 2);
269        out.push(0xFE);
270        out.push(0xFF);
271        for u in s.encode_utf16() {
272            out.push((u >> 8) as u8);
273            out.push((u & 0xFF) as u8);
274        }
275        out
276    }
277    fn des_ecb_pkcs5_base64(&mut self, plain: &[u8], key_str: &str) -> Result<String, &'static str> {
278        let kb = key_str.as_bytes();
279        if kb.len() < 8 {
280            return Err("DES key must be at least 8 bytes");
281        }
282        let mut key = [0u8; 8];
283        key.copy_from_slice(&kb[..8]);
284
285        let cipher = ecb::Encryptor::<Des>::new(&key.into());
286
287        // 预留 1 块给 PKCS#5/7 填充
288        let orig_len = plain.len();
289        let mut buf = vec![0u8; orig_len + 8];
290        buf[..orig_len].copy_from_slice(plain);
291
292        let ct = cipher.encrypt_padded_mut::<Pkcs7>(&mut buf, orig_len).map_err(|_| "encrypt error")?;
293
294        Ok(general_purpose::STANDARD.encode(ct))
295    }
296    fn https_cert(&mut self, text: &str) -> Result<JsonValue, String> {
297        let text = br_crypto::encoding::urlencoding_encode(text);
298        let mut http = br_reqwest::Client::new();
299        http.header("Connection", "close");
300        http.post(self.query_url.as_str()).form_urlencoded(object! {
301            requestXml:text
302        });
303        let res = http.send()?;
304        let ress = br_crypto::encoding::gb18030_to_utf8(res.stream())?;
305        let ress = ress.replace(r#"encoding="GB18030""#, r#"encoding="UTF-8""#);
306        match Element::parse(ress.trim().as_bytes()) {
307            Ok(e) => {
308                let json = xml_element_to_json(&e);
309                Ok(json)
310            }
311            Err(e) => Err(e.to_string())
312        }
313    }
314
315    fn get_xml_text(&mut self, name: &str) -> String {
316        let res = match name {
317            "5W1002.xml" => r#"<?xml version="1.0" encoding="GB2312" standalone="yes" ?>
318<TX>
319    <REQUEST_SN>{{REQUEST_SN}}</REQUEST_SN>
320    <CUST_ID>{{CUST_ID}}</CUST_ID>
321    <USER_ID>{{USER_ID}}</USER_ID>
322    <PASSWORD>{{PASSWORD}}</PASSWORD>
323    <TX_CODE>5W1002</TX_CODE>
324    <LANGUAGE>CN</LANGUAGE>
325    <TX_INFO>
326        <START>{{START}}</START>
327        <STARTHOUR>00</STARTHOUR>
328        <STARTMIN>00</STARTMIN>
329        <END>{{END}}</END>
330        <ENDHOUR>00</ENDHOUR>
331        <ENDMIN>00</ENDMIN>
332        <KIND>{{KIND}}</KIND>
333        <ORDER>{{ORDER}}</ORDER>
334        <ACCOUNT></ACCOUNT>
335        <DEXCEL>1</DEXCEL>
336        <MONEY></MONEY>
337        <NORDERBY>2</NORDERBY>
338        <PAGE>1</PAGE>
339        <POS_CODE>{{POS_CODE}}</POS_CODE>
340        <STATUS>{{STATUS}}</STATUS>
341        <Mrch_No>{{Mrch_No}}</Mrch_No>
342        <TXN_TPCD></TXN_TPCD>
343    </TX_INFO>
344</TX>"#,
345            "5W1003.xml" => r#"<?xml version="1.0" encoding="GB2312" standalone="yes" ?>
346<TX>
347    <REQUEST_SN>{{REQUEST_SN}}</REQUEST_SN>
348    <CUST_ID>{{CUST_ID}}</CUST_ID>
349    <USER_ID>{{USER_ID}}</USER_ID>
350    <PASSWORD>{{PASSWORD}}</PASSWORD>
351    <TX_CODE>5W1003</TX_CODE>
352    <LANGUAGE>CN</LANGUAGE>
353    <TX_INFO>
354        <START>{{START}}</START>
355        <STARTHOUR>00</STARTHOUR>
356        <STARTMIN>00</STARTMIN>
357        <END>{{END}}</END>
358        <ENDHOUR>00</ENDHOUR>
359        <ENDMIN>00</ENDMIN>
360        <KIND>{{KIND}}</KIND>
361        <ORDER>{{ORDER}}</ORDER>
362        <ACCOUNT></ACCOUNT>
363        <MONEY></MONEY>
364        <NORDERBY>2</NORDERBY>
365        <PAGE>1</PAGE>
366        <POS_CODE>{{POS_CODE}}</POS_CODE>
367        <STATUS>{{STATUS}}</STATUS>
368        <Mrch_No>{{Mrch_No}}</Mrch_No>
369        <TXN_TPCD></TXN_TPCD>
370    </TX_INFO>
371</TX>"#,
372            "5W1024.xml" => r#"<?xml version="1.0" encoding="GB2312" standalone="yes" ?>
373<TX>
374    <REQUEST_SN>{{REQUEST_SN}}</REQUEST_SN>
375    <CUST_ID>{{CUST_ID}}</CUST_ID>
376    <USER_ID>{{USER_ID}}</USER_ID>
377    <PASSWORD>{{PASSWORD}}</PASSWORD>
378    <TX_CODE>5W1024</TX_CODE>
379    <LANGUAGE>CN</LANGUAGE>
380    <TX_INFO>
381        <MONEY>{{MONEY}}</MONEY>
382        <ORDER>{{ORDER}}</ORDER>
383        <REFUND_CODE>{{REFUND_CODE}}</REFUND_CODE>
384        <Mrch_No>{{Mrch_No}}</Mrch_No>
385    </TX_INFO>
386    <SIGN_INFO></SIGN_INFO>
387    <SIGNCERT></SIGNCERT>
388</TX>"#,
389            _ => r#""#
390        };
391
392        let xml_text = res.as_bytes().to_vec();
393        let mut text = unsafe { String::from_utf8_unchecked(xml_text) };
394        let mut rng = rng();
395        let num: String = (0..14).map(|_| rng.random_range(0..10).to_string()).collect();
396        text = text.replace("{{REQUEST_SN}}", num.as_str());
397        text = text.replace("{{CUST_ID}}", self.sp_mchid.as_str());
398        text = text.replace("{{USER_ID}}", self.sp_user_id.as_str());
399        text = text.replace("{{PASSWORD}}", self.sp_pass.as_str());
400        text
401    }
402}
403impl PayMode for Ccbc {
404    fn check(&mut self) -> Result<bool, String> {
405        todo!()
406    }
407
408    fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
409        todo!()
410    }
411
412    fn config(&mut self) -> JsonValue {
413        todo!()
414    }
415
416
417    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> {
418        if self.public_key.is_empty() || self.public_key.len() < 30 {
419            return Err(String::from("Public key is empty"));
420        }
421        let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
422
423        let url = match channel {
424            "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
425            "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
426            _ => return Err(format!("Invalid channel: {channel}")),
427        };
428        let body = match channel {
429            "wechat" => {
430                let mut body = object! {
431                    MERCHANTID:sub_mchid,
432                    POSID:self.sub_posid.clone(),
433                    BRANCHID:self.branchid.clone(),
434                    ORDERID:out_trade_no,
435                    PAYMENT:total_fee,
436                    CURCODE:"01",
437                    TXCODE:"530590",
438                    REMARK1:"",
439                    REMARK2:"",
440                    TYPE:"1",
441                    PUB:pubtext,
442                    GATEWAY:"0",
443                    CLIENTIP:self.client_ip.clone(),
444                    REGINFO:"",
445                    PROINFO: self.escape_unicode(description),
446                    REFERER:"",
447                    TRADE_TYPE:"",
448                    SUB_APPID: "",
449                    SUB_OPENID:sp_openid,
450                    MAC:"",
451                };
452                body["TRADE_TYPE"] = match types {
453                    Types::Jsapi => {
454                        body["SUB_APPID"] = self.appid_subscribe.clone().into();
455                        "JSAPI"
456                    }
457                    Types::MiniJsapi => {
458                        body["SUB_APPID"] = self.appid.clone().into();
459                        "MINIPRO"
460                    }
461                    _ => return Err(format!("Invalid types: {types:?}")),
462                }.into();
463                body
464            }
465            "alipay" => {
466                let body = match types {
467                    Types::MiniJsapi => object! {
468                        MERCHANTID:sub_mchid,
469                        POSID:self.sub_posid.clone(),
470                        BRANCHID:self.branchid.clone(),
471                        ORDERID:out_trade_no,
472                        PAYMENT:total_fee,
473                        CURCODE:"01",
474                        TXCODE:"530591",
475                        TRADE_TYPE:"JSAPI",
476                        USERID:sp_openid,
477                        PUB:pubtext,
478                        MAC:""
479                    },
480                    Types::H5 => object! {
481                        BRANCHID:self.branchid.clone(),
482                        MERCHANTID:sub_mchid,
483                        POSID:self.sub_posid.clone(),
484                        TXCODE:"ZFBWAP",
485                        ORDERID:out_trade_no,
486                        AMOUNT:total_fee,
487                        TIMEOUT:"",
488                        REMARK1:"",
489                        REMARK2:"",
490                        PUB:pubtext,
491                        MAC:"",
492                        SUBJECT:description,
493                        AREA_INFO:""
494                    },
495                    Types::Jsapi => object! {
496                        MERCHANTID:sub_mchid,
497                        POSID:self.sub_posid.clone(),
498                        BRANCHID:self.branchid.clone(),
499                        ORDERID:out_trade_no,
500                        PAYMENT:total_fee,
501                        CURCODE:"01",
502                        TXCODE:"530550",
503                        REMARK1:"",
504                        REMARK2:"",
505                        RETURNTYPE:"3",
506                        TIMEOUT:"",
507                        PUB:pubtext,
508                        MAC:""
509                    },
510                    _ => return Err(format!("Invalid types: {types:?}")),
511                };
512                body
513            }
514            _ => return Err(format!("Invalid channel: {channel}")),
515        };
516        match (channel, types) {
517            ("wechat", Types::Jsapi | Types::MiniJsapi) => {
518                let res = self.http(url, body.clone())?;
519                if res.has_key("PAYURL") {
520                    let url = res["PAYURL"].to_string();
521                    let mut http = br_reqwest::Client::new();
522
523                    let re = match http.post(url.as_str()).send() {
524                        Ok(e) => e,
525                        Err(e) => {
526                            return Err(e.to_string());
527                        }
528                    };
529                    let re = re.body().to_string();
530                    let res = match json::parse(&re) {
531                        Ok(e) => e,
532                        Err(_) => return Err(re)
533                    };
534                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
535                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
536                    }
537                    Ok(res)
538                } else {
539                    Err(res.to_string())
540                }
541            }
542            ("alipay", Types::MiniJsapi) => {
543                let res = self.http(url, body)?;
544                if res.has_key("PAYURL") {
545                    let url = res["PAYURL"].to_string();
546                    let mut http = br_reqwest::Client::new();
547                    let re = match http.post(url.as_str()).send() {
548                        Ok(e) => e,
549                        Err(e) => {
550                            return Err(e.to_string());
551                        }
552                    };
553                    let re = re.body().to_string();
554                    let res = match json::parse(&re) {
555                        Ok(e) => e,
556                        Err(_) => return Err(re)
557                    };
558                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
559                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
560                    }
561                    Ok(res["jsapi"].clone())
562                } else {
563                    Err(res.to_string())
564                }
565            }
566            ("alipay", Types::H5) => {
567                let res = self.http_alipay(url, body)?;
568                if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
569                    return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
570                }
571                Ok(res["form_data"].clone())
572            }
573            ("alipay", Types::Jsapi) => {
574                let res = self.http(url, body)?;
575                if res.has_key("PAYURL") {
576                    let url = res["PAYURL"].to_string();
577                    if self.debug {
578                        debug!("{url:#}");
579                    }
580                    let mut http = br_reqwest::Client::new();
581
582                    let re = match http.post(url.as_str()).send() {
583                        Ok(e) => e,
584                        Err(e) => {
585                            return Err(e.to_string());
586                        }
587                    };
588                    let re = re.body().to_string();
589                    let res = match json::parse(&re) {
590                        Ok(e) => e,
591                        Err(_) => return Err(re)
592                    };
593                    if self.debug {
594                        debug!("{res:#}");
595                    }
596                    if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
597                        return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
598                    }
599                    let r = br_crypto::encoding::urlencoding_decode(res["QRURL"].as_str().unwrap());
600                    Ok(object! {
601                        url:r.clone()
602                    })
603                } else {
604                    Err(res.to_string())
605                }
606            }
607            _ => {
608                let res = self.http(url, body)?;
609                Ok(res)
610            }
611        }
612    }
613
614    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> {
615        let body = object! {
616            MERCHANTID:sub_mchid,
617            POSID:self.sub_posid.clone(),
618            BRANCHID:self.branchid.clone(),
619            MERFLAG:"1",
620            TERMNO1:"",
621            TERMNO2:"",
622            ORDERID:out_trade_no,
623            QRCODE:auth_code,
624            AMOUNT:total_fee,
625            TXCODE:"PAY100",
626            PROINFO:description,
627            REMARK1:"",
628            REMARK2:"",
629            SMERID:"",SMERNAME:"",SMERTYPEID:"",SMERTYPE:"",TRADECODE:"",TRADENAME:"",SMEPROTYPE:"",PRONAME:""
630        };
631        let url = "https://ibsbjstar.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY";
632        let res = self.http_ccb_param(url, body)?;
633
634        match res["RESULT"].as_str().unwrap_or("N") {
635            "Y" => {
636                Ok(self.pay_micropay_query(out_trade_no, sub_mchid, channel)?)
637            }
638            "N" => {
639                Err(res["ERRMSG"].to_string())
640            }
641            "U" => {
642                Err(res.to_string())
643            }
644            "Q" => {
645                let res = PayNotify {
646                    trade_type: TradeType::MICROPAY,
647                    out_trade_no: out_trade_no.to_string(),
648                    sp_mchid: self.sp_mchid.clone(),
649                    sub_mchid: sub_mchid.to_string(),
650                    sp_appid: "".to_string(),
651                    transaction_id: res["TRACEID"].to_string(),
652                    success_time: 0,
653                    sp_openid: "".to_string(),
654                    sub_openid: "".to_string(),
655                    total: total_fee,
656                    payer_total: total_fee,
657                    currency: "CNY".to_string(),
658                    payer_currency: "CNY".to_string(),
659                    trade_state: TradeState::NOTPAY,
660                };
661                Ok(res.json())
662            }
663            _ => {
664                Err(res.to_string())
665            }
666        }
667    }
668
669    fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
670        let today = Local::now().date_naive();
671        let date_str = today.format("%Y%m%d").to_string();
672        let order_date = &out_trade_no[0..8];
673        let kind = if date_str == order_date {
674            0
675        } else {
676            1
677        };
678
679        let mut text = self.get_xml_text("5W1002.xml");
680
681        text = text.replace("{{START}}", order_date);
682        text = text.replace("{{END}}", order_date);
683        text = text.replace("{{KIND}}", kind.to_string().as_str());
684        text = text.replace("{{ORDER}}", out_trade_no);
685
686        text = text.replace("{{POS_CODE}}", self.sp_posid.as_str());
687        text = text.replace("{{STATUS}}", "1");
688        text = text.replace("{{Mrch_No}}", sub_mchid);
689
690        let res = self.https_cert(&text)?;
691        if res.has_key("RETURN_CODE") && res["RETURN_CODE"].ne("000000") {
692            if res["RETURN_MSG"].eq("流水记录不存在") {
693                return Ok(true.into());
694            }
695            return Err(res["RETURN_MSG"].to_string());
696        }
697        let trade_state = match res["TX_INFO"]["LIST"]["ORDER_STATUS"].as_str().unwrap_or("") {
698            "0" => TradeState::PAYERROR,
699            "1" => TradeState::SUCCESS,
700            "2" | "5" => TradeState::USERPAYING,
701            "3" | "4" => TradeState::REFUND,
702            _ => return Err("未知状态".to_string()),
703        };
704        match trade_state {
705            TradeState::SUCCESS => Ok(false.into()),
706            TradeState::REFUND => Ok(false.into()),
707            TradeState::NOTPAY => Ok(true.into()),
708            TradeState::CLOSED => Ok(true.into()),
709            TradeState::REVOKED => Ok(true.into()),
710            TradeState::USERPAYING => Ok(false.into()),
711            TradeState::PAYERROR => Ok(true.into()),
712            TradeState::None => Ok(true.into()),
713        }
714    }
715
716    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
717        let today = Local::now().date_naive();
718        let date_str = today.format("%Y%m%d").to_string();
719        let order_date = &out_trade_no[0..8];
720        let kind = if date_str == order_date {
721            0
722        } else {
723            1
724        };
725
726        let mut text = self.get_xml_text("5W1002.xml");
727
728        text = text.replace("{{START}}", order_date);
729        text = text.replace("{{END}}", order_date);
730        text = text.replace("{{KIND}}", kind.to_string().as_str());
731        text = text.replace("{{ORDER}}", out_trade_no);
732
733        text = text.replace("{{POS_CODE}}", "");
734        text = text.replace("{{STATUS}}", "1");
735        text = text.replace("{{Mrch_No}}", sub_mchid);
736        info!("支付查询请求: {text}");
737        let res = self.https_cert(&text)?;
738        info!("支付查询响应: {res}");
739        if res.has_key("RETURN_CODE") && res["RETURN_CODE"].ne("000000") {
740            if res["RETURN_MSG"].eq("流水记录不存在") {
741                let ttt = PayNotify {
742                    trade_type: TradeType::None,
743                    out_trade_no: out_trade_no.to_string(),
744                    sp_mchid: self.sp_mchid.to_string(),
745                    sub_mchid: sub_mchid.to_string(),
746                    sp_appid: "".to_string(),
747                    transaction_id: "".to_string(),
748                    success_time: 0,
749                    sp_openid: "".to_string(),
750                    sub_openid: "".to_string(),
751                    total: 0.0,
752                    payer_total: 0.0,
753                    currency: "CNY".to_string(),
754                    payer_currency: "CNY".to_string(),
755                    trade_state: TradeState::NOTPAY,
756                };
757                return Ok(ttt.json());
758            }
759            return Err(res["RETURN_MSG"].to_string());
760        }
761        let trade_state = match res["TX_INFO"]["LIST"]["ORDER_STATUS"].as_str().unwrap_or("") {
762            "0" => TradeState::PAYERROR,
763            "1" => TradeState::SUCCESS,
764            "2" | "5" => TradeState::USERPAYING,
765            "3" | "4" => TradeState::REFUND,
766            _ => return Err("未知状态".to_string()),
767        };
768        let data = res["TX_INFO"]["LIST"].clone();
769        let res = match trade_state {
770            TradeState::SUCCESS => {
771                PayNotify {
772                    trade_type: TradeType::None,
773                    out_trade_no: out_trade_no.to_string(),
774                    sp_mchid: self.sp_mchid.to_string(),
775                    sub_mchid: sub_mchid.to_string(),
776                    sp_appid: "".to_string(),
777                    transaction_id: data["OriOvrlsttnEV_Trck_No"].to_string(),
778                    success_time: PayNotify::datetime_to_timestamp(data["TRAN_DATE"].as_str().unwrap_or(""), "%Y-%m-%d %H:%M:%S"),
779                    sp_openid: "".to_string(),
780                    sub_openid: "".to_string(),
781                    total: data["Orig_Amt"].to_string().parse::<f64>().unwrap_or(0.0),
782                    payer_total: data["Txn_ClrgAmt"].to_string().parse::<f64>().unwrap_or(0.0),
783                    currency: "CNY".to_string(),
784                    payer_currency: "CNY".to_string(),
785                    trade_state,
786                }
787            }
788            _ => PayNotify {
789                trade_type: TradeType::None,
790                out_trade_no: out_trade_no.to_string(),
791                sp_mchid: self.sp_mchid.to_string(),
792                sub_mchid: sub_mchid.to_string(),
793                sp_appid: "".to_string(),
794                transaction_id: res["ORDER"].to_string(),
795                success_time: 0,
796                sp_openid: "".to_string(),
797                sub_openid: "".to_string(),
798                total: 0.0,
799                payer_total: 0.0,
800                currency: "CNY".to_string(),
801                payer_currency: "CNY".to_string(),
802                trade_state,
803            }
804        };
805        Ok(res.json())
806    }
807
808    fn pay_micropay_query(&mut self, out_trade_no: &str, sub_mchid: &str, channel: &str) -> Result<JsonValue, String> {
809        let crcode_type = match channel {
810            "wechat" => "2",
811            "alipay" => "3",
812            _ => "5"
813        };
814
815        let body = object! {
816            MERCHANTID:sub_mchid,
817            POSID:self.sub_posid.clone(),
818            BRANCHID:self.branchid.clone(),
819            TXCODE:"PAY102",
820            MERFLAG:"1",
821            TERMNO1:"",
822            TERMNO2:"",
823            ORDERID:out_trade_no,
824            QRYTIME:"1",
825            QRCODETYPE:crcode_type,
826            QRCODE:"",
827            REMARK1:"",
828            REMARK2:"",
829        };
830        let url = "https://ibsbjstar.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY";
831        let res = self.http_ccb_param(url, body)?;
832
833        match res["RESULT"].as_str().unwrap_or("N") {
834            "Y" => {
835                let now = Local::now();
836                let formatted = now.format("%Y%m%d%H%M%S").to_string();
837                let transaction_id = match channel {
838                    "wechat" => res["WECHAT_NO"].to_string(),
839                    "alipay" => res["ZFB_NO"].to_string(),
840                    _ => "".to_string()
841                };
842                let res = PayNotify {
843                    trade_type: TradeType::MICROPAY,
844                    out_trade_no: out_trade_no.to_string(),
845                    sp_mchid: self.sp_mchid.clone(),
846                    sub_mchid: sub_mchid.to_string(),
847                    sp_appid: "".to_string(),
848                    transaction_id,
849                    success_time: PayNotify::datetime_to_timestamp(formatted.as_str(), "%Y%m%d%H%M%S"),
850                    sp_openid: "".to_string(),
851                    sub_openid: "".to_string(),
852                    total: res["AMOUNT"].to_string().parse::<f64>().unwrap_or(0.0),
853                    payer_total: res["AMOUNT"].to_string().parse::<f64>().unwrap_or(0.0),
854                    currency: "CNY".to_string(),
855                    payer_currency: "CNY".to_string(),
856                    trade_state: TradeState::SUCCESS,
857                };
858                Ok(res.json())
859            }
860            "N" => {
861                let crcode_type = match channel {
862                    "wechat" => res["WECHAT_STATE"].to_string(),
863                    "alipay" => res["ZFB_STATE"].to_string(),
864                    _ => "".to_string()
865                };
866                let res = PayNotify {
867                    trade_type: TradeType::MICROPAY,
868                    out_trade_no: out_trade_no.to_string(),
869                    sp_mchid: self.sp_mchid.clone(),
870                    sub_mchid: sub_mchid.to_string(),
871                    sp_appid: "".to_string(),
872                    transaction_id: "".to_string(),
873                    success_time: 0,
874                    sp_openid: "".to_string(),
875                    sub_openid: "".to_string(),
876                    total: 0.0,
877                    currency: "CNY".to_string(),
878                    payer_total: 0.0,
879                    payer_currency: "CNY".to_string(),
880                    trade_state: TradeState::from(crcode_type.as_str()),
881                };
882                Ok(res.json())
883            }
884            "U" => {
885                Err(res.to_string())
886            }
887            "Q" => {
888                let res = PayNotify {
889                    trade_type: TradeType::MICROPAY,
890                    out_trade_no: out_trade_no.to_string(),
891                    sp_mchid: self.sp_mchid.clone(),
892                    sub_mchid: sub_mchid.to_string(),
893                    sp_appid: "".to_string(),
894                    transaction_id: "".to_string(),
895                    success_time: 0,
896                    sp_openid: "".to_string(),
897                    sub_openid: "".to_string(),
898                    total: 0.0,
899                    payer_total: 0.0,
900                    currency: "CNY".to_string(),
901                    payer_currency: "CNY".to_string(),
902                    trade_state: TradeState::NOTPAY,
903                };
904                Ok(res.json())
905            }
906            _ => {
907                Err(res.to_string())
908            }
909        }
910    }
911
912    fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
913        Err("暂未开通".to_string())
914    }
915
916    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> {
917        let mut text = self.get_xml_text("5W1024.xml");
918        text = text.replace("{{MONEY}}", amount.to_string().as_str());
919        text = text.replace("{{ORDER}}", out_trade_no);
920        text = text.replace("{{REFUND_CODE}}", out_refund_no);
921        text = text.replace("{{Mrch_No}}", sub_mchid);
922        let res = self.https_cert(&text)?;
923        if res.has_key("RETURN_CODE") && res["RETURN_CODE"].ne("000000") {
924            return Err(res["RETURN_MSG"].to_string());
925        }
926        if !res.has_key("TX_INFO") {
927            return Err(res["RETURN_MSG"].to_string());
928        }
929        let timestamp = Local::now().timestamp();
930        let info = object! {
931                    refund_id:out_refund_no,
932                    status:"已退款",
933                    success_time:timestamp,
934                    out_refund_no: out_refund_no,
935                };
936        Ok(info)
937    }
938
939    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> {
940        Err("暂未开通".to_string())
941    }
942
943    fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
944        Err("暂未开通".to_string())
945    }
946
947    fn refund_query(&mut self, trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
948        let today = Local::now().date_naive();
949        let date_str = today.format("%Y%m%d").to_string();
950        let order_date = &out_refund_no[0..8];
951        let kind = if date_str == order_date {
952            0
953        } else {
954            1
955        };
956
957        let mut text = self.get_xml_text("5W1003.xml");
958
959        text = text.replace("{{START}}", order_date);
960        text = text.replace("{{END}}", order_date);
961        text = text.replace("{{KIND}}", kind.to_string().as_str());
962        text = text.replace("{{ORDER}}", out_refund_no);
963
964        text = text.replace("{{POS_CODE}}", self.sp_posid.as_str());
965        text = text.replace("{{STATUS}}", "1");
966        text = text.replace("{{Mrch_No}}", sub_mchid);
967
968        let res = self.https_cert(&text)?;
969
970        if res.has_key("RETURN_MSG") {
971            if res["RETURN_MSG"].eq("流水记录不存在") {
972                let res = RefundNotify {
973                    out_trade_no: trade_no.to_string(),
974                    refund_no: out_refund_no.to_string(),
975                    sp_mchid: self.sp_mchid.to_string(),
976                    sub_mchid: sub_mchid.to_string(),
977                    transaction_id: "".to_string(),
978                    refund_id: "".to_string(),
979                    success_time: 0,
980                    total: 0.0,
981                    refund: 0.0,
982                    payer_total: 0.0,
983                    payer_refund: 0.0,
984                    status: RefundStatus::None,
985                };
986                return Ok(res.json());
987            }
988            return Err(res["RETURN_MSG"].to_string());
989        }
990        Err("暂未开通".to_string())
991        //
992        //let today = Local::now().date_naive();
993        //let date_str = today.format("%Y%m%d").to_string();
994        //let body = object! {
995        //    MERCHANTID:sub_mchid,
996        //    BRANCHID:self.branchid.clone(),
997        //    POSID:self.sub_posid.clone(),
998        //    ORDERDATE:date_str,
999        //    BEGORDERTIME:"00:00:00",
1000        //    ENDORDERTIME:"23:59:59",
1001        //    ORDERID:out_refund_no,
1002        //    QUPWD:self.sub_pass.clone(),
1003        //    TXCODE:"410408",
1004        //    TYPE:"1",
1005        //    KIND:"0",
1006        //    STATUS:"1",
1007        //    SEL_TYPE:"3",
1008        //    PAGE:"1",
1009        //    OPERATOR:"",
1010        //    CHANNEL:"",
1011        //    MAC:""
1012        //};
1013        //let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
1014        //if res["RETURN_CODE"] != "000000" {
1015        //    if res["RETURN_MSG"].eq("流水记录不存在") {
1016        //        let res = PayNotify {
1017        //            trade_type: TradeType::None,
1018        //            out_trade_no: "".to_string(),
1019        //            sp_mchid: "".to_string(),
1020        //            sub_mchid: "".to_string(),
1021        //            sp_appid: "".to_string(),
1022        //            transaction_id: "".to_string(),
1023        //            success_time: 0,
1024        //            sp_openid: "".to_string(),
1025        //            sub_openid: "".to_string(),
1026        //            total: 0.0,
1027        //            payer_total: 0.0,
1028        //            currency: "".to_string(),
1029        //            payer_currency: "".to_string(),
1030        //            trade_state: TradeState::NOTPAY,
1031        //        };
1032        //        return Ok(res.json());
1033        //    }
1034        //    return Err(res["RETURN_MSG"].to_string());
1035        //}
1036        //let data = res["QUERYORDER"].clone();
1037        //let res = RefundNotify {
1038        //    out_trade_no: trade_no.to_string(),
1039        //    refund_no: out_refund_no.to_string(),
1040        //    sp_mchid: "".to_string(),
1041        //    sub_mchid: sub_mchid.to_string(),
1042        //    transaction_id: data["ORDERID"].to_string(),
1043        //    refund_id: data["refund_id"].to_string(),
1044        //    success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
1045        //    total: data["AMOUNT"].as_f64().unwrap_or(0.0),
1046        //    payer_total: data["amount"]["total"].to_string().parse::<f64>().unwrap(),
1047        //    refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
1048        //    payer_refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
1049        //    status: RefundStatus::from(data["STATUS"].as_str().unwrap()),
1050        //};
1051        //
1052        //Ok(res.json())
1053    }
1054
1055    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> {
1056        todo!()
1057    }
1058}
1059
1060fn xml_element_to_json(elem: &Element) -> JsonValue {
1061    let mut obj = object! {};
1062    for child in &elem.children {
1063        if let xmltree::XMLNode::Element(e) = child {
1064            obj[e.name.clone()] = xml_element_to_json(e);
1065        }
1066    }
1067    match elem.get_text() {
1068        None => obj,
1069        Some(text) => JsonValue::from(text.to_string()),
1070    }
1071}