Files
patbond-api/patbond-creation/src/main/resources/application.yml.sample
T
lixi 4b2550f88c
CI / backend-test (push) Successful in 5m43s
feat(creation): 新增 patbond-creation 模块骨架——端点、租约队列 Worker、provider 适配层(T4-02)
M4 新模块,:8085,前缀 /api/v1/creation/**(ADR-023 §2,E3 拍板方案 A)。
它编排生成任务但不调用 AI 服务商——上游调用在独立的 patbond-ai(Python)
仓库,经 REST 到达。

落地内容:
- 客户端端点 4 个(创建/查询/列表/取消)+ 目录端点。创建强制 Idempotency-Key
  (ADR-019:生成在上游要花钱,重试绝不能变成第二个计费任务)
- 租约队列仓储:FOR UPDATE SKIP LOCKED 领取 + 租约续期 + 回收器。目标模型
  只给了索引没给回收器代码,这里补上
- Worker 三条独立调度通道(提交/轮询/回收),任一卡住不饿死其余两条。
  轮询与续租在同一语句内完成——分开做会让长任务在跑时租约过期被抢,
  那正是重复扣费的经典路径
- provider 适配层接口按「两阶段 submit/poll」定形 + 两个实现位
- 服务身份 JWT 签发器(独立密钥对)
- common 增 3 个错误码(40407/40408/42206);配额与限流的两个 429 码随
  配额工单落地,此处不加死枚举

关键实现纪律(注释中已逐条说明原因):
- provider_request_id 在上游产生任何计费动作之前落库。顺序反过来,每次
  超时都会变成重复扣费
- 租约回收时刻意不清 provider_request_id:保留它,下次尝试才知道「已提交过,
  该轮询而非重发」
- provider 按 provider_code_snapshot 而非当前目录解析,故目录改指向后
  在途任务仍由原适配器轮询
- Worker 各 pass 捕获异常:Spring 会取消抛异常的 fixedDelay 任务,
  不捕获等于一次偶发失败静默停掉整个队列

刻意未实现且会响亮失败(不装作能用):provider 两个实现位(待服务商文档
确认幂等键支持、查询接口与频率限制、单任务最长时长、产物返回形式、
限流与配额响应形态五项)、预签名上传目标获取、产物入 media.assets
(后两者取决于挂起中的对象存储决策,ADR-023 §7)。

共享物按 ADR-023 §2 复制 3 件(JWT 校验栈、UuidV7、RequestHashes);
**存储适配层不复制**——presignPut 已存在,产物走预签名上传由 patbond-user
签发,故无需第 4 份存储层,也使 patbond-ai 零对象存储凭证。

测试:新增 GenerationJobQueueIntegrationTest 11 项,全部针对「写错了也
能跑、只在生产表现为丢任务或重复扣费」的语义:SKIP LOCKED 不把同一行
交给两个 worker、轮询确实续了它依赖的租约、回收器保留 provider_request_id、
first_attempt_at 跨重排队存活、write-once 保护、取消被状态守卫。
全量 393 → 404,零失败零跳过。

过程中被测试抓出两个真问题:UPDATE...FROM 的 RETURNING 列表未限定别名
导致 id 歧义(列表已改为单一来源派生两种形式,避免漂移);以及夹具伪造
了物理上不可能的历史(租约过期但 started_at 是当下),被
ck_generation_jobs_lease 正确拒绝。

Readme.md 全文改中文并补齐 M2 之后的内容:原文停在 M2(pet 骨架阶段)、
完全没有 patbond-community、配置表缺 M2 之后全部条目。现覆盖六模块、
迁移链归属说明、两对密钥的区别与原因、以及各服务端点。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 17:36:52 +08:00

87 lines
4.9 KiB
Plaintext
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.
server:
port: ${PATBOND_CREATION_PORT:8085}
spring:
application:
name: patbond-creation
datasource:
# 与 patbond-user 共库(MVP 单库多 schema);本服务只读写 creation schema,
# 并只读 community.posts 的 generation_job_id 关联(建草稿由 community 端点完成)。
# Flyway 迁移链(V1..V6,含 creation 基线)由 patbond-user 启动时统一执行,
# 本服务不携带 Flyway —— 单一 flyway_schema_history 归属不拆(ADR-023 §2)。
url: ${PATBOND_DB_URL:jdbc:postgresql://127.0.0.1:5432/patbond}
username: ${PATBOND_DB_USER:patbond}
password: ${PATBOND_DB_PASSWORD:patbond}
# /api/v1/** 业务端点自骨架起即接 RS256 校验,与 user/pet/community 同一约定。
patbond:
jwt:
# RS256 公钥,用于本地校验 patbond-auth 签发的 access token。
# 值可以是 PEM 文件路径,也可以是内联 PEM 内容(以 -----BEGIN 开头)。
# 私钥只给 patbond-auth,绝不入库。
public-key: ${PATBOND_JWT_PUBLIC_KEY:}
# /internal/** 服务间共享密钥(Java↔Java 既有通道),需与 patbond-user 同值。
# 注意:调用 patbond-ai 用的**不是**这个 token,而是下面的服务身份 JWT
# ——两套机制并存的原因见 ADR-023 §5。
internal-token: ${PATBOND_INTERNAL_TOKEN:dev-only-internal-token}
user-service:
# 预签名上传凭证来源:patbond-user 的 /internal/media/**(ADR-017 边界:
# 媒体上传流程归 user,本服务不直连对象存储、也不持有其凭证)。
url: ${PATBOND_USER_SERVICE_URL:http://127.0.0.1:8082}
creation:
# ---- 上游生成服务(patbond-ai,Python)----
ai-service:
url: ${PATBOND_AI_SERVICE_URL:http://127.0.0.1:8000}
# 单次 HTTP 调用超时。注意这是**提交/查询**的超时,不是生成耗时——
# Java↔Python 边界一律异步(ADR-023 §4),故此值与出图出片时长无关,
# 不得为了「等出图」而调大。
connect-timeout: ${PATBOND_AI_CONNECT_TIMEOUT:2s}
read-timeout: ${PATBOND_AI_READ_TIMEOUT:5s}
# ---- 服务身份 JWT(ADR-023 §5)----
# **独立密钥对**,与用户 token 的密钥对不同。理由:现有 JwtVerifier 只验
# 签名、不校验 aud/iss,用户 token 亦无 aud claim;若复用同一密钥对,
# 四个模块都会把服务票当合法用户 token 接受。独立密钥对无需改动任何
# 已发布的验证器。私钥只给本服务,公钥给 patbond-ai。
service-token:
private-key: ${PATBOND_SERVICE_JWT_PRIVATE_KEY:}
issuer: ${PATBOND_SERVICE_JWT_ISSUER:patbond-api}
audience: ${PATBOND_SERVICE_JWT_AUDIENCE:patbond-ai}
subject: ${PATBOND_SERVICE_JWT_SUBJECT:service:creation-worker}
# 短有效期:服务票每次调用现签,不缓存、不复用。
ttl: ${PATBOND_SERVICE_JWT_TTL:5m}
# ---- Worker 与租约(ADR-023 §3/§4)----
worker:
# 关掉后本服务只提供读写端点、不消费队列(便于单实例调试与测试隔离)。
enabled: ${PATBOND_CREATION_WORKER_ENABLED:true}
# 调度器 tick 间隔。真正的轮询节律由 next_attempt_at 控制,
# tick 只决定「最快多久发现一件该做的事」。
tick-interval: ${PATBOND_CREATION_TICK_INTERVAL:3s}
# 单 tick 最多领取多少条,防止一个实例吃光整个队列。
batch-size: ${PATBOND_CREATION_BATCH_SIZE:5}
# 租约时长。**必须大于最长的单次轮询间隔**(见下 poll-interval-late),
# 否则会出现「任务仍在跑但租约过期被他人抢占」。每次轮询顺带续租。
lease-ttl: ${PATBOND_CREATION_LEASE_TTL:60s}
# 服务端轮询 patbond-ai 的退避节律(ADR-023 §4)。视频任务绝对次数很多,
# 固定 3s 会产生数百次无谓轮询与续租写入。
poll-interval-early: ${PATBOND_CREATION_POLL_EARLY:3s} # 前 60 秒
poll-interval-mid: ${PATBOND_CREATION_POLL_MID:10s} # 60s ~ 5min
poll-interval-late: ${PATBOND_CREATION_POLL_LATE:30s} # 5min 以后
poll-early-until: ${PATBOND_CREATION_POLL_EARLY_UNTIL:60s}
poll-mid-until: ${PATBOND_CREATION_POLL_MID_UNTIL:5m}
# 失败重试的指数退避基数(实际间隔 = base * 2^(attempt-1))。
retry-backoff-base: ${PATBOND_CREATION_RETRY_BACKOFF:30s}
# 客户端硬超时由服务端下发而非客户端硬编码(ADR-023 §10):图片与视频
# 差一个量级,硬编码两个数会使换 provider 就要发版。
client-timeout-image: ${PATBOND_CREATION_CLIENT_TIMEOUT_IMAGE:5m}
client-timeout-video: ${PATBOND_CREATION_CLIENT_TIMEOUT_VIDEO:20m}
# 任务被判定为僵死的阈值(running 超过此时长)。对应客户端的第六个 UI 态,
# 它不是错误态(ADR-023 §10)。
stale-after-image: ${PATBOND_CREATION_STALE_IMAGE:10m}
stale-after-video: ${PATBOND_CREATION_STALE_VIDEO:40m}