From 4db955113cebb72242ff7a617a29f3b0058197a0 Mon Sep 17 00:00:00 2001 From: shay7sev Date: Thu, 29 Jan 2026 18:09:58 +0800 Subject: [PATCH] feat(mod): add response --- Cargo.toml | 14 +++-- README.md | 97 ++++++++++++++++++++++++++++----- src/error.rs | 29 ++++++++++ src/lib.rs | 12 +++- src/response.rs | 112 ++++++++++++++++++++++++++++++++++++++ tests/integration_test.rs | 86 ++++++++++++++++++++++++++++- types.ts | 2 + types/api-response.ts | 50 +++++++++++++++++ types/index.ts | 2 + 9 files changed, 379 insertions(+), 25 deletions(-) create mode 100644 src/response.rs create mode 100644 types.ts create mode 100644 types/api-response.ts create mode 100644 types/index.ts diff --git a/Cargo.toml b/Cargo.toml index da4062d..be92542 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common-telemetry" -version = "0.1.3" +version = "0.1.4" edition = "2024" description = "Microservice infrastructure library" @@ -10,7 +10,7 @@ publish = ["kellnr"] # 默认开启所有功能 default = ["full"] full = [ - "error", + "response", "telemetry", "with-sqlx", "with-redis", @@ -18,9 +18,11 @@ full = [ "with-validator", ] -# --- Error 模块依赖 --- -# 开启 error feature 将引入 thiserror, axum, serde -error = ["dep:thiserror", "dep:axum", "dep:serde", "dep:serde_json"] +# --- Response 模块依赖 --- +# 开启 response feature 将引入 thiserror, axum, serde +response = ["dep:thiserror", "dep:axum", "dep:serde", "dep:serde_json"] + +error = ["response"] # --- Telemetry 模块依赖 --- # 开启 telemetry feature 将引入 tracing 全家桶 @@ -34,7 +36,7 @@ with-anyhow = ["dep:anyhow"] with-validator = ["dep:validator"] [dependencies] -# Error 相关 +# Response 相关 thiserror = { version = "2.0.18", optional = true } axum = { version = "0.8.8", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/README.md b/README.md index fa6c1f3..0a7823e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # Microservice Common Lib (Rust) -这是微服务架构的通用基础库 (`common-telemetry`),旨在统一所有服务的错误处理标准、日志格式以及分布式链路追踪。 +这是微服务架构的通用基础库 (`common-telemetry`),用于统一服务的响应结构(成功/错误)、错误处理标准、日志格式以及分布式链路追踪。 ## ✨ 核心特性 -* **统一错误处理 (Error)**: - * 基于 `thiserror` 和 `anyhow` 的最佳实践。 - * **双 Token 支持**: 明确区分 `AccessTokenExpired` (20001) 和 `RefreshTokenExpired` (20002)。 - * **Axum 集成**: 实现了 `IntoResponse`,自动将错误转换为标准 JSON 格式并设置正确的 HTTP 状态码。 - * **第三方库适配**: 提供了 `sqlx`, `redis`, `validator` 的可选集成。 - * *智能转换*: 例如 `sqlx::Error::RowNotFound` 会自动转换为 **404 Not Found**,而不是 500 Database Error。 +* **统一 API 响应 (Response)**: + * **成功响应**:`AppResponse` 自动映射 HTTP 状态码(200/201/202)并返回统一 JSON 结构 `{ code, message, data?, trace_id? }`。 + * **错误响应**:`AppError` 实现 `IntoResponse`,自动映射 HTTP 状态码并返回统一 JSON 结构 `{ code, message, details?, trace_id? }`。 + * **业务码 (BizCode)**:前端可根据 `code` 做稳定分支;成功恒为 `0`。 + * **双 Token 场景**:明确区分 `AccessTokenExpired` (20001) 与 `RefreshTokenExpired` (20002)。 + * **第三方库适配**:可选集成 `sqlx` / `redis` / `validator` 的错误转换。 + * 例如:`sqlx::Error::RowNotFound` 自动转换为 **404 Not Found**。 * **可观测性 (Telemetry)**: * 基于 `tracing` 生态。 * 支持 **JSON 结构化日志** (适配 ELK/Loki)。 @@ -92,12 +93,15 @@ git push -u origin main ### 1. 引入依赖 #### 方式 A: 通过 Kellnr 引入 (如果已发布) -在 `user-service` 的 `Cargo.toml` 中,你可以根据需要开启 `sqlx` 或 `redis` 支持: +在 `user-service` 的 `Cargo.toml` 中,你可以按需选择“全功能”或“最小依赖”。 ```toml [dependencies] -# 引入基础功能 + SQLX 支持 -common-telemetry = { version = "0.1", registry = "kellnr", features = ["with-sqlx", "with-validator"] } +# 方案 1:默认全功能(等价于 features = ["full"]) +common-telemetry = { version = "0.1", registry = "kellnr" } + +# 方案 2:只启用响应模型 + SQLX/Validator(更轻量) +# common-telemetry = { version = "0.1", registry = "kellnr", default-features = false, features = ["response", "with-sqlx", "with-validator"] } # 注意:你需要确保服务本身引用的 sqlx 版本与 common-telemetry 兼容 sqlx = { version = "0.7", features = ["postgres", "runtime-tokio"] } @@ -139,6 +143,10 @@ async fn main() { } ``` +**重要:环境变量优先级** + +`telemetry::init` 会优先读取 `RUST_LOG` 环境变量;若未设置则使用 `TelemetryConfig.log_level`。 + #### B. 错误处理与第三方库集成 (Handler) 由于实现了 `From`,你可以直接使用 `?` 操作符,库会自动处理类型转换和 HTTP 映射。 @@ -175,6 +183,32 @@ async fn create_user( } ``` +#### C. 统一成功响应 (可选) + +如果你希望“成功”和“错误”都返回统一的 JSON 结构(成功包含 `code/message/data/trace_id`;错误包含 `code/message/details/trace_id`),建议让 handler 返回 `AppResponse`: + +```rust +use common_telemetry::{AppError, AppResponse}; + +async fn get_profile() -> Result, AppError> { + Ok(AppResponse::ok("profile data".to_string())) +} +``` + +#### D. 响应体格式(前后端对齐) + +成功响应(`AppResponse`): + +```json +{ "code": 0, "message": "Success", "data": { "any": "payload" }, "trace_id": null } +``` + +错误响应(`AppError`): + +```json +{ "code": 30001, "message": "Validation error: ...", "details": "field required", "trace_id": null } +``` + --- ## ⚙️ 功能模块说明 (Feature Flags) @@ -184,17 +218,19 @@ async fn create_user( | Feature | 说明 | 包含依赖 | | :--- | :--- | :--- | | **`default`** | 默认开启全功能 | `full` | -| **`full`** | 包含基础功能及所有第三方集成 | `error`, `telemetry`, `with-sqlx`, `with-redis`, `with-anyhow`, `with-validator` | -| **`error`** | 仅使用基础错误处理 | `thiserror`, `axum`, `serde` | +| **`full`** | 包含基础功能及所有第三方集成 | `response`, `telemetry`, `with-sqlx`, `with-redis`, `with-anyhow`, `with-validator` | +| **`response`** | 统一成功/错误响应 + 错误处理 | `thiserror`, `axum`, `serde` | | **`telemetry`** | 仅使用日志与链路追踪 | `tracing` 全家桶 | | **`with-sqlx`** | 集成 `sqlx` 错误转换 | `sqlx` (自动处理 RowNotFound) | | **`with-redis`** | 集成 `redis` 错误转换 | `redis` | | **`with-validator`** | 集成 `validator` 错误转换 | `validator` | | **`with-anyhow`** | 集成 `anyhow` 兜底错误 | `anyhow` | -**示例 (只用错误 + SQLX支持):** +兼容性说明:历史上的 `error` feature 已被重命名为 `response`,但仍保留 `error` 作为别名(`error = ["response"]`),以降低已有服务的迁移成本。 + +**示例 (只用响应 + SQLX支持):** ```toml -common-telemetry = { version = "0.1", default-features = false, features = ["error", "with-sqlx"] } +common-telemetry = { version = "0.1", default-features = false, features = ["response", "with-sqlx"] } ``` --- @@ -210,16 +246,26 @@ common-telemetry = { version = "0.1", default-features = false, features = ["err | `10000` | `ServerError` | 500 | 服务器内部错误 | 提示“系统繁忙” | | `10001` | `DbError` | 500 | 数据库错误 | 提示“系统繁忙” | | `10002` | `CacheError` | 500 | 缓存服务错误 | 提示“系统繁忙” | +| `10003` | `SerializationError` | 500 | 序列化/反序列化失败 | 提示“系统繁忙” | +| `10004` | `ExternalServiceError` | 500 | 下游/第三方调用失败 | 提示“系统繁忙” | +| `10005` | `ConfigError` | 500 | 配置加载失败 | 提示“系统繁忙” | | **20xxx: 认证授权** | | | | | | `20000` | `Unauthorized` | 401 | 未授权/签名无效 | 跳转登录 | | **`20001`** | **`AccessTokenExpired`** | **401** | **Access Token 过期** | **使用 Refresh Token 静默刷新** | | **`20002`** | **`RefreshTokenExpired`** | **401** | **Refresh Token 过期** | **强制登出,跳转登录页** | | `20003` | `PermissionDenied` | 403 | 权限不足 | 提示无权访问 | +| `20004` | `AccountDisabled` | 401 | 账号禁用/锁定 | 跳转登录或提示 | +| `20005` | `InvalidCredentials` | 401 | 账号或密码错误 | 提示重试 | +| `20006` | `MissingHeader` | 401 | 缺少必要 Header | 提示重试 | | **30xxx: 客户端错误** | | | | | | `30000` | `BadRequest` | 400 | 请求参数通用错误 | 提示错误信息 | | `30001` | `ValidationError` | 400 | 表单校验失败 | 提示具体字段错误 | | `30002` | `ResourceNotFound` | 404 | 资源不存在 | 提示“未找到数据” | | `30003` | `ResourceAlreadyExists`| 409 | 资源已存在 | 提示“重复创建” | +| `30004` | `MethodNotAllowed` | 405 | HTTP 方法不支持 | 提示“请求方式错误” | +| **40xxx: 业务流控/状态** | | | | | +| `40000` | `RateLimitExceeded` | 429 | 请求过于频繁 | 退避重试 | +| `40001` | `PreconditionFailed` | 400 | 业务前置条件不满足 | 提示原因 | --- @@ -230,4 +276,25 @@ common-telemetry = { version = "0.1", default-features = false, features = ["err ```bash # 使用 --nocapture 查看详细步骤打印 cargo test -- --nocapture - ``` \ No newline at end of file + ``` + +--- + +## 🧩 前端 TypeScript 类型 (ApiResponse) + +本仓库提供前后端对齐的 TypeScript 类型定义文件(非 NPM 包形式发布): + +- 入口: [types/index.ts](file:///home/shay/project/backend/common-telemetry/types/index.ts) +- 定义: [types/api-response.ts](file:///home/shay/project/backend/common-telemetry/types/api-response.ts) +- 便于引用的单文件 re-export: [types.ts](file:///home/shay/project/backend/common-telemetry/types.ts) + +前端可直接在代码库中引用(例如作为 git 子模块/直接拷贝),并使用: + +```ts +import { ApiResponse, isSuccessResponse } from "./path/to/common-telemetry/types"; + +function handle(res: ApiResponse) { + if (isSuccessResponse(res)) return res.data; + throw new Error(res.message); +} +``` diff --git a/src/error.rs b/src/error.rs index 7aa51d4..01e798b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,9 @@ pub enum BizCode { #[derive(Error, Debug)] pub enum AppError { // --- 基础设施错误 --- + #[error("Config error: {0}")] + ConfigError(String), + #[cfg(feature = "with-sqlx")] #[error("Database error: {0}")] DbError(sqlx::Error), @@ -80,6 +83,9 @@ pub enum AppError { #[error("Authentication failed: {0}")] AuthError(String), // 通用认证失败 (签名错误、格式错误) + #[error("Invalid credentials")] + InvalidCredentials, + #[error("Missing authorization header")] MissingAuthHeader, @@ -111,6 +117,9 @@ pub enum AppError { #[error("Validation error: {0}")] ValidationError(String), + #[error("Method not allowed")] + MethodNotAllowed, + // ====================================================== // 4. 业务逻辑层 (Business Logic) // ====================================================== @@ -127,6 +136,8 @@ pub struct ErrorResponse { pub code: u32, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub trace_id: Option, // 可选:返回 trace_id 方便排查 } @@ -157,6 +168,7 @@ impl AppError { // 映射 HTTP 状态码 (给网关/浏览器看) fn http_status(&self) -> StatusCode { match self { + AppError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR, #[cfg(feature = "with-sqlx")] AppError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, #[cfg(feature = "with-redis")] @@ -169,6 +181,7 @@ impl AppError { | AppError::SerdeError(_) => StatusCode::INTERNAL_SERVER_ERROR, // 401 Unauthorized AppError::AuthError(_) + | AppError::InvalidCredentials | AppError::MissingAuthHeader | AppError::AccessTokenExpired | AppError::RefreshTokenExpired @@ -186,6 +199,8 @@ impl AppError { // 429 Too Many Requests AppError::RateLimitExceeded => StatusCode::TOO_MANY_REQUESTS, + AppError::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED, + // 400 Bad Request (默认) #[cfg(feature = "with-validator")] AppError::ValidationError(_) => StatusCode::BAD_REQUEST, @@ -197,6 +212,7 @@ impl AppError { fn biz_code(&self) -> BizCode { match self { // Infra + AppError::ConfigError(_) => BizCode::ConfigError, #[cfg(feature = "with-sqlx")] AppError::DbError(_) => BizCode::DbError, #[cfg(feature = "with-redis")] @@ -209,6 +225,7 @@ impl AppError { // Auth AppError::AuthError(_) => BizCode::Unauthorized, + AppError::InvalidCredentials => BizCode::InvalidCredentials, AppError::MissingAuthHeader => BizCode::MissingHeader, AppError::AccessTokenExpired => BizCode::AccessTokenExpired, AppError::RefreshTokenExpired => BizCode::RefreshTokenExpired, @@ -221,12 +238,22 @@ impl AppError { AppError::BadRequest(_) => BizCode::BadRequest, #[cfg(feature = "with-validator")] AppError::ValidationError(_) => BizCode::ValidationError, + AppError::MethodNotAllowed => BizCode::MethodNotAllowed, // Biz AppError::RateLimitExceeded => BizCode::RateLimitExceeded, AppError::BusinessLogicError(_) => BizCode::PreconditionFailed, } } + + fn details(&self) -> Option { + match self { + AppError::BadRequest(details) => Some(serde_json::Value::String(details.clone())), + #[cfg(feature = "with-validator")] + AppError::ValidationError(details) => Some(serde_json::Value::String(details.clone())), + _ => None, + } + } } // 核心:实现 Axum 的 IntoResponse @@ -234,6 +261,7 @@ impl IntoResponse for AppError { fn into_response(self) -> Response { let status = self.http_status(); let biz_code = self.biz_code(); + let details = self.details(); // 生产环境通常不把详细的 DB 报错返回给前端,防止泄露表结构 // 但这里为了演示,我们先直接使用 self.to_string() // 建议:在生产环境针对 DbError/AnyhowError 返回统一的 "Internal Server Error" @@ -293,6 +321,7 @@ impl IntoResponse for AppError { let body = Json(ErrorResponse { code: biz_code as u32, message, + details, trace_id: None, // 实际项目中从 tracing context 获取 }); diff --git a/src/lib.rs b/src/lib.rs index db83a1d..b6e0beb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,17 @@ -// 只有开启了 'error' feature 才会编译 error 模块 -#[cfg(feature = "error")] +// 只有开启了 'response' feature 才会编译响应模块 +#[cfg(feature = "response")] pub mod error; +#[cfg(feature = "response")] +pub mod response; + // 只有开启了 'telemetry' feature 才会编译 telemetry 模块 #[cfg(feature = "telemetry")] pub mod telemetry; // 方便外部直接 use common_lib::AppError; -#[cfg(feature = "error")] +#[cfg(feature = "response")] pub use error::{AppError, BizCode}; + +#[cfg(feature = "response")] +pub use response::{AppResponse, SuccessResponse}; diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..87b0dc0 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,112 @@ +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde::Serialize; + +use crate::error::BizCode; + +#[derive(Serialize)] +pub struct SuccessResponse { + pub code: u32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub trace_id: Option, +} + +pub enum AppResponse { + Ok(SuccessResponse), + Created(SuccessResponse), + Accepted(SuccessResponse), +} + +impl AppResponse { + pub fn ok(data: T) -> Self { + Self::Ok(SuccessResponse { + code: BizCode::Success as u32, + message: "Success".into(), + data: Some(data), + trace_id: None, + }) + } + + pub fn created(data: T) -> Self { + Self::Created(SuccessResponse { + code: BizCode::Success as u32, + message: "Created".into(), + data: Some(data), + trace_id: None, + }) + } + + pub fn accepted(data: T) -> Self { + Self::Accepted(SuccessResponse { + code: BizCode::Success as u32, + message: "Accepted".into(), + data: Some(data), + trace_id: None, + }) + } + + pub fn message(mut self, message: impl Into) -> Self { + let message = message.into(); + match &mut self { + AppResponse::Ok(body) | AppResponse::Created(body) | AppResponse::Accepted(body) => { + body.message = message; + } + } + self + } + + pub fn trace_id(mut self, trace_id: impl Into) -> Self { + let trace_id = trace_id.into(); + match &mut self { + AppResponse::Ok(body) | AppResponse::Created(body) | AppResponse::Accepted(body) => { + body.trace_id = Some(trace_id); + } + } + self + } +} + +impl AppResponse<()> { + pub fn ok_empty() -> Self { + Self::Ok(SuccessResponse { + code: BizCode::Success as u32, + message: "Success".into(), + data: None, + trace_id: None, + }) + } + + pub fn created_empty() -> Self { + Self::Created(SuccessResponse { + code: BizCode::Success as u32, + message: "Created".into(), + data: None, + trace_id: None, + }) + } + + pub fn accepted_empty() -> Self { + Self::Accepted(SuccessResponse { + code: BizCode::Success as u32, + message: "Accepted".into(), + data: None, + trace_id: None, + }) + } +} + +impl IntoResponse for AppResponse { + fn into_response(self) -> Response { + match self { + AppResponse::Ok(body) => (StatusCode::OK, Json(body)).into_response(), + AppResponse::Created(body) => (StatusCode::CREATED, Json(body)).into_response(), + AppResponse::Accepted(body) => (StatusCode::ACCEPTED, Json(body)).into_response(), + } + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 6c82e3f..3a49c1c 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,6 +1,7 @@ use axum::{Router, body::Body, routing::get}; use common_telemetry::{ error::AppError, + response::AppResponse, telemetry::{self, TelemetryConfig}, }; use http::{Request, StatusCode}; @@ -28,6 +29,19 @@ async fn handler_success() -> Result { Ok("Success Data".to_string()) } +async fn handler_success_wrapped() -> Result, AppError> { + Ok(AppResponse::ok("Success Data".to_string())) +} + +async fn handler_bad_request() -> Result { + Err(AppError::BadRequest("bad params".into())) +} + +#[cfg(feature = "with-validator")] +async fn handler_validation_error() -> Result { + Err(AppError::ValidationError("field required".into())) +} + #[tokio::test] async fn test_full_flow_error_and_logging() { println!(">>> [Step 0] Test Initializing..."); @@ -55,7 +69,12 @@ async fn test_full_flow_error_and_logging() { let app = Router::new() .route("/access-expired", get(handler_access_expired)) .route("/refresh-expired", get(handler_refresh_expired)) - .route("/success", get(handler_success)); + .route("/success", get(handler_success)) + .route("/success-wrapped", get(handler_success_wrapped)) + .route("/bad-request", get(handler_bad_request)); + + #[cfg(feature = "with-validator")] + let app = app.route("/validation-error", get(handler_validation_error)); println!(">>> [Step 1] Running Case A: Access Token Expired..."); // --- 测试用例 A: Access Token 过期 --- @@ -120,6 +139,71 @@ async fn test_full_flow_error_and_logging() { .await .unwrap(); + // --- 测试用例 D: 统一成功响应格式 --- + println!(">>> [Step 4] Running Case D: Success Response Wrapper..."); + let response = app + .clone() + .oneshot( + Request::builder() + .uri("/success-wrapped") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let body: Value = serde_json::from_slice(&body_bytes).unwrap(); + assert_eq!(body["code"], 0); + assert_eq!(body["message"], "Success"); + assert_eq!(body["data"], "Success Data"); + + // --- 测试用例 E: 错误响应 details 字段 --- + println!(">>> [Step 5] Running Case E: Error Response Details..."); + let response = app + .clone() + .oneshot( + Request::builder() + .uri("/bad-request") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let body: Value = serde_json::from_slice(&body_bytes).unwrap(); + assert_eq!(body["code"], 30000); + assert_eq!(body["details"], "bad params"); + + #[cfg(feature = "with-validator")] + { + let response = app + .clone() + .oneshot( + Request::builder() + .uri("/validation-error") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let body: Value = serde_json::from_slice(&body_bytes).unwrap(); + assert_eq!(body["code"], 30001); + assert_eq!(body["details"], "field required"); + } + // 稍微等待异步日志写入磁盘 println!(" Waiting for logs to flush..."); tokio::time::sleep(Duration::from_millis(200)).await; diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..516c191 --- /dev/null +++ b/types.ts @@ -0,0 +1,2 @@ +export * from "./types"; + diff --git a/types/api-response.ts b/types/api-response.ts new file mode 100644 index 0000000..7669737 --- /dev/null +++ b/types/api-response.ts @@ -0,0 +1,50 @@ +export const BizCode = { + Success: 0, + ServerError: 10000, + DbError: 10001, + CacheError: 10002, + SerializationError: 10003, + ExternalServiceError: 10004, + ConfigError: 10005, + Unauthorized: 20000, + AccessTokenExpired: 20001, + RefreshTokenExpired: 20002, + PermissionDenied: 20003, + AccountDisabled: 20004, + InvalidCredentials: 20005, + MissingHeader: 20006, + BadRequest: 30000, + ValidationError: 30001, + ResourceNotFound: 30002, + ResourceAlreadyExists: 30003, + MethodNotAllowed: 30004, + RateLimitExceeded: 40000, + PreconditionFailed: 40001, +} as const; + +export type BizCode = (typeof BizCode)[keyof typeof BizCode]; + +export type SuccessResponse = { + code: typeof BizCode.Success; + message: string; + data?: T; + trace_id?: string; +}; + +export type ErrorResponse = { + code: Exclude | number; + message: string; + details?: unknown; + trace_id?: string; +}; + +export type ApiResponse = SuccessResponse | ErrorResponse; + +export function isSuccessResponse(res: ApiResponse): res is SuccessResponse { + return res.code === BizCode.Success; +} + +export function isErrorResponse(res: ApiResponse): res is ErrorResponse { + return res.code !== BizCode.Success; +} + diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..f1f103a --- /dev/null +++ b/types/index.ts @@ -0,0 +1,2 @@ +export * from "./api-response"; +