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