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.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user