Files
conti-docs/backend/05-integration-layer.md
T
Guangfei.Zhao 1e0cbb86a2 feat: Add comprehensive documentation for integration layer, API design, config governance, observability, build/deploy, and testing strategies
- Introduced integration layer design with Resilience4j for external vendor calls.
- Established API design standards with unified response structures and global exception handling.
- Defined configuration and service governance using Kubernetes native solutions.
- Implemented observability practices including trace ID propagation and structured logging.
- Outlined build and multi-environment deployment strategies using Gradle and GitLab CI/CD.
- Specified testing strategies across different layers, utilizing JUnit, MockK, Testcontainers, and WireMock.
2026-08-12 18:23:11 +08:00

132 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 05. 集成层设计(F6 / Mini 域)
## 决策
供应商(F6)和历史 Mini 域的调用统一收口在 `f6-integration` / `mini-clients` 模块,业务 domain 不直接持有 `WebClient` 或 HTTP 客户端;用 Resilience4j 统一管理超时、重试、熔断。
## 结构约定
```
platform-integration/
WebClientConfig # 统一封装 WebClient(连接池、超时基线配置)
Resilience4jDefaults # 超时/重试/熔断的公共默认配置
domains/f6-integration/
负责:换票、供应商访问上下文准备、超时/重试/熔断策略、异常转换为内部标准错误码
domains/mini-clients/
对 O2O/Warranty/Retail Store/ROOS 的只读客户端封装,供 workbench / bff-orchestration 调用
```
## `WebClient` + Resilience4j 配置示例
```yaml
# application.yml
resilience4j:
timelimiter:
instances:
f6-api:
timeout-duration: 2s
mini-o2o:
timeout-duration: 1s
retry:
instances:
f6-api:
max-attempts: 2
wait-duration: 200ms
retry-exceptions:
- java.net.SocketTimeoutException
- org.springframework.web.reactive.function.client.WebClientRequestException
circuitbreaker:
instances:
f6-api:
sliding-window-size: 20
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
permitted-number-of-calls-in-half-open-state: 5
```
```kotlin
// domains/f6-integration/.../F6ApiClient.kt
@Component
class F6ApiClient(
private val webClient: WebClient, // 来自 platform-integration 的统一封装
) {
@CircuitBreaker(name = "f6-api", fallbackMethod = "fallbackProcurementList")
@Retry(name = "f6-api")
@TimeLimiter(name = "f6-api")
fun fetchProcurementList(storeId: Long): Mono<ProcurementListResponse> =
webClient.get()
.uri("/f6/procurement/list?storeId={storeId}", storeId)
.retrieve()
.onStatus({ it.isError }) { resp ->
resp.bodyToMono(String::class.java)
.map { body -> F6IntegrationException("F6 采购列表调用失败: ${resp.statusCode()} $body") }
}
.bodyToMono(ProcurementListResponse::class.java)
// Resilience4j 约定:fallback 方法签名 = 原方法参数 + Throwable,返回类型一致
fun fallbackProcurementList(storeId: Long, ex: Throwable): Mono<ProcurementListResponse> =
Mono.just(ProcurementListResponse.degraded())
}
```
```kotlin
// 统一异常转换:F6IntegrationException -> 内部标准错误码,业务层不感知供应商原始协议
class F6IntegrationException(message: String) : RuntimeException(message)
@RestControllerAdvice
class F6ExceptionHandler {
@ExceptionHandler(F6IntegrationException::class)
fun handle(ex: F6IntegrationException): ResponseEntity<ApiResult<Nothing>> =
ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(ApiResult.error(code = "F6_UNAVAILABLE", message = "供应商服务暂不可用,请稍后重试"))
}
```
## Mini 域客户端示例(内部系统,策略更宽松)
```kotlin
// domains/mini-clients/.../O2OClient.kt
@Component
class O2OClient(private val webClient: WebClient) {
@TimeLimiter(name = "mini-o2o") // 只兜底超时,不需要熔断(内部系统,稳定性相对可控)
fun fetchOrderSummary(storeId: Long): Mono<OrderSummary> =
webClient.get()
.uri("/o2o/orders/summary?storeId={storeId}", storeId)
.retrieve()
.bodyToMono(OrderSummary::class.java)
.onErrorResume { Mono.just(OrderSummary.empty()) } // 局部降级,见 workbench 聚合规则
}
```
## 关键规则
- **F6 是外部供应商域**,稳定性不可控,必须配置超时 + 重试 + 熔断,且熔断后要有降级返回(`fallbackXxx` 方法),不能让异常直接穿透到 APP。
- **异常统一转换**:F6 / Mini 域返回的异常或非标准错误,在 `f6-integration` / `mini-clients` 内部转换成内部标准错误码(如 `F6_UNAVAILABLE`),业务 domain 和最终 API 响应都不暴露供应商侧的原始协议细节。
- **Mini 域调用相对可控**(内部系统),熔断策略可以比 F6 宽松(示例里只加超时兜底),但仍需要超时兜底,避免慢查询拖垮 `workbench` 聚合——对应架构图 Flow 3 "首页失败按 tile 降级"的要求。
- 业务 domain(如 `workbench`)只依赖 `mini-clients` / `f6-integration` 暴露的接口,不自己 `new WebClient` 发请求。
## 附录:超时、重试、熔断分别解决什么问题
三者经常被一起提,但作用点不同,配置的时候容易搞混:
- **超时(Timeout)**:解决"对方一直不回应,我方请求线程/连接被一直占着"的问题。没有超时,一个慢下游能拖垮整个调用方的线程池。这是三者里最基础、必须有的一道防线。
- **重试(Retry)**:解决"这次失败大概率是偶发的(网络抖动、瞬时过载)"的问题。重试的前提是**幂等**——`fetchProcurementList` 这种 GET 查询可以放心重试,但如果是"扣库存""创建订单"这类有副作用的调用,重试前要先确认接口本身幂等(比如带幂等 key),否则重试可能造成重复下单这类更严重的问题。
- **熔断(Circuit Breaker**:解决"对方已经持续故障,继续重试只是在浪费资源、拖慢自己"的问题。熔断器统计一个滑动窗口内的失败率,超过阈值后直接短路请求(进入 `OPEN` 状态,一段时间内不再真的发请求,直接走 fallback),过一段时间放几个探测请求(`HALF_OPEN`)判断对方是否恢复。
三者组合的顺序也有讲究:一次调用先看熔断器状态(`OPEN` 直接 fallback,不发请求)→ 没熔断就真的发请求 → 超时控制这次请求最多等多久 → 超时或失败了再看要不要重试。上面 Resilience4j 的注解顺序(`@CircuitBreaker` 在最外层,`@Retry``@TimeLimiter`在内层)就是按这个语义叠加的。
## 待补充
- 具体超时/重试参数需要结合 F6 实际 SLA 压测后调整,示例里的数值是起点,不是最终值。
- 熔断后降级返回的数据结构约定(`degraded()` 具体字段)。
- F6 换票具体协议细节(对接 [webview-ticket](./04-security-auth.md) 的会话失效联动)。
## 参考链接
- [Resilience4j 官方文档](https://resilience4j.readme.io/docs)
- [Spring WebFlux WebClient](https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html)
- [Martin Fowler: CircuitBreaker](https://martinfowler.com/bliki/CircuitBreaker.html)