"""错误码与异常。错误码一旦对外发布就**不许改号**(客户端靠它做分支)。""" from __future__ import annotations from enum import IntEnum from typing import Any class ErrorCode(IntEnum): OK = 0 # 1xxx 通用 BAD_REQUEST = 1001 VALIDATION_FAILED = 1002 NOT_FOUND = 1004 CONFLICT = 1009 PAYLOAD_TOO_LARGE = 1013 RATE_LIMITED = 1029 INTERNAL = 1500 # 2xxx 认证 / 账号 UNAUTHORIZED = 2001 TOKEN_EXPIRED = 2002 TOKEN_INVALID = 2003 SMS_CODE_INVALID = 2010 SMS_TOO_FREQUENT = 2011 ACCOUNT_DISABLED = 2012 PASSWORD_INVALID = 2013 PASSWORD_NOT_SET = 2014 PASSWORD_WEAK = 2015 LOGIN_TOO_MANY = 2016 # 3xxx 租户 / 权限 WORKSPACE_NOT_FOUND = 3001 NOT_A_MEMBER = 3003 INSUFFICIENT_ROLE = 3004 ALREADY_A_MEMBER = 3005 INVITATION_INVALID = 3010 # 4xxx 并发 VERSION_CONFLICT = 4009 # 5xxx 任务域 ITEM_NOT_FOUND = 5001 ITEM_STATUS_INVALID = 5002 TAG_NOT_FOUND = 5003 LINK_NOT_FOUND = 5004 LINK_CONFLICT = 5005 # 6xxx 工单域 TICKET_NOT_FOUND = 6001 TICKET_ALREADY_EXISTS = 6002 TICKET_STATUS_INVALID = 6003 TICKET_FORBIDDEN = 6004 NOTIFICATION_NOT_FOUND = 6005 # 7xxx 知识域 KNOWLEDGE_NOT_FOUND = 7001 KNOWLEDGE_ALREADY_EXISTS = 7002 KNOWLEDGE_STATUS_INVALID = 7003 KNOWLEDGE_FORBIDDEN = 7004 KNOWLEDGE_SOURCE_REQUIRED = 7005 KNOWLEDGE_CATEGORY_NOT_FOUND = 7006 KNOWLEDGE_CATEGORY_DEPTH = 7007 KNOWLEDGE_CATEGORY_CONFLICT = 7008 # 8xxx 附件 ATTACHMENT_NOT_FOUND = 8001 ATTACHMENT_TOO_LARGE = 8002 ATTACHMENT_FORBIDDEN = 8003 CONFIRM_REQUIRED = 8004 # 9xxx 客户端发布(OTA) APP_RELEASE_NOT_FOUND = 9001 # 默认 HTTP 状态:业务码自己带语义,HTTP 只是传输层 _HTTP_STATUS: dict[ErrorCode, int] = { ErrorCode.OK: 200, ErrorCode.BAD_REQUEST: 400, ErrorCode.VALIDATION_FAILED: 422, ErrorCode.NOT_FOUND: 404, ErrorCode.CONFLICT: 409, ErrorCode.PAYLOAD_TOO_LARGE: 413, ErrorCode.RATE_LIMITED: 429, ErrorCode.INTERNAL: 500, ErrorCode.UNAUTHORIZED: 401, ErrorCode.TOKEN_EXPIRED: 401, ErrorCode.TOKEN_INVALID: 401, ErrorCode.SMS_CODE_INVALID: 400, ErrorCode.SMS_TOO_FREQUENT: 429, ErrorCode.ACCOUNT_DISABLED: 403, ErrorCode.PASSWORD_INVALID: 400, ErrorCode.PASSWORD_NOT_SET: 400, ErrorCode.PASSWORD_WEAK: 422, ErrorCode.LOGIN_TOO_MANY: 429, ErrorCode.WORKSPACE_NOT_FOUND: 404, ErrorCode.NOT_A_MEMBER: 403, ErrorCode.INSUFFICIENT_ROLE: 403, ErrorCode.ALREADY_A_MEMBER: 409, ErrorCode.INVITATION_INVALID: 400, ErrorCode.VERSION_CONFLICT: 409, ErrorCode.ITEM_NOT_FOUND: 404, ErrorCode.ITEM_STATUS_INVALID: 409, ErrorCode.TAG_NOT_FOUND: 404, ErrorCode.LINK_NOT_FOUND: 404, ErrorCode.LINK_CONFLICT: 409, ErrorCode.TICKET_NOT_FOUND: 404, ErrorCode.TICKET_ALREADY_EXISTS: 409, ErrorCode.TICKET_STATUS_INVALID: 409, ErrorCode.TICKET_FORBIDDEN: 403, ErrorCode.NOTIFICATION_NOT_FOUND: 404, ErrorCode.KNOWLEDGE_NOT_FOUND: 404, ErrorCode.KNOWLEDGE_ALREADY_EXISTS: 409, ErrorCode.KNOWLEDGE_STATUS_INVALID: 409, ErrorCode.KNOWLEDGE_FORBIDDEN: 403, ErrorCode.KNOWLEDGE_SOURCE_REQUIRED: 409, ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND: 404, ErrorCode.KNOWLEDGE_CATEGORY_DEPTH: 422, ErrorCode.KNOWLEDGE_CATEGORY_CONFLICT: 409, ErrorCode.ATTACHMENT_NOT_FOUND: 404, ErrorCode.ATTACHMENT_TOO_LARGE: 413, ErrorCode.ATTACHMENT_FORBIDDEN: 403, ErrorCode.CONFIRM_REQUIRED: 409, ErrorCode.APP_RELEASE_NOT_FOUND: 404, } class AppError(Exception): """业务异常。__str__ 只用于日志,对外一律走 code + message。""" def __init__( self, code: ErrorCode, message: str | None = None, *, http_status: int | None = None, details: Any = None, ) -> None: self.code = code self.message = message or code.name self.http_status = http_status or _HTTP_STATUS.get(code, 400) self.details = details super().__init__(f"[{int(code)}] {self.message}") def http_status_for(code: ErrorCode) -> int: return _HTTP_STATUS.get(code, 400)