feat(creation): Java 侧接通 patbond-ai——Feign 客户端、两类错误分流、三值重试(T4-03)
CI / backend-test (push) Successful in 6m22s
CI / backend-test (push) Successful in 6m22s
按 openapi-internal.yaml 把 AiServiceGenerationProvider 从空实现位做成真实现。 全量 410 → 427,零失败零跳过。 **骨架阶段的三处假设被现实推翻,一并纠正:** 1. **去掉 submit() 的 idempotencyKey 参数。** 当时按「透传给厂商幂等头」设计, 但火山**没有任何幂等字段**(取证见 providers/volcengine-seedance.md §2-①)。 留一个无处可去的参数会让人以为双重扣费已经解决——它没有,只由 max_attempts 兜住,恢复路径是已落库的 provider_request_id 而不是某个 key。 2. **Worker 不再在 submit 前调 uploadTargets.presignedPutFor。** 那个方法目前 抛异常,而它在 submit **之前**被调用——**所以在此之前整条链一步都跑不动**。 契约把 upload_target 标为可选未启用,故传 null 才是正确而非权宜。 3. **PollResult 扩容 + retryable 由布尔改三值 Retry。** 布尔表达不了最关键的 区别:限流几秒后恢复,配额耗尽要等到明天。把后者当可重试会白烧完整个重试 预算再失败,而用户看到的是转圈而不是「额度用完」的出口(ADR-023 §9)。 同时补齐 assetUrl/expiresIn/coverUrl/尺寸/时长/outputFormat/billingTokens ——这些最终都要写进 media.assets,现在丢掉后面就得再改一遍。 **两类错误落在两个异常上**(AiProviderException / AiServiceException),判别用 error.kind 而非 HTTP 状态码(状态码在两类之间会撞)。若这个分流塌成一个笼统的 FeignException,Worker 就只能猜要不要重试。未知的 retry/user_facing/state 取值 一律偏向「可重试 / 仍在运行」:契约可能先长出一个取值、代码后知道,把可恢复的 抖动判成用户可见的失败是更糟的错误,而重试预算本身有上限。 **snake_case 只作用于这一个客户端**:用 SpringEncoder/SpringDecoder 换 ObjectMapper, 不引 feign-jackson(其他模块都不需要它),也不用逐字段 @JsonProperty——后者漏一个 注解就会静默发出对端忽略的字段,而「静默被忽略的字段」是最难察觉的失败。 另配 followRedirects=false:内部服务没有理由重定向我们,跟随会把服务票发给 重定向指定的任意主机。 **新增两个 429 码**(ADR-023 §9):42900 配额耗尽 / 42901 频率过快。必须是两个 ——配额等的是次日或充值(弹 sheet 给出口),频率等的是数秒(弹 SnackBar 可自动 重试),共用一码必然使一半场景的文案与出口是错的。 写进任务行的 error_code 用适配层的 userFacing 类别而非厂商原始码:厂商词汇不该 渗进我们的数据,它会随换供应商而变,客户端会据此分支。原始码留在 error_message 里供排查。 safety_identifier 传 userId 的 SHA-256 hex 而非明文(ADR-023 §5 数据最小化), 测试断言明文 userId 不出现在请求体里。 **新增 AiGenerationClientWireTest(17 项)**,照 community 的 AuthorProfileClientWireTest 先例用 JDK 内置 HttpServer 起桩:snake_case 双向、 服务票每次现签(断言两次调用的票不同——缓存复用会让短期凭证悄悄变成长期凭证)、 两类错误各自的分流、配额与限流的 retry 不同、无法识别的错误体按可重试并说明原因。 **一处真实缺陷由全量跑抓出**:@FeignClient 的 url 占位符原先无默认值,而 Feign 在 **上下文启动时**解析它——导致任何没显式配置该属性的上下文都启动失败,包括与 AI 毫无关系的两个仓储测试类(17 个测试全红)。已给占位符加上与 CreationProperties 一致的默认值。单跑某个测试类时不会暴露这个问题,只有全量跑才会。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ public enum ErrorCode {
|
||||
COMMENT_NOT_FOUND(40404, 404, "评论不存在"),
|
||||
TARGET_USER_NOT_FOUND(40406, 404, "用户不存在"),
|
||||
// M4 生成域(ADR-023 §9)。40407/40408 与既有 404 族同列;配额与限流的
|
||||
// 两个 429 码随配额工单落地时补入——此处只加骨架实际使用的码,避免死枚举。
|
||||
// 骨架阶段只加了实际使用的码;两个 429 已随 AI 适配层落地(见下)。
|
||||
GENERATION_JOB_NOT_FOUND(40407, 404, "生成任务不存在"),
|
||||
GENERATION_MODEL_NOT_FOUND(40408, 404, "模型或风格不可用"),
|
||||
VACCINATION_RULE_VIOLATION(42201, 422, "疫苗状态或日期约束不满足"),
|
||||
@@ -39,6 +39,11 @@ public enum ErrorCode {
|
||||
MEDIA_UPLOAD_STATE_INVALID(42205, 422, "上传状态不允许确认"),
|
||||
GENERATION_STATE_INVALID(42206, 422, "生成任务状态不允许该操作"),
|
||||
LOGIN_LOCKED(42300, 423, "登录失败次数过多,账号已临时锁定"),
|
||||
// M4 生成域的两个 429(ADR-023 §9)。**必须是两个码**:配额耗尽等的是次日或
|
||||
// 充值(客户端弹 sheet 给出口),频率过快等的是数秒(弹 SnackBar 可自动重试)。
|
||||
// 共用一码必然使一半场景的文案与出口是错的。
|
||||
GENERATION_QUOTA_EXHAUSTED(42900, 429, "今日 AI 生成额度已用完"),
|
||||
GENERATION_RATE_LIMITED(42901, 429, "操作过于频繁,请稍后再试"),
|
||||
INTERNAL_ERROR(50000, 500, "服务器内部错误"),
|
||||
DOWNSTREAM_UNAVAILABLE(50300, 503, "依赖服务暂不可用");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user