1use std::collections::{HashMap};
2use chrono::Local;
3use json::{object, JsonValue};
4use log::{warn};
5use reqwest::header;
6use reqwest::tls::Version;
7use xmltree::Element;
8use crate::{PayMode, PayNotify, TradeState, TradeType, Types};
9
10#[derive(Clone, Debug)]
12pub struct Ccbc {
13 pub appid: String,
15 pub secret: String,
17 pub pass: String,
19 pub mchid: String,
21 pub sp_mchid: String,
23 pub notify_url: String,
25 pub posid: String,
27 pub branchid: String,
29 pub smername: String,
31 pub smertypeid: String,
33 pub smertype: String,
35 pub public_key: String,
37 pub client_ip: String,
38 pub retry: usize,
40}
41
42impl Ccbc {
43 pub fn http(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
44 let mut mac = vec![];
45 let mut path = vec![];
46 let fields = ["MAC"];
47 for (key, value) in body.entries() {
48 if value.is_empty() && fields.contains(&key) {
49 continue;
50 }
51 if key != "PUB" {
52 path.push(format!("{key}={value}"));
53 }
54 mac.push(format!("{key}={value}"));
55 }
56
57
58 let mac_text = mac.join("&");
59 let path = path.join("&");
60 body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
61 body.remove("PUB");
62 let mac = format!("{}&MAC={}", path, body["MAC"]);
63
64 let urls = format!("{url}&{mac}");
65
66
67 let mut map = HashMap::new();
68 for (key, value) in body.entries_mut() {
69 map.insert(key, value.to_string());
70 }
71
72 let http = match reqwest::blocking::Client::builder().build() {
73 Ok(e) => e,
74 Err(e) => return Err(e.to_string())
75 };
76
77 let res = match http.post(urls).json(&map).send() {
78 Ok(e) => e,
79 Err(e) => {
80 if self.retry > 2 {
81 return Err(e.to_string());
82 }
83 self.retry += 1;
84 warn!("建行接口重试: {}", self.retry);
85 body.remove("MAC");
86 let res = self.http(url, body.clone())?;
87 return Ok(res);
88 }
89 };
90 let res = res.text().unwrap();
91 match json::parse(&res) {
92 Ok(e) => Ok(e),
93 Err(_) => Err(res)
94 }
95 }
96 fn escape_unicode(&mut self, s: &str) -> String {
97 s.chars().map(|c| {
98 if c.is_ascii() {
99 c.to_string()
100 } else {
101 format!("%u{:04X}", c as u32)
102 }
103 }).collect::<String>()
104 }
105 fn _unescape_unicode(&mut self, s: &str) -> String {
106 let mut output = String::new();
107 let mut chars = s.chars().peekable();
108 while let Some(c) = chars.next() {
109 if c == '%' && chars.peek() == Some(&'u') {
110 chars.next(); let codepoint: String = chars.by_ref().take(4).collect();
112 if let Ok(value) = u32::from_str_radix(&codepoint, 16) {
113 if let Some(ch) = std::char::from_u32(value) {
114 output.push(ch);
115 }
116 }
117 } else {
118 output.push(c);
119 }
120 }
121 output
122 }
123 pub fn http_q(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
124 let mut path = vec![];
125 let fields = ["MAC"];
126 for (key, value) in body.entries() {
127 if value.is_empty() && fields.contains(&key) {
128 continue;
129 }
130 if key.contains("QUPWD") {
131 path.push(format!("{key}="));
132 continue;
133 }
134 path.push(format!("{key}={value}"));
135 }
136
137 let mac = path.join("&");
138 body["MAC"] = br_crypto::md5::encrypt_hex(mac.as_bytes()).into();
139
140 let mut map = vec![];
141 for (key, value) in body.entries() {
142 map.push((key, value.to_string()));
143 }
144 let mut headers = header::HeaderMap::new();
145 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"));
146
147 let http = match reqwest::blocking::Client::builder().default_headers(headers).build() {
148 Ok(e) => e,
149 Err(e) => return Err(e.to_string())
150 };
151
152 let res = match http.post(url).form(&map).send() {
153 Ok(e) => e,
154 Err(e) => {
155 if self.retry > 2 {
156 return Err(e.to_string());
157 }
158 self.retry += 1;
159 warn!("建行查询接口重试: {}", self.retry);
160 body.remove("MAC");
161 let res = self.http_q(url, body)?;
162 return Ok(res);
163 }
164 };
165 let res = res.text().unwrap().trim().to_string();
166 match Element::parse(res.as_bytes()) {
167 Ok(e) => Ok(xml_element_to_json(&e)),
168 Err(e) => Err(e.to_string())
169 }
170 }
171}
172impl PayMode for Ccbc {
173 fn check(&mut self) -> Result<bool, String> {
174 todo!()
175 }
176
177 fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
178 todo!()
179 }
180
181 fn notify(&mut self, _data: JsonValue) -> Result<JsonValue, String> {
182 todo!()
183 }
184
185 fn config(&mut self) -> JsonValue {
186 todo!()
187 }
188
189
190 fn auth(&mut self, _code: &str) -> Result<JsonValue, String> {
191 todo!()
192 }
193
194 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> {
195 if self.public_key.is_empty() {
196 return Err(String::from("Public key is empty"));
197 }
198 let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
199 let mut body = object! {
200 MERCHANTID:self.mchid.clone(),
201 POSID:self.posid.clone(),
202 BRANCHID:self.branchid.clone(),
203 ORDERID:out_trade_no,
204 PAYMENT:total_fee,
205 CURCODE:"01",
206 TXCODE:"530590",
207 REMARK1:"",
208 REMARK2:"",
209 TYPE:"1",
210 PUB:pubtext,
211 GATEWAY:"0",
212 CLIENTIP:self.client_ip.clone(),
213 REGINFO:self.escape_unicode(&self.smername.clone()),
214 PROINFO: self.escape_unicode(description),
215 REFERER:"",
216 TRADE_TYPE:"",
217 MAC:"",
218 };
219
220 let url = match channel {
221 "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
222 "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
223 _ => return Err(format!("Invalid channel: {channel}")),
224 };
225
226 match channel {
227 "wechat" => {
228 body["TRADE_TYPE"] = match types {
229 Types::Jsapi => "JSAPI",
230 Types::MiniJsapi => "MINIPRO",
231 _ => return Err(format!("Invalid channel: {types:?}")),
232 }.into();
233 body["SUB_APPID"] = self.appid.clone().into();
234 body["SUB_OPENID"] = sp_openid.into();
235
236 }
252 "alipay" => {
253 body["TXCODE"] = "530591".into();
254 body["TRADE_TYPE"] = match types {
255 Types::Jsapi => "JSAPI",
256 Types::MiniJsapi => "JSAPI",
257 _ => return Err(format!("Invalid channel: {types:?}")),
258 }.into();
259 body["USERID"] = sp_openid.into();
260 }
261 _ => return Err(format!("Invalid channel: {channel}")),
262 }
263 let res = self.http(url, body)?;
264 match types {
265 Types::Jsapi | Types::MiniJsapi => {
266 if res.has_key("PAYURL") {
267 let url = res["PAYURL"].to_string();
268
269 let http = match reqwest::blocking::Client::builder().min_tls_version(Version::TLS_1_1).max_tls_version(Version::TLS_1_1).danger_accept_invalid_certs(true).build() {
270 Ok(e) => e,
271 Err(e) => return Err(e.to_string())
272 };
273 let re = match http.post(url.as_str()).send() {
274 Ok(e) => e,
275 Err(e) => {
276 return Err(e.to_string());
277 }
278 };
279 let re = re.text().unwrap();
280 let res = match json::parse(&re) {
281 Ok(e) => e,
282 Err(_) => return Err(re)
283 };
284 if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
285 return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
286 }
287 Ok(res)
288 } else {
289 Err(res.to_string())
290 }
291 }
292 _ => {
293 Ok(res)
294 }
295 }
296 }
297
298 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> {
299 let mut body = object! {
300 MERCHANTID:self.sp_mchid.clone(),
301 POSID:self.posid.clone(),
302 BRANCHID:self.branchid.clone(),
303 ccbParam:"",
304 TXCODE:"PAY100",
305 MERFLAG:"1",
306 ORDERID:out_trade_no,
307 QRCODE:auth_code,
308 AMOUNT:total_fee,
309 PROINFO:"商品名称",
310 REMARK1:description
311 };
312
313 let url = match channel {
314 "wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
315 "alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
316 _ => return Err(format!("Invalid channel: {channel}")),
317 };
318
319 match channel {
320 "wechat" => {
321 body["SUB_APPID"] = self.appid.clone().into();
322 }
323 "alipay" => {}
324 _ => return Err(format!("Invalid channel: {channel}")),
325 }
326
327 let res = self.http(url, body)?;
328 Ok(res)
329 }
330
331 fn close(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
332 Ok(true.into())
333 }
334
335 fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
336 let today = Local::now().date_naive();
337 let date_str = today.format("%Y%m%d").to_string();
338 let body = object! {
339 MERCHANTID:self.mchid.clone(),
340 BRANCHID:self.branchid.clone(),
341 POSID:self.posid.clone(),
342 ORDERDATE:date_str,
343 BEGORDERTIME:"00:00:00",
344 ENDORDERTIME:"23:59:59",
345 ORDERID:out_trade_no,
346 QUPWD:self.pass.clone(),
347 TXCODE:"410408",
348 TYPE:"0",
349 KIND:"0",
350 STATUS:"1",
351 SEL_TYPE:"3",
352 PAGE:"1",
353 OPERATOR:"",
354 CHANNEL:"",
355 MAC:""
356 };
357 let res = self.http_q("https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain", body)?;
358 if res["RETURN_CODE"] != "000000" {
359 if res["RETURN_MSG"].eq("流水记录不存在") {
360 let res = PayNotify {
361 trade_type: TradeType::None,
362 out_trade_no: "".to_string(),
363 sp_mchid: "".to_string(),
364 sub_mchid: "".to_string(),
365 sp_appid: "".to_string(),
366 transaction_id: "".to_string(),
367 success_time: 0,
368 sp_openid: "".to_string(),
369 sub_openid: "".to_string(),
370 total: 0.0,
371 payer_total: 0.0,
372 currency: "".to_string(),
373 payer_currency: "".to_string(),
374 trade_state: TradeState::NOTPAY,
375 };
376 return Ok(res.json());
377 }
378 return Err(res["RETURN_MSG"].to_string());
379 }
380 let data = res["QUERYORDER"].clone();
381 let res = PayNotify {
382 trade_type: TradeType::None,
383 out_trade_no: data["ORDERID"].to_string(),
384 sp_mchid: "".to_string(),
385 sub_mchid: sub_mchid.to_string(),
386 sp_appid: "".to_string(),
387 transaction_id: data["ORDERID"].to_string(),
388 success_time: PayNotify::datetime_to_timestamp(data["ORDERDATE"].as_str().unwrap_or(""), "%Y%m%d%H%M%S"),
389 sp_openid: "".to_string(),
390 sub_openid: "".to_string(),
391 total: data["AMOUNT"].as_f64().unwrap_or(0.0),
392 currency: "CNY".to_string(),
393 payer_total: data["AMOUNT"].as_f64().unwrap_or(0.0),
394 payer_currency: "CNY".to_string(),
395 trade_state: TradeState::from(data["STATUS"].as_str().unwrap()),
396 };
397 Ok(res.json())
398 }
399
400 fn pay_micropay_query(&mut self, _out_trade_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
401 todo!()
402 }
403
404 fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
405 todo!()
406 }
407
408 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> {
409 todo!()
410 }
411
412 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> {
413 todo!()
414 }
415
416 fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
417 todo!()
418 }
419
420 fn refund_query(&mut self, _trade_no: &str, _out_refund_no: &str, _sub_mchid: &str) -> Result<JsonValue, String> {
421 todo!()
422 }
423
424 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> {
425 todo!()
426 }
427}
428
429fn xml_element_to_json(elem: &Element) -> JsonValue {
430 let mut obj = object! {};
431
432 for child in &elem.children {
433 if let xmltree::XMLNode::Element(e) = child {
434 obj[e.name.clone()] = xml_element_to_json(e);
435 }
436 }
437
438 match elem.get_text() {
439 None => obj,
440 Some(text) => JsonValue::from(text.to_string()),
441 }
442}