Files
Bruno GaoandQwen-Coder 43d1229bd7 M1: Add complete documentation structure and contract specifications
- Add comprehensive docs/ directory with standardized structure
- Include architecture specification and contract documents
- Add prompt templates for development workflows
- Establish M1 deliverables as frozen specifications

Co-authored-by: Qwen-Coder <[email protected]>
2026-02-11 23:41:04 +08:00

4.6 KiB
Raw Permalink Blame History

异常处理契约 v1.0

1. 异常体系

DomainException(基类) ├── 携带 ErrorCode(枚举) ├── 携带 message(面向用户的描述) └── 可选携带 cause(原始异常)

所有业务异常必须继承 DomainException 并携带 ErrorCode。 不允许直接抛出 RuntimeException / IllegalStateException 等通用异常表达业务规则。

2. ErrorCode 组织方式

按模块前缀分组(锁定):

COMMON_VALIDATION_ERROR
COMMON_INTERNAL_ERROR
COMMON_OPTIMISTIC_LOCK_CONFLICT
AUTH_INVALID_CREDENTIALS
AUTH_TOKEN_EXPIRED
AUTH_ACCESS_DENIED
ORDER_NOT_FOUND
ORDER_ALREADY_CANCELLED
ORDER_STATUS_INVALID
...

命名规则:{MODULE}_{SCENE},全大写蛇形。 新增模块时在 ErrorCode 枚举中添加对应前缀组。

3. ErrorCode → HTTP Status 映射表

ErrorCode 前缀/具体值 HTTP Status 说明
COMMON_VALIDATION_ERROR 400 参数校验失败
AUTH_INVALID_CREDENTIALS 401 认证失败
AUTH_TOKEN_EXPIRED 401 令牌过期
AUTH_ACCESS_DENIED 403 无权限
*_NOT_FOUND 404 资源不存在
ALREADY / *_STATUS_INVALID 409 状态冲突
COMMON_OPTIMISTIC_LOCK_CONFLICT 409 乐观锁冲突
COMMON_INTERNAL_ERROR 500 系统内部错误

4. 统一错误响应格式

{ "code": 404, // HTTP Status(固定,便于前端/网关) "errorCode": "ORDER_NOT_FOUND", // 稳定枚举值(便于埋点/告警/统计) "message": "订单不存在", // 面向用户,可本地化 "traceId": "abc-123-def", // 链路追踪 ID "timestamp": "2026-02-11T10:30:00Z", // 服务器时间 "details": null // 仅 VALIDATION_ERROR 时填充字段级明细 }

各字段职责:

  • code:前端快速判断错误大类(4xx / 5xx)
  • errorCode:后端精确定位错误场景,用于日志检索 / 告警规则 / 监控大盘
  • message:展示给终端用户
  • details:VALIDATION_ERROR 时返回 [{"field": "xxx", "message": "xxx"}]
  • 生产环境【禁止】在响应中返回堆栈信息

5. 异常转换规则

规则 1:异常只在 API 层的 GlobalExceptionHandler 转换为 HTTP 响应

  • domain/application 层只抛 DomainException
  • infrastructure 层将技术异常包装为 DomainException:
    • DataAccessException → COMMON_INTERNAL_ERROR
    • OptimisticLockingFailureException → COMMON_OPTIMISTIC_LOCK_CONFLICT
    • ConnectException / TimeoutException → COMMON_INTERNAL_ERROR

规则 2:禁止吞没异常

  • 任何 catch 块必须:要么重新抛出,要么记录 WARN/ERROR 日志
  • 空 catch 块会被 SpotBugs 检测并阻断 CI

规则 3:参数校验 vs 业务校验

  • 参数格式校验:Jakarta Validation(@NotNull, @Size, @Pattern 等), Controller 层 @Valid/@Validated 拦截,GlobalExceptionHandler 捕获 MethodArgumentNotValidException → 400 + COMMON_VALIDATION_ERROR + details
  • 业务规则校验:Domain 层方法内判断并抛出具体 DomainException

✅ 可自动检查项

规则 检查方式
所有自定义异常继承 DomainException ArchUnit: classes in ..exception.. that extend RuntimeException should also extend DomainException
禁止空 catch 块 SpotBugs: DE_MIGHT_IGNORE + REC_CATCH_EXCEPTION
GlobalExceptionHandler 存在且标注 @RestControllerAdvice ArchUnit: 至少有一个类标注 @RestControllerAdvice 且在 ..api.exception.. 包下
Controller 中 @Valid/@Validated 注解存在 人工评审 + Prompt 约束

❌ 典型违规示例

违规 1:直接抛 RuntimeException 表达业务规则

// 错误! if (order == null) { throw new RuntimeException("订单不存在"); // 无 ErrorCode,GlobalExceptionHandler 只能兜底 500 }

// 正确 if (order == null) { throw new DomainException(ErrorCode.ORDER_NOT_FOUND); }

违规 2:Infrastructure 层不包装技术异常

// 错误!让 DataAccessException 直接穿透到 API 层 public class OrderPersistenceAdapter implements OrderPersistencePort { public Order findById(OrderId id, String customerId) { return repo.findByIdAndCustomerId(id.value(), customerId) .map(mapper::toDomain) .orElse(null); // 返回 null,让上层 NPE → 500 } }

// 正确:包装为领域异常 public Order findById(OrderId id, String customerId) { return repo.findByIdAndCustomerId(id.value(), customerId) .map(mapper::toDomain) .orElseThrow(() -> new DomainException(ErrorCode.ORDER_NOT_FOUND)); }