creation schema 三表(generation_models / generation_styles / generation_jobs)逐列取自已评审目标模型 patbond_postgresql.sql (schema 60/68 + 对象 556-717),含 14 索引与 3 个 updated_at 触发器。 - 零外键需要裁剪:这是第一个不用砍跨 schema 外键的 baseline 迁移, identity/media(V1)与 pet_health(V3)目标 schema 均已存在 - 按 V5 文件头指令「M4 补回」补回 community.posts.generation_job_id → creation.generation_jobs(id) ON DELETE SET NULL;约束名显式写为 posts_generation_job_id_fkey,即目标模型内联声明时 PostgreSQL 会 自动生成的名字,使 V1..V6 建库与照目标模型建库约束名一致 - posts.region_id 保持裸列(区域系统属 M5,ADR-018 裁剪,M5 补回) - 两条偏索引对齐 worker 语句:ix_generation_jobs_queue 的列序与出队 ORDER BY 完全一致(SKIP LOCKED 领取走索引扫描不排序); ix_generation_jobs_running 驱动租约回收与待轮询扫描 两处刻意偏离目标模型,文件头已逐条记录原因: 1. input_asset_id 改可空(目标模型 NOT NULL)——参考图选填拍板;原 NOT NULL 会使文生图无法表达。未发明 requires_input 标记(不超出 已评审模型),「某模型是否必须有输入」由应用层查目录判断 2. 新增 first_attempt_at(目标模型无此列)——iteration-4/06 §4.2 指标 口径要求。目标模型无法表达:其 ck_generation_jobs_state 强制 queued 分支 started_at IS NULL,故重排队必然擦除首次尝试时间, 导致排队时长与纯执行耗时永久无法分离。该列仅首次进入 running 时 写入、不覆盖,并刻意排除在 queued 分支的置空要求之外; ck_generation_jobs_first_attempt 钉住其与 attempt_count 的不变式 测试: - 新增 CreationMigrationIntegrationTest(12 项)。重点覆盖「日后同步回 目标模型会静默改回去」的行为:复合外键真实拦截跨 media_kind 引用、 queued→running→failed→queued 全程后 first_attempt_at 保留而 started_at 被清空、不变式双向拒绝、provider_request_id 部分唯一索引 (NULL 可共存、同值冲突)、idempotency_key 的按用户作用域 - CommunityMigrationIntegrationTest 原断言 %generation% 与 %region% 外键合计为 0,V6 后必然变红(iteration-4/02 已预警)。拆为两条独立 断言(generation=1、region=0)而非改成合计 1,使任一侧回退可单独定位 全量 381 → 393,零失败零跳过(auth 40 / common 3 / community 107 / pet 100 / user 131→143)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Patbond API
Patbond API is a Spring Boot multi-module backend.
Modules
patbond-common: stable cross-module contracts (response wrapper, shared DTOs). No web/messaging dependencies.patbond-user: user service for user creation, profile lookup, and password verification.patbond-auth: authentication service for registration and login. Callspatbond-userover HTTP via OpenFeign with a configured static URL (no service discovery in the MVP, see ADR-002).patbond-pet: pet profile and health record service (M2, ADR-009). Shares the database withpatbond-userand only reads/writes thepet_healthschema; the Flyway migration chain stays owned bypatbond-user. First-wave skeleton: liveness endpoint only.
Technology Stack
- Java 17 (build baseline; use JDK 17 for release builds)
- Spring Boot 3.5.16
- Spring Cloud 2025.0.3 (OpenFeign only)
- PostgreSQL 18 + Flyway (patbond-user owns the migration chain:
identity/media/platform/pet_healthschemas) - Maven (use the committed Maven Wrapper
./mvnw)
Build and Test
./mvnw clean test
Integration tests start a disposable postgres:18 via Testcontainers, so a
running Docker daemon is required (no local PostgreSQL installation or
credentials are needed).
If your default JDK is not 17, point JAVA_HOME at a JDK 17 installation first, e.g.:
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test
Run
Running patbond-user requires a reachable PostgreSQL 18 database; Flyway
applies the versioned migrations in
patbond-user/src/main/resources/db/migration automatically on startup.
Development seed data (db/dev, regions reference rows) is opt-in via a dev
profile — see application.yml.sample.
Each service ships a committed application.yml.sample; the real application.yml
is git-ignored. First copy the samples (defaults work locally, overrides via
environment variables):
cp patbond-user/src/main/resources/application.yml.sample \
patbond-user/src/main/resources/application.yml
cp patbond-auth/src/main/resources/application.yml.sample \
patbond-auth/src/main/resources/application.yml
cp patbond-pet/src/main/resources/application.yml.sample \
patbond-pet/src/main/resources/application.yml
Install the shared module once, then run each service in its own terminal:
./mvnw -pl patbond-common install
./mvnw -pl patbond-user spring-boot:run
./mvnw -pl patbond-auth spring-boot:run
Running the services also needs an RS256 key pair for access tokens (the
private key signs in patbond-auth, the public key verifies in
patbond-user; neither is committed):
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out jwt-private.pem
openssl pkey -in jwt-private.pem -pubout -out jwt-public.pem
export PATBOND_JWT_PRIVATE_KEY=$PWD/jwt-private.pem # patbond-auth
export PATBOND_JWT_PUBLIC_KEY=$PWD/jwt-public.pem # patbond-user
Tests do not require this copy step — patbond-auth/src/test/resources/application.yml
keeps ./mvnw clean test self-contained on a clean checkout.
Smoke check:
curl -X POST http://127.0.0.1:8081/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"username":"demo_user","password":"secret123"}'
Configuration
| Environment variable | Default | Used by |
|---|---|---|
PATBOND_USER_PORT |
8082 |
patbond-user |
PATBOND_AUTH_PORT |
8081 |
patbond-auth |
PATBOND_USER_SERVICE_URL |
http://127.0.0.1:8082 |
patbond-auth (Feign target for patbond-user) |
PATBOND_DB_URL |
jdbc:postgresql://127.0.0.1:5432/patbond |
patbond-user |
PATBOND_DB_USER |
patbond |
patbond-user |
PATBOND_DB_PASSWORD |
patbond |
patbond-user |
PATBOND_INTERNAL_TOKEN |
dev-only-internal-token |
both (shared secret for /internal/**; inject a strong random value outside local dev) |
PATBOND_JWT_PRIVATE_KEY |
(none, required) | patbond-auth (RS256 private key: PEM path or inline PEM) |
PATBOND_JWT_PUBLIC_KEY |
(none) | patbond-user (RS256 public key: PEM path or inline PEM) |
PATBOND_ACCESS_TTL |
15m |
patbond-auth (access token lifetime, ADR-003) |
PATBOND_REFRESH_TTL |
30d |
patbond-user (refresh token lifetime, ADR-003) |
PATBOND_LOGIN_LOCK_MAX_FAILURES |
5 |
patbond-user (login failures before lockout) |
PATBOND_LOGIN_LOCK_WINDOW |
15m |
patbond-user (failure counting window) |
PATBOND_LOGIN_LOCK_DURATION |
15m |
patbond-user (lock duration) |
Machine-specific values live in the git-ignored application.yml (copied from the
committed .sample); never commit secrets to the samples.
Docker Compose
MVP 编排(ADR-007):postgres:18(数据落 volume)+ 两个无状态应用容器。
配置与本机运行同一套约定 —— 容器内挂载 application.yml.sample 作为配置,
PATBOND_* 环境变量注入实际值。
# 1. 生成 RS256 密钥对与 .env(DB 口令、内部令牌;产物被 .gitignore 忽略)
./deploy/init-secrets.sh
# 2. 构建可执行 jar
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package
# 3. 启动(首次会构建镜像)
docker compose up -d --build
# 冒烟
curl -s -X POST http://127.0.0.1:8081/api/v1/auth/register \
-H 'Content-Type: application/json' \
-d '{"username":"demo_user","password":"secret123"}'
# 停止(-v 同时删除数据库数据)
docker compose down
注意:数据库端口不对宿主机发布;8082(user)在 MVP 阶段直连暴露以提供
/api/v1/me,/internal/** 由服务间令牌保护,规模化阶段应改由网关统一入口。
Services
Auth Service
- Application name:
patbond-auth - Default port:
8081 - Register:
POST /api/v1/auth/register - Login:
POST /api/v1/auth/login - Refresh (rotates the refresh token):
POST /api/v1/auth/refresh - Logout (revokes the current session):
POST /api/v1/auth/logout
User Service
- Application name:
patbond-user - Default port:
8082 - Current user profile:
GET /api/v1/me(Bearer access token, verified locally with the RS256 public key) - Internal (require
X-Internal-Token):- Create user:
POST /internal/users - Verify password:
POST /internal/users/verify-password - Get user by id:
GET /internal/users/{id} - Get user by username:
GET /internal/users/by-username/{username} - Sessions:
POST /internal/sessions,POST /internal/sessions/refresh,POST /internal/sessions/revoke
- Create user:
Note: user data is persisted in PostgreSQL (
identity.users/identity.user_credentials, bcrypt password hashes, UUIDv7 ids generated in the application). Refresh sessions live inidentity.auth_sessions(SHA-256 digests only, rotation on every refresh, token-family revocation on reuse, ADR-003). Errors follow the{code, message, data}envelope with stable business codes and matching HTTP statuses; the public contract is documented inpatbond-doc/docs/api/openapi.yaml.