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 let r = br_crypto::encoding::urlencoding_decode(res["QRURL"].as_str().unwrap());
480 Ok(object! {
481 url:r.clone()
482 })
483 } else {
484 Err(res.to_string())
485 }
486 }
487 _ => {
488 let res = self.http(url, body)?;
489 Ok(res)
490 }
491 }
492 }
493
494 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> {
495 let mut body = object! {
496 MERCHANTID:sub_mchid,
497 POSID:self.posid.clone(),
498 BRANCHID:self.branchid.clone(),
499 ccbParam:"",
500 MERFLAG:"1",
501 TERMNO1:"",
502 TERMNO2:"",
503 ORDERID:out_trade_no,
504 QRCODE:auth_code,
505 AMOUNT:total_fee,
506 TXCODE:"PAY100",
507 PROINFO:description,
508 REMARK1:"",
509 REMARK2:"",
510 SUB_APPID:"",
511 };
512 let url = "https://ebanking2.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY";
513
514 match channel {
515 "wechat" => {
516 body["SUB_APPID"] = self.appid.clone().into();
517 }
518 "alipay" => {}
519 _ => return Err(format!("Invalid channel: {channel}")),
520 }
521 let res = self.http_ccb_param(url, body)?;
522 Ok(res)
523 }
524
525 fn close(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
526 Ok(true.into())
527 }
528
529 fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
530 let today = Local::now().date_naive();
531 let date_str = today.format("%Y%m%d").to_string();
532
533 let order_date = &out_trade_no[0..8];
534
535 let kind = if date_str == order_date {
536 0
537 } else {
538 1
539 };
540
541 let body = object! {
542 MERCHANTID:sub_mchid,
543 BRANCHID:self.branchid.clone(),
544 POSID:self.posid.clone(),
545 ORDERDATE:order_date,
546 BEGORDERTIME:"00:00:00",
547 ENDORDERTIME:"23:59:59",
548 ORDERID:out_trade_no,
549 QUPWD:self.pass.clone(),
550 TXCODE:"410408",
551 TYPE:"0",
552 KIND:kind,
553 STATUS:"1",
554 SEL_TYPE:"3",
555 PAGE:"1",
556 OPERATOR:"",
557 CHANNEL:"",
558 MAC:""
559 };
560 let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
561 if res["RETURN_CODE"] != "000000" {
562 if res["RETURN_MSG"].eq("流水记录不存在") {
563 let res = PayNotify {
564 trade_type: TradeType::None,
565 out_trade_no: "".to_string(),
566 sp_mchid: "".to_string(),
567 sub_mchid: "".to_string(),
568 sp_appid: "".to_string(),
569 transaction_id: "".to_string(),
570 success_time: 0,
571 sp_openid: "".to_string(),
572 sub_openid: "".to_string(),
573 total: 0.0,
574 payer_total: 0.0,
575 currency: "".to_string(),
576 payer_currency: "".to_string(),
577 trade_state: TradeState::NOTPAY,
578 };
579 return Ok(res.json());
580 }
581 return Err(res["RETURN_MSG"].to_string());
582 }
583 let data = res["QUERYORDER"].clone();
584 let res = PayNotify {
585 trade_type: TradeType::None,
586 out_trade_no: data["ORDERID"].to_string(),
587 sp_mchid: "".to_string(),
588 sub_mchid: sub_mchid.to_string(),
589 sp_appid: "".to_string(),
590 transaction_id: data["ORDERID"].to_string(),
591 success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
592 sp_openid: "".to_string(),
593 sub_openid: "".to_string(),
594 total: data["AMOUNT"].as_f64().unwrap_or(0.0),
595 currency: "CNY".to_string(),
596 payer_total: data["AMOUNT"].as_f64().unwrap_or(0.0),
597 payer_currency: "CNY".to_string(),
598 trade_state: TradeState::from(data["STATUS"].as_str().unwrap()),
599 };
600 Ok(res.json())
601 }
602
603 fn pay_micropay_query(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
604 Err("暂未开通".to_string())
605 }
606
607 fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
608 Err("暂未开通".to_string())
609 }
610
611 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> {
612 Err("暂未开通".to_string())
621 }
622
623 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> {
624 Err("暂未开通".to_string())
625 }
626
627 fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
628 Err("暂未开通".to_string())
629 }
630
631 fn refund_query(&mut self, trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
632 let today = Local::now().date_naive();
633 let date_str = today.format("%Y%m%d").to_string();
634 let body = object! {
635 MERCHANTID:sub_mchid,
636 BRANCHID:self.branchid.clone(),
637 POSID:self.posid.clone(),
638 ORDERDATE:date_str,
639 BEGORDERTIME:"00:00:00",
640 ENDORDERTIME:"23:59:59",
641 ORDERID:out_refund_no,
642 QUPWD:self.pass.clone(),
643 TXCODE:"410408",
644 TYPE:"1",
645 KIND:"0",
646 STATUS:"1",
647 SEL_TYPE:"3",
648 PAGE:"1",
649 OPERATOR:"",
650 CHANNEL:"",
651 MAC:""
652 };
653 let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
654 if res["RETURN_CODE"] != "000000" {
655 if res["RETURN_MSG"].eq("流水记录不存在") {
656 let res = PayNotify {
657 trade_type: TradeType::None,
658 out_trade_no: "".to_string(),
659 sp_mchid: "".to_string(),
660 sub_mchid: "".to_string(),
661 sp_appid: "".to_string(),
662 transaction_id: "".to_string(),
663 success_time: 0,
664 sp_openid: "".to_string(),
665 sub_openid: "".to_string(),
666 total: 0.0,
667 payer_total: 0.0,
668 currency: "".to_string(),
669 payer_currency: "".to_string(),
670 trade_state: TradeState::NOTPAY,
671 };
672 return Ok(res.json());
673 }
674 return Err(res["RETURN_MSG"].to_string());
675 }
676 println!("refund_query: {res:#}");
677 let data = res["QUERYORDER"].clone();
678
679 let res = RefundNotify {
680 out_trade_no: trade_no.to_string(),
681 refund_no: out_refund_no.to_string(),
682 sp_mchid: "".to_string(),
683 sub_mchid: sub_mchid.to_string(),
684 transaction_id: data["ORDERID"].to_string(),
685 refund_id: data["refund_id"].to_string(),
686 success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
687 total: data["AMOUNT"].as_f64().unwrap_or(0.0),
688 payer_total: data["amount"]["total"].to_string().parse::<f64>().unwrap(),
689 refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
690 payer_refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
691 status: RefundStatus::from(data["STATUS"].as_str().unwrap()),
692 };
693
694 Ok(res.json())
695 }
696
697 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> {
698 todo!()
699 }
700}
701
702fn xml_element_to_json(elem: &Element) -> JsonValue {
703 let mut obj = object! {};
704
705 for child in &elem.children {
706 if let xmltree::XMLNode::Element(e) = child {
707 obj[e.name.clone()] = xml_element_to_json(e);
708 }
709 }
710
711 match elem.get_text() {
712 None => obj,
713 Some(text) => JsonValue::from(text.to_string()),
714 }
715}