extern crate curl;
extern crate time;
extern crate uuid;
extern crate url;
extern crate md5;
extern crate xml;
use std::io::{Read, Write};
use std::string::ToString;
use std::collections::BTreeMap;
use curl::easy::Easy;
use url::form_urlencoded;
use xml::writer::{events};
use time::{strftime};
use uuid::Uuid;
const _CURRENCY_CNY: &'static str = "CNY";
const UNIFIEDORDER_URL: &'static str = "https://api.mch.weixin.qq.com/pay/unifiedorder";
const MICROPAY_URL: &'static str = "https://api.mch.weixin.qq.com/pay/micropay";
const ORDERQUERY_URL: &'static str = "https://api.mch.weixin.qq.com/pay/orderquery";
impl ToString for TradeType {
fn to_string(&self) -> String {
(match *self {
TradeType::Micro => "MICRO",
TradeType::Jsapi => "JSAPI",
TradeType::Native | TradeType::Qrcode => "NATIVE",
TradeType::App => "APP"
}).to_string()
}
}
pub enum BankType {}
enum ParamsCheckType {
Required,
Forbidden
}
pub enum WechatpayError {
MissingField(String),
RedundantField(String),
Curl(curl::Error),
Request,
Unknown
}
pub enum OrderIdentifier {
TransactionId(String),
OutTradeNo(String)
}
pub type WechatpayResult = Result<BTreeMap<String, String>, WechatpayError>;
pub struct WechatpayClient {
appid: String,
mch_id: String,
api_key: String,
notify_url: String,
cert: String,
}
impl WechatpayClient {
pub fn new(appid: &str, mch_id: &str, api_key: &str, notify_url: &str, cert: &str) -> WechatpayClient {
WechatpayClient{
appid: appid.to_string(),
mch_id: mch_id.to_string(),
api_key: api_key.to_string(),
notify_url: notify_url.to_string(),
cert: cert.to_string()
}
}
fn check_params(&self,
params: &BTreeMap<String, String>,
keys: Vec<&str>,
check_type: ParamsCheckType) -> Option<WechatpayError> {
for key in keys.iter() {
match check_type {
ParamsCheckType::Required => {
if params.get(&key.to_string()).unwrap_or(&"".to_string()).is_empty() {
return Some(WechatpayError::MissingField(key.to_string()));
}
}
ParamsCheckType::Forbidden => {
if params.get(&key.to_string()).unwrap_or(&"".to_string()).is_empty() {
return Some(WechatpayError::RedundantField(key.to_string()));
}
}
}
}
None
}
fn request(&self,
url: &str,
params: BTreeMap<String, String>,
retries: Option<u32>,
require_cert: bool) -> WechatpayResult {
let api_key = self.api_key.to_string();
let sign_str = get_sign(¶ms, &api_key);
let mut params = params;
params.insert("sign".to_string(), sign_str);
let xml_str = to_xml_str(¶ms);
let mut handle = Easy::new();
let mut err = WechatpayError::Request;
let _ = handle.url(url).map_err(|e| {
err = WechatpayError::Curl(e);
});
if require_cert {
let _ = handle.ssl_cert(&self.cert).map_err(|e| {
err = WechatpayError::Curl(e);
});
}
let _ = handle.read_function(move |buf| {
Ok(xml_str.as_bytes().read(buf).unwrap_or(0))
}).map_err(|e| {
err = WechatpayError::Curl(e);
});
for _ in 0..retries.unwrap_or(1) {
let mut data = Vec::<u8>::new();
{
let mut handle = handle.transfer();
let _ = handle.write_function(|text| {
Ok(match data.write_all(text) {
Ok(_) => text.len(),
Err(_) => 0
})
}).map_err(|e| {
err = WechatpayError::Curl(e);
});
let _ = handle.perform().map_err(|e|{
err = WechatpayError::Curl(e);
});
}
let status_code = match handle.response_code() {
Ok(code) => code,
Err(e) => {
err = WechatpayError::Curl(e);
0
}
};
if status_code == 200 || status_code == 201 {
let s = String::from_utf8(data).unwrap();
return Ok(from_xml_str(s.as_ref()))
}
}
Err(err)
}
pub fn pay(&self,
params: BTreeMap<String, String>,
trade_type: TradeType,
retries: Option<u32>) -> WechatpayResult {
if let Some(e) = self.check_params(¶ms, vec!["key", "sign"],
ParamsCheckType::Forbidden) {
return Err(e);
}
if let Some(e) = self.check_params(¶ms,
vec!["body", "out_trade_no", "total_fee", "spbill_create_ip"],
ParamsCheckType::Required) {
return Err(e);
}
match trade_type {
TradeType::Native => {
if let Some(e) = self.check_params(¶ms, vec!["product_id"], ParamsCheckType::Required) {
return Err(e);
}
}
TradeType::Jsapi => {
if let Some(e) = self.check_params(¶ms, vec!["openid"], ParamsCheckType::Required) {
return Err(e);
}
}
TradeType::Micro => {
if let Some(e) = self.check_params(¶ms, vec!["auth_code"], ParamsCheckType::Required) {
return Err(e);
}
}
_ => {}
}
let url = if trade_type == TradeType::Micro { MICROPAY_URL } else { UNIFIEDORDER_URL };
let body = params.get("body").unwrap_or(&"Test Request".to_string()).to_string();
let mut params = params;
params.insert("trade_type".to_string(), trade_type.to_string());
params.insert("appid".to_string(), self.appid.clone());
params.insert("mch_id".to_string(), self.mch_id.clone());
params.insert("nonce_str".to_string(), get_nonce_str());
params.insert("body".to_string(), body);
if trade_type != TradeType::Micro {
params.insert("notify_url".to_string(), self.notify_url.clone());
}
self.request(url, params, retries, false)
}
pub fn micro_pay(&self,
params: BTreeMap<String, String>,
retries: Option<u32>) -> WechatpayResult {
self.pay(params, TradeType::Micro, retries)
}
pub fn jsapi_pay(&self,
params: BTreeMap<String, String>,
retries: Option<u32>) -> WechatpayResult {
self.pay(params, TradeType::Jsapi, retries)
}
pub fn qrcode_pay(&self,
params: BTreeMap<String, String>,
retries: Option<u32>) -> WechatpayResult {
self.pay(params, TradeType::Qrcode, retries)
}
pub fn app_pay(&self,
params: BTreeMap<String, String>,
retries: Option<u32>) -> WechatpayResult {
self.pay(params, TradeType::App, retries)
}
pub fn query_order(&self, id: OrderIdentifier) -> WechatpayResult {
let mut params = BTreeMap::new();
match id {
OrderIdentifier::TransactionId(s) => {
params.insert("transaction_id".to_string(), s);
}
OrderIdentifier::OutTradeNo(s) => {
params.insert("out_trade_no".to_string(), s);
}
}
params.insert("appid".to_string(), self.appid.clone());
params.insert("mch_id".to_string(), self.mch_id.clone());
params.insert("nonce_str".to_string(), get_nonce_str());
self.request(ORDERQUERY_URL, params, None, false)
}
}
#[derive(PartialEq)]
pub enum TradeType {
Micro,
Jsapi,
Native, Qrcode,
App
}
pub fn get_trade_amount(v: f32) -> u32 {
(v * 100.0).round() as u32
}
pub fn get_time_str() -> String {
strftime("%Y%m%d%H%M%S", &time::now()).unwrap()
}
pub fn get_timestamp() -> i64 {
time::get_time().sec
}
pub fn get_nonce_str() -> String {
Uuid::new_v4().simple().to_string()
}
pub fn get_order_no() -> String {
get_time_str() + &((&get_nonce_str())[..18])
}
pub fn get_sign(pairs: &BTreeMap<String, String>, api_key: &String) -> String {
let keys = pairs
.iter()
.filter(|pair| {
pair.0.ne("key") && pair.0.ne("sign") && !pair.1.is_empty()
})
.map(|pair| {pair.0.to_string()})
.collect::<Vec<String>>();
let mut encoder = form_urlencoded::Serializer::new(String::new());
for key in keys {
encoder.append_pair(&key, &pairs[&key]);
}
encoder.append_pair("key", api_key);
let encoded = encoder.finish();
let mut context = md5::Context::new();
context.consume(encoded.as_bytes());
let mut digest = String::with_capacity(32);
for x in &context.compute()[..] {
digest.push_str(&format!("{:02X}", x));
}
digest
}
pub fn from_xml_str(data: &str) -> BTreeMap<String, String> {
let mut pairs = BTreeMap::new();
let reader = xml::reader::EventReader::from_str(data);
let mut tag: String = "".to_string();
for event in reader {
match event {
Ok(xml::reader::XmlEvent::StartElement{name, ..}) => {
tag = name.local_name;
}
Ok(xml::reader::XmlEvent::CData(value)) => {
pairs.insert(tag.clone(), value);
}
Err(e) => {
println!("Parse xml error: {:?}", e);
break;
}
_ => {}
}
}
pairs
}
pub fn to_xml_str(pairs: &BTreeMap<String, String>) -> String {
let mut target: Vec<u8> = Vec::new();
{
let mut writer = xml::writer::EmitterConfig::new()
.write_document_declaration(false)
.create_writer(&mut target);
let _ = writer.write::<events::XmlEvent>(events::XmlEvent::start_element("xml").into());
for (key, value) in pairs{
let _ = writer.write::<events::XmlEvent>(events::XmlEvent::start_element(key.as_ref()).into());
let _ = writer.write::<events::XmlEvent>(events::XmlEvent::characters(value.as_ref()).into());
let _ = writer.write::<events::XmlEvent>(events::XmlEvent::end_element().into());
}
let _ = writer.write::<events::XmlEvent>(events::XmlEvent::end_element().into());
}
String::from_utf8(target).unwrap()
}
#[cfg(test)]
mod tests {
extern crate time;
extern crate xml;
use std::collections::BTreeMap;
use xml::reader::{EventReader, XmlEvent};
#[test]
fn test_from_xml_str() {
let source = r#"
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<appid><![CDATA[wx2421b1c4370ec43b]]></appid>
<mch_id><![CDATA[10000100]]></mch_id>
<device_info><![CDATA[1000]]></device_info>
<nonce_str><![CDATA[TN55wO9Pba5yENl8]]></nonce_str>
<sign><![CDATA[BDF0099C15FF7BC6B1585FBB110AB635]]></sign>
<result_code><![CDATA[SUCCESS]]></result_code>
<openid><![CDATA[oUpF8uN95-Ptaags6E_roPHg7AG0]]></openid>
<is_subscribe><![CDATA[Y]]></is_subscribe>
<trade_type><![CDATA[APP]]></trade_type>
<bank_type><![CDATA[CCB_DEBIT]]></bank_type>
<total_fee>1</total_fee>
<fee_type><![CDATA[CNY]]></fee_type>
<transaction_id><![CDATA[1008450740201411110005820873]]></transaction_id>
<out_trade_no><![CDATA[1415757673]]></out_trade_no>
<attach><![CDATA[订单额外描述]]></attach>
<time_end><![CDATA[20141111170043]]></time_end>
<trade_state><![CDATA[SUCCESS]]></trade_state>
</xml>
"#;
let pairs = ::from_xml_str(source);
for &(k, v) in [
("return_code" , "SUCCESS"),
("return_msg" , "OK"),
("appid" , "wx2421b1c4370ec43b"),
("mch_id" , "10000100"),
("result_code" , "SUCCESS"),
("attach" , "订单额外描述"),
("transaction_id" , "1008450740201411110005820873"),
("time_end" , "20141111170043"),
("trade_type" , "APP")
].iter() {
assert_eq!(pairs.get(k), Some(&v.to_string()));
}
}
fn check_xml_str(pairs: &BTreeMap<String, String>, data: &str) {
let reader = EventReader::from_str(data);
let mut tag: String = "".to_string();
for event in reader {
match event {
Ok(XmlEvent::StartElement{name, ..}) => {
tag = name.local_name;
}
Ok(XmlEvent::Characters(s)) => {
assert_eq!(Some(&s), pairs.get(&tag));
}
Err(e) => {
panic!(format!("Parse error: {:?}", e));
}
_ => {}
}
}
}
#[test]
fn test_to_xml_str() {
let output = r#"
<xml>
<appid>wx2421b1c4370ec43b</appid>
<attach>支付测试</attach>
<body>APP支付测试</body>
<mch_id>10000100</mch_id>
<nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str>
<notify_url>http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php</notify_url>
<out_trade_no>1415659990</out_trade_no>
<spbill_create_ip>14.23.150.211</spbill_create_ip>
<total_fee>1</total_fee>
<trade_type>APP</trade_type>
<sign>0CB01533B8C1EF103065174F50BCA001</sign>
</xml>
"#;
let mut pairs = BTreeMap::new();
for &(k, v) in [
("appid" , "wx2421b1c4370ec43b"),
("attach" , "支付测试"),
("body" , "APP支付测试"),
("mch_id" , "10000100"),
("nonce_str" , "1add1a30ac87aa2db72f57a2375d8fec"),
("notify_url" , "http://wxpay.weixin.qq.com/pub_v2/pay/notify.v2.php"),
("out_trade_no" , "1415659990"),
("spbill_create_ip" , "14.23.150.211"),
("total_fee" , "1"),
("trade_type" , "APP"),
("sign" , "0CB01533B8C1EF103065174F50BCA001")
].iter() {
pairs.insert(k.to_string(), v.to_string());
}
check_xml_str(&pairs, output);
check_xml_str(&pairs, &(::to_xml_str(&pairs)));
}
#[test]
fn test_trade_amount() {
assert_eq!(::get_trade_amount(0.99), 99_u32);
assert_eq!(::get_trade_amount(0.999), 100_u32);
assert_eq!(::get_trade_amount(3.3), 330_u32);
assert_eq!(::get_trade_amount(20_f32), 2000_u32);
}
#[test]
fn test_string_length() {
assert_eq!(format!("{}", ::get_timestamp()).len(), 10);
assert_eq!(::get_time_str().len(), 14);
assert_eq!(::get_nonce_str().len(), 32);
assert_eq!(::get_order_no().len(), 32);
}
#[test]
fn test_sign() {
let mut pairs = BTreeMap::new();
for &(k, v) in [
("appid" , "wxd930ea5d5a258f4f"),
("mch_id" , "10000100"),
("device_info" , "1000"),
("body" , "test"),
("nonce_str" , "ibuaiVcKdpRxkhJA")
].iter() {
pairs.insert(k.to_string(), v.to_string());
}
let api_key = "192006250b4c09247ec02edce69f6a2d".to_string();
assert_eq!(::get_sign(&pairs, &api_key), "9A0A8659F005D6984697E2CA0A9CF3B7");
}
}