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