- 状态:已接受
- 日期:2026-07(Payment 域建立时)
背景
集成第三方服务时,失败是常态(网络错误、厂商返回业务错误、签名失败、参数无效)。对外暴露失败信息有两种范式:
- 抛异常:调用失败时
throw,消费者用 try/catch 处理。 - 返回 Result:方法返回一个包含成败状态 + 错误码 + 错误信息的结果对象。
本库不同域历史上两种都有:Sms 用 SMSResult(吞异常成 Result),TeamWork 用 TeamWorkException(抛异常)。
决策
新域对外用 Result 包装(推荐),内部用异常,在 Provider 方法边界捕获并转换。
Payment 域是标准范例:PaymentResult<T>(IsSuccess / Data / ErrorCode / ErrorMessage + Success() / Fail() 工厂方法)。
public async Task<PaymentResult<PaymentStatusResult>> QueryStatusAsync(string outTradeNo, CancellationToken ct){ try { var json = await _httpClient.PostAsync(..., ct); // ① 业务码非成功 → return PaymentResult.Fail(...) return PaymentResult<PaymentStatusResult>.Success(result); } catch (PaymentException ex) { // ② 领域异常 → 提取 ErrorCode 和 ProviderMessage return PaymentResult<PaymentStatusResult>.Fail(ex.ErrorCode, ex.ProviderMessage); } catch (HttpRequestException ex) { // ③ 网络异常 → 统一错误码 return PaymentResult<PaymentStatusResult>.Fail("HTTP_ERROR", ex.Message); }}理由
- 集成失败的预期性:第三方 API 调用失败不是”异常情况”而是”常见分支”,用 Result 比异常更符合语义。
- 错误信息结构化:
ErrorCode+ErrorMessage比裸异常消息更易被消费者程序化处理(映射到 HTTP 响应、记日志、重试判断)。 - 避免吞异常丢失信息:如果用
catch(Exception)吞成失败 Result,至少保留了错误码和消息。 - 异步友好:Result 天然适合 async/await 流程,不引入异常处理的控制流跳跃。
后果
- Provider 实现要写较多 try/catch 样板代码(每个方法 catch
DomainException→ catchHttpRequestException→ 转 Result)。 - 消费者需要检查
result.IsSuccess而非依赖 try/catch,心智模型略有不同。 - TeamWork 等历史域仍抛异常——风格不统一,但改造成本高,暂不动。
OperationCanceledException不应被吞:取消是控制流语义,不是失败。新代码应单独catch (OperationCanceledException)并重抛。
例外
- 配置校验失败仍用异常(
ThrowIfNullOrEmpty/IValidateOptions.Fail)——这属于编程错误,应该 fail-fast。 - TeamWork 域:历史遗留抛异常范式,未改造。