Skip to main content

sz_rust_http_facade/
request.rs

1//! 请求模块 — postData/getData/file/upload
2//!
3//! 对齐 PHP `$this->request->post()` / `$this->request->get()` / `$this->request->param()`。
4//! 强制 POST,不使用 GET 分支(遵循项目规范)。
5//!
6//! ## PHP 对齐
7//!
8//! | PHP 方法 | 行为 | Rust 等价 |
9//! |---------|------|-----------|
10//! | `$this->request->param()` | 合并 POST + GET + route 参数 | [`fetch_post_data`](合并 body + query) |
11//! | `$this->request->post()` | 仅 POST body | [`fetch_body_data`] |
12//! | `$this->request->get()` | 仅 GET query | [`fetch_query_data`] |
13//! | `postData($key)` | `param($key.'/a')`(强制数组) | [`fetch_post_data_by_key`] |
14//! | `getData($key)` | `get($key)` | [`fetch_query_data_by_key`] |
15//!
16//! ## 注意
17//!
18//! - `param()` 在 PHP 中还会合并 route 参数(`{id}` 等),在 axum 中这些由 handler 参数捕获,
19//!   所以本模块仅合并 body + query。
20//! - `/a` 强制数组在 Rust 中不适用(强类型语言),调用方需自行处理类型转换。
21//! - 本模块提供低层数据获取;上层控制器在 controller 模块实现 `postData()` 方法时封装。
22//!
23//! ## 用法
24//!
25//! ```ignore
26//! use sz_rust_http_facade::request::fetch_post_data;
27//! use axum::http::Request;
28//! use axum::body::Body;
29//!
30//! async fn handler(req: Request<Body>) {
31//!     // 合并 body + query
32//!     let data = fetch_post_data(req).await.unwrap();
33//!     println!("{}", data);
34//! }
35//! ```
36
37use std::collections::HashMap;
38
39use axum::body::Body;
40use axum::http::Request;
41use http_body_util::BodyExt;
42use serde_json::{Map, Value};
43
44/// 从请求中获取合并参数(body + query)
45///
46/// 对齐 PHP `$this->request->param()`:合并 POST body 和 GET query,
47/// body 中的字段优先级高于 query。
48///
49/// ## 参数
50///
51/// - `req`:`axum::http::Request<Body>`
52///
53/// ## 返回
54///
55/// - `Ok(Value::Object)`:合并后的 JSON Object
56/// - `Err(String)`:body 不可读或 JSON 解析失败
57///
58/// ## 行为
59///
60/// 1. 解析 query string 为 `Value::Object`(每个键值对均为字符串)
61/// 2. 读取 body bytes
62/// 3. 如果 Content-Type 为 `application/json`,解析 body 为 JSON 并合并到 query
63/// 4. 如果 Content-Type 为 `application/x-www-form-urlencoded`,解析为表单并合并
64/// 5. body 字段覆盖 query 字段
65pub async fn fetch_post_data(req: Request<Body>) -> Result<Value, String> {
66    // 1. 解析 query
67    let query_map = parse_query(req.uri().query().unwrap_or(""));
68
69    // 2. 读取 body
70    let (parts, body) = req.into_parts();
71    let bytes = body
72        .collect()
73        .await
74        .map_err(|e| format!("read body failed: {e}"))?
75        .to_bytes();
76
77    // 3. 根据 Content-Type 解析 body
78    let content_type = parts
79        .headers
80        .get(axum::http::header::CONTENT_TYPE)
81        .and_then(|v| v.to_str().ok())
82        .unwrap_or("")
83        .to_lowercase();
84
85    let mut result = Map::new();
86
87    // query 先写入(低优先级)
88    for (k, v) in query_map {
89        result.insert(k, Value::String(v));
90    }
91
92    // body 后写入(高优先级,覆盖 query)
93    if !bytes.is_empty() {
94        if content_type.contains("application/json") {
95            let body_value: Value =
96                serde_json::from_slice(&bytes).map_err(|e| format!("invalid JSON body: {e}"))?;
97            if let Value::Object(body_map) = body_value {
98                for (k, v) in body_map {
99                    result.insert(k, v);
100                }
101            } else {
102                // 非 object 的 body,整体作为 "data" 字段
103                result.insert("data".to_string(), body_value);
104            }
105        } else if content_type.contains("application/x-www-form-urlencoded") {
106            let body_str = String::from_utf8_lossy(&bytes);
107            let body_map = parse_query(&body_str);
108            for (k, v) in body_map {
109                result.insert(k, Value::String(v));
110            }
111        } else {
112            // 未知 Content-Type:尝试当 JSON 解析,失败则作为 raw 字符串
113            if let Ok(body_value) = serde_json::from_slice::<Value>(&bytes) {
114                if let Value::Object(body_map) = body_value {
115                    for (k, v) in body_map {
116                        result.insert(k, v);
117                    }
118                } else {
119                    result.insert("data".to_string(), body_value);
120                }
121            } else {
122                // 当作 raw 字符串
123                let raw = String::from_utf8_lossy(&bytes).to_string();
124                if !raw.is_empty() {
125                    result.insert("data".to_string(), Value::String(raw));
126                }
127            }
128        }
129    }
130
131    Ok(Value::Object(result))
132}
133
134/// 从请求中获取单个合并参数(body + query)
135///
136/// 对齐 PHP `postData($key)`:返回单个字段的值。
137pub async fn fetch_post_data_by_key(
138    req: Request<Body>,
139    key: &str,
140) -> Result<Option<Value>, String> {
141    let data = fetch_post_data(req).await?;
142    Ok(data.get(key).cloned())
143}
144
145/// 从请求中仅获取 body 参数(不含 query)
146///
147/// 对齐 PHP `$this->request->post()`。
148pub async fn fetch_body_data(req: Request<Body>) -> Result<Value, String> {
149    let (parts, body) = req.into_parts();
150    let bytes = body
151        .collect()
152        .await
153        .map_err(|e| format!("read body failed: {e}"))?
154        .to_bytes();
155
156    let content_type = parts
157        .headers
158        .get(axum::http::header::CONTENT_TYPE)
159        .and_then(|v| v.to_str().ok())
160        .unwrap_or("")
161        .to_lowercase();
162
163    let mut result = Map::new();
164
165    if bytes.is_empty() {
166        return Ok(Value::Object(result));
167    }
168
169    if content_type.contains("application/json") {
170        let body_value: Value =
171            serde_json::from_slice(&bytes).map_err(|e| format!("invalid JSON body: {e}"))?;
172        if let Value::Object(body_map) = body_value {
173            for (k, v) in body_map {
174                result.insert(k, v);
175            }
176        } else {
177            result.insert("data".to_string(), body_value);
178        }
179    } else if content_type.contains("application/x-www-form-urlencoded") {
180        let body_str = String::from_utf8_lossy(&bytes);
181        let body_map = parse_query(&body_str);
182        for (k, v) in body_map {
183            result.insert(k, Value::String(v));
184        }
185    } else if let Ok(body_value) = serde_json::from_slice::<Value>(&bytes) {
186        if let Value::Object(body_map) = body_value {
187            for (k, v) in body_map {
188                result.insert(k, v);
189            }
190        } else {
191            result.insert("data".to_string(), body_value);
192        }
193    } else {
194        let raw = String::from_utf8_lossy(&bytes).to_string();
195        result.insert("data".to_string(), Value::String(raw));
196    }
197
198    Ok(Value::Object(result))
199}
200
201/// 从请求中仅获取 query 参数
202///
203/// 对齐 PHP `$this->request->get()` / `getData()`。
204pub fn fetch_query_data(req: &Request<Body>) -> Value {
205    let query_map = parse_query(req.uri().query().unwrap_or(""));
206    let mut result = Map::new();
207    for (k, v) in query_map {
208        result.insert(k, Value::String(v));
209    }
210    Value::Object(result)
211}
212
213/// 从请求中仅获取 query 参数的单个字段
214///
215/// 对齐 PHP `getData($key)`。
216pub fn fetch_query_data_by_key(req: &Request<Body>, key: &str) -> Option<Value> {
217    let query_map = parse_query(req.uri().query().unwrap_or(""));
218    query_map.get(key).map(|v| Value::String(v.clone()))
219}
220
221/// 解析 query string 为 `HashMap<String, String>`
222///
223/// 支持 `key=value&key2=value2` 格式,URL 解码。
224pub fn parse_query(query: &str) -> HashMap<String, String> {
225    let mut result = HashMap::new();
226    if query.is_empty() {
227        return result;
228    }
229
230    for pair in query.split('&') {
231        if pair.is_empty() {
232            continue;
233        }
234        let mut split = pair.splitn(2, '=');
235        let key = url_decode(split.next().unwrap_or(""));
236        let value = url_decode(split.next().unwrap_or(""));
237        result.insert(key, value);
238    }
239
240    result
241}
242
243/// 简单 URL 解码(支持 %XX 与 + → space)
244///
245/// 对齐 PHP `urldecode` 语义:`%XX` 按字节解码后整体按 UTF-8 还原,
246/// 支持多字节字符(如中文 `%E9%B2%9C` → `鲜`)。
247/// 无效 UTF-8 字节序列以 U+FFFD 替换(容错,不 panic)。
248pub fn url_decode(s: &str) -> String {
249    let mut bytes: Vec<u8> = Vec::with_capacity(s.len());
250    let mut chars = s.chars().peekable();
251
252    while let Some(c) = chars.next() {
253        match c {
254            '+' => bytes.push(b' '),
255            '%' => {
256                let h1 = chars.next();
257                let h2 = chars.next();
258                if let (Some(a), Some(b)) = (h1, h2) {
259                    if let Ok(byte) = u8::from_str_radix(&format!("{a}{b}"), 16) {
260                        bytes.push(byte);
261                    } else {
262                        // 非法十六进制:保留原样
263                        bytes.push(b'%');
264                        push_char_utf8(&mut bytes, a);
265                        push_char_utf8(&mut bytes, b);
266                    }
267                } else {
268                    bytes.push(b'%');
269                }
270            }
271            _ => push_char_utf8(&mut bytes, c),
272        }
273    }
274
275    String::from_utf8_lossy(&bytes).into_owned()
276}
277
278/// 将字符按 UTF-8 编码追加到字节缓冲
279fn push_char_utf8(bytes: &mut Vec<u8>, c: char) {
280    let mut buf = [0u8; 4];
281    bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use axum::http::{Method, Request, StatusCode};
288
289    fn make_json_request(body: &str, query: Option<&str>) -> Request<Body> {
290        let uri = match query {
291            Some(q) => format!("/?{q}"),
292            None => "/".to_string(),
293        };
294        Request::builder()
295            .method(Method::POST)
296            .uri(&uri)
297            .header("content-type", "application/json")
298            .body(Body::from(body.to_string()))
299            .unwrap()
300    }
301
302    fn make_form_request(body: &str, query: Option<&str>) -> Request<Body> {
303        let uri = match query {
304            Some(q) => format!("/?{q}"),
305            None => "/".to_string(),
306        };
307        Request::builder()
308            .method(Method::POST)
309            .uri(&uri)
310            .header("content-type", "application/x-www-form-urlencoded")
311            .body(Body::from(body.to_string()))
312            .unwrap()
313    }
314
315    // ====================================================================
316    // parse_query / url_decode 单元测试
317    // ====================================================================
318
319    #[test]
320    fn test_parse_query_empty() {
321        let m = parse_query("");
322        assert!(m.is_empty());
323    }
324
325    #[test]
326    fn test_parse_query_single_pair() {
327        let m = parse_query("key=value");
328        assert_eq!(m.get("key"), Some(&"value".to_string()));
329        assert_eq!(m.len(), 1);
330    }
331
332    #[test]
333    fn test_parse_query_multiple_pairs() {
334        let m = parse_query("a=1&b=2&c=3");
335        assert_eq!(m.get("a"), Some(&"1".to_string()));
336        assert_eq!(m.get("b"), Some(&"2".to_string()));
337        assert_eq!(m.get("c"), Some(&"3".to_string()));
338    }
339
340    #[test]
341    fn test_parse_query_no_value() {
342        let m = parse_query("key");
343        assert_eq!(m.get("key"), Some(&"".to_string()));
344    }
345
346    #[test]
347    fn test_parse_query_url_encoded() {
348        let m = parse_query("name=hello%20world&email=a%40b.com");
349        assert_eq!(m.get("name"), Some(&"hello world".to_string()));
350        assert_eq!(m.get("email"), Some(&"a@b.com".to_string()));
351    }
352
353    #[test]
354    fn test_parse_query_plus_for_space() {
355        let m = parse_query("q=hello+world");
356        assert_eq!(m.get("q"), Some(&"hello world".to_string()));
357    }
358
359    #[test]
360    fn test_parse_query_skip_empty_pairs() {
361        let m = parse_query("a=1&&b=2&");
362        assert_eq!(m.len(), 2);
363        assert_eq!(m.get("a"), Some(&"1".to_string()));
364        assert_eq!(m.get("b"), Some(&"2".to_string()));
365    }
366
367    #[test]
368    fn test_url_decode_basic() {
369        assert_eq!(url_decode("hello"), "hello");
370        assert_eq!(url_decode("hello%20world"), "hello world");
371        assert_eq!(url_decode("a%40b"), "a@b");
372        assert_eq!(url_decode("a+b"), "a b");
373    }
374
375    #[test]
376    fn test_url_decode_utf8_multibyte() {
377        // 对齐 PHP urldecode:%XX 字节序列按 UTF-8 还原(多字节中文)
378        assert_eq!(url_decode("%E9%B2%9C%E8%A7%86%E8%BE%BE"), "鲜视达");
379        assert_eq!(url_decode("%E5%B7%A5%E5%85%B7%E7%AE%B1"), "工具箱");
380        // 混合:普通字符 + 中文编码 + 空格
381        assert_eq!(
382            url_decode("q=%E9%B2%9C%E8%A7%86%E8%BE%BE+plus"),
383            "q=鲜视达 plus"
384        );
385    }
386
387    #[test]
388    fn test_url_decode_invalid_utf8_lossy() {
389        // 无效 UTF-8 序列不 panic,替换为 U+FFFD(容错语义)
390        let decoded = url_decode("%FF%FE");
391        assert!(decoded.contains('\u{FFFD}'));
392    }
393
394    #[test]
395    fn test_url_decode_trailing_percent() {
396        // 末尾单独的 % 应当保留
397        assert_eq!(url_decode("100%"), "100%");
398    }
399
400    // ====================================================================
401    // fetch_post_data 集成测试
402    // ====================================================================
403
404    #[tokio::test]
405    async fn test_fetch_post_data_json_body_only() {
406        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
407        let data = fetch_post_data(req).await.unwrap();
408        assert_eq!(data["name"], "alice");
409        assert_eq!(data["age"], 30);
410    }
411
412    #[tokio::test]
413    async fn test_fetch_post_data_query_only() {
414        let req = make_json_request("", Some("page=1&size=10"));
415        let data = fetch_post_data(req).await.unwrap();
416        assert_eq!(data["page"], "1");
417        assert_eq!(data["size"], "10");
418    }
419
420    #[tokio::test]
421    async fn test_fetch_post_data_body_overrides_query() {
422        // body 与 query 同 key 时,body 优先
423        let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
424        let data = fetch_post_data(req).await.unwrap();
425        assert_eq!(data["page"], 99); // 来自 body
426        assert_eq!(data["size"], "10"); // 来自 query
427    }
428
429    #[tokio::test]
430    async fn test_fetch_post_data_form_urlencoded() {
431        let req = make_form_request("name=bob&age=25", None);
432        let data = fetch_post_data(req).await.unwrap();
433        assert_eq!(data["name"], "bob");
434        assert_eq!(data["age"], "25");
435    }
436
437    #[tokio::test]
438    async fn test_fetch_post_data_empty_body() {
439        let req = make_json_request("", None);
440        let data = fetch_post_data(req).await.unwrap();
441        assert!(data.as_object().unwrap().is_empty());
442    }
443
444    #[tokio::test]
445    async fn test_fetch_post_data_invalid_json() {
446        let req = make_json_request("{invalid}", None);
447        let result = fetch_post_data(req).await;
448        assert!(result.is_err());
449    }
450
451    #[tokio::test]
452    async fn test_fetch_post_data_by_key() {
453        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
454        let name = fetch_post_data_by_key(req, "name").await.unwrap();
455        assert_eq!(name, Some(Value::String("alice".to_string())));
456    }
457
458    #[tokio::test]
459    async fn test_fetch_post_data_by_key_missing() {
460        let req = make_json_request(r#"{"name":"alice"}"#, None);
461        let age = fetch_post_data_by_key(req, "age").await.unwrap();
462        assert_eq!(age, None);
463    }
464
465    #[tokio::test]
466    async fn test_fetch_post_data_array_value_in_body() {
467        let req = make_json_request(r#"{"ids":[1,2,3]}"#, None);
468        let data = fetch_post_data(req).await.unwrap();
469        assert_eq!(data["ids"], serde_json::json!([1, 2, 3]));
470    }
471
472    #[tokio::test]
473    async fn test_fetch_post_data_nested_object_in_body() {
474        let req = make_json_request(r#"{"user":{"name":"alice","age":30}}"#, None);
475        let data = fetch_post_data(req).await.unwrap();
476        assert_eq!(data["user"]["name"], "alice");
477        assert_eq!(data["user"]["age"], 30);
478    }
479
480    // ====================================================================
481    // fetch_body_data 单元测试
482    // ====================================================================
483
484    #[tokio::test]
485    async fn test_fetch_body_data_json() {
486        let req = make_json_request(r#"{"name":"alice"}"#, Some("ignored=1"));
487        let data = fetch_body_data(req).await.unwrap();
488        assert_eq!(data["name"], "alice");
489        // query 应当被忽略
490        assert!(data.get("ignored").is_none());
491    }
492
493    #[tokio::test]
494    async fn test_fetch_body_data_empty() {
495        let req = make_json_request("", None);
496        let data = fetch_body_data(req).await.unwrap();
497        assert!(data.as_object().unwrap().is_empty());
498    }
499
500    // ====================================================================
501    // fetch_query_data 单元测试
502    // ====================================================================
503
504    #[test]
505    fn test_fetch_query_data_basic() {
506        let req = Request::builder()
507            .method(Method::GET)
508            .uri("/?page=1&size=10")
509            .body(Body::empty())
510            .unwrap();
511        let data = fetch_query_data(&req);
512        assert_eq!(data["page"], "1");
513        assert_eq!(data["size"], "10");
514    }
515
516    #[test]
517    fn test_fetch_query_data_no_query() {
518        let req = Request::builder()
519            .method(Method::GET)
520            .uri("/")
521            .body(Body::empty())
522            .unwrap();
523        let data = fetch_query_data(&req);
524        assert!(data.as_object().unwrap().is_empty());
525    }
526
527    #[test]
528    fn test_fetch_query_data_by_key_found() {
529        let req = Request::builder()
530            .method(Method::GET)
531            .uri("/?page=1&size=10")
532            .body(Body::empty())
533            .unwrap();
534        assert_eq!(
535            fetch_query_data_by_key(&req, "page"),
536            Some(Value::String("1".to_string()))
537        );
538        assert_eq!(
539            fetch_query_data_by_key(&req, "size"),
540            Some(Value::String("10".to_string()))
541        );
542    }
543
544    #[test]
545    fn test_fetch_query_data_by_key_not_found() {
546        let req = Request::builder()
547            .method(Method::GET)
548            .uri("/?page=1")
549            .body(Body::empty())
550            .unwrap();
551        assert_eq!(fetch_query_data_by_key(&req, "missing"), None);
552    }
553
554    // ====================================================================
555    // 集成测试:完整请求流程
556    // ====================================================================
557
558    #[tokio::test]
559    async fn test_post_data_via_axum_handler() {
560        use axum::routing::post;
561        use tower::ServiceExt;
562
563        async fn handler(req: Request<Body>) -> (StatusCode, String) {
564            let data = fetch_post_data(req).await.unwrap();
565            let name = data["name"].as_str().unwrap_or("unknown");
566            let age = data["age"].as_i64().unwrap_or(0);
567            (StatusCode::OK, format!("{name} is {age}"))
568        }
569
570        let router = axum::Router::new().route("/", post(handler));
571
572        let req = Request::builder()
573            .method(Method::POST)
574            .uri("/")
575            .header("content-type", "application/json")
576            .body(Body::from(r#"{"name":"alice","age":30}"#))
577            .unwrap();
578        let resp = router.oneshot(req).await.unwrap();
579        assert_eq!(resp.status(), StatusCode::OK);
580
581        use http_body_util::BodyExt;
582        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
583        assert_eq!(&bytes[..], b"alice is 30");
584    }
585
586    // ====================================================================
587    // PHP 一致性测试(R5: PHP/Rust 行为对比)
588    //
589    // 对齐 PHP `SzController::postData($key = null)` 与 `getData($key = null)`:
590    // - `postData()` 调用 `$this->request->param('')`,合并 route + GET + POST,
591    //   POST 优先级最高;ThinkPHP 8 自动解析 application/json body 到 POST 参数
592    // - `postData($key)` 调用 `param($key.'/a')`,返回单字段值(强制数组类型,
593    //   Rust 中用 `Option<Value>` 代替,调用方自行处理类型)
594    // - `getData()` 调用 `$this->request->get('')`,仅返回 query 参数
595    // - `getData($key)` 调用 `get($key)`,返回单字段值
596    // ====================================================================
597
598    #[tokio::test]
599    async fn test_php_consistency_post_data_merges_body_and_query() {
600        // PHP `postData()`:body 优先级 > query(对齐 ThinkPHP `param()` 合并顺序)
601        // 场景:body 含 page=99,query 含 page=1&size=10
602        // 预期:page 来自 body(99),size 来自 query("10")
603        let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
604        let data = fetch_post_data(req).await.unwrap();
605        assert_eq!(data["page"], 99, "body 应覆盖 query 同名字段");
606        assert_eq!(data["size"], "10", "query 字段应保留");
607    }
608
609    #[tokio::test]
610    async fn test_php_consistency_post_data_form_urlencoded_body() {
611        // PHP `postData()`:application/x-www-form-urlencoded body 也应被解析
612        // 场景:form-urlencoded body 含 name=bob&age=25
613        // 预期:name="bob", age="25"(form 字段值类型为字符串)
614        let req = make_form_request("name=bob&age=25", None);
615        let data = fetch_post_data(req).await.unwrap();
616        assert_eq!(data["name"], "bob");
617        assert_eq!(data["age"], "25");
618    }
619
620    #[tokio::test]
621    async fn test_php_consistency_post_data_by_key_returns_value() {
622        // PHP `postData($key)`:返回单字段值(对齐 `param($key.'/a')`)
623        // 场景:body 含 name=alice&age=30,取 name
624        // 预期:Some("alice")
625        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
626        let name = fetch_post_data_by_key(req, "name").await.unwrap();
627        assert_eq!(name, Some(Value::String("alice".to_string())));
628    }
629
630    #[test]
631    fn test_php_consistency_get_data_returns_only_query() {
632        // PHP `getData()`:仅返回 query 参数(对齐 `$this->request->get('')`)
633        // 场景:GET 请求含 page=1&size=10
634        // 预期:返回 {page:"1", size:"10"},不包含 body
635        let req = Request::builder()
636            .method(Method::GET)
637            .uri("/?page=1&size=10")
638            .body(Body::empty())
639            .unwrap();
640        let data = fetch_query_data(&req);
641        assert_eq!(data["page"], "1");
642        assert_eq!(data["size"], "10");
643        // 不应有 body 字段
644        assert!(data.get("body").is_none());
645    }
646
647    #[test]
648    fn test_php_consistency_get_data_by_key_returns_query_value() {
649        // PHP `getData($key)`:返回 query 中单字段值(对齐 `get($key)`)
650        // 场景:GET 请求含 page=1&size=10
651        // 预期:page=Some("1"), missing=None
652        let req = Request::builder()
653            .method(Method::GET)
654            .uri("/?page=1&size=10")
655            .body(Body::empty())
656            .unwrap();
657        assert_eq!(
658            fetch_query_data_by_key(&req, "page"),
659            Some(Value::String("1".to_string()))
660        );
661        assert_eq!(fetch_query_data_by_key(&req, "missing"), None);
662    }
663}