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}
154impl PayMode for Ccbc {
155 fn check(&mut self) -> Result<bool, String> {
156 todo!()
157 }
158
159 fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
160 todo!()
161 }
162
163 fn config(&mut self) -> JsonValue {
164 todo!()
165 }
166
167
168 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> {
169 if self.public_key.is_empty() || self.public_key.len() < 30 {
170 return Err(String::from("Public key is empty"));
171 }
172 let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
173
174 let url = match channel {
175 "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
176 "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
177 _ => return Err(format!("Invalid channel: {channel}")),
178 };
179
180 let body = match channel {
181 "wechat" => {
182 let mut body = object! {
183 MERCHANTID:sub_mchid,
184 POSID:self.posid.clone(),
185 BRANCHID:self.branchid.clone(),
186 ORDERID:out_trade_no,
187 PAYMENT:total_fee,
188 CURCODE:"01",
189 TXCODE:"530590",
190 REMARK1:"",
191 REMARK2:"",
192 TYPE:"1",
193 PUB:pubtext,
194 GATEWAY:"0",
195 CLIENTIP:self.client_ip.clone(),
196 REGINFO:"",
197 PROINFO: self.escape_unicode(description),
198 REFERER:"",
199 TRADE_TYPE:"",
200 SUB_APPID: "",
201 SUB_OPENID:sp_openid,
202 MAC:"",
203 };
204 body["TRADE_TYPE"] = match types {
205 Types::Jsapi => {
206 body["SUB_APPID"] = self.appid_subscribe.clone().into();
207 "JSAPI"
208 }
209 Types::MiniJsapi => {
210 body["SUB_APPID"] = self.appid.clone().into();
211 "MINIPRO"
212 }
213 _ => return Err(format!("Invalid types: {types:?}")),
214 }.into();
215
216 body
232 }
233 "alipay" => {
234 let body = match types {
235 Types::Jsapi | Types::MiniJsapi => object! {
236 MERCHANTID:sub_mchid,
237 POSID:self.posid.clone(),
238 BRANCHID:self.branchid.clone(),
239 ORDERID:out_trade_no,
240 PAYMENT:total_fee,
241 CURCODE:"01",
242 TXCODE:"530591",
243 TRADE_TYPE:"JSAPI",
244 USERID:sp_openid,
245 PUB:pubtext,
246 MAC:""
247 },
248 Types::H5 => object! {
249 BRANCHID:self.branchid.clone(),
250 MERCHANTID:sub_mchid,
251 POSID:self.posid.clone(),
252 TXCODE:"ZFBWAP",
253 ORDERID:out_trade_no,
254 AMOUNT:total_fee,
255 TIMEOUT:"",
256 REMARK1:"",
257 REMARK2:"",
258 PUB:pubtext,
259 MAC:"",
260 SUBJECT:description,
261 AREA_INFO:""
262 },
263 _ => return Err(format!("Invalid types: {types:?}")),
264 };
265 body
266 }
267 _ => return Err(format!("Invalid channel: {channel}")),
268 };
269 let res = self.http(url, body)?;
270 match (channel, types) {
271 ("wechat", Types::Jsapi | Types::MiniJsapi) => {
272 if res.has_key("PAYURL") {
273 let url = res["PAYURL"].to_string();
274 let mut http = br_reqwest::Client::new();
275
276 let re = match http.post(url.as_str()).send() {
277 Ok(e) => e,
278 Err(e) => {
279 return Err(e.to_string());
280 }
281 };
282 let re = re.body().to_string();
283 let res = match json::parse(&re) {
284 Ok(e) => e,
285 Err(_) => return Err(re)
286 };
287 if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
288 return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
289 }
290 Ok(res)
291 } else {
292 Err(res.to_string())
293 }
294 }
295 ("alipay", Types::MiniJsapi) => {
296 if res.has_key("PAYURL") {
297 let url = res["PAYURL"].to_string();
298 let mut http = br_reqwest::Client::new();
299
300 let re = match http.post(url.as_str()).send() {
301 Ok(e) => e,
302 Err(e) => {
303 return Err(e.to_string());
304 }
305 };
306 let re = re.body().to_string();
307 let res = match json::parse(&re) {
308 Ok(e) => e,
309 Err(_) => return Err(re)
310 };
311 if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
312 return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
313 }
314 Ok(res)
315 } else {
316 Err(res.to_string())
317 }
318 }
319 ("alipay", Types::H5) => {
320 if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
321 return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
322 }
323 Ok(res["form_data"].clone())
324 }
325 _ => {
326 Ok(res)
327 }
328 }
329 }
330
331 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> {
332 let mut body = object! {
333 MERCHANTID:self.sp_mchid.clone(),
334 POSID:self.posid.clone(),
335 BRANCHID:self.branchid.clone(),
336 ccbParam:"",
337 TXCODE:"PAY100",
338 MERFLAG:"1",
339 ORDERID:out_trade_no,
340 QRCODE:auth_code,
341 AMOUNT:total_fee,
342 PROINFO:"商品名称",
343 REMARK1:description
344 };
345
346 let url = match channel {
347 "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
348 "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
349 _ => return Err(format!("Invalid channel: {channel}")),
350 };
351
352 match channel {
353 "wechat" => {
354 body["SUB_APPID"] = self.appid.clone().into();
355 }
356 "alipay" => {}
357 _ => return Err(format!("Invalid channel: {channel}")),
358 }
359
360 let res = self.http(url, body)?;
361 Ok(res)
362 }
363
364 fn close(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
365 Ok(true.into())
366 }
367
368 fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
369 let today = Local::now().date_naive();
370 let date_str = today.format("%Y%m%d").to_string();
371 let body = object! {
372 MERCHANTID:sub_mchid,
373 BRANCHID:self.branchid.clone(),
374 POSID:self.posid.clone(),
375 ORDERDATE:date_str,
376 BEGORDERTIME:"00:00:00",
377 ENDORDERTIME:"23:59:59",
378 ORDERID:out_trade_no,
379 QUPWD:self.pass.clone(),
380 TXCODE:"410408",
381 TYPE:"0",
382 KIND:"0",
383 STATUS:"1",
384 SEL_TYPE:"3",
385 PAGE:"1",
386 OPERATOR:"",
387 CHANNEL:"",
388 MAC:""
389 };
390 let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
391 if res["RETURN_CODE"] != "000000" {
392 if res["RETURN_MSG"].eq("流水记录不存在") {
393 let res = PayNotify {
394 trade_type: TradeType::None,
395 out_trade_no: "".to_string(),
396 sp_mchid: "".to_string(),
397 sub_mchid: "".to_string(),
398 sp_appid: "".to_string(),
399 transaction_id: "".to_string(),
400 success_time: 0,
401 sp_openid: "".to_string(),
402 sub_openid: "".to_string(),
403 total: 0.0,
404 payer_total: 0.0,
405 currency: "".to_string(),
406 payer_currency: "".to_string(),
407 trade_state: TradeState::NOTPAY,
408 };
409 return Ok(res.json());
410 }
411 return Err(res["RETURN_MSG"].to_string());
412 }
413 let data = res["QUERYORDER"].clone();
414 let res = PayNotify {
415 trade_type: TradeType::None,
416 out_trade_no: data["ORDERID"].to_string(),
417 sp_mchid: "".to_string(),
418 sub_mchid: sub_mchid.to_string(),
419 sp_appid: "".to_string(),
420 transaction_id: data["ORDERID"].to_string(),
421 success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
422 sp_openid: "".to_string(),
423 sub_openid: "".to_string(),
424 total: data["AMOUNT"].as_f64().unwrap_or(0.0),
425 currency: "CNY".to_string(),
426 payer_total: data["AMOUNT"].as_f64().unwrap_or(0.0),
427 payer_currency: "CNY".to_string(),
428 trade_state: TradeState::from(data["STATUS"].as_str().unwrap()),
429 };
430 Ok(res.json())
431 }
432
433 fn pay_micropay_query(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
434 Err("暂未开通".to_string())
435 }
436
437 fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
438 Err("暂未开通".to_string())
439 }
440
441 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> {
442 Err("暂未开通".to_string())
451 }
452
453 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> {
454 Err("暂未开通".to_string())
455 }
456
457 fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
458 Err("暂未开通".to_string())
459 }
460
461 fn refund_query(&mut self, trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
462 let today = Local::now().date_naive();
463 let date_str = today.format("%Y%m%d").to_string();
464 let body = object! {
465 MERCHANTID:sub_mchid,
466 BRANCHID:self.branchid.clone(),
467 POSID:self.posid.clone(),
468 ORDERDATE:date_str,
469 BEGORDERTIME:"00:00:00",
470 ENDORDERTIME:"23:59:59",
471 ORDERID:out_refund_no,
472 QUPWD:self.pass.clone(),
473 TXCODE:"410408",
474 TYPE:"1",
475 KIND:"0",
476 STATUS:"1",
477 SEL_TYPE:"3",
478 PAGE:"1",
479 OPERATOR:"",
480 CHANNEL:"",
481 MAC:""
482 };
483 let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
484 if res["RETURN_CODE"] != "000000" {
485 if res["RETURN_MSG"].eq("流水记录不存在") {
486 let res = PayNotify {
487 trade_type: TradeType::None,
488 out_trade_no: "".to_string(),
489 sp_mchid: "".to_string(),
490 sub_mchid: "".to_string(),
491 sp_appid: "".to_string(),
492 transaction_id: "".to_string(),
493 success_time: 0,
494 sp_openid: "".to_string(),
495 sub_openid: "".to_string(),
496 total: 0.0,
497 payer_total: 0.0,
498 currency: "".to_string(),
499 payer_currency: "".to_string(),
500 trade_state: TradeState::NOTPAY,
501 };
502 return Ok(res.json());
503 }
504 return Err(res["RETURN_MSG"].to_string());
505 }
506 println!("refund_query: {res:#}");
507 let data = res["QUERYORDER"].clone();
508
509 let res = RefundNotify {
510 out_trade_no: trade_no.to_string(),
511 refund_no: out_refund_no.to_string(),
512 sp_mchid: "".to_string(),
513 sub_mchid: sub_mchid.to_string(),
514 transaction_id: data["ORDERID"].to_string(),
515 refund_id: data["refund_id"].to_string(),
516 success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
517 total: data["AMOUNT"].as_f64().unwrap_or(0.0),
518 payer_total: data["amount"]["total"].to_string().parse::<f64>().unwrap(),
519 refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
520 payer_refund: data["amount"]["refund"].to_string().parse::<f64>().unwrap(),
521 status: RefundStatus::from(data["STATUS"].as_str().unwrap()),
522 };
523
524 Ok(res.json())
525 }
526
527 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> {
528 todo!()
529 }
530}
531
532fn xml_element_to_json(elem: &Element) -> JsonValue {
533 let mut obj = object! {};
534
535 for child in &elem.children {
536 if let xmltree::XMLNode::Element(e) = child {
537 obj[e.name.clone()] = xml_element_to_json(e);
538 }
539 }
540
541 match elem.get_text() {
542 None => obj,
543 Some(text) => JsonValue::from(text.to_string()),
544 }
545}