use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum ErrorCategory {
Auth,
Request,
Server,
Business,
Network,
Unknown,
}
#[derive(Debug, Error, Serialize)]
pub enum BpiError {
#[error("网络请求失败: {message}")]
Network { message: String },
#[error("transport request failed: {source}")]
Transport {
#[serde(skip)]
source: reqwest::Error,
},
#[error("HTTP请求失败,状态码: {status}")]
Http { status: u16 },
#[error("HTTP request failed with status {status}")]
HttpStatus { status: u16 },
#[error("数据解析失败: {message}")]
Parse { message: String },
#[error("failed to decode response: {source}")]
Decode {
#[serde(skip)]
source: serde_json::Error,
},
#[error("API错误 [{code}]: {message}")]
Api {
code: i32,
message: String,
category: ErrorCategory,
},
#[error("验证失败: {message}")]
Authentication { message: String },
#[error("authentication failed: {message}")]
Auth { message: String },
#[error("参数错误 [{field}]: {message}")]
InvalidParameter {
field: &'static str,
message: &'static str,
},
#[error("missing response data")]
MissingData,
#[error("unsupported response: {message}")]
UnsupportedResponse { message: String },
}
impl BpiError {
pub fn missing_csrf() -> Self {
BpiError::InvalidParameter {
field: "csrf",
message: "缺少CSRF",
}
}
pub fn missing_data() -> Self {
BpiError::MissingData
}
pub fn auth_required() -> Self {
BpiError::Auth {
message: "需要登录".to_string(),
}
}
}
impl BpiError {
pub fn from_code(code: i32) -> Self {
let message = super::code::get_error_message(code);
let category = super::code::categorize_error(code);
BpiError::Api {
code,
message,
category,
}
}
pub fn from_code_message(code: i32, message: String) -> Self {
let category = super::code::categorize_error(code);
BpiError::Api {
code,
message,
category,
}
}
pub fn from_api_response<T>(resp: crate::response::ApiEnvelope<T>) -> Self {
if resp.code == 0 {
return BpiError::Api {
code: 0,
message: "API返回成功状态但被当作错误处理".to_string(),
category: ErrorCategory::Unknown,
};
}
if resp.message.is_empty() || resp.message == "0" {
Self::from_code(resp.code)
} else {
Self::from_code_message(resp.code, resp.message)
}
}
}
impl BpiError {
pub fn code(&self) -> Option<i32> {
match self {
BpiError::Api { code, .. } => Some(*code),
_ => None,
}
}
pub fn http_status(&self) -> Option<u16> {
match self {
BpiError::Http { status } | BpiError::HttpStatus { status } => Some(*status),
_ => None,
}
}
pub fn category(&self) -> ErrorCategory {
match self {
BpiError::Api { category, .. } => category.clone(),
BpiError::Network { .. } => ErrorCategory::Network,
BpiError::Transport { .. } => ErrorCategory::Network,
BpiError::Http { .. } => ErrorCategory::Network,
BpiError::HttpStatus { .. } => ErrorCategory::Network,
BpiError::Parse { .. } => ErrorCategory::Request,
BpiError::Decode { .. } => ErrorCategory::Request,
BpiError::InvalidParameter { .. } => ErrorCategory::Request,
BpiError::Authentication { .. } => ErrorCategory::Auth,
BpiError::Auth { .. } => ErrorCategory::Auth,
BpiError::MissingData => ErrorCategory::Request,
BpiError::UnsupportedResponse { .. } => ErrorCategory::Request,
}
}
}
impl BpiError {
pub fn network(message: impl Into<String>) -> Self {
BpiError::Network {
message: message.into(),
}
}
pub fn http(status: u16) -> Self {
BpiError::HttpStatus { status }
}
pub fn parse(message: impl Into<String>) -> Self {
BpiError::Parse {
message: message.into(),
}
}
pub fn invalid_parameter(field: &'static str, message: &'static str) -> Self {
BpiError::InvalidParameter { field, message }
}
pub fn auth(message: impl Into<String>) -> Self {
BpiError::Auth {
message: message.into(),
}
}
pub fn unsupported_response(message: impl Into<String>) -> Self {
BpiError::UnsupportedResponse {
message: message.into(),
}
}
}
impl BpiError {
pub fn requires_login(&self) -> bool {
matches!(self.code(), Some(-101) | Some(-401) | Some(800501007))
|| matches!(self.http_status(), Some(401))
}
pub fn is_permission_error(&self) -> bool {
matches!(self.category(), ErrorCategory::Auth)
|| matches!(self.code(), Some(-403) | Some(-4))
|| matches!(self.http_status(), Some(403))
}
pub fn requires_vip(&self) -> bool {
matches!(self.code(), Some(-106) | Some(-650))
}
pub fn is_risk_control(&self) -> bool {
matches!(self.code(), Some(-352) | Some(-412)) || matches!(self.http_status(), Some(412))
}
pub fn is_business_error(&self) -> bool {
matches!(self.category(), ErrorCategory::Business)
}
pub fn semantic_error(&self) -> Option<&'static str> {
if self.requires_login() {
Some("requires_login")
} else if self.requires_vip() {
Some("requires_vip")
} else if self.is_risk_control() {
Some("risk_control")
} else if self.is_permission_error() {
Some("permission_denied")
} else if self.is_business_error() {
Some("business_error")
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_status_returns_status_for_legacy_and_current_http_variants() {
assert_eq!(BpiError::Http { status: 412 }.http_status(), Some(412));
assert_eq!(BpiError::http(403).http_status(), Some(403));
}
#[test]
fn requires_login_recognizes_api_and_http_unauthorized_errors() {
assert!(BpiError::from_code(-101).requires_login());
assert!(BpiError::from_code(800501007).requires_login());
assert!(BpiError::http(401).requires_login());
}
#[test]
fn is_permission_error_recognizes_api_and_http_forbidden_errors() {
assert!(BpiError::from_code(-403).is_permission_error());
assert!(BpiError::http(403).is_permission_error());
}
#[test]
fn is_risk_control_recognizes_api_and_http_risk_blocks() {
assert!(BpiError::from_code(-352).is_risk_control());
assert!(BpiError::from_code(-412).is_risk_control());
assert!(BpiError::http(412).is_risk_control());
}
#[test]
fn semantic_error_returns_stable_contract_labels() {
assert_eq!(
BpiError::from_code(-101).semantic_error(),
Some("requires_login")
);
assert_eq!(
BpiError::from_code(-106).semantic_error(),
Some("requires_vip")
);
assert_eq!(
BpiError::from_code(-352).semantic_error(),
Some("risk_control")
);
assert_eq!(
BpiError::from_code(-403).semantic_error(),
Some("permission_denied")
);
}
}