Files
patbond-api/patbond-creation/pom.xml
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

149 lines
6.1 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.patbond.patbond</groupId>
<artifactId>patbond-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>patbond-creation</artifactId>
<packaging>jar</packaging>
<name>patbond-creation</name>
<description>AI generation job orchestration service for Patbond (ADR-023)</description>
<!-- M4 skeleton: RS256 bearer auth on /api/v1/** (fourth copy of the
user/pet/community stack), creation schema access via JDBC, and the
lease queue worker. This module owns job state; the upstream provider
call lives in patbond-ai (Python) and is reached over REST.
Flyway remains absent — the migration chain is owned by patbond-user. -->
<dependencies>
<dependency>
<groupId>com.patbond.patbond</groupId>
<artifactId>patbond-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Outbound calls to patbond-ai (generation) and patbond-user
(/internal/media/** upload credentials), static direct URLs per
ADR-002. feign-hc5 for the same reason as auth/community: the JDK
default client loses error bodies on some replies. -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Access token verification (RS256 public key) AND service-identity
token signing (RS256 private key, a SEPARATE key pair per
ADR-023 §5). jjwt is not in the Boot BOM; version pinned in step
with patbond-user/auth/pet/community. -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Tests need the creation schema. The migration chain (V1..V6) is
owned by patbond-user (single flyway_schema_history); pulling its
plain jar plus Flyway into the TEST classpath lets Boot's Flyway
auto-config apply the same chain to the disposable container.
Production wiring is unchanged (same mechanism as pet/community). -->
<dependency>
<groupId>com.patbond.patbond</groupId>
<artifactId>patbond-user</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- No spring-boot-starter-parent in this build, so the
executable-jar repackaging must be bound explicitly. -->
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<!-- Keep the plain jar as the main artifact so other
modules can depend on this one; the runnable fat
jar gets the -exec classifier and is what the
Dockerfile ships (same pattern as the others). -->
<classifier>exec</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>