Skip to main content

alun_core/
api.rs

1//! 框架核心 API 类型:响应体、错误、分页数据、错误码
2//!
3//! 这些类型是框架的公共"语言",所有 crate 都依赖它们。
4//! `IntoResponse` 实现通过 `features = ["web"]` 开启。
5
6use crate::Error;
7use serde::Serialize;
8
9#[cfg(feature = "axum")]
10use axum::{
11    response::{IntoResponse, Json},
12    http::StatusCode,
13};
14
15// ──── 错误码常量 ────────────────────────────────────
16
17pub mod codes {
18    pub const OK: i32 = 0;
19    pub const BAD_REQUEST: i32 = 400;
20    pub const UNAUTHORIZED: i32 = 401;
21    pub const FORBIDDEN: i32 = 403;
22    pub const NOT_FOUND: i32 = 404;
23    pub const METHOD_NOT_ALLOWED: i32 = 405;
24    pub const CONFLICT: i32 = 409;
25    pub const UNPROCESSABLE_ENTITY: i32 = 422;
26    pub const TOO_MANY_REQUESTS: i32 = 429;
27    pub const INTERNAL: i32 = 500;
28    pub const SERVICE_UNAVAILABLE: i32 = 503;
29}
30
31// ──── 统一响应体 ─────────────────────────────────────
32
33/// 统一 API 响应结构
34#[derive(Debug, Clone, Serialize)]
35pub struct Res<T: Serialize = ()> {
36    /// 业务码,0 表示成功
37    pub code: i32,
38    /// 提示信息
39    pub msg: String,
40    /// 数据载荷
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub data: Option<T>,
43}
44
45/// 分页数据结构
46#[derive(Debug, Clone, Serialize)]
47pub struct PageData<T: Serialize> {
48    /// 数据列表
49    pub list: T,
50    /// 总条数
51    pub total: u64,
52    /// 当前页码
53    pub page: u64,
54    /// 每页条数
55    pub page_size: u64,
56}
57
58/// API 响应结果类型
59pub type ResResult<T> = std::result::Result<Res<T>, ApiError>;
60
61// ──── 分页参数 ──────────────────────────────────────
62
63/// 分页查询参数(公共类型,所有 crate 可用)
64#[derive(Debug, Clone, serde::Deserialize)]
65pub struct PageQuery {
66    /// 页码(从 1 开始)
67    pub page: u64,
68    /// 每页条数
69    pub page_size: u64,
70}
71
72impl PageQuery {
73    /// 创建分页参数,自动规整到合法范围
74    ///
75    /// - `page`: 页码,最小为 1
76    /// - `page_size`: 每页条数,范围 [1, 1000]
77    ///
78    /// 超出范围的值会被自动修正到边界。
79    pub fn new(page: u64, page_size: u64) -> Self {
80        let page = if page < 1 { 1 } else { page };
81        let page_size = if page_size < 1 { 10 } else if page_size > 1000 { 1000 } else { page_size };
82        Self { page, page_size }
83    }
84
85    /// 计算 SQL OFFSET:`(page - 1) * page_size`
86    pub fn offset(&self) -> u64 { (self.page - 1) * self.page_size }
87    /// 获取 LIMIT 值(即 `page_size`)
88    pub fn limit(&self) -> u64 { self.page_size }
89}
90
91// ──── Res 实现 ──────────────────────────────────────
92
93impl Res<()> {
94    /// 成功(无数据载荷),返回 `{code: 0, msg: "ok", data: null}`
95    pub fn ok_empty() -> Self {
96        Self { code: codes::OK, msg: "ok".into(), data: None }
97    }
98
99    /// 成功(自定义消息,无数据载荷)
100    pub fn ok_msg(msg: impl Into<String>) -> Self {
101        Self { code: codes::OK, msg: msg.into(), data: None }
102    }
103}
104
105impl<T: Serialize> Res<T> {
106    /// 成功响应,携带数据载荷
107    ///
108    /// # 示例
109    ///
110    /// ```ignore
111    /// Res::ok(user)          // => {code: 0, msg: "ok", data: user}
112    /// Res::ok("hello")       // => {code: 0, msg: "ok", data: "hello"}
113    /// ```
114    pub fn ok(data: T) -> Self {
115        Self { code: codes::OK, msg: "ok".into(), data: Some(data) }
116    }
117
118    /// 成功响应,携带数据载荷和自定义消息
119    pub fn ok_with_msg(data: T, msg: impl Into<String>) -> Self {
120        Self { code: codes::OK, msg: msg.into(), data: Some(data) }
121    }
122
123    /// 失败响应(自定义错误码和消息,无数据载荷)
124    ///
125    /// # 示例
126    ///
127    /// ```ignore
128    /// Res::fail(codes::BAD_REQUEST, "用户名不能为空")
129    /// ```
130    pub fn fail(code: i32, msg: impl Into<String>) -> Self {
131        Self { code, msg: msg.into(), data: None }
132    }
133}
134
135impl<T: Serialize> Res<PageData<T>> {
136    /// 分页响应
137    pub fn page(list: T, total: u64, page: u64, page_size: u64) -> Self {
138        Self::ok(PageData { list, total, page, page_size })
139    }
140}
141
142// ──── API 错误 ──────────────────────────────────────
143
144/// API 错误(对外暴露的统一错误类型)
145///
146/// HTTP 状态码使用 u16 存储,与 Web 框架解耦。
147#[derive(Debug)]
148pub struct ApiError {
149    /// 业务码
150    pub code: i32,
151    /// 对外消息(已脱敏,不泄露内部信息)
152    pub msg: String,
153    /// HTTP 状态码(u16,与 axum::StatusCode 互转)
154    pub status: u16,
155    /// 内部调试信息(仅日志记录,不返回前端)
156    pub internal_detail: Option<String>,
157}
158
159impl ApiError {
160    /// 创建 API 错误
161    ///
162    /// - `status`: HTTP 状态码(如 400、401、500)
163    /// - `code`: 业务错误码
164    /// - `msg`: 对外提示消息
165    pub fn new(status: u16, code: i32, msg: impl Into<String>) -> Self {
166        Self { status, code, msg: msg.into(), internal_detail: None }
167    }
168
169    /// 附加内部调试详情(仅写入日志,不暴露给前端)
170    fn with_detail(mut self, detail: impl Into<String>) -> Self {
171        self.internal_detail = Some(detail.into());
172        self
173    }
174
175    // ── 工厂方法 ──
176
177    /// 400 Bad Request:客户端请求格式或参数错误
178    pub fn bad_request(msg: impl Into<String>) -> Self {
179        Self::new(400, codes::BAD_REQUEST, msg)
180    }
181
182    /// 401 Unauthorized:未认证或 Token 无效
183    pub fn unauthorized(msg: impl Into<String>) -> Self {
184        Self::new(401, codes::UNAUTHORIZED, msg)
185    }
186
187    /// 403 Forbidden:已认证但权限不足
188    pub fn forbidden(msg: impl Into<String>) -> Self {
189        Self::new(403, codes::FORBIDDEN, msg)
190    }
191
192    /// 404 Not Found:资源不存在
193    pub fn not_found(msg: impl Into<String>) -> Self {
194        Self::new(404, codes::NOT_FOUND, msg)
195    }
196
197    /// 405 Method Not Allowed:HTTP 方法不正确
198    pub fn method_not_allowed(msg: impl Into<String>) -> Self {
199        Self::new(405, codes::METHOD_NOT_ALLOWED, msg)
200    }
201
202    /// 409 Conflict:资源冲突(如重复创建)
203    pub fn conflict(msg: impl Into<String>) -> Self {
204        Self::new(409, codes::CONFLICT, msg)
205    }
206
207    /// 422 Unprocessable Entity:请求体语义错误(如字段校验失败)
208    pub fn unprocessable_entity(msg: impl Into<String>) -> Self {
209        Self::new(422, codes::UNPROCESSABLE_ENTITY, msg)
210    }
211
212    /// 429 Too Many Requests:请求频率超限
213    pub fn too_many_requests(msg: impl Into<String>) -> Self {
214        Self::new(429, codes::TOO_MANY_REQUESTS, msg)
215    }
216
217    /// 500 Internal Server Error:服务端内部错误
218    ///
219    /// 前端仅看到模糊提示;完整错误需通过日志排查。
220    pub fn internal(msg: impl Into<String>) -> Self {
221        Self::new(500, codes::INTERNAL, msg)
222    }
223
224    /// 500 Internal Server Error(带调试详情)
225    ///
226    /// - `public_msg`: 返回前端的模糊提示
227    /// - `detail`: 内部日志记录的详细信息(如完整的错误栈)
228    pub fn internal_masked(public_msg: impl Into<String>, detail: impl Into<String>) -> Self {
229        Self::new(500, codes::INTERNAL, public_msg)
230            .with_detail(detail)
231    }
232
233    /// 503 Service Unavailable:服务暂时不可用
234    pub fn service_unavailable(msg: impl Into<String>) -> Self {
235        Self::new(503, codes::SERVICE_UNAVAILABLE, msg)
236    }
237}
238
239impl From<Error> for ApiError {
240    fn from(e: Error) -> Self {
241        ApiError::internal_masked("服务器内部错误", e.to_string())
242    }
243}
244
245// ──── IntoResponse 实现(需 web feature) ───────────
246
247#[cfg(feature = "axum")]
248impl<T: Serialize> IntoResponse for Res<T> {
249    fn into_response(self) -> axum::response::Response {
250        let mut resp = Json(self).into_response();
251        resp.headers_mut().insert(
252            axum::http::HeaderName::from_static("content-type"),
253            axum::http::HeaderValue::from_static("application/json; charset=utf-8"),
254        );
255        resp
256    }
257}
258
259#[cfg(feature = "axum")]
260impl IntoResponse for ApiError {
261    fn into_response(self) -> axum::response::Response {
262        if let Some(ref detail) = self.internal_detail {
263            let status = StatusCode::from_u16(self.status)
264                .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
265            tracing::error!(status = status.as_u16(), code = self.code, detail = %detail,
266                "请求处理异常");
267        }
268        let body = Res::<()>::fail(self.code, self.msg);
269        let status = StatusCode::from_u16(self.status)
270            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
271        let mut resp = (status, Json(body)).into_response();
272        resp.headers_mut().insert(
273            axum::http::HeaderName::from_static("content-type"),
274            axum::http::HeaderValue::from_static("application/json; charset=utf-8"),
275        );
276        resp
277    }
278}
279
280