use base64::Engine;
use base64::engine::general_purpose;
use chrono::Local;
use json::{object, JsonValue};
use log::{debug, info, warn};
use xmltree::Element;
use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
use cipher::{BlockEncryptMut, KeyInit};
use cipher::block_padding::Pkcs7;
use des::Des;
use rand::{rng, Rng};
#[derive(Clone, Debug)]
pub struct Ccbc {
pub debug: bool,
pub appid: String,
pub appid_subscribe: String,
pub sp_mchid: String,
pub sp_user_id: String,
pub sp_pass: String,
pub sp_posid: String,
pub notify_url: String,
pub sub_posid: String,
pub branchid: String,
pub public_key: String,
pub client_ip: String,
pub retry: usize,
pub query_url: String,
}
impl Ccbc {
pub fn http(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
let mut mac = vec![];
let mut path = vec![];
let fields = ["MAC"];
for (key, value) in body.entries() {
if value.is_empty() && fields.contains(&key) {
continue;
}
if key != "PUB" {
path.push(format!("{key}={value}"));
}
mac.push(format!("{key}={value}"));
}
let mac_text = mac.join("&");
let path = path.join("&");
if self.debug {
debug!("MERCHANTID=105000373721227&POSID=091864103&BRANCHID=530000000&ORDERID=96398&PAYMENT=0.01&CURCODE=01&TXCODE=530550&REMARK1=&REMARK2=&RETURNTYPE=3&TIMEOUT=&PUB=93bd9affe92c29dea12e5d79020111");
debug!("{mac_text:#}");
}
body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
if self.debug {
debug!("{body:#}");
}
body.remove("PUB");
let mac = format!("{}&MAC={}", path, body["MAC"]);
if self.debug {
debug!("{mac:#}");
}
let urls = format!("{url}&{mac}");
if self.debug {
debug!("{urls:#}");
}
let mut http = br_reqwest::Client::new();
let res = http.post(urls.as_str()).raw_json(body.clone()).set_retry(3).send()?;
let res = res.body().to_string();
match json::parse(&res) {
Ok(e) => Ok(e),
Err(e) => Err(e.to_string())
}
}
pub fn http_alipay(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
let mut mac = vec![];
let mut path = vec![];
let fields = ["MAC", "SUBJECT", "AREA_INFO"];
for (key, value) in body.entries() {
if value.is_empty() && fields.contains(&key) {
continue;
}
if fields.contains(&key) {
continue;
}
if key != "PUB" {
path.push(format!("{key}={value}"));
}
mac.push(format!("{key}={value}"));
}
let mac_text = mac.join("&");
let path = path.join("&");
body["MAC"] = br_crypto::md5::encrypt_hex(mac_text.as_bytes()).into();
body.remove("PUB");
let mac = format!("{}&MAC={}", path, body["MAC"]);
let urls = format!("{url}&{mac}");
let mut http = br_reqwest::Client::new();
let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
Ok(e) => e,
Err(e) => {
if self.retry > 2 {
return Err(e.to_string());
}
self.retry += 1;
warn!("建行接口重试: {}", self.retry);
body.remove("MAC");
let res = self.http_alipay(url, body.clone())?;
return Ok(res);
}
};
let res = res.body().to_string();
match json::parse(&res) {
Ok(e) => Ok(e),
Err(_) => Err(res)
}
}
fn escape_unicode(&mut self, s: &str) -> String {
s.chars().map(|c| {
if c.is_ascii() {
c.to_string()
} else {
format!("%u{:04X}", c as u32)
}
}).collect::<String>()
}
fn _unescape_unicode(&mut self, s: &str) -> String {
let mut output = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' && chars.peek() == Some(&'u') {
chars.next(); let codepoint: String = chars.by_ref().take(4).collect();
if let Ok(value) = u32::from_str_radix(&codepoint, 16) {
if let Some(ch) = std::char::from_u32(value) {
output.push(ch);
}
}
} else {
output.push(c);
}
}
output
}
pub fn http_q(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
let mut path = vec![];
let fields = ["MAC"];
for (key, value) in body.entries() {
if value.is_empty() && fields.contains(&key) {
continue;
}
if key.contains("QUPWD") {
path.push(format!("{key}="));
continue;
}
path.push(format!("{key}={value}"));
}
let mac = path.join("&");
body["MAC"] = br_crypto::md5::encrypt_hex(mac.as_bytes()).into();
let mut map = vec![];
for (key, value) in body.entries() {
map.push((key, value.to_string()));
}
let mut http = br_reqwest::Client::new();
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");
let res = match http.post(url).form_urlencoded(body.clone()).send() {
Ok(d) => d,
Err(e) => {
if self.retry > 2 {
return Err(e.to_string());
}
self.retry += 1;
warn!("建行查询接口重试: {}", self.retry);
body.remove("MAC");
let res = self.http_q(url, body.clone())?;
return Ok(res);
}
};
let res = res.body().to_string().trim().to_string();
match Element::parse(res.as_bytes()) {
Ok(e) => Ok(xml_element_to_json(&e)),
Err(e) => Err(e.to_string())
}
}
pub fn http_ccb_param(&mut self, url: &str, mut body: JsonValue) -> Result<JsonValue, String> {
let mer_info = format!("MERCHANTID={}&POSID={}&BRANCHID={}", body["MERCHANTID"], body["POSID"], body["BRANCHID"]);
let ccb_param = self.make_ccb_param(body.clone())?;
let urls = format!("{url}?{mer_info}&ccbParam={ccb_param}");
let mut http = br_reqwest::Client::new();
let res = match http.post(urls.as_str()).raw_json(body.clone()).send() {
Ok(e) => e,
Err(e) => {
if self.retry > 2 {
return Err(e.to_string());
}
self.retry += 1;
warn!("建行接口重试: {}", self.retry);
body.remove("MAC");
let res = self.http_ccb_param(url, body.clone())?;
return Ok(res);
}
};
let res = res.body().to_string();
match json::parse(&res) {
Ok(e) => Ok(e),
Err(_) => Err(res)
}
}
pub fn make_ccb_param(&mut self, body: JsonValue) -> Result<String, String> {
if self.public_key.is_empty() || self.public_key.len() < 30 {
return Err(String::from("Public key is empty"));
}
let pubkey = self.public_key[self.public_key.len() - 30..].to_string();
if pubkey.len() < 8 {
return Err(String::from("Public key len 8"));
}
let pubkey = &pubkey[..8];
let mut mac = vec![];
let mut arr = vec![];
let mut params = vec![];
for (key, value) in body.entries() {
arr.push(key);
params.push(format!("{key}={value}"));
}
arr.sort();
for key in arr.iter() {
if body.has_key(key) && !body[key.to_string()].is_empty() {
mac.push(format!("{key}={}", body[key.to_string()]));
}
}
let ccb_param = mac.join("&");
let ccb_param = format!("{}20120315201809041004", ccb_param);
let ccb_param = br_crypto::md5::encrypt_hex(ccb_param.as_bytes());
let params = params.join("&");
let params = format!("{params}&SIGN={ccb_param}");
let bytes = self.utf16_bytes(params.as_str());
let encrypt = self.des_ecb_pkcs5_base64(&bytes, pubkey)?;
let encrypt = encrypt.replace("+", ",");
let url = br_crypto::encoding::urlencoding_encode(encrypt.as_str());
Ok(url)
}
fn utf16_bytes(&mut self, s: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(2 + s.len() * 2);
out.push(0xFE);
out.push(0xFF);
for u in s.encode_utf16() {
out.push((u >> 8) as u8);
out.push((u & 0xFF) as u8);
}
out
}
fn des_ecb_pkcs5_base64(&mut self, plain: &[u8], key_str: &str) -> Result<String, &'static str> {
let kb = key_str.as_bytes();
if kb.len() < 8 {
return Err("DES key must be at least 8 bytes");
}
let mut key = [0u8; 8];
key.copy_from_slice(&kb[..8]);
let cipher = ecb::Encryptor::<Des>::new(&key.into());
let orig_len = plain.len();
let mut buf = vec![0u8; orig_len + 8];
buf[..orig_len].copy_from_slice(plain);
let ct = cipher.encrypt_padded_mut::<Pkcs7>(&mut buf, orig_len).map_err(|_| "encrypt error")?;
Ok(general_purpose::STANDARD.encode(ct))
}
fn https_cert(&mut self, text: &str) -> Result<JsonValue, String> {
let text = br_crypto::encoding::urlencoding_encode(text);
let mut http = br_reqwest::Client::new();
http.header("Connection", "close");
http.post(self.query_url.as_str()).form_urlencoded(object! {
requestXml:text
});
let res = http.send()?;
let ress = br_crypto::encoding::gb18030_to_utf8(res.stream())?;
let ress = ress.replace(r#"encoding="GB18030""#, r#"encoding="UTF-8""#);
match Element::parse(ress.trim().as_bytes()) {
Ok(e) => {
let json = xml_element_to_json(&e);
Ok(json)
}
Err(e) => Err(e.to_string())
}
}
fn get_xml_text(&mut self, name: &str) -> String {
let res = match name {
"5W1002.xml" => r#"<?xml version="1.0" encoding="GB2312" standalone="yes" ?>
<TX>
<REQUEST_SN>{{REQUEST_SN}}</REQUEST_SN>
<CUST_ID>{{CUST_ID}}</CUST_ID>
<USER_ID>{{USER_ID}}</USER_ID>
<PASSWORD>{{PASSWORD}}</PASSWORD>
<TX_CODE>5W1002</TX_CODE>
<LANGUAGE>CN</LANGUAGE>
<TX_INFO>
<START>{{START}}</START>
<STARTHOUR>00</STARTHOUR>
<STARTMIN>00</STARTMIN>
<END>{{END}}</END>
<ENDHOUR>00</ENDHOUR>
<ENDMIN>00</ENDMIN>
<KIND>{{KIND}}</KIND>
<ORDER>{{ORDER}}</ORDER>
<ACCOUNT></ACCOUNT>
<DEXCEL>1</DEXCEL>
<MONEY></MONEY>
<NORDERBY>2</NORDERBY>
<PAGE>1</PAGE>
<POS_CODE>{{POS_CODE}}</POS_CODE>
<STATUS>{{STATUS}}</STATUS>
<Mrch_No>{{Mrch_No}}</Mrch_No>
<TXN_TPCD></TXN_TPCD>
</TX_INFO>
</TX>"#,
"5W1003.xml" => r#"<?xml version="1.0" encoding="GB2312" standalone="yes" ?>
<TX>
<REQUEST_SN>{{REQUEST_SN}}</REQUEST_SN>
<CUST_ID>{{CUST_ID}}</CUST_ID>
<USER_ID>{{USER_ID}}</USER_ID>
<PASSWORD>{{PASSWORD}}</PASSWORD>
<TX_CODE>5W1003</TX_CODE>
<LANGUAGE>CN</LANGUAGE>
<TX_INFO>
<START>{{START}}</START>
<STARTHOUR>00</STARTHOUR>
<STARTMIN>00</STARTMIN>
<END>{{END}}</END>
<ENDHOUR>00</ENDHOUR>
<ENDMIN>00</ENDMIN>
<KIND>{{KIND}}</KIND>
<ORDER>{{ORDER}}</ORDER>
<ACCOUNT></ACCOUNT>
<MONEY></MONEY>
<NORDERBY>2</NORDERBY>
<PAGE>1</PAGE>
<POS_CODE>{{POS_CODE}}</POS_CODE>
<STATUS>{{STATUS}}</STATUS>
<Mrch_No>{{Mrch_No}}</Mrch_No>
<TXN_TPCD></TXN_TPCD>
</TX_INFO>
</TX>"#,
"5W1024.xml" => r#"<?xml version="1.0" encoding="GB2312" standalone="yes" ?>
<TX>
<REQUEST_SN>{{REQUEST_SN}}</REQUEST_SN>
<CUST_ID>{{CUST_ID}}</CUST_ID>
<USER_ID>{{USER_ID}}</USER_ID>
<PASSWORD>{{PASSWORD}}</PASSWORD>
<TX_CODE>5W1024</TX_CODE>
<LANGUAGE>CN</LANGUAGE>
<TX_INFO>
<MONEY>{{MONEY}}</MONEY>
<ORDER>{{ORDER}}</ORDER>
<REFUND_CODE>{{REFUND_CODE}}</REFUND_CODE>
<Mrch_No>{{Mrch_No}}</Mrch_No>
</TX_INFO>
<SIGN_INFO></SIGN_INFO>
<SIGNCERT></SIGNCERT>
</TX>"#,
_ => r#""#
};
let xml_text = res.as_bytes().to_vec();
let mut text = unsafe { String::from_utf8_unchecked(xml_text) };
let mut rng = rng();
let num: String = (0..14).map(|_| rng.random_range(0..10).to_string()).collect();
text = text.replace("{{REQUEST_SN}}", num.as_str());
text = text.replace("{{CUST_ID}}", self.sp_mchid.as_str());
text = text.replace("{{USER_ID}}", self.sp_user_id.as_str());
text = text.replace("{{PASSWORD}}", self.sp_pass.as_str());
text
}
}
impl PayMode for Ccbc {
fn check(&mut self) -> Result<bool, String> {
todo!()
}
fn get_sub_mchid(&mut self, _sub_mchid: &str) -> Result<JsonValue, String> {
todo!()
}
fn config(&mut self) -> JsonValue {
todo!()
}
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> {
if self.public_key.is_empty() || self.public_key.len() < 30 {
return Err(String::from("Public key is empty"));
}
let pubtext = self.public_key[self.public_key.len() - 30..].to_string();
let url = match channel {
"wechat" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
"alipay" => "https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain?CCB_IBSVersion=V6",
_ => return Err(format!("Invalid channel: {channel}")),
};
let body = match channel {
"wechat" => {
let mut body = object! {
MERCHANTID:sub_mchid,
POSID:self.sub_posid.clone(),
BRANCHID:self.branchid.clone(),
ORDERID:out_trade_no,
PAYMENT:total_fee,
CURCODE:"01",
TXCODE:"530590",
REMARK1:"",
REMARK2:"",
TYPE:"1",
PUB:pubtext,
GATEWAY:"0",
CLIENTIP:self.client_ip.clone(),
REGINFO:"",
PROINFO: self.escape_unicode(description),
REFERER:"",
TRADE_TYPE:"",
SUB_APPID: "",
SUB_OPENID:sp_openid,
MAC:"",
};
body["TRADE_TYPE"] = match types {
Types::Jsapi => {
body["SUB_APPID"] = self.appid_subscribe.clone().into();
"JSAPI"
}
Types::MiniJsapi => {
body["SUB_APPID"] = self.appid.clone().into();
"MINIPRO"
}
_ => return Err(format!("Invalid types: {types:?}")),
}.into();
body
}
"alipay" => {
let body = match types {
Types::MiniJsapi => object! {
MERCHANTID:sub_mchid,
POSID:self.sub_posid.clone(),
BRANCHID:self.branchid.clone(),
ORDERID:out_trade_no,
PAYMENT:total_fee,
CURCODE:"01",
TXCODE:"530591",
TRADE_TYPE:"JSAPI",
USERID:sp_openid,
PUB:pubtext,
MAC:""
},
Types::H5 => object! {
BRANCHID:self.branchid.clone(),
MERCHANTID:sub_mchid,
POSID:self.sub_posid.clone(),
TXCODE:"ZFBWAP",
ORDERID:out_trade_no,
AMOUNT:total_fee,
TIMEOUT:"",
REMARK1:"",
REMARK2:"",
PUB:pubtext,
MAC:"",
SUBJECT:description,
AREA_INFO:""
},
Types::Jsapi => object! {
MERCHANTID:sub_mchid,
POSID:self.sub_posid.clone(),
BRANCHID:self.branchid.clone(),
ORDERID:out_trade_no,
PAYMENT:total_fee,
CURCODE:"01",
TXCODE:"530550",
REMARK1:"",
REMARK2:"",
RETURNTYPE:"3",
TIMEOUT:"",
PUB:pubtext,
MAC:""
},
_ => return Err(format!("Invalid types: {types:?}")),
};
body
}
_ => return Err(format!("Invalid channel: {channel}")),
};
match (channel, types) {
("wechat", Types::Jsapi | Types::MiniJsapi) => {
let res = self.http(url, body.clone())?;
if res.has_key("PAYURL") {
let url = res["PAYURL"].to_string();
let mut http = br_reqwest::Client::new();
let re = match http.post(url.as_str()).send() {
Ok(e) => e,
Err(e) => {
return Err(e.to_string());
}
};
let re = re.body().to_string();
let res = match json::parse(&re) {
Ok(e) => e,
Err(_) => return Err(re)
};
if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
}
Ok(res)
} else {
Err(res.to_string())
}
}
("alipay", Types::MiniJsapi) => {
let res = self.http(url, body)?;
if res.has_key("PAYURL") {
let url = res["PAYURL"].to_string();
let mut http = br_reqwest::Client::new();
let re = match http.post(url.as_str()).send() {
Ok(e) => e,
Err(e) => {
return Err(e.to_string());
}
};
let re = re.body().to_string();
let res = match json::parse(&re) {
Ok(e) => e,
Err(_) => return Err(re)
};
if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
}
Ok(res["jsapi"].clone())
} else {
Err(res.to_string())
}
}
("alipay", Types::H5) => {
let res = self.http_alipay(url, body)?;
if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
}
Ok(res["form_data"].clone())
}
("alipay", Types::Jsapi) => {
let res = self.http(url, body)?;
if res.has_key("PAYURL") {
let url = res["PAYURL"].to_string();
if self.debug {
debug!("{url:#}");
}
let mut http = br_reqwest::Client::new();
let re = match http.post(url.as_str()).send() {
Ok(e) => e,
Err(e) => {
return Err(e.to_string());
}
};
let re = re.body().to_string();
let res = match json::parse(&re) {
Ok(e) => e,
Err(_) => return Err(re)
};
if self.debug {
debug!("{res:#}");
}
if res.has_key("ERRCODE") && res["ERRCODE"] != "000000" {
return Err(format!("获取支付参数: 错误码: [{}] {} 失败", res["ERRCODE"], res["ERRMSG"]));
}
let r = br_crypto::encoding::urlencoding_decode(res["QRURL"].as_str().unwrap());
Ok(object! {
url:r.clone()
})
} else {
Err(res.to_string())
}
}
_ => {
let res = self.http(url, body)?;
Ok(res)
}
}
}
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> {
let body = object! {
MERCHANTID:sub_mchid,
POSID:self.sub_posid.clone(),
BRANCHID:self.branchid.clone(),
MERFLAG:"1",
TERMNO1:"",
TERMNO2:"",
ORDERID:out_trade_no,
QRCODE:auth_code,
AMOUNT:total_fee,
TXCODE:"PAY100",
PROINFO:description,
REMARK1:"",
REMARK2:"",
SMERID:"",SMERNAME:"",SMERTYPEID:"",SMERTYPE:"",TRADECODE:"",TRADENAME:"",SMEPROTYPE:"",PRONAME:""
};
let url = "https://ibsbjstar.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY";
let res = self.http_ccb_param(url, body)?;
match res["RESULT"].as_str().unwrap_or("N") {
"Y" => {
Ok(self.pay_micropay_query(out_trade_no, sub_mchid, channel)?)
}
"N" => {
Err(res["ERRMSG"].to_string())
}
"U" => {
Err(res.to_string())
}
"Q" => {
let res = PayNotify {
trade_type: TradeType::MICROPAY,
out_trade_no: out_trade_no.to_string(),
sp_mchid: self.sp_mchid.clone(),
sub_mchid: sub_mchid.to_string(),
sp_appid: "".to_string(),
transaction_id: res["TRACEID"].to_string(),
success_time: 0,
sp_openid: "".to_string(),
sub_openid: "".to_string(),
total: total_fee,
payer_total: total_fee,
currency: "CNY".to_string(),
payer_currency: "CNY".to_string(),
trade_state: TradeState::NOTPAY,
};
Ok(res.json())
}
_ => {
Err(res.to_string())
}
}
}
fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
let today = Local::now().date_naive();
let date_str = today.format("%Y%m%d").to_string();
let order_date = &out_trade_no[0..8];
let kind = if date_str == order_date {
0
} else {
1
};
let mut text = self.get_xml_text("5W1002.xml");
text = text.replace("{{START}}", order_date);
text = text.replace("{{END}}", order_date);
text = text.replace("{{KIND}}", kind.to_string().as_str());
text = text.replace("{{ORDER}}", out_trade_no);
text = text.replace("{{POS_CODE}}", self.sp_posid.as_str());
text = text.replace("{{STATUS}}", "1");
text = text.replace("{{Mrch_No}}", sub_mchid);
let res = self.https_cert(&text)?;
if res.has_key("RETURN_CODE") && res["RETURN_CODE"].ne("000000") {
if res["RETURN_MSG"].eq("流水记录不存在") {
return Ok(true.into());
}
return Err(res["RETURN_MSG"].to_string());
}
let trade_state = match res["TX_INFO"]["LIST"]["ORDER_STATUS"].as_str().unwrap_or("") {
"0" => TradeState::PAYERROR,
"1" => TradeState::SUCCESS,
"2" | "5" => TradeState::USERPAYING,
"3" | "4" => TradeState::REFUND,
_ => return Err("未知状态".to_string()),
};
match trade_state {
TradeState::SUCCESS => Ok(false.into()),
TradeState::REFUND => Ok(false.into()),
TradeState::NOTPAY => Ok(true.into()),
TradeState::CLOSED => Ok(true.into()),
TradeState::REVOKED => Ok(true.into()),
TradeState::USERPAYING => Ok(false.into()),
TradeState::PAYERROR => Ok(true.into()),
TradeState::None => Ok(true.into()),
}
}
fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
let today = Local::now().date_naive();
let date_str = today.format("%Y%m%d").to_string();
let order_date = &out_trade_no[0..8];
let kind = if date_str == order_date {
0
} else {
1
};
let mut text = self.get_xml_text("5W1002.xml");
text = text.replace("{{START}}", order_date);
text = text.replace("{{END}}", order_date);
text = text.replace("{{KIND}}", kind.to_string().as_str());
text = text.replace("{{ORDER}}", out_trade_no);
text = text.replace("{{POS_CODE}}", "");
text = text.replace("{{STATUS}}", "1");
text = text.replace("{{Mrch_No}}", sub_mchid);
info!("支付查询请求: {text}");
let res = self.https_cert(&text)?;
info!("支付查询响应: {res}");
if res.has_key("RETURN_CODE") && res["RETURN_CODE"].ne("000000") {
if res["RETURN_MSG"].eq("流水记录不存在") {
let ttt = PayNotify {
trade_type: TradeType::None,
out_trade_no: out_trade_no.to_string(),
sp_mchid: self.sp_mchid.to_string(),
sub_mchid: sub_mchid.to_string(),
sp_appid: "".to_string(),
transaction_id: "".to_string(),
success_time: 0,
sp_openid: "".to_string(),
sub_openid: "".to_string(),
total: 0.0,
payer_total: 0.0,
currency: "CNY".to_string(),
payer_currency: "CNY".to_string(),
trade_state: TradeState::NOTPAY,
};
return Ok(ttt.json());
}
return Err(res["RETURN_MSG"].to_string());
}
let trade_state = match res["TX_INFO"]["LIST"]["ORDER_STATUS"].as_str().unwrap_or("") {
"0" => TradeState::PAYERROR,
"1" => TradeState::SUCCESS,
"2" | "5" => TradeState::USERPAYING,
"3" | "4" => TradeState::REFUND,
_ => return Err("未知状态".to_string()),
};
let data = res["TX_INFO"]["LIST"].clone();
let res = match trade_state {
TradeState::SUCCESS => {
PayNotify {
trade_type: TradeType::None,
out_trade_no: out_trade_no.to_string(),
sp_mchid: self.sp_mchid.to_string(),
sub_mchid: sub_mchid.to_string(),
sp_appid: "".to_string(),
transaction_id: data["OriOvrlsttnEV_Trck_No"].to_string(),
success_time: PayNotify::datetime_to_timestamp(data["TRAN_DATE"].as_str().unwrap_or(""), "%Y-%m-%d %H:%M:%S"),
sp_openid: "".to_string(),
sub_openid: "".to_string(),
total: data["Orig_Amt"].to_string().parse::<f64>().unwrap_or(0.0),
payer_total: data["Txn_ClrgAmt"].to_string().parse::<f64>().unwrap_or(0.0),
currency: "CNY".to_string(),
payer_currency: "CNY".to_string(),
trade_state,
}
}
_ => PayNotify {
trade_type: TradeType::None,
out_trade_no: out_trade_no.to_string(),
sp_mchid: self.sp_mchid.to_string(),
sub_mchid: sub_mchid.to_string(),
sp_appid: "".to_string(),
transaction_id: res["ORDER"].to_string(),
success_time: 0,
sp_openid: "".to_string(),
sub_openid: "".to_string(),
total: 0.0,
payer_total: 0.0,
currency: "CNY".to_string(),
payer_currency: "CNY".to_string(),
trade_state,
}
};
Ok(res.json())
}
fn pay_micropay_query(&mut self, out_trade_no: &str, sub_mchid: &str, channel: &str) -> Result<JsonValue, String> {
let crcode_type = match channel {
"wechat" => "2",
"alipay" => "3",
_ => "5"
};
let body = object! {
MERCHANTID:sub_mchid,
POSID:self.sub_posid.clone(),
BRANCHID:self.branchid.clone(),
TXCODE:"PAY102",
MERFLAG:"1",
TERMNO1:"",
TERMNO2:"",
ORDERID:out_trade_no,
QRYTIME:"1",
QRCODETYPE:crcode_type,
QRCODE:"",
REMARK1:"",
REMARK2:"",
};
let url = "https://ibsbjstar.ccb.com.cn/CCBIS/B2CMainPlat_00_BEPAY";
let res = self.http_ccb_param(url, body)?;
match res["RESULT"].as_str().unwrap_or("N") {
"Y" => {
let now = Local::now();
let formatted = now.format("%Y%m%d%H%M%S").to_string();
let transaction_id = match channel {
"wechat" => res["WECHAT_NO"].to_string(),
"alipay" => res["ZFB_NO"].to_string(),
_ => "".to_string()
};
let res = PayNotify {
trade_type: TradeType::MICROPAY,
out_trade_no: out_trade_no.to_string(),
sp_mchid: self.sp_mchid.clone(),
sub_mchid: sub_mchid.to_string(),
sp_appid: "".to_string(),
transaction_id,
success_time: PayNotify::datetime_to_timestamp(formatted.as_str(), "%Y%m%d%H%M%S"),
sp_openid: "".to_string(),
sub_openid: "".to_string(),
total: res["AMOUNT"].to_string().parse::<f64>().unwrap_or(0.0),
payer_total: res["AMOUNT"].to_string().parse::<f64>().unwrap_or(0.0),
currency: "CNY".to_string(),
payer_currency: "CNY".to_string(),
trade_state: TradeState::SUCCESS,
};
Ok(res.json())
}
"N" => {
let crcode_type = match channel {
"wechat" => res["WECHAT_STATE"].to_string(),
"alipay" => res["ZFB_STATE"].to_string(),
_ => "".to_string()
};
let res = PayNotify {
trade_type: TradeType::MICROPAY,
out_trade_no: out_trade_no.to_string(),
sp_mchid: self.sp_mchid.clone(),
sub_mchid: sub_mchid.to_string(),
sp_appid: "".to_string(),
transaction_id: "".to_string(),
success_time: 0,
sp_openid: "".to_string(),
sub_openid: "".to_string(),
total: 0.0,
currency: "CNY".to_string(),
payer_total: 0.0,
payer_currency: "CNY".to_string(),
trade_state: TradeState::from(crcode_type.as_str()),
};
Ok(res.json())
}
"U" => {
Err(res.to_string())
}
"Q" => {
let res = PayNotify {
trade_type: TradeType::MICROPAY,
out_trade_no: out_trade_no.to_string(),
sp_mchid: self.sp_mchid.clone(),
sub_mchid: sub_mchid.to_string(),
sp_appid: "".to_string(),
transaction_id: "".to_string(),
success_time: 0,
sp_openid: "".to_string(),
sub_openid: "".to_string(),
total: 0.0,
payer_total: 0.0,
currency: "CNY".to_string(),
payer_currency: "CNY".to_string(),
trade_state: TradeState::NOTPAY,
};
Ok(res.json())
}
_ => {
Err(res.to_string())
}
}
}
fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
Err("暂未开通".to_string())
}
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> {
let mut text = self.get_xml_text("5W1024.xml");
text = text.replace("{{MONEY}}", amount.to_string().as_str());
text = text.replace("{{ORDER}}", out_trade_no);
text = text.replace("{{REFUND_CODE}}", out_refund_no);
text = text.replace("{{Mrch_No}}", sub_mchid);
let res = self.https_cert(&text)?;
if res.has_key("RETURN_CODE") && res["RETURN_CODE"].ne("000000") {
return Err(res["RETURN_MSG"].to_string());
}
if !res.has_key("TX_INFO") {
return Err(res["RETURN_MSG"].to_string());
}
let timestamp = Local::now().timestamp();
let info = object! {
refund_id:out_refund_no,
status:"已退款",
success_time:timestamp,
out_refund_no: out_refund_no,
};
Ok(info)
}
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> {
Err("暂未开通".to_string())
}
fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
Err("暂未开通".to_string())
}
fn refund_query(&mut self, trade_no: &str, out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
let today = Local::now().date_naive();
let date_str = today.format("%Y%m%d").to_string();
let order_date = &out_refund_no[0..8];
let kind = if date_str == order_date {
0
} else {
1
};
let mut text = self.get_xml_text("5W1003.xml");
text = text.replace("{{START}}", order_date);
text = text.replace("{{END}}", order_date);
text = text.replace("{{KIND}}", kind.to_string().as_str());
text = text.replace("{{ORDER}}", out_refund_no);
text = text.replace("{{POS_CODE}}", self.sp_posid.as_str());
text = text.replace("{{STATUS}}", "1");
text = text.replace("{{Mrch_No}}", sub_mchid);
let res = self.https_cert(&text)?;
if res.has_key("RETURN_MSG") {
if res["RETURN_MSG"].eq("流水记录不存在") {
let res = RefundNotify {
out_trade_no: trade_no.to_string(),
refund_no: out_refund_no.to_string(),
sp_mchid: self.sp_mchid.to_string(),
sub_mchid: sub_mchid.to_string(),
transaction_id: "".to_string(),
refund_id: "".to_string(),
success_time: 0,
total: 0.0,
refund: 0.0,
payer_total: 0.0,
payer_refund: 0.0,
status: RefundStatus::None,
};
return Ok(res.json());
}
return Err(res["RETURN_MSG"].to_string());
}
Err("暂未开通".to_string())
}
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> {
todo!()
}
}
fn xml_element_to_json(elem: &Element) -> JsonValue {
let mut obj = object! {};
for child in &elem.children {
if let xmltree::XMLNode::Element(e) = child {
obj[e.name.clone()] = xml_element_to_json(e);
}
}
match elem.get_text() {
None => obj,
Some(text) => JsonValue::from(text.to_string()),
}
}