Skip to main content

sz_rust_core/
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_core::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)
244pub fn url_decode(s: &str) -> String {
245    let mut result = String::with_capacity(s.len());
246    let mut chars = s.chars().peekable();
247
248    while let Some(c) = chars.next() {
249        match c {
250            '+' => result.push(' '),
251            '%' => {
252                let h1 = chars.next();
253                let h2 = chars.next();
254                if let (Some(a), Some(b)) = (h1, h2) {
255                    if let Ok(byte) = u8::from_str_radix(&format!("{a}{b}"), 16) {
256                        result.push(byte as char);
257                    } else {
258                        result.push('%');
259                        result.push(a);
260                        result.push(b);
261                    }
262                } else {
263                    result.push('%');
264                }
265            }
266            _ => result.push(c),
267        }
268    }
269
270    result
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use axum::http::{Method, Request, StatusCode};
277
278    fn make_json_request(body: &str, query: Option<&str>) -> Request<Body> {
279        let uri = match query {
280            Some(q) => format!("/?{q}"),
281            None => "/".to_string(),
282        };
283        Request::builder()
284            .method(Method::POST)
285            .uri(&uri)
286            .header("content-type", "application/json")
287            .body(Body::from(body.to_string()))
288            .unwrap()
289    }
290
291    fn make_form_request(body: &str, query: Option<&str>) -> Request<Body> {
292        let uri = match query {
293            Some(q) => format!("/?{q}"),
294            None => "/".to_string(),
295        };
296        Request::builder()
297            .method(Method::POST)
298            .uri(&uri)
299            .header("content-type", "application/x-www-form-urlencoded")
300            .body(Body::from(body.to_string()))
301            .unwrap()
302    }
303
304    // ====================================================================
305    // parse_query / url_decode 单元测试
306    // ====================================================================
307
308    #[test]
309    fn test_parse_query_empty() {
310        let m = parse_query("");
311        assert!(m.is_empty());
312    }
313
314    #[test]
315    fn test_parse_query_single_pair() {
316        let m = parse_query("key=value");
317        assert_eq!(m.get("key"), Some(&"value".to_string()));
318        assert_eq!(m.len(), 1);
319    }
320
321    #[test]
322    fn test_parse_query_multiple_pairs() {
323        let m = parse_query("a=1&b=2&c=3");
324        assert_eq!(m.get("a"), Some(&"1".to_string()));
325        assert_eq!(m.get("b"), Some(&"2".to_string()));
326        assert_eq!(m.get("c"), Some(&"3".to_string()));
327    }
328
329    #[test]
330    fn test_parse_query_no_value() {
331        let m = parse_query("key");
332        assert_eq!(m.get("key"), Some(&"".to_string()));
333    }
334
335    #[test]
336    fn test_parse_query_url_encoded() {
337        let m = parse_query("name=hello%20world&email=a%40b.com");
338        assert_eq!(m.get("name"), Some(&"hello world".to_string()));
339        assert_eq!(m.get("email"), Some(&"a@b.com".to_string()));
340    }
341
342    #[test]
343    fn test_parse_query_plus_for_space() {
344        let m = parse_query("q=hello+world");
345        assert_eq!(m.get("q"), Some(&"hello world".to_string()));
346    }
347
348    #[test]
349    fn test_parse_query_skip_empty_pairs() {
350        let m = parse_query("a=1&&b=2&");
351        assert_eq!(m.len(), 2);
352        assert_eq!(m.get("a"), Some(&"1".to_string()));
353        assert_eq!(m.get("b"), Some(&"2".to_string()));
354    }
355
356    #[test]
357    fn test_url_decode_basic() {
358        assert_eq!(url_decode("hello"), "hello");
359        assert_eq!(url_decode("hello%20world"), "hello world");
360        assert_eq!(url_decode("a%40b"), "a@b");
361        assert_eq!(url_decode("a+b"), "a b");
362    }
363
364    #[test]
365    fn test_url_decode_trailing_percent() {
366        // 末尾单独的 % 应当保留
367        assert_eq!(url_decode("100%"), "100%");
368    }
369
370    // ====================================================================
371    // fetch_post_data 集成测试
372    // ====================================================================
373
374    #[tokio::test]
375    async fn test_fetch_post_data_json_body_only() {
376        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
377        let data = fetch_post_data(req).await.unwrap();
378        assert_eq!(data["name"], "alice");
379        assert_eq!(data["age"], 30);
380    }
381
382    #[tokio::test]
383    async fn test_fetch_post_data_query_only() {
384        let req = make_json_request("", Some("page=1&size=10"));
385        let data = fetch_post_data(req).await.unwrap();
386        assert_eq!(data["page"], "1");
387        assert_eq!(data["size"], "10");
388    }
389
390    #[tokio::test]
391    async fn test_fetch_post_data_body_overrides_query() {
392        // body 与 query 同 key 时,body 优先
393        let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
394        let data = fetch_post_data(req).await.unwrap();
395        assert_eq!(data["page"], 99); // 来自 body
396        assert_eq!(data["size"], "10"); // 来自 query
397    }
398
399    #[tokio::test]
400    async fn test_fetch_post_data_form_urlencoded() {
401        let req = make_form_request("name=bob&age=25", None);
402        let data = fetch_post_data(req).await.unwrap();
403        assert_eq!(data["name"], "bob");
404        assert_eq!(data["age"], "25");
405    }
406
407    #[tokio::test]
408    async fn test_fetch_post_data_empty_body() {
409        let req = make_json_request("", None);
410        let data = fetch_post_data(req).await.unwrap();
411        assert!(data.as_object().unwrap().is_empty());
412    }
413
414    #[tokio::test]
415    async fn test_fetch_post_data_invalid_json() {
416        let req = make_json_request("{invalid}", None);
417        let result = fetch_post_data(req).await;
418        assert!(result.is_err());
419    }
420
421    #[tokio::test]
422    async fn test_fetch_post_data_by_key() {
423        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
424        let name = fetch_post_data_by_key(req, "name").await.unwrap();
425        assert_eq!(name, Some(Value::String("alice".to_string())));
426    }
427
428    #[tokio::test]
429    async fn test_fetch_post_data_by_key_missing() {
430        let req = make_json_request(r#"{"name":"alice"}"#, None);
431        let age = fetch_post_data_by_key(req, "age").await.unwrap();
432        assert_eq!(age, None);
433    }
434
435    #[tokio::test]
436    async fn test_fetch_post_data_array_value_in_body() {
437        let req = make_json_request(r#"{"ids":[1,2,3]}"#, None);
438        let data = fetch_post_data(req).await.unwrap();
439        assert_eq!(data["ids"], serde_json::json!([1, 2, 3]));
440    }
441
442    #[tokio::test]
443    async fn test_fetch_post_data_nested_object_in_body() {
444        let req = make_json_request(r#"{"user":{"name":"alice","age":30}}"#, None);
445        let data = fetch_post_data(req).await.unwrap();
446        assert_eq!(data["user"]["name"], "alice");
447        assert_eq!(data["user"]["age"], 30);
448    }
449
450    // ====================================================================
451    // fetch_body_data 单元测试
452    // ====================================================================
453
454    #[tokio::test]
455    async fn test_fetch_body_data_json() {
456        let req = make_json_request(r#"{"name":"alice"}"#, Some("ignored=1"));
457        let data = fetch_body_data(req).await.unwrap();
458        assert_eq!(data["name"], "alice");
459        // query 应当被忽略
460        assert!(data.get("ignored").is_none());
461    }
462
463    #[tokio::test]
464    async fn test_fetch_body_data_empty() {
465        let req = make_json_request("", None);
466        let data = fetch_body_data(req).await.unwrap();
467        assert!(data.as_object().unwrap().is_empty());
468    }
469
470    // ====================================================================
471    // fetch_query_data 单元测试
472    // ====================================================================
473
474    #[test]
475    fn test_fetch_query_data_basic() {
476        let req = Request::builder()
477            .method(Method::GET)
478            .uri("/?page=1&size=10")
479            .body(Body::empty())
480            .unwrap();
481        let data = fetch_query_data(&req);
482        assert_eq!(data["page"], "1");
483        assert_eq!(data["size"], "10");
484    }
485
486    #[test]
487    fn test_fetch_query_data_no_query() {
488        let req = Request::builder()
489            .method(Method::GET)
490            .uri("/")
491            .body(Body::empty())
492            .unwrap();
493        let data = fetch_query_data(&req);
494        assert!(data.as_object().unwrap().is_empty());
495    }
496
497    #[test]
498    fn test_fetch_query_data_by_key_found() {
499        let req = Request::builder()
500            .method(Method::GET)
501            .uri("/?page=1&size=10")
502            .body(Body::empty())
503            .unwrap();
504        assert_eq!(
505            fetch_query_data_by_key(&req, "page"),
506            Some(Value::String("1".to_string()))
507        );
508        assert_eq!(
509            fetch_query_data_by_key(&req, "size"),
510            Some(Value::String("10".to_string()))
511        );
512    }
513
514    #[test]
515    fn test_fetch_query_data_by_key_not_found() {
516        let req = Request::builder()
517            .method(Method::GET)
518            .uri("/?page=1")
519            .body(Body::empty())
520            .unwrap();
521        assert_eq!(fetch_query_data_by_key(&req, "missing"), None);
522    }
523
524    // ====================================================================
525    // 集成测试:完整请求流程
526    // ====================================================================
527
528    #[tokio::test]
529    async fn test_post_data_via_axum_handler() {
530        use axum::routing::post;
531        use tower::ServiceExt;
532
533        async fn handler(req: Request<Body>) -> (StatusCode, String) {
534            let data = fetch_post_data(req).await.unwrap();
535            let name = data["name"].as_str().unwrap_or("unknown");
536            let age = data["age"].as_i64().unwrap_or(0);
537            (StatusCode::OK, format!("{name} is {age}"))
538        }
539
540        let router = axum::Router::new().route("/", post(handler));
541
542        let req = Request::builder()
543            .method(Method::POST)
544            .uri("/")
545            .header("content-type", "application/json")
546            .body(Body::from(r#"{"name":"alice","age":30}"#))
547            .unwrap();
548        let resp = router.oneshot(req).await.unwrap();
549        assert_eq!(resp.status(), StatusCode::OK);
550
551        use http_body_util::BodyExt;
552        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
553        assert_eq!(&bytes[..], b"alice is 30");
554    }
555
556    // ====================================================================
557    // PHP 一致性测试(R5: PHP/Rust 行为对比)
558    //
559    // 对齐 PHP `SzController::postData($key = null)` 与 `getData($key = null)`:
560    // - `postData()` 调用 `$this->request->param('')`,合并 route + GET + POST,
561    //   POST 优先级最高;ThinkPHP 8 自动解析 application/json body 到 POST 参数
562    // - `postData($key)` 调用 `param($key.'/a')`,返回单字段值(强制数组类型,
563    //   Rust 中用 `Option<Value>` 代替,调用方自行处理类型)
564    // - `getData()` 调用 `$this->request->get('')`,仅返回 query 参数
565    // - `getData($key)` 调用 `get($key)`,返回单字段值
566    // ====================================================================
567
568    #[tokio::test]
569    async fn test_php_consistency_post_data_merges_body_and_query() {
570        // PHP `postData()`:body 优先级 > query(对齐 ThinkPHP `param()` 合并顺序)
571        // 场景:body 含 page=99,query 含 page=1&size=10
572        // 预期:page 来自 body(99),size 来自 query("10")
573        let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
574        let data = fetch_post_data(req).await.unwrap();
575        assert_eq!(data["page"], 99, "body 应覆盖 query 同名字段");
576        assert_eq!(data["size"], "10", "query 字段应保留");
577    }
578
579    #[tokio::test]
580    async fn test_php_consistency_post_data_form_urlencoded_body() {
581        // PHP `postData()`:application/x-www-form-urlencoded body 也应被解析
582        // 场景:form-urlencoded body 含 name=bob&age=25
583        // 预期:name="bob", age="25"(form 字段值类型为字符串)
584        let req = make_form_request("name=bob&age=25", None);
585        let data = fetch_post_data(req).await.unwrap();
586        assert_eq!(data["name"], "bob");
587        assert_eq!(data["age"], "25");
588    }
589
590    #[tokio::test]
591    async fn test_php_consistency_post_data_by_key_returns_value() {
592        // PHP `postData($key)`:返回单字段值(对齐 `param($key.'/a')`)
593        // 场景:body 含 name=alice&age=30,取 name
594        // 预期:Some("alice")
595        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
596        let name = fetch_post_data_by_key(req, "name").await.unwrap();
597        assert_eq!(name, Some(Value::String("alice".to_string())));
598    }
599
600    #[test]
601    fn test_php_consistency_get_data_returns_only_query() {
602        // PHP `getData()`:仅返回 query 参数(对齐 `$this->request->get('')`)
603        // 场景:GET 请求含 page=1&size=10
604        // 预期:返回 {page:"1", size:"10"},不包含 body
605        let req = Request::builder()
606            .method(Method::GET)
607            .uri("/?page=1&size=10")
608            .body(Body::empty())
609            .unwrap();
610        let data = fetch_query_data(&req);
611        assert_eq!(data["page"], "1");
612        assert_eq!(data["size"], "10");
613        // 不应有 body 字段
614        assert!(data.get("body").is_none());
615    }
616
617    #[test]
618    fn test_php_consistency_get_data_by_key_returns_query_value() {
619        // PHP `getData($key)`:返回 query 中单字段值(对齐 `get($key)`)
620        // 场景:GET 请求含 page=1&size=10
621        // 预期:page=Some("1"), missing=None
622        let req = Request::builder()
623            .method(Method::GET)
624            .uri("/?page=1&size=10")
625            .body(Body::empty())
626            .unwrap();
627        assert_eq!(
628            fetch_query_data_by_key(&req, "page"),
629            Some(Value::String("1".to_string()))
630        );
631        assert_eq!(fetch_query_data_by_key(&req, "missing"), None);
632    }
633}