fix: Alipay回调手动解析表单支持GBK/GB2312编码回退
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2341,6 +2341,7 @@ dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"encoding_rs",
|
||||
"include_dir",
|
||||
"jsonwebtoken",
|
||||
"openssl",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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<String, String> {
|
||||
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<u8> {
|
||||
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<PgPool>,
|
||||
body: web::Form<BTreeMap<String, String>>,
|
||||
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)]
|
||||
|
||||
Reference in New Issue
Block a user