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