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