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