Add skills-lock.json to manage skill dependencies for drawio-skill and prd

This commit is contained in:
Guangfei.Zhao
2026-08-21 13:46:56 +08:00
parent 257b9cda08
commit 88f4a6e8a7
26 changed files with 1542 additions and 1320 deletions
+61 -4
View File
@@ -1,8 +1,8 @@
# 05. 集成层设计(F6 / Mini 域)
# 05. 集成层设计(F6 / Mini 域 / 阿里云 OCR
## 决策
供应商(F6历史 Mini 域的调用统一收口在 `integration/f6-adapter` / `integration/mini-clients` 模块(模块位置见 [01-project-structure.md](./01-project-structure.md)),业务 domain 不直接持有 HTTP 客户端;用 Resilience4j 统一管理超时、重试、熔断、并发隔离。
供应商(F6历史 Mini 域、以及阿里云 OCR(车牌识别)的调用统一收口在 `integration/f6-adapter` / `integration/mini-clients` / `integration/aliyun-ocr` 模块(模块位置见 [01-project-structure.md](./01-project-structure.md)),业务 domain 不直接持有 HTTP 客户端;用 Resilience4j 统一管理超时、重试、熔断、并发隔离。
HTTP 客户端用 **`RestClient`(同步)**,底层是 Apache HttpClient 5 连接池,不用 `WebClient`/`Mono`
@@ -30,6 +30,9 @@ integration/f6-adapter/
integration/mini-clients/
对 O2O/Warranty/Retail Store/ROOS 的只读客户端封装,供 workbench / bff-orchestration 调用
integration/aliyun-ocr/
车牌识别:OSS 临时上传 + RecognizeLicensePlate 调用、AK/SK 持有、按次计费的用量管控
```
`platform-integration` 依赖 `platform-web`(为了复用 `BusinessException``ErrorCode`)是本次允许的少数几个 platform 间依赖之一。
@@ -221,7 +224,7 @@ class F6ClientException(message: String) :
IntegrationException(ErrorCode.F6_BUSINESS_ERROR, "供应商请求被拒绝")
```
错误码是 `Int`,落在 `3xxxx` 集成段(`ErrorCode.F6_UNAVAILABLE = 30001`),完整分段表见 [06-api-design.md](./06-api-design.md)——客户端 [../12-error-and-api-contract.md](../12-error-and-api-contract.md) 的 `ApiCode` 也是数字,两边必须一致。异常里带的原始 `message`(含 F6 的状态码和响应体)只进日志,**不进返回给 APP 的 `message`**,避免把供应商的协议细节泄漏出去。
错误码是 `Int`,落在 `3xxxx` 集成段(`ErrorCode.F6_UNAVAILABLE = 30001`),完整分段表见 [06-api-design.md](./06-api-design.md)——客户端 [../flutter-app/12-error-and-api-contract.md](../flutter-app/12-error-and-api-contract.md) 的 `ApiCode` 也是数字,两边必须一致。异常里带的原始 `message`(含 F6 的状态码和响应体)只进日志,**不进返回给 APP 的 `message`**,避免把供应商的协议细节泄漏出去。
## Mini 域客户端示例(内部系统,策略更宽松)
@@ -244,6 +247,59 @@ class O2OClient(private val miniRestClient: RestClient) {
}
```
## 阿里云 OCR 客户端(车牌识别,按次计费)
车牌识别用[阿里云视觉智能开放平台](https://help.aliyun.com/zh/viapi/developer-reference/api-u92rj0)的 `RecognizeLicensePlate`,客户端不直连、由后端代理,理由与整条链路见 [../flutter-app/07-native-integration.md](../flutter-app/07-native-integration.md)。它和 F6、Mini 域是同一级别的外部依赖,收口在 `integration/aliyun-ocr`
它有两点和前面两个都不一样,配置策略要跟着变:
**1. 按次计费,所以不配 `@Retry`。** F6 重试一次只是多花点时间,OCR 重试一次是多花一次钱,而且识别失败通常是图片本身的问题(模糊、遮挡、角度太偏),重试同一张图大概率还是失败。**失败就返回失败,让用户重拍或手工输码**——这一条要在代码注释里写清楚,否则后人看到别的 client 都有 `@Retry` 会以为这里是漏了。
**2. 收 `ImageURL` 不收图片二进制**,所以调 OCR 之前必须先把图片落到 OSS。这两步一起构成一次识别,中间任何一步失败对客户端都是"识别失败"。
```kotlin
// integration/aliyun-ocr/.../LicensePlateClient.kt
@Component
class LicensePlateClient(
private val ossClient: OssClient,
private val ocrRestClient: RestClient,
) {
// 只做超时 + 舱壁 + 熔断,不加 @Retry:按次计费,且重试同一张图收益极低
@Bulkhead(name = "aliyun-ocr")
@CircuitBreaker(name = "aliyun-ocr", fallbackMethod = "fallbackRecognize")
fun recognize(image: ByteArray, contentType: String): PlateRecognition {
require(image.size <= 4 * 1024 * 1024) { "图片超过阿里云 4 MB 限制" }
val objectKey = ossClient.putTemp(image, contentType) // 临时对象,见下方生命周期
val resp = ocrRestClient.post()
.uri("/?Action=RecognizeLicensePlate")
.body(mapOf("ImageURL" to ossClient.signedUrl(objectKey)))
.retrieve()
.body(AliyunPlateResponse::class.java)!!
return PlateRecognition(
plateNumber = resp.data.plateNumber,
plateType = resp.data.plateType,
confidence = resp.data.confidence, // 阈值判断交给上层,这里只如实返回
)
}
private fun fallbackRecognize(image: ByteArray, contentType: String, ex: Throwable): PlateRecognition {
log.warn("车牌识别不可用,转手工输码", ex)
throw IntegrationException(ErrorCode.OCR_UNAVAILABLE, "车牌识别暂不可用,请手动输入车牌")
}
}
```
这里的降级和 Mini 域的"返回空对象"不同:**车牌识别没有合理的空值**,识别不出来就是识别不出来,只能明确抛错让 APP 切到手工输码。返回一个空字符串会被误当成识别成功。
几条配套约束:
- **AK/SK 走 [07-config-governance.md](./07-config-governance.md) 的密钥管理**,和数据库口令同级,绝不进代码库、绝不下发到客户端。
- **上传的车辆照片是临时对象**:OSS 上设生命周期规则自动过期(识别完就没用了),不要让门店车辆照片无限期堆在桶里——这既是成本问题也是合规问题。
- **要有调用量监控和告警**。按次计费的接口一旦被误用(比如客户端做成连续帧调用)账单会很难看,用量指标进 [08-observability.md](./08-observability.md) 的看板。
- **Region 固定在上海或深圳**(该能力只在这两个 Region 提供),配置里写死,不要跟着其它服务的 region 走。
## traceId 透传
```kotlin
@@ -267,7 +323,8 @@ class TracePropagationInterceptor : ClientHttpRequestInterceptor {
- **F6 是外部供应商域**,稳定性不可控,必须配齐超时 + 重试 + 熔断 + 舱壁,且熔断后要有降级返回(`fallbackXxx` 方法),不能让异常直接穿透到 APP。
- **异常统一转换**:F6 / Mini 域的异常和非标准错误,在集成模块内部转换成内部标准错误码,业务 domain 和最终 API 响应都不暴露供应商侧的原始协议细节。
- **Mini 域调用相对可控**(内部系统),熔断可以不加,但超时和舱壁不能省,避免慢查询拖垮 `workbench` 聚合——对应架构图 Flow 3 "首页失败按 tile 降级"。
- **只对幂等调用配 `@Retry`**。GET 查询可以放心重试;有副作用的调用(下单、扣减)**默认不重试**,确实需要时接口必须带幂等 key,规则见 [12-concurrency-and-scheduling.md](./12-concurrency-and-scheduling.md)。
- **只对幂等调用配 `@Retry`**。GET 查询可以放心重试;有副作用的调用(下单、扣减)**默认不重试**,确实需要时接口必须带幂等 key,规则见 [12-concurrency-and-scheduling.md](./12-concurrency-and-scheduling.md)。**按次计费的调用(阿里云 OCR)同样不重试**,理由见上文。
- **降级返回要和业务语义对得上**:Mini 域可以返回空对象(首页 tile 空着好过整页 500),但**车牌识别不能**——识别不出来只能明确抛错让 APP 切手工输码,返回空字符串会被误当成识别成功。
- **不在数据库事务里调外部 HTTP**,理由见 [03-persistence.md](./03-persistence.md)。
- 业务 domain(如 `workbench`)只依赖 `integration/*` 暴露的接口,不自己构造 `RestClient` 发请求。
- `workbench` 首页把多个下游并行拉起来的写法(线程池、整体超时预算、按 tile 降级)见 [11-cross-domain-collaboration.md](./11-cross-domain-collaboration.md)——那不属于单个客户端的职责。