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