errors.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. """DMS业务异常。"""
  2. from __future__ import annotations
  3. from typing import Any
  4. class DmsError(Exception):
  5. code = "INTERNAL_ERROR"
  6. status_code = 500
  7. default_message = "服务器内部错误"
  8. def __init__(
  9. self,
  10. message: str | None = None,
  11. *,
  12. details: Any = None,
  13. code: str | None = None,
  14. ) -> None:
  15. super().__init__(message or self.default_message)
  16. self.message = message or self.default_message
  17. self.details = details
  18. self.code = code or self.code
  19. class InvalidArgumentError(DmsError):
  20. code = "INVALID_ARGUMENT"
  21. status_code = 400
  22. default_message = "请求参数错误"
  23. class InvalidCaptchaError(InvalidArgumentError):
  24. """登录图片验证码缺失/错误/过期(安全需求4),不泄露具体原因。"""
  25. code = "INVALID_CAPTCHA"
  26. default_message = "验证码错误或已过期"
  27. class AuthenticationError(DmsError):
  28. code = "TOKEN_INVALID"
  29. status_code = 401
  30. default_message = "未认证或登录凭证无效"
  31. class InvalidCredentialsError(AuthenticationError):
  32. code = "INVALID_CREDENTIALS"
  33. default_message = "用户名或密码错误"
  34. class TokenExpiredError(AuthenticationError):
  35. code = "TOKEN_EXPIRED"
  36. default_message = "登录凭证已过期"
  37. class UserDisabledError(AuthenticationError):
  38. code = "USER_DISABLED"
  39. default_message = "用户已被禁用"
  40. class AuthVersionMismatchError(AuthenticationError):
  41. code = "AUTH_VERSION_MISMATCH"
  42. default_message = "登录状态已失效,请重新登录"
  43. class AccountLockedError(AuthenticationError):
  44. """连续密码错误触发临时锁定(策略见 services/login_lockout.py)。"""
  45. code = "ACCOUNT_LOCKED"
  46. default_message = "失败次数过多,账号已临时锁定,请15分钟后重试"
  47. class PortalTokenInvalidError(AuthenticationError):
  48. code = "PORTAL_TOKEN_INVALID"
  49. default_message = "门户跳转凭证无效或已过期"
  50. class PortalTicketInvalidError(AuthenticationError):
  51. code = "PORTAL_TICKET_INVALID"
  52. default_message = "设置密码凭证无效或已过期,请从门户重新跳转"
  53. class ForbiddenError(DmsError):
  54. code = "FORBIDDEN"
  55. status_code = 403
  56. default_message = "无权限执行此操作"
  57. class SecurityLevelForbiddenError(ForbiddenError):
  58. code = "SECURITY_LEVEL_FORBIDDEN"
  59. default_message = "用户密级不足,无法查看该文档"
  60. class DocumentViewForbiddenError(ForbiddenError):
  61. code = "DOCUMENT_VIEW_FORBIDDEN"
  62. default_message = "无权查看该文档"
  63. class DocumentDownloadForbiddenError(ForbiddenError):
  64. code = "DOCUMENT_DOWNLOAD_FORBIDDEN"
  65. default_message = "无权下载该文档"
  66. class DocumentActionForbiddenError(ForbiddenError):
  67. """写路径按文档权限(矩阵/ACL)拒绝:区别于密级不足与查看/下载拒绝。"""
  68. code = "DOCUMENT_ACTION_FORBIDDEN"
  69. default_message = "无权对该文档执行此操作"
  70. class ResourceNotFoundError(DmsError):
  71. code = "RESOURCE_NOT_FOUND"
  72. status_code = 404
  73. default_message = "请求的资源不存在"
  74. class ConflictError(DmsError):
  75. code = "DATA_VERSION_CONFLICT"
  76. status_code = 409
  77. default_message = "数据冲突"
  78. class CategoryInUseError(ConflictError):
  79. code = "CATEGORY_IN_USE"
  80. default_message = "分类仍被子分类或方案文档占用"
  81. class CategoryNotLeafError(ConflictError):
  82. code = "CATEGORY_NOT_LEAF"
  83. default_message = "只有有效叶子分类可以直接存放主案"
  84. class CategoryFlatModeConflictError(ConflictError):
  85. code = "CATEGORY_FLAT_MODE_CONFLICT"
  86. default_message = "该一级分类下已存在子方案或配套资料挂载,请先解绑后再切换为扁平模式"
  87. class MainPlanNotFoundError(ResourceNotFoundError):
  88. code = "MAIN_PLAN_NOT_FOUND"
  89. default_message = "主案不存在"
  90. class FileNotFoundError(ResourceNotFoundError):
  91. code = "FILE_NOT_FOUND"
  92. default_message = "文档文件不存在"
  93. class PreviewUnavailableError(DmsError):
  94. code = "PREVIEW_UNAVAILABLE"
  95. status_code = 415
  96. default_message = "当前文件类型不支持在线预览"
  97. class PreviewConverterUnavailableError(DmsError):
  98. code = "PREVIEW_CONVERTER_UNAVAILABLE"
  99. status_code = 503
  100. default_message = "预览转换器未配置或不可用"
  101. class PreviewConversionTimeoutError(DmsError):
  102. code = "PREVIEW_CONVERSION_TIMEOUT"
  103. status_code = 504
  104. default_message = "文档转换超时,请稍后重试"
  105. class PreviewConversionFailedError(DmsError):
  106. code = "PREVIEW_CONVERSION_FAILED"
  107. status_code = 502
  108. default_message = "文档转换为PDF失败"
  109. class BatchManifestMismatchError(InvalidArgumentError):
  110. code = "BATCH_MANIFEST_MISMATCH"
  111. default_message = "批量文件数量与元数据数量不一致"
  112. class MainPlanHasChildrenError(ConflictError):
  113. code = "MAIN_PLAN_HAS_CHILDREN"
  114. default_message = "主案仍存在有效子方案"
  115. class DocumentNameConflictError(ConflictError):
  116. code = "DOCUMENT_NAME_CONFLICT"
  117. default_message = "同一范围内已存在同名方案"
  118. class AttachmentInUseError(ConflictError):
  119. code = "ATTACHMENT_IN_USE"
  120. default_message = "共享附件仍被主案挂载"
  121. class AttachmentHasNoAclError(InvalidArgumentError):
  122. code = "ATTACHMENT_HAS_NO_ACL"
  123. default_message = "共享附件不配置访问权限"
  124. class SubPlanPermissionInheritedError(ConflictError):
  125. code = "SUB_PLAN_PERMISSION_INHERITED"
  126. default_message = "子方案动态继承主案权限,不能独立配置"
  127. class DuplicatePermissionSubjectError(InvalidArgumentError):
  128. code = "DUPLICATE_PERMISSION_SUBJECT"
  129. default_message = "权限主体不能重复"
  130. class UnsupportedFileTypeError(DmsError):
  131. code = "UNSUPPORTED_FILE_TYPE"
  132. status_code = 415
  133. default_message = "不支持的文件类型"
  134. class PayloadTooLargeError(DmsError):
  135. code = "PAYLOAD_TOO_LARGE"
  136. status_code = 413
  137. default_message = "请求内容过大"
  138. class InternalError(DmsError):
  139. code = "INTERNAL_ERROR"
  140. status_code = 500
  141. default_message = "服务器内部错误"
  142. class DocumentNotDeletedError(ConflictError):
  143. code = "DOCUMENT_NOT_DELETED"
  144. default_message = "文档当前不在回收站中"
  145. class FileIntegrityMismatchError(ConflictError):
  146. code = "FILE_INTEGRITY_MISMATCH"
  147. default_message = "文档文件完整性校验失败"
  148. class FilePathInvalidError(InternalError):
  149. code = "FILE_PATH_INVALID"
  150. default_message = "文档存储路径异常"
  151. class RestoreFileChangedError(ConflictError):
  152. code = "RESTORE_FILE_CHANGED"
  153. default_message = "校验期间文件发生变化"
  154. class RestoreCategoryInvalidError(ConflictError):
  155. code = "RESTORE_CATEGORY_INVALID"
  156. default_message = "原方案分类无效,无法恢复"
  157. class RestoreParentInvalidError(ConflictError):
  158. code = "RESTORE_PARENT_INVALID"
  159. default_message = "原所属主案无效,无法恢复"
  160. class RestoreRelationConflictError(ConflictError):
  161. code = "RESTORE_RELATION_CONFLICT"
  162. default_message = "历史权限或挂载关系与当前有效关系冲突"
  163. class DocumentNotInMoveScopeError(ConflictError):
  164. """跨页移动时文档不在请求指定的可见范围内(分类或附件类型不符)。"""
  165. code = "DOCUMENT_NOT_IN_MOVE_SCOPE"
  166. default_message = "方案不在当前调整范围内"
  167. class OrganizationInUseError(ConflictError):
  168. code = "ORGANIZATION_IN_USE"
  169. default_message = "机构仍存在下级机构或挂载用户"
  170. class UserSelfProtectionError(ForbiddenError):
  171. code = "USER_SELF_PROTECTION"
  172. default_message = "不能对本账号执行该操作"
  173. class RoleInUseError(ConflictError):
  174. code = "ROLE_IN_USE"
  175. default_message = "角色仍有关联用户,无法删除"
  176. class DocumentPasswordRequiredError(ForbiddenError):
  177. code = "DOCUMENT_PASSWORD_REQUIRED"
  178. default_message = "该方案已设置密码保护,请输入密码后重试"
  179. class DocumentPasswordInvalidError(ForbiddenError):
  180. code = "DOCUMENT_PASSWORD_INVALID"
  181. default_message = "方案密码错误"
  182. class LoginIpForbiddenError(ForbiddenError):
  183. code = "LOGIN_IP_FORBIDDEN"
  184. default_message = "当前IP不在该部门允许的登录IP范围内"
  185. class ReauthRequiredError(ForbiddenError):
  186. """敏感操作缺少或未通过二次认证(重输登录密码)。"""
  187. code = "REAUTH_REQUIRED"
  188. default_message = "敏感操作需要二次认证,请重新输入登录密码"
  189. class BackupRunningError(ConflictError):
  190. """已有备份任务运行中(引擎单任务互斥)。"""
  191. code = "BACKUP_RUNNING"
  192. default_message = "已有备份任务正在运行,请等待完成后再试"
  193. class BackupConfigInvalidError(DmsError):
  194. code = "BACKUP_CONFIG_INVALID"
  195. status_code = 400
  196. default_message = "备份配置无效"
  197. class BackupTargetUnavailableError(DmsError):
  198. """备份目标不可达或不可写(网络/权限/路径问题)。"""
  199. code = "BACKUP_TARGET_UNAVAILABLE"
  200. status_code = 503
  201. default_message = "备份目标不可达或不可写"
  202. class BackupToolMissingError(DmsError):
  203. """rsync 等外部工具缺失或不可执行。"""
  204. code = "BACKUP_TOOL_MISSING"
  205. status_code = 503
  206. default_message = "备份所需外部工具不可用"
  207. class BackupFailedError(DmsError):
  208. code = "BACKUP_FAILED"
  209. status_code = 500
  210. default_message = "备份执行失败,请查看服务端日志"
  211. class BackupNotFoundError(ResourceNotFoundError):
  212. code = "BACKUP_NOT_FOUND"
  213. default_message = "指定的备份不存在"
  214. class MetricUnavailableError(DmsError):
  215. """资源监控采集能力不可用(psutil 缺失或采样器未启动)。"""
  216. code = "METRIC_UNAVAILABLE"
  217. status_code = 503
  218. default_message = "资源监控采集能力不可用"