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