use serde_json::Value;
use thiserror::Error;
pub type WxPayResult<T> = Result<T, WxPayError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WxPayErrorKind {
InvalidParameter,
Authentication,
Signature,
ResourceNotFound,
RateLimited,
BusinessBlocked,
Internal,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WxPayAlertLevel {
Low,
Medium,
High,
Critical,
}
impl WxPayAlertLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Critical => "critical",
}
}
}
impl WxPayErrorKind {
pub const fn as_str(&self) -> &'static str {
match self {
Self::InvalidParameter => "invalid_parameter",
Self::Authentication => "authentication",
Self::Signature => "signature",
Self::ResourceNotFound => "resource_not_found",
Self::RateLimited => "rate_limited",
Self::BusinessBlocked => "business_blocked",
Self::Internal => "internal",
Self::Unknown => "unknown",
}
}
pub fn from_code(code: &str) -> Self {
match code {
"PARAM_ERROR" | "INVALID_REQUEST" | "INVALID_PARAMETER" => Self::InvalidParameter,
"NO_AUTH" | "SIGN_ERROR" | "INVALID_SIGN" | "PERMISSION_DENIED" | "AUTH_ERROR"
| "INVALID_CREDENTIAL" => Self::Authentication,
"SIGNATURE_ERROR" | "VERIFY_SIGNATURE_ERROR" => Self::Signature,
"ORDER_NOT_EXIST" | "NOT_FOUND" | "RESOURCE_NOT_FOUND" => Self::ResourceNotFound,
"FREQ_LIMIT" | "RATE_LIMIT" | "V2_API_DISABLED" => Self::RateLimited,
"NO_AUTHORITY" | "NOT_PERMIT" | "ILLEGAL_REQUEST" => Self::BusinessBlocked,
"SYSTEM_ERROR" | "SERVICE_UNAVAILABLE" => Self::Internal,
_ => Self::Unknown,
}
}
}
impl From<WxPayErrorKind> for WxPayAlertLevel {
fn from(kind: WxPayErrorKind) -> Self {
match kind {
WxPayErrorKind::Authentication | WxPayErrorKind::Signature => Self::Critical,
WxPayErrorKind::RateLimited | WxPayErrorKind::BusinessBlocked => Self::High,
WxPayErrorKind::Internal => Self::High,
WxPayErrorKind::ResourceNotFound => Self::Medium,
WxPayErrorKind::InvalidParameter | WxPayErrorKind::Unknown => Self::Low,
}
}
}
#[derive(Error, Debug)]
pub enum WxPayError {
#[error("配置错误:{message}")]
ConfigError { message: String },
#[error("无效的私钥:{0}")]
InvalidPrivateKey(String),
#[error("无效的证书:{0}")]
InvalidCertificate(String),
#[error("缺少必填配置项:{field}")]
MissingConfig { field: String },
#[error("签名生成失败:{0}")]
SignError(String),
#[error("签名验证失败")]
SignatureVerificationFailed,
#[error("无效的签名格式:{0}")]
InvalidSignatureFormat(String),
#[error("加密失败:{0}")]
EncryptionError(String),
#[error("解密失败:{0}")]
DecryptionError(String),
#[error("无效的密钥:{0}")]
InvalidKey(String),
#[error("无效的密文格式:{0}")]
InvalidCiphertext(String),
#[error("证书下载失败:{0}")]
CertificateDownloadError(String),
#[error("证书解析失败:{0}")]
CertificateParseError(String),
#[error("证书已过期")]
CertificateExpired,
#[error("证书验证失败:{0}")]
CertificateVerificationError(String),
#[error("找不到匹配的证书:serial_number={0}")]
CertificateNotFound(String),
#[error("网络错误:{0}")]
NetworkError(#[from] reqwest::Error),
#[error("HTTP 请求构建失败:{0}")]
RequestBuildError(String),
#[error("HTTP 响应解析失败:{0}")]
ResponseParseError(String),
#[error("请求超时")]
Timeout,
#[error("API 错误:code={code}, message={message}")]
ApiError {
code: String,
message: String,
},
#[error("意外的 HTTP 状态码:{0}")]
UnexpectedStatusCode(u16),
#[error("业务错误:{0}")]
BusinessError(String),
#[error("通知签名验证失败")]
NotifySignatureVerificationFailed,
#[error("通知解密失败:{0}")]
NotifyDecryptionError(String),
#[error("无效的通知格式:{0}")]
InvalidNotifyFormat(String),
#[error("无效的通知类型:{0}")]
InvalidNotifyType(String),
#[error("JSON 错误:{0}")]
JsonError(#[from] serde_json::Error),
#[error("URL 编码错误:{0}")]
UrlEncodeError(String),
#[error("URL 解析错误:{0}")]
UrlParseError(#[from] url::ParseError),
#[error("内部错误:{0}")]
InternalError(String),
#[error("不支持的操作:{0}")]
UnsupportedOperation(String),
#[error("参数错误:{0}")]
InvalidParameter(String),
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct ErrorResponse {
pub code: String,
pub message: String,
pub detail: Option<Value>,
}
impl WxPayError {
pub fn config(message: impl Into<String>) -> Self {
Self::ConfigError {
message: message.into(),
}
}
pub fn missing_config(field: impl Into<String>) -> Self {
Self::MissingConfig {
field: field.into(),
}
}
pub fn sign(message: impl Into<String>) -> Self {
Self::SignError(message.into())
}
pub fn encryption(message: impl Into<String>) -> Self {
Self::EncryptionError(message.into())
}
pub fn decryption(message: impl Into<String>) -> Self {
Self::DecryptionError(message.into())
}
pub fn certificate_parse(message: impl Into<String>) -> Self {
Self::CertificateParseError(message.into())
}
pub fn certificate_download(message: impl Into<String>) -> Self {
Self::CertificateDownloadError(message.into())
}
pub fn certificate_verification(message: impl Into<String>) -> Self {
Self::CertificateVerificationError(message.into())
}
pub fn api(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::ApiError {
code: code.into(),
message: message.into(),
}
}
pub fn internal(message: impl Into<String>) -> Self {
Self::InternalError(message.into())
}
pub fn invalid_parameter(message: impl Into<String>) -> Self {
Self::InvalidParameter(message.into())
}
pub fn business(message: impl Into<String>) -> Self {
Self::BusinessError(message.into())
}
pub fn api_kind(&self) -> Option<WxPayErrorKind> {
match self {
Self::ApiError { code, .. } => Some(WxPayErrorKind::from_code(code)),
_ => None,
}
}
pub fn api_code(&self) -> Option<&str> {
match self {
Self::ApiError { code, .. } => Some(code.as_str()),
_ => None,
}
}
pub fn alert_level(&self) -> WxPayAlertLevel {
match self {
Self::NetworkError(_) | Self::Timeout => WxPayAlertLevel::Critical,
Self::ApiError { code, .. } => WxPayErrorKind::from_code(code).into(),
Self::CertificateExpired
| Self::CertificateVerificationError(_)
| Self::CertificateDownloadError(_)
| Self::CertificateNotFound(_)
| Self::CertificateParseError(_)
| Self::SignatureVerificationFailed => WxPayAlertLevel::High,
Self::UnexpectedStatusCode(status) => {
if *status >= 500 {
WxPayAlertLevel::High
} else {
WxPayAlertLevel::Medium
}
}
Self::SignError(_)
| Self::InvalidSignatureFormat(_)
| Self::RequestBuildError(_)
| Self::ResponseParseError(_)
| Self::JsonError(_)
| Self::BusinessError(_) => WxPayAlertLevel::High,
Self::InternalError(_) | Self::EncryptionError(_) | Self::DecryptionError(_) => {
WxPayAlertLevel::Medium
}
_ => WxPayAlertLevel::Low,
}
}
pub fn alert_policy(&self) -> &'static str {
match self {
Self::ApiError { code, .. } => match WxPayErrorKind::from_code(code) {
WxPayErrorKind::Authentication => "security.auth",
WxPayErrorKind::Signature => "security.signature",
WxPayErrorKind::RateLimited => "business.ratelimit",
WxPayErrorKind::ResourceNotFound => "business.notfound",
WxPayErrorKind::BusinessBlocked => "business.blocked",
WxPayErrorKind::InvalidParameter => "params.invalid",
WxPayErrorKind::Internal => "system.internal",
WxPayErrorKind::Unknown => "unknown",
},
Self::NetworkError(_) | Self::Timeout => "network",
Self::SignatureVerificationFailed
| Self::SignError(_)
| Self::InvalidSignatureFormat(_) => "security.signature",
Self::CertificateExpired
| Self::CertificateVerificationError(_)
| Self::CertificateParseError(_)
| Self::CertificateDownloadError(_)
| Self::CertificateNotFound(_) => "certificate",
Self::UnexpectedStatusCode(status) if *status >= 500 => "system.internal",
Self::UnexpectedStatusCode(status) if *status >= 400 => "business.http",
Self::UnexpectedStatusCode(_) => "business.http",
Self::InternalError(_) => "system.internal",
_ => "unknown",
}
}
pub fn should_retry(&self) -> bool {
match self {
Self::NetworkError(_) | Self::Timeout => true,
Self::UnexpectedStatusCode(status) if *status >= 500 => true,
Self::ApiError { code, .. } => matches!(
WxPayErrorKind::from_code(code),
WxPayErrorKind::RateLimited | WxPayErrorKind::Internal
),
_ => false,
}
}
pub fn is_network_error(&self) -> bool {
matches!(self, Self::NetworkError(_) | Self::Timeout)
}
pub fn is_api_error(&self) -> bool {
matches!(self, Self::ApiError { .. })
}
pub fn is_auth_error(&self) -> bool {
matches!(
self.api_kind(),
Some(WxPayErrorKind::Authentication | WxPayErrorKind::Signature)
)
}
pub fn is_signature_error(&self) -> bool {
matches!(
self,
Self::SignError(_)
| Self::SignatureVerificationFailed
| Self::InvalidSignatureFormat(_)
)
}
pub fn is_certificate_error(&self) -> bool {
matches!(
self,
Self::CertificateExpired
| Self::CertificateNotFound(_)
| Self::CertificateParseError(_)
| Self::CertificateDownloadError(_)
| Self::CertificateVerificationError(_)
)
}
}
impl From<base64::DecodeError> for WxPayError {
fn from(err: base64::DecodeError) -> Self {
Self::InternalError(format!("Base64 解码错误:{}", err))
}
}
impl From<rsa::Error> for WxPayError {
fn from(err: rsa::Error) -> Self {
Self::SignError(format!("RSA 错误:{}", err))
}
}
impl From<pkcs8::Error> for WxPayError {
fn from(err: pkcs8::Error) -> Self {
Self::InvalidPrivateKey(format!("PKCS8 错误:{}", err))
}
}
impl From<der::Error> for WxPayError {
fn from(err: der::Error) -> Self {
Self::CertificateParseError(format!("DER 解码错误:{}", err))
}
}
impl From<chrono::ParseError> for WxPayError {
fn from(err: chrono::ParseError) -> Self {
Self::InternalError(format!("时间解析错误:{}", err))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = WxPayError::config("missing app_id");
assert_eq!(err.to_string(), "配置错误:missing app_id");
let err = WxPayError::api("PARAM_ERROR", "参数错误");
assert_eq!(
err.to_string(),
"API 错误:code=PARAM_ERROR, message=参数错误"
);
}
#[test]
fn test_error_classification() {
let err = WxPayError::Timeout;
assert!(err.is_network_error());
assert!(!err.is_api_error());
assert!(matches!(err.alert_level(), WxPayAlertLevel::Critical));
assert_eq!(err.alert_policy(), "network");
assert!(err.should_retry());
let err = WxPayError::api("ERROR", "msg");
assert!(err.is_api_error());
assert!(!err.is_network_error());
assert_eq!(err.alert_policy(), "unknown");
let err = WxPayError::SignatureVerificationFailed;
assert!(err.is_signature_error());
assert_eq!(err.alert_policy(), "security.signature");
let err = WxPayError::CertificateExpired;
assert!(err.is_certificate_error());
}
}