Skip to main content

bpi_rs/err/
error.rs

1use serde::Serialize;
2use thiserror::Error;
3
4/// 错误类型分类
5#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6pub enum ErrorCategory {
7    /// 权限认证类错误
8    Auth,
9    /// 请求参数类错误
10    Request,
11    /// 服务器类错误
12    Server,
13    /// 业务逻辑类错误
14    Business,
15    /// 网络类错误
16    Network,
17    /// 未知错误
18    Unknown,
19}
20
21#[derive(Debug, Error, Serialize)]
22pub enum BpiError {
23    /// 网络请求失败
24    #[error("网络请求失败: {message}")]
25    Network { message: String },
26
27    /// Transport-level request failure.
28    #[error("transport request failed: {source}")]
29    Transport {
30        #[serde(skip)]
31        source: reqwest::Error,
32    },
33
34    /// HTTP状态码错误
35    #[error("HTTP请求失败,状态码: {status}")]
36    Http { status: u16 },
37
38    /// HTTP status error.
39    #[error("HTTP request failed with status {status}")]
40    HttpStatus { status: u16 },
41
42    /// JSON解析失败
43    #[error("数据解析失败: {message}")]
44    Parse { message: String },
45
46    /// Response decode failure.
47    #[error("failed to decode response: {source}")]
48    Decode {
49        #[serde(skip)]
50        source: serde_json::Error,
51    },
52
53    /// API返回的业务错误
54    #[error("API错误 [{code}]: {message}")]
55    Api {
56        code: i32,
57        message: String,
58        category: ErrorCategory,
59    },
60
61    /// 验证错误
62    #[error("验证失败: {message}")]
63    Authentication { message: String },
64
65    /// Authentication or authorization error.
66    #[error("authentication failed: {message}")]
67    Auth { message: String },
68
69    /// # 参数错误
70    #[error("参数错误 [{field}]: {message}")]
71    InvalidParameter {
72        field: &'static str,
73        message: &'static str,
74    },
75
76    /// API response succeeded but did not include required payload data.
77    #[error("missing response data")]
78    MissingData,
79
80    /// Response format is not supported by the current parser.
81    #[error("unsupported response: {message}")]
82    UnsupportedResponse { message: String },
83}
84
85impl BpiError {
86    pub fn missing_csrf() -> Self {
87        BpiError::InvalidParameter {
88            field: "csrf",
89            message: "缺少CSRF",
90        }
91    }
92
93    pub fn missing_data() -> Self {
94        BpiError::MissingData
95    }
96
97    pub fn auth_required() -> Self {
98        BpiError::Auth {
99            message: "需要登录".to_string(),
100        }
101    }
102}
103
104/// 生成Error的From实现
105impl BpiError {
106    /// 根据API错误码创建BpiError
107    pub fn from_code(code: i32) -> Self {
108        let message = super::code::get_error_message(code);
109        let category = super::code::categorize_error(code);
110
111        BpiError::Api {
112            code,
113            message,
114            category,
115        }
116    }
117
118    // 不在错误码表中的API错误
119    pub fn from_code_message(code: i32, message: String) -> Self {
120        let category = super::code::categorize_error(code);
121        BpiError::Api {
122            code,
123            message,
124            category,
125        }
126    }
127
128    /// 从API响应创建BpiError
129    pub fn from_api_response<T>(resp: crate::response::ApiEnvelope<T>) -> Self {
130        if resp.code == 0 {
131            return BpiError::Api {
132                code: 0,
133                message: "API返回成功状态但被当作错误处理".to_string(),
134                category: ErrorCategory::Unknown,
135            };
136        }
137
138        if resp.message.is_empty() || resp.message == "0" {
139            Self::from_code(resp.code)
140        } else {
141            Self::from_code_message(resp.code, resp.message)
142        }
143    }
144}
145
146/// 获取错误属性
147impl BpiError {
148    /// 获取错误码
149    pub fn code(&self) -> Option<i32> {
150        match self {
151            BpiError::Api { code, .. } => Some(*code),
152            _ => None,
153        }
154    }
155
156    /// 获取 HTTP 状态码
157    pub fn http_status(&self) -> Option<u16> {
158        match self {
159            BpiError::Http { status } | BpiError::HttpStatus { status } => Some(*status),
160            _ => None,
161        }
162    }
163
164    /// 获取错误分类
165    pub fn category(&self) -> ErrorCategory {
166        match self {
167            BpiError::Api { category, .. } => category.clone(),
168            BpiError::Network { .. } => ErrorCategory::Network,
169            BpiError::Transport { .. } => ErrorCategory::Network,
170            BpiError::Http { .. } => ErrorCategory::Network,
171            BpiError::HttpStatus { .. } => ErrorCategory::Network,
172            BpiError::Parse { .. } => ErrorCategory::Request,
173            BpiError::Decode { .. } => ErrorCategory::Request,
174            BpiError::InvalidParameter { .. } => ErrorCategory::Request,
175            BpiError::Authentication { .. } => ErrorCategory::Auth,
176            BpiError::Auth { .. } => ErrorCategory::Auth,
177            BpiError::MissingData => ErrorCategory::Request,
178            BpiError::UnsupportedResponse { .. } => ErrorCategory::Request,
179        }
180    }
181}
182
183/// 错误创建函数
184impl BpiError {
185    /// 创建网络错误
186    pub fn network(message: impl Into<String>) -> Self {
187        BpiError::Network {
188            message: message.into(),
189        }
190    }
191
192    /// 创建HTTP错误
193    pub fn http(status: u16) -> Self {
194        BpiError::HttpStatus { status }
195    }
196
197    /// 创建解析错误
198    pub fn parse(message: impl Into<String>) -> Self {
199        BpiError::Parse {
200            message: message.into(),
201        }
202    }
203
204    /// 创建参数错误
205    pub fn invalid_parameter(field: &'static str, message: &'static str) -> Self {
206        BpiError::InvalidParameter { field, message }
207    }
208
209    pub fn auth(message: impl Into<String>) -> Self {
210        BpiError::Auth {
211            message: message.into(),
212        }
213    }
214
215    /// Creates an unsupported response error.
216    pub fn unsupported_response(message: impl Into<String>) -> Self {
217        BpiError::UnsupportedResponse {
218            message: message.into(),
219        }
220    }
221}
222
223/// 错误判断
224impl BpiError {
225    /// 判断是否需要用户登录
226    pub fn requires_login(&self) -> bool {
227        matches!(self.code(), Some(-101) | Some(-401) | Some(800501007))
228            || matches!(self.http_status(), Some(401))
229    }
230
231    /// 判断是否为权限问题
232    pub fn is_permission_error(&self) -> bool {
233        matches!(self.category(), ErrorCategory::Auth)
234            || matches!(self.code(), Some(-403) | Some(-4))
235            || matches!(self.http_status(), Some(403))
236    }
237
238    /// 判断是否需要VIP权限
239    pub fn requires_vip(&self) -> bool {
240        matches!(self.code(), Some(-106) | Some(-650))
241    }
242
243    /// 判断是否为风控拦截
244    pub fn is_risk_control(&self) -> bool {
245        matches!(self.code(), Some(-352) | Some(-412)) || matches!(self.http_status(), Some(412))
246    }
247
248    /// 判断是否为业务逻辑错误
249    pub fn is_business_error(&self) -> bool {
250        matches!(self.category(), ErrorCategory::Business)
251    }
252
253    /// 获取可写入合同的稳定语义错误标签
254    pub fn semantic_error(&self) -> Option<&'static str> {
255        if self.requires_login() {
256            Some("requires_login")
257        } else if self.requires_vip() {
258            Some("requires_vip")
259        } else if self.is_risk_control() {
260            Some("risk_control")
261        } else if self.is_permission_error() {
262            Some("permission_denied")
263        } else if self.is_business_error() {
264            Some("business_error")
265        } else {
266            None
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn http_status_returns_status_for_legacy_and_current_http_variants() {
277        assert_eq!(BpiError::Http { status: 412 }.http_status(), Some(412));
278        assert_eq!(BpiError::http(403).http_status(), Some(403));
279    }
280
281    #[test]
282    fn requires_login_recognizes_api_and_http_unauthorized_errors() {
283        assert!(BpiError::from_code(-101).requires_login());
284        assert!(BpiError::from_code(800501007).requires_login());
285        assert!(BpiError::http(401).requires_login());
286    }
287
288    #[test]
289    fn is_permission_error_recognizes_api_and_http_forbidden_errors() {
290        assert!(BpiError::from_code(-403).is_permission_error());
291        assert!(BpiError::http(403).is_permission_error());
292    }
293
294    #[test]
295    fn is_risk_control_recognizes_api_and_http_risk_blocks() {
296        assert!(BpiError::from_code(-352).is_risk_control());
297        assert!(BpiError::from_code(-412).is_risk_control());
298        assert!(BpiError::http(412).is_risk_control());
299    }
300
301    #[test]
302    fn semantic_error_returns_stable_contract_labels() {
303        assert_eq!(
304            BpiError::from_code(-101).semantic_error(),
305            Some("requires_login")
306        );
307        assert_eq!(
308            BpiError::from_code(-106).semantic_error(),
309            Some("requires_vip")
310        );
311        assert_eq!(
312            BpiError::from_code(-352).semantic_error(),
313            Some("risk_control")
314        );
315        assert_eq!(
316            BpiError::from_code(-403).semantic_error(),
317            Some("permission_denied")
318        );
319    }
320}