br_pay/
allinpay.rs

1use br_reqwest::Client;
2use base64::Engine;
3use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
4use json::{array, object, JsonValue};
5use openssl::hash::MessageDigest;
6use openssl::pkey::PKey;
7use openssl::rsa::Padding;
8use openssl::sign::Signer;
9use rand::distr::Alphanumeric;
10use rand::Rng;
11
12/// 通联支付
13/// 文档: https://prodoc.allinpay.com/project/38/
14/// 统一下单: https://prodoc.allinpay.com/project/17/
15#[derive(Clone, Debug)]
16pub struct Allinpay {
17    pub debug: bool,
18    /// 应用appid
19    pub appid: String,
20    /// 服务商商家号
21    pub sp_mchid: String,
22    /// 通知地址
23    pub notify_url: String,
24    /// 微信小程序appid
25    pub appid_mini: String,
26    /// RSA私钥
27    pub rsa_private: String,
28}
29impl Allinpay {
30    pub fn https(&mut self, url: &str, mut data: JsonValue) -> Result<JsonValue, String> {
31        let randomstr: String = rand::rng().sample_iter(&Alphanumeric).take(16).map(char::from).collect();
32        data["randomstr"] = randomstr.into();
33        data["signtype"] = "RSA".into();
34
35        let http_url = if self.debug {
36            format!("https://syb-test.allinpay.com{}", url)
37        } else {
38            format!("https://vsp.allinpay.com{}", url)
39        };
40        let mut http = Client::new();
41
42        let mut keys = vec![];
43
44        for (key, value) in data.entries() {
45            if !value.is_empty() && key != "sign" {
46                keys.push(key);
47            }
48        }
49        keys.sort();
50
51        let mut txt = vec![];
52        for key in keys {
53            txt.push(format!("{}={}", key, data[key]));
54        }
55        let txt = txt.join("&");
56        let sig_b64 = match sign_sha1withrsa_base64(self.rsa_private.as_str(), txt.as_bytes()) {
57            Ok(e) => e,
58            Err(e) => {
59                return Err(e.to_string())
60            }
61        };
62        data["sign"] = sig_b64.into();
63        let res = match http.post(&http_url).form_urlencoded(data).send() {
64            Ok(e) => e,
65            Err(e) => return Err(e.to_string())
66        };
67        let res = res.json()?;
68        if res["retcode"].eq("FAIL") {
69            return Err(res["retmsg"].to_string());
70        }
71        println!("{:#}", res);
72        Ok(res)
73    }
74}
75impl PayMode for Allinpay {
76    fn check(&mut self) -> Result<bool, String> {
77        let data = object! {
78            appid:self.appid.clone(),
79            orgid:self.sp_mchid.clone(),
80            cusid:"",
81            reqsn:"",
82        };
83        match self.https("/apiweb/tranx/query", data) {
84            Ok(e) => e,
85            Err(e) => {
86                if e.contains("auth_code不存在") {
87                    return Ok(true);
88                }
89                return Err(e);
90            }
91        };
92        Ok(true)
93    }
94
95    fn get_sub_mchid(&mut self, sub_mchid: &str) -> Result<JsonValue, String> {
96        let res = self.https("alipay.open.agent.signstatus.query", object! {
97            "biz_content":{
98                "pid":sub_mchid,
99                "product_codes":array!["QUICK_WAP_WAY"]
100            }
101        })?;
102        if !res["code"].eq("10000") {
103            return Err(res["msg"].to_string());
104        }
105        for item in res["sign_status_list"].members() {
106            if item["status"].eq("none") {
107                return Err(format!("{} 未开通", res["product_name"]));
108            }
109        }
110        Ok(true.into())
111    }
112
113
114    fn config(&mut self) -> JsonValue {
115        todo!()
116    }
117
118
119    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> {
120        let total = format!("{:.0}", total_fee * 100.0);
121
122        let mut order = object! {
123            appid:self.appid.clone(),
124            orgid:self.sp_mchid.clone(),
125            cusid:sub_mchid,
126            trxamt:total,
127            reqsn:out_trade_no,
128            paytype:"",
129            body:description,
130            acct:"",
131            notify_url:self.notify_url.clone()
132        };
133        match (types.clone(), channel) {
134            (Types::MiniJsapi, "wechat") => {
135                order["paytype"] = "W06".into();
136                order["sub_appid"] = self.appid_mini.clone().into();
137                order["acct"] = sp_openid.into();
138            }
139            (Types::MiniJsapi, "alipay") => {
140                order["paytype"] = "A02".into();
141                order["sub_appid"] = self.appid_mini.clone().into();
142                order["acct"] = sp_openid.into();
143            }
144            (Types::Jsapi, "wechat") => {
145                order["paytype"] = "W02".into();
146            }
147            (Types::Jsapi, "alipay") => {
148                order["paytype"] = "A02".into();
149            }
150            (Types::H5, "wechat") => {
151                order["paytype"] = "W02".into();
152            }
153            (Types::H5, "alipay") => {
154                order["paytype"] = "A02".into();
155            }
156            (Types::Native, "wechat") => {
157                order["paytype"] = "W01".into();
158            }
159            (Types::Native, "alipay") => {
160                order["paytype"] = "A01".into();
161            }
162            _ => {
163                order["paytype"] = "".into();
164            }
165        };
166        match self.https("/apiweb/unitorder/pay", order) {
167            Ok(e) => {
168                match types {
169                    Types::Native => {}
170                    Types::Jsapi | Types::H5 => {
171                        return Ok(object! {url:e});
172                    }
173                    Types::MiniJsapi => {
174                        return Ok(e);
175                    }
176                    Types::App => {}
177                    Types::Micropay => {}
178                }
179                Ok(e)
180            }
181            Err(e) => {
182                if e == "ACCESS_FORBIDDEN" {
183                    return Err("请商户授权JSAPI 绑定 服务商小程序APPID".to_string());
184                }
185                println!("Err: {e:#}");
186                Err(e)
187            }
188        }
189    }
190
191    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> {
192        let total = format!("{:.0}", total_fee * 100.0);
193
194        let order = object! {
195            appid:self.appid.clone(),
196            orgid:self.sp_mchid.clone(),
197            cusid:sub_mchid,
198            trxamt:total,
199            reqsn:out_trade_no,
200            body:description,
201            authcode:auth_code,
202            operatorid:org_openid,
203        };
204        match self.https("/apiweb/unitorder/scanqrpay", order) {
205            Ok(e) => {
206                if e["code"].ne("10000") {
207                    return Err(e["msg"].to_string());
208                }
209                if e["msg"].eq("FAIL") {
210                    if e["err_code_des"].ne("需要用户输入支付密码") {
211                        return Err(e["err_code_des"].to_string());
212                    }
213                    let res = PayNotify {
214                        trade_type: TradeType::MICROPAY,
215                        out_trade_no: out_trade_no.to_string(),
216                        sp_mchid: self.sp_mchid.clone(),
217                        sub_mchid: sub_mchid.to_string(),
218                        sp_appid: self.appid.to_string(),
219                        transaction_id: "".to_string(),
220                        success_time: 0,
221                        sp_openid: "".to_string(),
222                        sub_openid: "".to_string(),
223                        total: total_fee,
224                        payer_total: total_fee,
225                        currency: "CNY".to_string(),
226                        payer_currency: "CNY".to_string(),
227                        trade_state: TradeState::NOTPAY,
228                    };
229                    return Ok(res.json());
230                }
231                let res = PayNotify {
232                    trade_type: TradeType::MICROPAY,
233                    out_trade_no: out_trade_no.to_string(),
234                    sp_mchid: self.sp_mchid.clone(),
235                    sub_mchid: sub_mchid.to_string(),
236                    sp_appid: self.appid.to_string(),
237                    transaction_id: e["trade_no"].to_string(),
238                    success_time: PayNotify::datetime_to_timestamp(e["gmt_payment"].as_str().unwrap(), "%Y-%m-%d %H:%M:%S"),
239                    sp_openid: e["buyer_open_id"].to_string(),
240                    sub_openid: e["buyer_open_id"].to_string(),
241                    total: total_fee,
242                    payer_total: total_fee,
243                    currency: "CNY".to_string(),
244                    payer_currency: "CNY".to_string(),
245                    trade_state: TradeState::SUCCESS,
246                };
247                Ok(res.json())
248            }
249            Err(e) => Err(e)
250        }
251    }
252
253
254    fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
255        let order = object! {
256            appid:self.appid.clone(),
257            orgid:self.sp_mchid.clone(),
258            cusid:sub_mchid,
259            oldreqsn:out_trade_no,
260        };
261        match self.https("/apiweb/tranx/close", order) {
262            Ok(_) => Ok(true.into()),
263            Err(e) => {
264                if e.contains("交易不存在") {
265                    return Ok(true.into());
266                }
267                Err(e)
268            }
269        }
270    }
271
272    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
273        let order = object! {
274            appid:self.appid.clone(),
275            orgid:self.sp_mchid.clone(),
276            cusid:sub_mchid,
277            reqsn:out_trade_no,
278        };
279        match self.https("/apiweb/tranx/query", order) {
280            Ok(e) => {
281                if e.has_key("code") && e["code"].as_str().unwrap() != "10000" {
282                    return Err(e["msg"].to_string());
283                }
284                let buyer_open_id = if e.has_key("buyer_open_id") {
285                    e["buyer_open_id"].to_string()
286                } else {
287                    e["buyer_user_id"].to_string()
288                };
289                let send_pay_date = e["send_pay_date"].as_str().unwrap_or("");
290                let success_time = if !send_pay_date.is_empty() {
291                    PayNotify::datetime_to_timestamp(send_pay_date, "%Y-%m-%d %H:%M:%S")
292                } else {
293                    0
294                };
295                let res = PayNotify {
296                    trade_type: TradeType::None,
297                    out_trade_no: e["out_trade_no"].to_string(),
298                    sp_mchid: "".to_string(),
299                    sub_mchid: sub_mchid.to_string(),
300                    sp_appid: "".to_string(),
301                    transaction_id: e["trade_no"].to_string(),
302                    success_time,
303                    sp_openid: buyer_open_id.clone(),
304                    sub_openid: buyer_open_id.clone(),
305                    total: (e["total_amount"].to_string().parse::<f64>().unwrap_or(0.0)),
306                    payer_total: (e["total_amount"].to_string().parse::<f64>().unwrap_or(0.0)),
307                    currency: "CNY".to_string(),
308                    payer_currency: "CNY".to_string(),
309                    trade_state: TradeState::from(e["trade_status"].as_str().unwrap_or("")),
310                };
311                Ok(res.json())
312            }
313            Err(e) => Err(e)
314        }
315    }
316
317    fn pay_micropay_query(&mut self, out_trade_no: &str, sub_mchid: &str, _channel: &str) -> Result<JsonValue, String> {
318        self.pay_query(out_trade_no, sub_mchid)
319    }
320
321    fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
322        todo!()
323    }
324
325    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> {
326        let total = format!("{:.0}", amount * 100.0);
327
328        let body = object! {
329            appid:self.appid.clone(),
330            orgid:self.sp_mchid.clone(),
331            cusid:sub_mchid,
332            trxamt:total.clone(),
333            reqsn:out_refund_no,
334            oldreqsn:out_trade_no,
335            notify_url:self.notify_url.clone(),
336
337        };
338        match self.https("/apiweb/tranx/refund", body.clone()) {
339            Ok(e) => {
340                if e.has_key("code") && e["code"].as_str().unwrap() != "10000" {
341                    return Err(e["msg"].to_string());
342                }
343                let res = RefundNotify {
344                    out_trade_no: e["out_trade_no"].to_string(),
345                    refund_no: out_refund_no.to_string(),
346                    sp_mchid: "".to_string(),
347                    sub_mchid: sub_mchid.to_string(),
348                    transaction_id: e["trade_no"].to_string(),
349                    refund_id: out_refund_no.to_string(),
350                    success_time: PayNotify::datetime_to_timestamp(e["gmt_refund_pay"].as_str().unwrap(), "%Y-%m-%d %H:%M:%S"),
351                    total: total.to_string().parse::<f64>().unwrap_or(0.0),
352                    refund: e["refund_fee"].to_string().parse::<f64>().unwrap(),
353                    payer_total: e["refund_fee"].to_string().parse::<f64>().unwrap(),
354                    payer_refund: e["send_back_fee"].to_string().parse::<f64>().unwrap(),
355                    status: RefundStatus::from(e["fund_change"].as_str().unwrap()),
356                };
357                Ok(res.json())
358            }
359            Err(e) => Err(e)
360        }
361    }
362
363    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> {
364        todo!()
365    }
366
367    fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
368        todo!()
369    }
370
371    fn refund_query(&mut self, trade_no: &str, _out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
372        self.pay_query(trade_no, sub_mchid)
373    }
374
375    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> {
376        todo!()
377    }
378}
379
380
381fn sign_sha1withrsa_base64(private_pem: &str, data: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
382    let private_pem = pkcs1_private_pem_from_base64(private_pem);
383    let pkey = PKey::private_key_from_pem(private_pem.as_bytes())?;
384    let mut signer = Signer::new(MessageDigest::sha1(), &pkey)?;
385    signer.set_rsa_padding(Padding::PKCS1)?;
386    signer.update(data)?;
387    let sig = signer.sign_to_vec()?;
388    Ok(base64::engine::general_purpose::STANDARD.encode(sig))
389}
390
391fn pkcs1_private_pem_from_base64(body: &str) -> String {
392    fn wrap64(s: &str) -> String {
393        s.as_bytes().chunks(64).map(|c| std::str::from_utf8(c).unwrap()).collect::<Vec<&str>>().join("\n")
394    }
395    format!(
396        "-----BEGIN RSA PRIVATE KEY-----\n{}\n-----END RSA PRIVATE KEY-----\n",
397        wrap64(body.trim().replace(['\r', '\n', ' '], "").as_str())
398    )
399}