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