diff --git a/Cargo.lock b/Cargo.lock index f2312c5..b266e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2341,6 +2341,7 @@ dependencies = [ "base64", "chrono", "dotenvy", + "encoding_rs", "include_dir", "jsonwebtoken", "openssl", diff --git a/Cargo.toml b/Cargo.toml index 26b7108..59be19b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ base64 = "0.22" rsa = { version = "0.9", features = ["pem", "sha2"] } pkcs8 = "0.10" sha2 = "0.10" +encoding_rs = "0.8" [dev-dependencies] tokio = { version = "1", features = ["full"] } diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index 65b6efa..de30fa3 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -800,12 +800,86 @@ pub async fn alipay_pay_page( // ===== Handler: POST /payment/notify — 支付宝异步回调 ===== +/// 解析 x-www-form-urlencoded 请求体,支持 UTF-8 和 GBK/GB2312 编码回退 +fn parse_alipay_form(body: &[u8]) -> BTreeMap { + use std::collections::BTreeMap; + + // 先尝试 UTF-8,失败则回退到 GBK/GB2312 + let body_str = match String::from_utf8(body.to_vec()) { + Ok(s) => s, + Err(_) => { + // GBK/GB2312 回退 + if let Some(enc) = encoding_rs::Encoding::for_label(b"gbk") { + let (s, _enc, had_errors) = enc.decode(body); + if had_errors { + tracing::warn!("支付宝回调 GBK 解码有部分字节失败"); + } else { + tracing::info!("支付宝回调使用 GBK 编码解码成功"); + } + s.into_owned() + } else { + // 最后兜底:lossy UTF-8 + tracing::warn!("支付宝回调编码未知,使用 lossy UTF-8 兜底"); + String::from_utf8_lossy(body).into_owned() + } + } + }; + + if body_str.is_empty() && !body.is_empty() { + tracing::warn!("支付宝回调解析结果为空(原始 {} 字节)", body.len()); + } + + // 手动解析 x-www-form-urlencoded(兼容各种 charset) + let mut map = BTreeMap::new(); + for pair in body_str.split('&') { + if let Some((k, v)) = pair.split_once('=') { + let key = urlencoding(k); + let val = urlencoding(v); + map.insert(key, val); + } + } + map +} + +/// 手动 URL 解码(percent-decoding) +fn urlencoding(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + let mut chars = s.bytes().peekable(); + let mut had_escape = false; + while let Some(b) = chars.next() { + if b == b'%' { + had_escape = true; + let hi = chars.next().and_then(|c| hex_val(c)); + let lo = chars.next().and_then(|c| hex_val(c)); + if let (Some(h), Some(l)) = (hi, lo) { + result.push((h << 4 | l) as char); + } else { + result.push('%'); + } + } else if b == b'+' && had_escape { + result.push(' '); + } else { + result.push(b as char); + } + } + result +} + +fn hex_val(c: u8) -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + b'A'..=b'F' => Some(c - b'A' + 10), + _ => None, + } +} + #[post("/payment/notify")] pub async fn alipay_notify( pool: web::Data, - body: web::Form>, + body_bytes: web::Bytes, ) -> HttpResponse { - let body = body.into_inner(); + let body = parse_alipay_form(&body_bytes); let out_trade_no = body.get("out_trade_no").cloned().unwrap_or_default(); let trade_status = body.get("trade_status").cloned().unwrap_or_default(); @@ -855,6 +929,7 @@ pub async fn alipay_notify( } } + // ===== Handler: GET /payment/success — 支付成功页面 ===== #[derive(Debug, Deserialize)]