1use serde::Serialize;
2use thiserror::Error;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6pub enum ErrorCategory {
7 Auth,
9 Request,
11 Server,
13 Business,
15 Network,
17 Unknown,
19}
20
21#[derive(Debug, Error, Serialize)]
22pub enum BpiError {
23 #[error("网络请求失败: {message}")]
25 Network { message: String },
26
27 #[error("transport request failed: {source}")]
29 Transport {
30 #[serde(skip)]
31 source: reqwest::Error,
32 },
33
34 #[error("HTTP请求失败,状态码: {status}")]
36 Http { status: u16 },
37
38 #[error("HTTP request failed with status {status}")]
40 HttpStatus { status: u16 },
41
42 #[error("数据解析失败: {message}")]
44 Parse { message: String },
45
46 #[error("failed to decode response: {source}")]
48 Decode {
49 #[serde(skip)]
50 source: serde_json::Error,
51 },
52
53 #[error("API错误 [{code}]: {message}")]
55 Api {
56 code: i32,
57 message: String,
58 category: ErrorCategory,
59 },
60
61 #[error("验证失败: {message}")]
63 Authentication { message: String },
64
65 #[error("authentication failed: {message}")]
67 Auth { message: String },
68
69 #[error("参数错误 [{field}]: {message}")]
71 InvalidParameter {
72 field: &'static str,
73 message: &'static str,
74 },
75
76 #[error("missing response data")]
78 MissingData,
79
80 #[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
104impl BpiError {
106 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 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 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
146impl BpiError {
148 pub fn code(&self) -> Option<i32> {
150 match self {
151 BpiError::Api { code, .. } => Some(*code),
152 _ => None,
153 }
154 }
155
156 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 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
183impl BpiError {
185 pub fn network(message: impl Into<String>) -> Self {
187 BpiError::Network {
188 message: message.into(),
189 }
190 }
191
192 pub fn http(status: u16) -> Self {
194 BpiError::HttpStatus { status }
195 }
196
197 pub fn parse(message: impl Into<String>) -> Self {
199 BpiError::Parse {
200 message: message.into(),
201 }
202 }
203
204 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 pub fn unsupported_response(message: impl Into<String>) -> Self {
217 BpiError::UnsupportedResponse {
218 message: message.into(),
219 }
220 }
221}
222
223impl BpiError {
225 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 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 pub fn requires_vip(&self) -> bool {
240 matches!(self.code(), Some(-106) | Some(-650))
241 }
242
243 pub fn is_risk_control(&self) -> bool {
245 matches!(self.code(), Some(-352) | Some(-412)) || matches!(self.http_status(), Some(412))
246 }
247
248 pub fn is_business_error(&self) -> bool {
250 matches!(self.category(), ErrorCategory::Business)
251 }
252
253 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}