Files
patbond-doc/docs/development/iterations/iteration-4/01-pm-task-breakdown.md
T
lixi 79b33dba31 docs: M4「AI 创作」开工分析八份报告 + 汇总拍板页入档挂导航
八角色并行开工分析,合计 7448 行;另出 00 汇总页(跨角色收敛结论、
13 项待拍板、6 项待仲裁分歧、未取证项汇总),挂第四迭代导航最前。
mkdocs build --strict 通过。

基线实测修正(文档与实况不符):
- api 测试 381(releases.md 记 379,成因待仲裁)
- 埋点白名单 41(四份文档记 42,experiment_exposed 重复计数)
- 真机验证挂起 10 项(转述链 4→6→8→10 每跳丢项)
- E2E 断言机械可数 226(声称 234 无可复核来源)
- v0.4.0 实际发布 09-14 11:17;CI 非红,三仓五上下文全绿

多方独立收敛(无需拍板):
- 队列用 Postgres SKIP LOCKED + 租约列,不引入 Redis/MQ
- 服务端零对象写能力(ObjectStorage 无 put/get),M4 立足点缺地基
- 「四模块字节级快照锁 CI」不存在,实际门禁仅结构断言
- 定稿模型 input_asset_id NOT NULL,即图生图不支持文生图
- 跨 schema 外键补回是 V5 自身指令,裁剪理由已不成立

阻塞项与安全缺口:
- AI provider BLOCKED:零 SDK/endpoint/额度,正典种子即 fixture
- 分支保护必需上下文选错触发器:(push) 限定 branches:[dev],
  致「推 dev 即满足门禁」且「非 dev 分支 PR 永久无法合并」
- check-secrets.sh 对 sk-/sk-ant- 零覆盖,须先于任何 AI key 落地
- 北极星 09-21 窗口已于 09-13 关闭,补救无从下手,建议改事件驱动

本批核心教训:13 处文档/注释与代码相反且多已被下游采信,其中
5 处造成实际规模误判(widthPx M→S、数据模型早已定稿 L→M、
社区侧 purpose 校验实际不存在等)。汇总页 §0 立转述纪律。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 15:48:56 +08:00

743 lines
121 KiB
Markdown
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.
# Patbond 第四迭代任务分解(M4 AI 创作)
> 作者:Senior Project Manager
> 日期:2026-09-14
> 依据:`docs/development/development-plan.md`(第 7 节 M4、第 4/6 节规范、第 9/10 节质量门禁与 DoD)、`iterations/iteration-3/29-m3-summary.md` §4、`iterations/iteration-3.5/06-wave2-closure.md` §7、`docs/architecture/backend-modules.md`、`docs/architecture/decisions.md`ADR-001~022)、`docs/database/patbond_postgresql.sql``creation` schema 3 表,已评审定稿)、`docs/api/openapi.yaml` v1.4.032 路径 / 45 操作 / 75 schema,冻结中)
> 编号约定:本迭代工单以 `T4-` 前缀编号,决策以 `D4-` 前缀编号。
> 范围声明:严格限定为 M4 AI 创作。本地服务与预约(M5)、通知推送与交付加固(M6)不在本迭代范围;`posts.region_id` 仍仅作预留。范围外需求一律记 backlog。
**结论摘要**:工单 **28** 个(其中条件单 3 个)分 **4 波**;待用户拍板决策 **14** 项,头号三项为 AI 提供方选型、模块归属与 Worker 形态、生成输入图是否必填。开工审计**推翻或修正了 8 处既有文档结论**,其中两处直接改变规模预估:`creation` schema 的完整设计早已评审定稿(V6 是提取而非设计,且零跨 schema 外键需裁剪),而「输出写入媒体表」一句话背后隐藏着「服务端目前根本不能写对象存储」这一真实工作量。
---
## 1. 范围界定与依据
### 1.1 开发计划 M4 原文(正典依据)
开发计划第 7 节 M4`development-plan.md:246-255`)逐字引用:
- 目标:「替换上传和生成延时模拟。」(:248)
- 「实现模型/风格目录、生成任务创建、查询和取消。」(:250)
- 「Worker 通过队列消费任务,使用租约、重试和幂等键防止重复生成。」(:251)
- 「输出写入媒体表,成功后可一键创建社区草稿。」(:252)
- 「Flutter 展示排队、生成进度、失败重试和取消状态。」(:253)
- 验收标准:「任务状态流转合法;Worker 重启不丢任务;相同幂等请求不重复扣费或生成;失败原因可追踪。」(:255)
四条验收标准与工单的映射:
| 验收标准 | 主责工单 | 取证方式 |
| --- | --- | --- |
| 任务状态流转合法 | T4-06 / T4-07 / T4-08 | `ck_generation_jobs_state` 数据库层强制五态字段组合(目标模型 `patbond_postgresql.sql:667-692`),非法迁移**在库层即被拒**;应用层再补六类路径测试 + T4-25 E2E |
| Worker 重启不丢任务 | T4-08 | 租约过期回收(`lease_expires_at` + `ix_generation_jobs_running` partial 索引);T4-25 E2E 在 running 态重启容器后断言任务被重新领取并完成 |
| 相同幂等请求不重复扣费或生成 | T4-06 | `UNIQUE (user_id, idempotency_key)` + `uq_generation_jobs_provider_request`;配额按 `generation_jobs` 行数计(见 D4-7),幂等命中不新建行即天然不重复扣费 |
| 失败原因可追踪 | T4-07 / T4-08 | `error_code` / `error_message` / `attempt_count` 三列经查询端点外露;provider 原始失败经日志(不含密钥)留痕 |
### 1.2 其他正典约束(M4 必须遵守)
- **业务域→schema 映射已定**`development-plan.md:67`「AI 创作 = 模型、风格、异步任务、重试、生成结果 → `creation` schema」;:64「媒体域含 AI 输入输出」。
- **写接口幂等强制**`development-plan.md:173`「创建帖子、**生成任务**和预约等写接口必须支持 `Idempotency-Key`」。
- **可观测性硬要求**`development-plan.md:329`「技术指标须含**队列积压和 Worker 失败率**」;:330 产品漏斗须含「**AI 任务成功**」。
- **`patbond-common` 纪律**`development-plan.md:79`「不应让所有服务被动引入 RabbitMQ、Feign 等依赖」——这条对 D4-3 队列载体选型有直接约束力。
- **Flutter feature 拆分已预留 `creation`**`development-plan.md:96` 明列 `auth/pets/community/creation/marketplace`
- **单迁移链**`backend-modules.md:41`「Flyway 迁移链唯一持有者……**其他模块不得携带 Flyway**」——V6 必须进 `patbond-user`,与模块归属决策(D4-2)解耦。
- **结构性变更须经 ADR**`backend-modules.md:3-5`「新增/拆分模块须经 ADR 决策并同步更新本页」。M4 是 **ADR-023 起**的落点(现存最高 ADR-022)。
- **AI 提供方在正典中仍为未决项**:`development-plan.md:358`「AI 提供方、对象存储、天气/地图供应商和通知渠道尚未确定」、:365 待确认事项同列。对象存储已由 ADR-016 解决,**AI 提供方是 M4 唯一从头拍板的供应商项**(D4-1)。
## 2. 开工前的关键事实(PM 逐项核实原始 SQL / 契约 / 源码)
> **纪律说明**M3.5 的教训(`iteration-3.5/06-wave2-closure.md:42`「转述不可采信」——一份报告写「无 nickname 字段」实指「契约未暴露」,被当成缺列,规模高估一整档)在本节被严格执行。下列每一条「存在 / 不存在」的判断都注明了核实的**原始文件与行号**,未经原文核实的一律标注「未取证」。
### 2.1 `creation` schema 的完整设计早已评审定稿——V6 是**提取**而非设计(推翻「M4 需从零设计数据模型」的隐含预期)
`docs/database/patbond_postgresql.sql`(评审定稿的目标模型)已含 `creation` schema 全部三张表:
| 对象 | 行号 | 关键内容 |
| --- | --- | --- |
| `CREATE SCHEMA creation` | :60 | 七个 schema 之一(platform/identity/media/pet_health/community/**creation**/marketplace |
| `creation.generation_models` | :556-573 | `code`/`display_name`/`provider_code`/`provider_model_name`/`media_kind`/`enabled`/`sort_order``UNIQUE (code, media_kind)` + `UNIQUE (id, media_kind)`;索引 :575-577partial `WHERE enabled` |
| `creation.generation_styles` | :580-598 | `code`/`title`/`subtitle`/`media_kind`/`preview_asset_id`(FK→media.assets ON DELETE SET NULL)/`enabled`/`sort_order`;索引 :600-602 |
| `creation.generation_jobs` | :605-696 | **40 列**,含完整队列语义(见 2.2 |
| 队列索引 | :711-715 | `ix_generation_jobs_queue (priority DESC, next_attempt_at, created_at, id) WHERE status='queued'``ix_generation_jobs_running (lease_expires_at, id) WHERE status='running'` |
| provider 幂等唯一索引 | :699-700 | `uq_generation_jobs_provider_request (provider_code_snapshot, provider_request_id) WHERE provider_request_id IS NOT NULL` |
| `updated_at` 触发器 | :1245-1249 | 三张表均复用 `platform.set_updated_at()`V1 已建) |
| 开发种子 | :1495-1523 | 2 模型(image+video/ 5 风格 / 1 已完成任务;`provider_code = 'fixture'` |
**规模含义**T4-01 的性质与 M3 的 T3-01 完全同构(「从 bootstrap SQL 提取」,`iteration-3/01:52`),是**抄录 + 裁剪判断**,不是建模。据此定为 **M** 而非 L。
### 2.2 M4 的迁移**零跨 schema 外键需要裁剪**——与 V3/V5 的先例相反
V3 剪了 4 条 marketplace 外键、V5 剪了 2 条(`V5__community_baseline.sql:15-25` 原文)。M4 的 V6 **一条都不用剪**
- `generation_jobs` 的三条跨 schema 外键目标全部已存在:`identity.users`V1:60)、`pet_health.pets`V3:35)、`media.assets`V1:205)。`generation_styles.preview_asset_id → media.assets` 同理。
- `generation_jobs` 内部两条复合外键(→`generation_models(id, media_kind)`、→`generation_styles(id, media_kind)`,:644-647)同一迁移内建表即可满足。
- **反向的一条要补回**`community.posts.generation_job_id` 目前是裸列无外键——`V5__community_baseline.sql:47` 行内注释原文 `-- generation_job_id: bare nullable uuid, FK to creation.generation_jobs stripped (M4 补回)`,列定义 :48 `generation_job_id uuid`nullable),索引 `ix_posts_generation_job` 已在 :95 建好。V5 文件头 :17-19 亦言明「the M4 migration that creates the creation schema re-adds this constraint」。
### 2.3 队列语义在库层已完备——**不需要引入任何新中间件**
`generation_jobs` 已含全套 DB-as-queue 列(`patbond_postgresql.sql` 行号):`status`(:625)、`progress`(:626)、`priority`(:627)、`attempt_count`/`max_attempts`(:628-629)、`provider_request_id`(:630)、`error_code`/`error_message`(:631-632)、`idempotency_key`/`request_hash`(:633-634 均 NOT NULL)、`next_attempt_at`(:635)、`lease_owner`/`lease_expires_at`(:636-637)、`started_at`/`completed_at`(:640-641)、`version`(:642)。
配合 :711-715 的两条 partial 索引,`SELECT … FOR UPDATE SKIP LOCKED` 即构成带优先级、退避与租约回收的完整队列。
**与开发计划的偏离提示**`development-plan.md:205` 写「RabbitMQ 在异步任务阶段启用」。核实基础设施现状后(见 2.4),PM 建议本迭代**不引入 RabbitMQ**,改用 DB 队列——这属于对正典的建议性偏离,须经 D4-3 拍板并落 ADR。
### 2.4 现有异步基础设施:几乎为零(这是 T4-08 的真实起点)
| 事实 | 核实位置 |
| --- | --- |
| 无 Redis / RabbitMQ / Kafka(依赖与容器均无) | `docker-compose.yml` 六服务:postgres:13 / minio:34 / user:49 / auth:79 / pet:97 / community:125;各模块 pom 依赖清单(如 `patbond-user/pom.xml:28-104`)无任何 MQ/Redis 坐标 |
| `@EnableScheduling` 仅一处 | `patbond-user/.../UserApplication.java:7` |
| `@Scheduled` 唯一使用点 | `patbond-user/.../session/SessionCleanupJob.java:32-41``fixedDelayString = "${patbond.session.cleanup-interval:PT6H}"`),配置 `patbond-user/src/main/resources/application.yml:24-26` |
| `@Async` / `@EnableAsync` / `TaskExecutor` / `ThreadPoolTaskScheduler` | **全仓零命中**(主代码与测试) |
| 队列消费循环 / MQ 监听器 / outbox 发布器 | **不存在**`platform` schema 注释(V1:24)承诺了 "reliable outbox",但表与消费代码均未落地 |
| 无限流 | 全仓无 `RateLimit`/`bucket4j`/`429``patbond-common/.../error/ErrorCode.java` 无 429 段(最接近的 `LOGIN_LOCKED(42300, 423, …)`:36 是账号锁定不是限流) |
| 无 feature flag 设施 | 全仓零实现(详见 2.9) |
结论:M4 的 Worker 只有**一个先例可循**`SessionCleanupJob` 式的 `@Scheduled` + DB 幂等操作,Spring 默认单线程 scheduler、多实例并跑无锁)。这既是 D4-2/D4-3 的现实约束,也说明 T4-08 必须自带线程池与并发度配置。
### 2.5 「输出写入媒体表」一句话背后:**服务端目前根本不能写对象存储**(本迭代最易被低估的工作量)
`ObjectStorage` 接口只有四个方法(`patbond-user/.../media/ObjectStorage.java:15-37`):`ensureBucket()`:18、`presignPut(...)`:25、`stat(...)`:28、`presignGet(...)`:31。实现 `S3ObjectStorage.java``S3Client` 实际只用于 `headBucket`/`createBucket`(:66-79) 与 `headObject`(:102)`putObject` 仅出现在**预签名构造**里(:83-86 `presigner.presignPutObject(...)`)。
即:现有媒体链路是「客户端直传,服务端只签名与校验」。AI 输出的字节由 provider 返回、必须**由服务端落盘**,因此 M4 必须:
1.`ObjectStorage` 上新增写方法 + `S3ObjectStorage` 实现 + `MediaStorageConfig.java:33-59``UnconfiguredObjectStorage` 补桩;
2. 解决「谁来写 `media.assets` 行」——ADR-017`decisions.md:156`)定死「media 上传流程实现在 `patbond-user`」,且 `media` 写入侧代码全在 `patbond-user/.../media/``MediaAssetRepository.insertUploading`:32-41、`markReady`:81-86);`pet`/`community` 侧只有 `MediaUrlSigner`(本地 SigV4 签名,不持 `S3Client`)与只读 `MediaAssetGateway`
这直接决定 D4-2 的子问题:新模块如何把生成结果落进 media 域。已单列为工单 **T4-03**
### 2.6 `media.assets` 的尺寸列早已存在——`widthPx/heightPx` 恒 null 不是缺列(M3.5 同型教训的复现)
- `width_px integer` / `height_px integer` 均在 `V1__identity_media_baseline.sql:217-218`,约束仅 `ck_media_dimensions`:241-245,正数校验),**nullable**。
- 契约侧唯一关于回填的原文在 `openapi.yaml:3536``description: complete 后回填,可空`;但 `POST /api/v1/media/uploads/{assetId}/complete`:1256-1299**没有 requestBody**:1274 直接进 parameters),也没有任何字段承载尺寸;`MediaAsset.required` = `[id, kind, purpose, mimeType, status, createdAt]`(:3516),两个尺寸字段不在其中。
- 故此项的真实缺口是「**服务端无尺寸探测**」+「契约声明了来源不存在的回填」,规模为 **S**(配合 T4-03 新增的对象读能力),不是「加列迁移」。处置方案见 D4-9。
### 2.7 `ai_creation` 分类在写侧被**双重封死**(代码 + 契约)
- 数据库允许:`V5__community_baseline.sql:68` `CONSTRAINT ck_posts_category CHECK (category IN ('general', 'help', 'ai_creation'))`
- 后端写侧拒绝:`patbond-community/.../dto/CreatePostRequest.java:26` `@Pattern(regexp = "general|help", message = "category 仅支持 general/help")`
- 契约写侧拒绝:`openapi.yaml:3657` `CreatePostRequest.category` enum `[general, help]`:3659 注明「`ai_creation` 为 M4 预留值,M3 不开放写入(提交 400/40000)」;读侧 `Post.category`(:3747-3748) 与 `FeedCard.category`(:3826) 已含三值。
- `generationJob` **在契约里没有字段本体**——`openapi.yaml:136` 与 :3717 只是说明文字,`PostResponse.java:12` 亦注明该字段「do not appear at all」。
- 落库通路也缺:`patbond-community/.../repository/PostRepository.java:56-58``insertPost(...)` 签名与 :60-64 的 INSERT 列清单**均无 `generation_job_id`**。
即 T4-10 需同时改:Java 校验、Repository INSERT、契约 enum、契约新增 `generationJob` 字段。
### 2.8 目标模型内部存在一处矛盾,必须在 V6 前裁决
`generation_jobs.model_version_snapshot varchar(64) **NOT NULL**``patbond_postgresql.sql:622` 区块,:620-623 三个 snapshot 列)——但 `creation.generation_models`:556-573**没有任何 version 列**,种子数据把它硬编码为 `'1.0'`(:1514)。这是评审定稿模型里的一处自相矛盾,V6 必须选边(D4-6)。
另一处需注意的约束后果(可满足但不直观):`ck_generation_jobs_state`:667-692)要求 `queued` 态必须 `started_at IS NULL AND progress = 0 AND lease_* IS NULL`。因此**租约过期回收/重试时必须把 `started_at` 清回 NULL**,首次尝试的开始时间会丢失。T4-08 需在实现说明中显式记录这一点。
### 2.9 A/B 前置「6 绿 1 半」不成立——实测为 **5 绿 3 半**
`iteration-3/06-experiment-tracking-plan.md:336-351` 的八项前置表逐项对代码核实后:
| # | 前置 | 文档判定 | 代码核实 |
| --- | --- | --- | --- |
| 4 | 稳定分流组件 | 绿(:345) | **部分绿**`anonymousId` 持久化已落地(`analytics_service.dart:51`、:177-190),但 `hash(userId, salt) % buckets` 分流组件**全仓零实现** |
| 5 | 曝光事件 | 绿(:346 | **部分绿**:后端字典已有(`EventDictionary.java:103`props `{experimentKey, variant}`),Flutter 侧 `grep -rn experiment lib test integration_test` **零命中** |
| 7 | 护栏监控与回滚 | 部分绿(:348,「feature flag 随社区功能发布开关顺带落地」) | **回滚也不绿**feature flag / 社区发布开关在两仓中**完全不存在**,`feature-checklist.md` 连该条目都未登记 |
另:#2「指标基线」判绿,但北极星出数 SQL 仅以 Markdown 代码块存在于 `iteration-2/06-experiment-tracking-plan.md:200-229`,三仓 `scripts/` 下**无任何可执行巡检/看板脚本**(只有 `check-secrets.sh``hooks/`)。
含义:「M4 可启首个实验」这一结论的前提比文档描述的弱。若要在 M4 启动实验,需先补分流 + 曝光 + 开关三件(工单 T4-23,条件单,随 D4-11)。
### 2.10 埋点白名单实测 **41** 条,文档三处写 42`experiment_exposed` 被重复计数)
- 权威定义在**代码常量**`patbond-user/.../analytics/EventDictionary.java:42-104``grep -c 'Map.entry("'` = **41**。无字典表(V4 只 seed `breeds``vaccine_catalog`),DB 侧仅正则约束 `V2:21`
- 文档写 42 的三处:`iteration-3/22-event-whitelist-v3.md:4,14``iteration-3/29-m3-summary.md:20``feature-checklist.md:218`
- 根因:设计源文档自己写的是 19 个新事件(`iteration-3/06-experiment-tracking-plan.md:12,149`),22v2+ 19 = 4122 号报告把 `experiment_exposed` 既算进 19 又单列一次。
- **无门禁能拦**`EventDictionaryTest.java` 无任何总量断言(无 `hasSize`/`size()`)。T4-22 顺手补一条总量断言即可永久钉住。
### 2.11 真机验证挂起实为 **10** 项(不是 8 项),M2 两项带 2026-09-21 硬时限
`device-verification.md` 逐项清点:M2 **2** 项(`:42` 验证一 Android 事件落库、`:63` 验证二 SessionTracker 30 分钟换会话)+ M3 **4** 项(`:114`/`:124`/`:134`/`:144`+ M3.5 **4** 项(`:176`/`:190`/`:200`/`:209`= **10 项**,三个执行记录(`:106`/`:168`/`:221`)全为「_(待补)_」,即 10 项全部未执行。
M2 两项的时限来自 `iteration-3/29-m3-summary.md:52`(埋点角色建议 2026-09-21 北极星首次出数日前完成,否则首批读数只能标未验收)与 `iteration-2/06-experiment-tracking-plan.md:308-312`(出数日历:2026-09-21 周一首次出数)。**今日 09-14,仅剩 7 天**——已按硬时限单独排为 T4-24 并置于第一波。
### 2.12 发布流程与回归门禁的实况(含一处文档自相矛盾)
- `main` 已受分支保护禁直推,发布必须走 Gitea PR:`releases.md:126-138` 五步流程;状态检查上下文 `CI / backend-test (push)`api)与 `CI / flutter-gates (push)`flutter),见 `releases.md:110-114`。api 的 `ci.yml:15-18` 确认 `pull_request` 触发器在位、job 名 `backend-test`
- **回归门禁已由两份改为四份**`releases.md:56` 原文「回归清单由两份改为四份……原则是『**每个引入对外端点的迭代都应有对应的 E2E 脚本,并在此后每次发布回归**』」;v0.4.0 实测 42/42 场景、234 断言(`releases.md:36`)。
- **文档自相矛盾(须在 M4 收官修正)**:同一文件 `releases.md:131` 的「发布后生效的纪律」第 1 步仍写「E2E **双份**回归 PASS」,与 :36/:56 的四份口径冲突;且该原则**从未固化进 `git-workflow.md`**(该文件 :56-64 的门禁表只列三仓的 format/analyze/test/mkdocs 四命令,无 E2E 条目)。M4 引入生成域对外端点,按 :56 原则须新增**第五份** E2E 脚本(T4-25),并把「双份」改为「五份」+ 把该原则写进 `git-workflow.md`T4-27)。
- 现有四份脚本实为纯 Dart CLI(`dart run`,非 `flutter test`,不计入 597):`test_e2e_manual.dart`M17 场景)、`test_e2e_m2_manual.dart`11)、`test_e2e_m3_manual.dart`14)、`test_e2e_m35_manual.dart`10);四份同构骨架(`fail()`/`check()`/`_passed` 计数/`Resp` 包装/通用 `call()`/脚本内现注册账号取 token/token 与预签名 URL 脱敏),T4-25 照抄即可。
### 2.13 契约快照锁的实际强度**低于文档口径**(「四模块字节级快照锁 CI」不成立)
- 正典 `docs/api/openapi.yaml` 与四份快照(`patbond-{auth,user,pet,community}/src/test/resources/contract/openapi-v1.4.0.yaml`)实测 md5 五处一致(`a7081fb84f1207eef579ab94025f5801`),字节级同步这一**事实**成立。
- 但**CI 里没有任何 md5/checksum 校验**`patbond-api/.gitea/workflows/ci.yml` 只有三步(secret scan :41-42、装 JDK17 :46、`./mvnw -B clean test` :55);`scripts/` 下只有 `check-secrets.sh`。md5 比对只出现在人工冻结记录里(`iteration-3.5/04-contract-freeze-v140.md:77``03-backend-profile-avatar.md:287`「字节级复制正典 + md5 逐一比对」= 人工步骤)。
- CI 实际锁住的是**每模块一条守卫测试**的「版本 + 三个计数 + tag 分组」:`AuthContractConformanceTest.java:399-407``MediaContractConformanceTest.java:246-251`、pet `ContractConformanceTest.java:755-760``CommunityContractConformanceTest.java:522-527`,断言值 `1.4.0 / 32 / 45 / 75`
- **缺口**:任何「计数守恒的字节修改」(改 description、改 enum 取值、改 min/max、同时增删各一字段)都能悄悄通过。M4 契约面显著扩大(新增生成域 + 可能第五个模块快照),建议顺手补一条真校验和断言(T4-14,S)。
- 第二道门禁在位且有效:`@Order(99) everyDeclaredResponseCellIsExercised()`auth :414-425 / pet :771 / community :538 / user :261),v1.4.0 共 181 格、豁免 1 格。
### 2.14 Flutter create 页现状:563 行、零测试、全假
`lib/features/create/create_page.dart`563 行,`lib/features/create/` 下仅此一个文件);`grep -rn "CreatePage" test integration_test` **零命中**——这页没有任何 widget 测试。页首注释 :10-15 自述「AI 生成模拟(700/650/500ms 假延时、风格/模型/分辨率设置、结果卡)属 M4 范围,T3-17 原样保留」。
| 模拟点 | 位置 | 实况 |
| --- | --- | --- |
| 假上传 | `:55-64 simulateUpload()` | `Future.delayed(700ms)`;展示的图直接取 demo 宠物头像 `appState.pet.avatarUrl`:162 |
| 假生成 | `:66-92 generate()` | 4 步进度,`650ms × 3` + `500ms` |
| 结果图 | `:86` | `selectedStyle.image`,即 `lib/data/demo_data.dart:206-234` 的 4 个 unsplash 硬编码 URL |
| 结果文案 | `:87-90` | 硬编码「豆豆的{风格}冒险」等 |
| 发布 | `:122-129 publish()` | 纯占位 SnackBar「AI 作品发布随 AI 创作能力上线(M4)」 |
| 模型下拉 | `:179` | 硬编码 `['Patbond-V1','Pet-Art Pro','Cute Motion']` |
| 风格选择器 | `:218-292` | 横向 150×125 卡 + 渐变遮罩 + 选中描边,数据源 `creationStyles`demo |
| 进度条 | `:294`,定义 `:532-563` | `LinearProgressIndicator(value: step/4)` + 4 条硬编码 label |
| 结果区 | `:312-386` | 16:10 图 + 标题/正文 TextField + 话题 Chip + 硬编码位置「北京市 · 朝阳区」 |
| 真实入口(保留) | `:136`,定义 `:394-419` | `_ComposeEntryCard` → push 真实 `PostComposePage` |
**可复用的 UI 骨架**:模式分段器、风格横滑卡、进度条、结果编辑区四块布局可整体保留、只换数据源与状态源——这降低了 T4-17/T4-18 的视觉返工风险(M3 的 R10 风险在此不复现)。
**顺带命中一项主题债**:该页 `:138``SegmentedButton` 选中态粉底根因是主题里**没有 `segmentedButtonTheme`**`lib/core/theme/app_theme.dart:94-196` 只定制了 card/filledButton/inputDecoration/snackBar/datePicker/navigationBar),因此吃了 `ColorScheme.fromSeed`(:88-92)的派生色。已审计色对表在 `app_theme.dart:199-214`,落地范本是 `_datePickerTheme`:215-296)。M4 既然要重做该控件,此债搭车成本为 S(详见 §8)。
### 2.15 Flutter 架构与可复用资产
- **分层**feature 内平铺 `page/controller/repository/models/display/analytics/exceptions`,约定原文 `lib/features/community/community_controller.dart:14-20``Page/Widget → Controller → Repository → ApiClient`)。
- **状态管理**:无 Riverpod/Bloc/provider——原生 `ChangeNotifier` + `ListenableBuilder`,手写 DI 全在 `lib/app/app.dart:80-147`
- **路由**:无 go_router、无路由表;`MaterialApp` 只给 `home:``app.dart:308-318`),跳转靠散落的 `Navigator.push`;路由名仅服务埋点(`lib/analytics/analytics_page_name.dart:7-25`)。**新增 AI 页面需自建 `AnalyticsPageName` 枚举值**,否则 `fromRouteName` 返回 null 不上报。
- **API Client**dio;四条 baseUrl 由 `--dart-define` 注入(`lib/core/network/api_client.dart:8-34`)。**若 D4-2 新建模块,须新增第五条 `PATBOND_CREATION_API_BASE_URL`**,并同步四份 E2E 脚本的硬编码常量与真机清单的 `flutter run` 参数说明。
- **429 已有粗分支、无 Retry-After**`api_client.dart:164-167``status == 429 → ApiRateLimitException`);`lib/analytics/analytics_service.dart:268-272` 注释明写「Retry-After 分支待其落地后一并做」。T4-11 落地后此处可一并收口。
- **`CursorPage` 已有**`lib/core/models/cursor_page.dart:3-27`。**但没有可复用的分页列表容器**——四态骨架各页各写一份;可照抄的两个范本:Feed(`community_controller.dart:9,12,146-200` + `home_page.dart:456-523,536-580`)与页面级自持游标(`lib/features/pets/health_events_page.dart:19,71-127`,同款 `_ListPhase` 已复制 4 份)。
- **`MediaUploader` 可直接复用于 AI 输入图**`lib/features/community/media_uploader.dart:127-144``purpose` 由调用方注入(:154-161),已被发布页(9 图并发 2)与头像 sheet(1 图并发 1)两处复用。
### 2.16 收藏与草稿两页:后端与仓库层全就绪,**只缺页面**
- `listMyBookmarks()``GET /api/v1/me/bookmarks``community_repository.dart:52`、实现 :274-283**全仓无调用点**。
- `listMyPosts()``GET /api/v1/me/posts?status=``community_repository.dart:26-30`、:179-193;唯一调用点是发布页恢复最新一条草稿(`post_compose_page.dart:196-199``limit:1`)。
- 契约侧路径名须注意:**不存在** `/me/drafts``/me/favorites`;正确路径是 `openapi.yaml:1462``/api/v1/me/posts`,含 inline `status` enum `[draft, published]` :1476-1482)与 :1749`/api/v1/me/bookmarks`,项形态为 `FeedCard`)。全域术语是 **bookmark**,无 favorite。
- 入口现为 demo SnackBar`lib/features/profile/profile_page.dart:49` 菜单项 + :235 `showDemoMessage`,注释 :43-46 已自认「后端能力已就位,列表页本单未做」。
- 故此项规模确为 **M**(两页 + 导航 + 测试,照抄 `_ListPhase` 范本),与 M4 契约零耦合,可在第一波并行消化。
### 2.17 基线数字的口径澄清
| 指标 | 数字 | 口径说明 |
| --- | --- | --- |
| patbond-api 测试 | **379**surefire 运行数) / **381**`@Test`+`@ParameterizedTest`+`@RepeatedTest` 注解静态计数,62 个测试类) | 文档登记的 379 来自 `releases.md:17,35` 的 surefire 汇总;两者差异属参数化用例展开口径,**不是回归**。M4 报告统一以 `./mvnw -B clean test` 输出为准并注明口径 |
| patbond-flutter 测试 | **597 通过 + 2 skip** | `flutter test` 输出原文 `00:30 +597 ~2: All tests passed!``iteration-3.5/08-release-e2e-regression.md:277`);2 skip 是环境门控冒烟(`test/smoke/detail_interactions_smoke_test.dart:173``media_upload_smoke_test.dart:133`),不含 `integration_test/`(4 份桌面实测)与仓库根四份 E2E |
| 埋点事件白名单 | **41**(非 42 | 见 2.10 |
| 真机挂起项 | **10**(非 8 | 见 2.11 |
| 契约 | v1.4.0 / 32 路径 / 45 操作 / 75 schema / 181 格矩阵 | 自行计数复核一致(`openapi.yaml:4` 版本,路径与操作按缩进计数,schemas 自 :2082 起) |
| Flyway | V1~V5,最高 V5`flyway_schema_history` 单一,归属 `patbond-user` | `patbond-user/src/main/resources/application.yml:11-12`pet/community 仅 test 作用域带 Flyway`patbond-pet/pom.xml:79-100``patbond-community/pom.xml:91-113`);auth 无库 |
| ADR | 最高 **ADR-022**M4 新决策自 **ADR-023** 起 | `docs/architecture/decisions.md:184-195` |
| 错误码 | 27 个,最大段位见 `ErrorCode.java` | 下一可用:404 段 `40407`、403 段 `40302`、422 段 `42206`、**429 段全新** |
### 2.18 未取证事项(明确声明)
1. **AI 提供方的可用性与价格**:本机为 AMD Radeon Vega 集显、无 `nvidia-smi`(即无 CUDA),自托管 SD/ComfyUI 在当前工作机不可行;生产侧腾讯云轻量服务器是否有 GPU **未取证**(需用户确认)。云 API 的具体额度与单价**未取证**(需用户提供账号或授权调研)。
2. **实验设计模板与样本量规则文档**A/B 前置 #3/#6,文档判绿):两仓中**未定位到该文档实体**,仅在 `iteration-3/06` 表格里被声明为已交付。
3. **`patbond-doc` 无仓库级 `.gitignore`**`site/` 目前仅靠用户全局 `~/.gitignore_global:183` 排除(实测未被 tracked)。换机器或 CI 检出时 `mkdocs build` 产物可被误提交,与 `git-workflow.md:62` 的纪律冲突。已并入 T4-27(一行修复)。
## 3. 本迭代 MVP 范围
PM 建议口径,待 §6 拍板确认。
**纳入**
- `creation` schema 三表落 V6 + 补回 `posts.generation_job_id` 外键(T4-01
- 模型/风格只读目录 + 种子(T4-04)
- provider 适配层 + 至少一个可跑通全链路的 provider 实现(T4-05,形态随 D4-1
- 生成任务创建(`Idempotency-Key` 强制)/ 查询 / 列表 / 取消(T4-06、T4-07
- Worker 队列消费:DB 队列 + 租约 + 指数退避重试 + provider 幂等(T4-08
- 输出落 `media.assets`(含服务端对象写入能力)与真实尺寸回填(T4-03、T4-09)
- 一键建社区草稿:开放 `category='ai_creation'` 写侧 + `generationJobId` 落库与外露(T4-10
- 生成域配额与限流(429 + `Retry-After`,契约首个响应头)(T4-11
- Flutter`creation` feature 分层、create 页真实化、排队/进度/失败重试/取消四态、结果页与一键建草稿(T4-16~T4-19
- 契约 v1.5.0 扩展与冻结(T4-13+ 快照锁补强(T4-14
- 埋点字典 v4AI 创作漏斗)+ `eventVersion` 口径定型 + 白名单总量断言(T4-22)
- 历史遗留搭车五项:`widthPx/heightPx`、429 限流、uploading 超时清理、收藏与草稿两页、真机 M2 两项(详见 §8)
- 第五份 E2E 脚本 + 五份回归 + v0.5.0 走 PR 发布(T4-25、T4-26
**待拍板裁剪项**(默认建议见 §6):
- 视频生成(D4-5,建议剪出,仅保留 `media_kind` 列与枚举)
- 文生图(无输入图,D4-4,建议剪出,图生图先行)
- 我的生成记录列表页(T4-20,条件单,建议纳入——排查失败任务的唯一入口)
- A/B 首个实验(T4-23,条件单,随 D4-11
- 文档站访问控制(T4-28,条件单,随 D4-12
**默认剪出**:真实计费与支付(无计费表,以配额替代)、生成结果的审核与水印、生成完成推送通知(M6)、多模型并行对比、逐帖曝光与服务端排序实验日志(ADR-020 backlog)、完整可观测性栈(M6)——逐条理由见 §7。
## 4. 工单列表
预估规模口径沿用前三迭代:**S ≈ 半天内,M ≈ 1-2 天,L ≈ 3-5 天**(含测试与文档)。
### A 组:数据、地基与工程基础(后端)
#### T4-01 Flyway V6creation schema 提取与 `posts.generation_job_id` 外键补回
- **仓库**patbond-api(迁移进 `patbond-user/src/main/resources/db/migration/`,单迁移链纪律 `backend-modules.md:41`),patbond-doc(迁移说明)
- **描述**:从目标模型 `docs/database/patbond_postgresql.sql:556-716` 提取 `creation` schema 三表(`generation_models` / `generation_styles` / `generation_jobs`)为 `V6__creation_baseline.sql`,含全部 CHECK、复合外键、11 条索引(两条队列 partial 索引 :711-715 必须逐字照抄)与三个 `updated_at` 触发器(:1245-1249,复用 V1 的 `platform.set_updated_at()`);**补回 `community.posts.generation_job_id → creation.generation_jobs(id) ON DELETE SET NULL`**(V5:47 承诺的 M4 义务,索引 `ix_posts_generation_job` 已存在无需重建)。按 D4-4 决定 `input_asset_id` 是否保持 `NOT NULL`、按 D4-6 处置 `model_version_snapshot` 的来源矛盾——**两处偏离目标模型的地方必须在迁移文件头注释里逐条写明理由**(沿 V5:1-33 的写法)。种子数据独立为不进生产链的 dev 脚本(沿 `db/dev/afterMigrate__dev_seed.sql` 先例)。
- **验收标准**
- 全新 postgres:18Testcontainers)上 V1→V6 全量迁移一次成功;表结构与目标模型逐列比对一致(偏离项除外,差异入迁移说明)。
- **存量库实证**:在已迁至 V5 的库上单独跑 V6 成功(沿 v0.4.0 的零迁移实证惯例 `releases.md:36`),`flyway_schema_history` 只增一行。
- 新增迁移测试断言:`creation` schema 存在、三表列数与关键 CHECK 名在位、`posts` 上出现指向 `creation.generation_jobs` 的外键约束(与 `CommunityMigrationIntegrationTest.java:79-85` 现有的「不存在该外键」断言**互为镜像,必须同步反转**,否则该测试必红)。
- `./mvnw -B clean test` 全绿(既有 379/381 测试不回归)。
- **依赖**:D4-4、D4-6 拍板(第一波首项,两项决策不定则 V6 无法定稿)。
- **规模**M
#### T4-02 生成域模块落位与鉴权接入(形态随 D4-2)
- **仓库**patbond-apipatbond-doc`architecture/backend-modules.md` 与 ADR-023 随收口)
- **描述**:按 D4-2 拍板结果落位生成域。**PM 建议方案**:沿 ADR-009/ADR-017 先例新建 Maven 模块 `patbond-creation`(:8085),复用 JWT 资源侧校验与当前用户解析(照抄 `patbond-community``security/``config/SecurityConfig.java` 结构),只读写 `creation` schema**test 作用域引入 Flyway**(照抄 `patbond-community/pom.xml:91-113` 的注释与依赖,生产侧不携带 Flyway);`application.yml``.sample` 模式(pet/community 的主 yml 被 `.gitignore:10-11` 排除,compose 直接挂载 `.sample`);compose 编排纳入第七个容器(`depends_on: postgres service_healthy` + `user service_started`)。若 D4-2 选择并入 `patbond-user`,本单退化为「新增 `creation` 包 + 端点前缀 + 属性类」,规模降为 S。
- **验收标准**
- 模块编译入构建链,`./mvnw -B clean test` 全绿;无 token / 过期 token 返回 401 + 既有 `40100` 系错误码逐字节一致。
- compose 起七容器(postgres + minio + auth + user + pet + community + creation)全部 healthy**跨服务接线以 compose 实测验证**M3 的 T3-13 曾抓出「media 端点挂错服务,单测无法覆盖」,`iteration-3/29:45`)。
- `backend-modules.md` 模块表与端口表同步更新;ADR-023 记录模块归属与 Worker 形态决策。
- **依赖**:D4-2 拍板(骨架期变更成本最低,可先按建议方案搭);T4-01。
- **规模**M(若并入 user 则 S
#### T4-03 对象存储服务端写入能力与媒体内部登记端点
- **仓库**patbond-api`patbond-user/.../media/`),patbond-doc(媒体链路说明补章)
- **描述**:**本迭代最易被低估的一单,见 §2.5**。现有 `ObjectStorage``patbond-user/.../media/ObjectStorage.java:15-37`)只有 `ensureBucket`/`presignPut`/`stat`/`presignGet`,**服务端无法写对象**。本单:① 在 `ObjectStorage` 上新增写方法(建议 `void put(String objectKey, byte[] bytes, String contentType)` 或流式重载)与读方法(`Optional<byte[]> get(...)` 或仅取图片头部字节,供 T4-09 探测尺寸),在 `S3ObjectStorage` 实现并给 `MediaStorageConfig.java:33-59``UnconfiguredObjectStorage` 补桩(保持未配置即 500 的既有语义);② 新增 `/internal` 媒体登记端点(建议 `POST /internal/media/assets`),一次调用完成「写对象 + 插入 `media.assets` 行并直接置 `ready`」,供生成域落盘输出——**这样 ADR-017「media 上传流程实现在 patbond-user」的边界不被打破**;③ `MediaProperties.allowedPurposes``MediaProperties.java:66`,当前 `post_image,user_avatar,pet_avatar`YAML `application.yml:45`)与 mime 白名单按 D4-14 扩充新用途(**纯配置变更,`media.assets.purpose` 无 CHECK 约束,无需迁移**——ADR-022 已验证同一路径)。若 D4-2 选择并入 user,②可退化为内部服务方法调用。
- **验收标准**
- MinIO Testcontainer 全链路:服务端 put → `media.assets``status='ready'``ready_at` 非空 → 预签名 GET 可取回同一字节(sha256 比对)。
- `/internal/media/assets``X-Internal-Token` / 错误 token 返回 401 + `TOKEN_INVALID`(复用 `InternalAuthFilter.java:28,54-56` 的常量时间比较,**未配置密钥时 fail closed** 语义不变)。
- `ck_media_location` / `ck_media_ready` / `uq_media_object (bucket, object_key)` 四条数据库约束与应用层校验一致,各有测试。
- 未配置对象存储时行为与既有一致(500,不静默成功)。
- **依赖**:T4-01(不强依赖,可与之并行);D4-2 决定②的形态;D4-14 决定新 purpose 命名。
- **规模**M
#### T4-04 模型/风格只读目录端点与种子数据
- **仓库**patbond-apipatbond-doc(契约草案)
- **描述**:实现模型与风格的只读目录(建议 `GET /api/v1/creation/models``GET /api/v1/creation/styles`,均支持 `mediaKind` 过滤,`enabled=true` 过滤走 partial 索引 `ix_generation_models_kind_order` / `ix_generation_styles_kind_order`,按 `sort_order, id` 排序,**不分页**——沿 care-reminders/breeds/vaccine-catalog 的既有例外先例 `openapi.yaml:98`)。风格响应含 `previewUrl``preview_asset_id` 现签预签名 GET,与 `Pet.avatarUrl`/`AuthorSummary.avatarUrl` 同口径:三种情况为 null、键恒在)。种子按 D4-5 只入 image 模型与风格(目标模型 :1495-1508 的 5 条风格里 4 条为 image、1 条为 video);`provider_code` 取值随 D4-1。**风格预览图本身需要真实资产**:种子引用的 `preview_asset_id` 在目标模型里指向 fixture asset,本单需明确预览图来源(建议随种子上传 4 张占位图并记录 assetId,或首版允许 `previewUrl` 为 null 由客户端回落纯色卡)。
- **验收标准**:目录端点返回按 `sort_order` 稳定排序;`enabled=false` 的行不出现;`mediaKind` 非法值 400/40000`previewUrl` 为 null 时键仍在;六类测试路径中适用的四类(成功/参数错/无权限/不存在)覆盖。
- **依赖**T4-01T4-02。
- **规模**S
#### T4-05 provider 适配层与首个 provider 实现
- **仓库**patbond-apipatbond-docprovider 接入说明 + ADR-023
- **描述**:**依赖 D4-1 拍板,是关键路径起点**。定义 provider 抽象接口(提交生成请求 → 返回 `providerRequestId`;轮询或回调获取状态与结果字节/URL;显式区分**可重试失败**与**永久失败**,后者不再消耗 `max_attempts`);把供应商差异全部收敛在适配层(沿 ADR-016 对象存储适配层的同一手法:「代码经存储适配层隔离供应商」)。按 D4-1 实现首个 provider。**PM 建议先实现 fixture provider**:可配置延时与进度、返回内置图片字节、可注入失败/超时/慢响应用于测试——它同时是 CI 与 E2E 的唯一可用 provider(无外部依赖、无密钥、无成本)。若 D4-1 同时批准接入云 API,则云 provider 作为第二实现另计(见 T4-05b 说明:不单列工单,按 D4-1 结果并入本单并把规模提为 L)。
- **验收标准**
- 适配层接口下至少一个实现全链路可跑;provider 密钥一律经 `.env` 注入、`.sample` 占位,`scripts/check-secrets.sh --all` 零命中(凭证防泄漏两层门禁在位,`git-workflow.md:36-54`)。
- fixture provider 可注入四类故障:可重试失败、永久失败、超时、返回损坏字节;各有测试。
- 日志不含 provider 密钥与完整 prompt 明文(`development-plan.md` 第 6.1 节日志纪律)。
- **依赖**:**D4-1 拍板**(未拍板期间可先写接口与 fixture 实现,明确止损线:适配层以上不写任何供应商特定代码)。
- **规模**:M(若同时接云 API 则 L)
### B 组:后端生成纵切
#### T4-06 生成任务创建:幂等键、快照与配额
- **仓库**patbond-api
- **描述**`POST /api/v1/creation/jobs`**`Idempotency-Key` 必带**`development-plan.md:173` 强制;沿 `POST /api/v1/posts` 的既有形态 `openapi.yaml:1320``IdempotencyKeyRequiredHeader` :1928-1944,同键异 payload → 409/40905)。落 `UNIQUE (user_id, idempotency_key)` + `request_hash`(规范化请求体的 sha256,32 字节,`ck_generation_jobs_idempotency` 校验)。校验链:模型/风格存在且 `enabled``media_kind` 一致(复合外键 `(model_id, media_kind)` 天然兜底)、输入 asset 属本人且 `status='ready'`(复用 community 的 `MediaAssetGateway` 只读模式;非 ready → 422/42203 沿用既有 `MEDIA_NOT_READY`)、宠物归属校验(若带 `petId`)、尺寸在 `ck_generation_jobs_dimensions` 的 64~8192 内。**四个 snapshot 列在创建时一次性冻结**(`provider_code_snapshot` / `provider_model_snapshot` / `model_version_snapshot` / `style_code_snapshot`)——这是「失败原因可追踪」的基础:目录改了不影响历史任务的可复现性。配额检查按 D4-7(PM 建议直接 `COUNT` 本人窗口内的 `generation_jobs` 行,**不新建计数表**:幂等命中不新建行,故天然满足「相同幂等请求不重复扣费」)。任务落库即 `status='queued'`(满足 `ck_generation_jobs_state` 的 queued 分支:`progress=0``started_at`/`completed_at`/`output_asset_id`/`error_code`/`lease_*` 全 NULL)。
- **验收标准**
- 六类测试路径全覆盖(成功 / 参数错 / 资源不存在 / 无权限 / 并发冲突 / 幂等重试),走真实 PostgreSQL。
- **相同 `Idempotency-Key` 并发 N 次只产生一行**,且响应为同一 `jobId`(多线程真并发测试,沿 `LikeBookmarkIntegrationTest.java:141,167` 的既有手法);同键异 payload 返回 409/40905。
- 引用他人 / 非 ready asset 被拒且错误码稳定;`enabled=false` 的模型被拒。
- 超配额返回 429 + `Retry-After`(依赖 T4-11);幂等重试**不**消耗配额,有专项测试。
- snapshot 四列非空且与创建时刻的目录值一致;事后改目录不改历史行,有测试。
- **依赖**T4-01、T4-02、T4-04、T4-05(接口即可,不需 provider 实现完成);D4-7 拍板配额口径。
- **规模**L
#### T4-07 生成任务查询、列表与取消
- **仓库**patbond-api
- **描述**`GET /api/v1/creation/jobs/{jobId}`(详情,含 `status`/`progress`/`errorCode`/`errorMessage`/`attemptCount`/`outputAsset` 现签 URL)、`GET /api/v1/creation/jobs`(我的生成记录,cursor 分页走 `ix_generation_jobs_user_created (user_id, created_at DESC, id DESC)`**禁 OFFSET**,支持 `status` 白名单过滤)、`DELETE``POST .../cancel`(取消,语义随 D4-8 子项)。取消的状态机后果必须逐条对齐 `ck_generation_jobs_state`:667-692):`cancelled` 要求 `completed_at IS NOT NULL``lease_*` 全 NULL——因此**取消 `queued` 任务需同时写 `completed_at`**;取消 `running` 任务需清租约(此时 provider 侧可能仍在跑,须明确「取消是尽力而为,已产生的 provider 消耗不退配额」并写入契约描述);`succeeded`/`failed`/`cancelled` 的重复取消为幂等 no-op 还是 422,随 D4-8 定型。他人任务一律 404(防枚举,沿 `POST_NOT_FOUND` 的既有做法 `openapi.yaml:45`)。新增错误码建议:`GENERATION_JOB_NOT_FOUND(40407, 404)``GENERATION_STATE_INVALID(42206, 422)`
- **验收标准**
- 五个状态各自的详情响应形态定型并入契约;`errorCode`/`errorMessage` 在 failed 态必非空、在其他态必为 null(与库层 CHECK 互证)。
- 分页不丢不重:以 `limit=7` 多页与 `limit=100` 单页两次全量翻页,**有序 id 列表逐位相等**(沿 M3 T3-05 的取证手法 `iteration-3/29:39`)。
- 取消四种源状态(queued / running / 已终态 / 他人任务)各有测试;取消后库层 CHECK 不被违反。
- 六类测试路径覆盖。
- **依赖**T4-06。可与 T4-08 并行。
- **规模**M
#### T4-08 Worker 队列消费:租约、退避重试与幂等
- **仓库**patbond-apipatbond-docWorker 运行手册 + ADR-023
- **描述**:**M4 的技术核心,两条验收标准(状态流转合法、Worker 重启不丢任务)的主责单**。按 D4-3 实现 DB 队列消费(PM 建议):
- **领取**`SELECT … FROM creation.generation_jobs WHERE status='queued' AND next_attempt_at <= now() ORDER BY priority DESC, next_attempt_at, created_at, id LIMIT :n FOR UPDATE SKIP LOCKED`(正对 `ix_generation_jobs_queue` :711-713),随即写 `status='running'` + `started_at` + `lease_owner`(实例标识)+ `lease_expires_at`
- **续租**:长任务周期性延长 `lease_expires_at` 并更新 `progress`
- **回收**:独立扫描 `status='running' AND lease_expires_at < now()`(正对 `ix_generation_jobs_running` :714-715),重置为 `queued`。**⚠️ 实现陷阱(§2.8**`ck_generation_jobs_state` 的 queued 分支要求 `started_at IS NULL AND progress = 0`,故回收时必须把 `started_at``progress` 清零——首次尝试的开始时间会丢失,须在代码注释与迁移说明中记录该取舍。
- **重试**`attempt_count + 1`;未达 `max_attempts` 且属可重试失败 → 回 `queued` 并按指数退避写 `next_attempt_at`;达上限或永久失败 → `failed` + `error_code`/`error_message` + `completed_at`
- **provider 幂等**`provider_request_id` 落库并受 `uq_generation_jobs_provider_request` 保护(:699-700),重试时优先**查询已有 provider 请求状态**而非重新提交——这是「相同幂等请求不重复扣费或生成」在 provider 侧的落点。
- **调度基座**`@EnableScheduling` + 显式 `TaskExecutor`(全仓当前无任何线程池配置,§2.4),并发度、租约时长、退避参数、轮询间隔全部配置化。Worker 形态(同进程 vs 独立容器)随 D4-2。
- **验收标准**
- **Worker 重启不丢任务**:集成测试在 `running` 态强杀 Worker(或直接令租约过期)后,任务被重新领取并最终 `succeeded`T4-25 E2E 用 `docker compose restart` 做真容器取证。
- 多 Worker 并发领取同一批任务,**同一任务不被两个实例同时持有**(`SKIP LOCKED` + 租约双重保证),有并发测试。
- 四类 provider 故障(可重试 / 永久 / 超时 / 损坏输出,由 T4-05 fixture 注入)各自的终态与 `attempt_count` 正确;退避间隔递增有测试。
- 全部状态迁移穷举测试:每条合法迁移成功、每条非法迁移被应用层拒绝**且**被库层 CHECK 兜底(两层各有断言)。
- 队列积压与 Worker 失败率**可查**`development-plan.md:329` 的最小兑现:SQL 可查 + 结构化日志留痕;完整指标栈 M6,见 §7)。
- **依赖**T4-06、T4-05T4-03(输出落盘经 T4-09);D4-2、D4-3 拍板。
- **规模**L
#### T4-09 生成输出落媒体表与真实尺寸回填(含 `widthPx/heightPx` 遗留清偿)
- **仓库**patbond-apipatbond-doc(契约描述修订)
- **描述**Worker 成功后把 provider 输出经 T4-03 的写能力落 `media.assets``purpose` 随 D4-14、`owner_user_id` = 请求用户、`storage_type='object'``status='ready'` + `ready_at`),回填 `generation_jobs.output_asset_id` 并置 `succeeded``ck_generation_jobs_state` 要求 succeeded 时 `progress=100``started_at`/`completed_at`/`output_asset_id` 非空、`error_code`/`lease_*` 为 NULL——五个条件同事务写齐)。**AI 输出的 `width_px`/`height_px` 天然已知**(请求参数 + provider 返回),直接写入。同时按 D4-9 处置 M3 遗留:PM 建议在 `POST /api/v1/media/uploads/{assetId}/complete` 里补服务端尺寸探测(读对象头部字节解析 JPEG/PNG/WebP 尺寸),使契约 `openapi.yaml:3536` 的「complete 后回填」名副其实;同时把 `PostMediaItem.widthPx/heightPx`:3605-3610,当前无 description)补上口径说明。**本单不做**:视频时长探测(D4-5 剪出视频)。
- **验收标准**
- 生成成功后 `media.assets``status='ready'``width_px`/`height_px` 与请求尺寸一致;预签名 GET 可取回字节。
- 五个 succeeded 字段条件同事务写齐;中途失败不留「有 output 但非 succeeded」的中间态,有测试。
- 用户上传路径的尺寸回填:jpeg/png/webp 三种 mime 各有测试,探测失败时回落 null 且不影响 `ready`(向后兼容,`MediaAsset.required` 不含尺寸字段)。
- 单图帖不再一律回落 4:3(M3 观察项 2 闭环,`iteration-3/29:56`)——客户端侧验收在 T4-19/T4-21。
- **依赖**T4-03、T4-08D4-9 拍板。
- **规模**M
#### T4-10 一键建社区草稿:`ai_creation` 写侧开放与 `generationJobId` 打通
- **仓库**patbond-api`patbond-community`),patbond-doc(契约)
- **描述**:打通「生成成功 → 社区草稿」。改动面(§2.7 已逐处定位):① `CreatePostRequest.java:26``@Pattern(regexp = "general|help")` 放开为含 `ai_creation`;② `CreatePostRequest` 新增 `generationJobId`;③ `PostRepository.insertPost`:56-58 签名、:60-64 INSERT 列清单)与 `PostService`:88 category 默认值、:98/:102 canonicalize 与插入、:151/:178 更新路径)补该列;④ `PostResponse.java``FeedCardResponse` 按 D4-8 决定是否外露 `generationJob`(当前 `PostResponse.java:12` 注明该字段完全不出现)。**归属与防伪校验**:`generationJobId` 必须属于当前用户且 `status='succeeded'`,否则 404/40407;按 D4-8 定型「`category='ai_creation'` 是否强制要求带 `generationJobId`」(PM 建议强制,防止用户伪造 AI 分类)。草稿创建沿用既有两步语义(`status='draft'` 创建 → `PATCH status='published'` 发布,`openapi.yaml:3660-3664`、:3701-3707),本单**不新增发布路径**。
- **验收标准**
- 生成任务 → 建草稿 → 发布 → Feed 可见全链路走真实 PostgreSQL`posts.generation_job_id` 落库正确且外键约束生效(引用不存在的 jobId 被库层拒绝)。
- 他人 jobId / 未成功 jobId / 无 jobId 但 `category='ai_creation'` 三种非法组合各有测试与稳定错误码。
- `ai_creation` 帖在 Feed 与详情的读侧形态与 `general` 一致(`FeedCard.category` 枚举早已含三值 `openapi.yaml:3826`,无需改读侧枚举)。
- 既有 107 个 community 测试不回归;契约矩阵新增格全部被真实触发(`everyDeclaredResponseCellIsExercised` 门禁)。
- **依赖**T4-01(外键)、T4-06/T4-09succeeded 任务);D4-8 拍板。
- **规模**M
#### T4-11 生成域配额与限流:429 + `Retry-After`(清偿 M3 遗留)
- **仓库**patbond-api`patbond-common` 加错误码 + 生成域实现),patbond-doc(契约首个响应头)
- **描述**:**契约史上第一个响应头声明**——`openapi.yaml` 全文当前**零处 `headers:` 键**(§2.13 核实)。新增 `RATE_LIMITED(42900, 429, …)``ErrorCode.java`(该文件当前无 429 段),按 D4-7 实现配额与限流:建议 per-user 并发上限(`COUNT` 本人 `queued|running` 行)+ 每日次数上限(`COUNT` 本人当日 `created_at` 行),**均直接查 `generation_jobs` 不新建表**;超限返回 429/42900 + `Retry-After`(秒数,指向下一次可提交时间)。**无 Redis 的现实约束**(§2.4)决定了计数只能落 PG 或进程内——建议落 PG(多实例正确,代价是每次创建多一次 COUNT,走 `ix_generation_jobs_user_created`)。同时清偿客户端侧遗留:`lib/core/network/api_client.dart:164-167` 已有 429 粗分支但无 `Retry-After` 解析,`lib/analytics/analytics_service.dart:268-272` 注释明写等待后端落地——本单交付后由 T4-16 收口客户端分支。**本单不做**:全站通用限流(属 M6「限流、审计、结构化日志」范围),只做生成域。
- **验收标准**
- 超并发 / 超日限两种超限各返回 429 + `Retry-After`,头值可解析为正整数秒;有集成测试。
- **幂等重试不计入配额**(同 `Idempotency-Key` 重试在超限后仍返回原任务而非 429),有专项测试——这是「相同幂等请求不重复扣费」的一半取证。
- 契约新增 429 响应与 `Retry-After` 头声明;契约测试覆盖该格(新增矩阵格必须被真实触发)。
- 客户端 429 分支与 `Retry-After` 尊重在 T4-16 有测试。
- **依赖**T4-06D4-7 拍板具体数值。
- **规模**M
#### T4-12 uploading 超时未确认 asset 清理定时任务(清偿 M3 遗留)
- **仓库**patbond-api`patbond-user`),patbond-docfeature-checklist 转绿)
- **描述**:M3 T3-13 已有方案未实现(`iteration-3/29:58``feature-checklist.md:193`)。本单实现 `@Scheduled` 清理:把超过阈值仍为 `status='uploading'` 的 asset 置 `failed`(或按方案置 `deleted` + `deleted_at`,须满足 `ck_media_deleted`),并按需删除孤儿对象。**搭车理由**:M4 本就要建 Worker 调度基座(T4-08),且 `@Scheduled` 已有先例可照抄(`SessionCleanupJob.java:32-41` 的幂等、可多实例并跑写法 + `application.yml:24-26` 的配置形态);同时 AI 输入图会放大孤儿资产量(每次生成都要先传一张图),此项从「可选清理」升为「成本控制项」。索引已就绪:`ix_media_uploading_created`V1:255 区域)。
- **验收标准**:阈值内的 uploading 不被误清;超阈值被置终态且 `identity.users.avatar_asset_id` / `pet_health.pets.avatar_asset_id` / `post_media` / `generation_jobs.input_asset_id` 等引用方**不出现悬空引用**(外键为 RESTRICT 的路径须验证清理不会失败);任务幂等、多实例并跑无害,有测试。
- **依赖**T4-02(或直接在 user 模块内);与主线解耦,可任意波次插入。
- **规模**S
### C 组:契约与测试
#### T4-13 OpenAPI v1.5.0 扩展与冻结(**本波闸门**)
- **仓库**patbond-doc`docs/api/openapi.yaml`),patbond-api(四份或五份快照字节级同步 + 守卫测试期望值更新)
- **描述**:沿用三个迭代验证过的**迭代式契约冻结**:第一波按 §1.1 与目标模型出草案(放在 `iterations/iteration-4/openapi-creation-draft.yaml`,沿 M2/M3 的 `openapi-pets-draft.yaml`/`openapi-community-draft.yaml` 先例),以 `TODO-FREEZE` 标注**四处待定型点**——① 生成任务响应的五态字段形态、② `Retry-After` 头与 429 的声明形态(契约首个响应头)、③ `generationJob``Post`/`FeedCard` 的外露程度、④ 新 `purpose` 枚举值命名;随 T4-06/07/09/10/11 实现定型回填 → 拍板 → 冻结合入 + api 侧快照同步升版。
变更清单(**对 v1.4.0 应为纯增量**,延续 v1.4.0 写入 `info.description` 的承诺):新增生成域路径与 schema;`CreateMediaUploadRequest.purpose` enum 增值(:3458);`CreatePostRequest.category` enum 增 `ai_creation`:3657,并删去 :3659 那句「M3 不开放写入」);`CreatePostRequest`/`Post` 新增 `generationJobId`/`generationJob``MediaAsset.widthPx` description 修订(:3536);新增错误码 `40407`/`42206`/`42900``info.description` 错误码表(:35-62,现 27 个码);新增 429 响应与 `Retry-After` 头。
**同步义务(缺一 CI 必红)**:四份守卫测试的期望值 `1.4.0 / 32 / 45 / 75` 必须整体更新(`AuthContractConformanceTest.java:399-407``MediaContractConformanceTest.java:246-251`、pet `ContractConformanceTest.java:755-760``CommunityContractConformanceTest.java:522-527`),四份 `OpenApiContract.java``RESOURCE` 常量(pet:35 / auth:39 / user:39 / community:39)须指向 `openapi-v1.5.0.yaml`;若 D4-2 新建模块则为**五份**。
- **验收标准**:契约评审通过;四/五份快照 md5 与正典完全一致;守卫测试与 `everyDeclaredResponseCellIsExercised` 双门禁全绿、矩阵零漂移;新增格全部被真实触发(不得豁免);`mkdocs build --strict` 零 warning;冻结后任何变更须显著上报两端同步。
- **依赖**:草案仅依赖目标模型;冻结须 T4-06/07 状态形态 + T4-09 输出形态 + T4-10 权限语义 + T4-11 限流形态定型。**不冻结不放行第三波两端联调。**
- **规模**M
#### T4-14 契约快照锁补强:从「计数守恒」到真校验和
- **仓库**patbond-api(四/五模块测试),patbond-doc(冻结纪律补章)
- **描述**:§2.13 核实的缺口——CI 只锁 `info.version` + 3 个计数 + tag 分组,md5 比对是**人工冻结步骤**,任何「计数守恒的字节修改」(改 description、改 enum 取值、改 min/max、同时增删各一字段)都能悄悄通过。本单在每模块守卫测试中补一条**快照文件校验和断言**(对 `src/test/resources/contract/openapi-v1.x.y.yaml` 算 sha256 与硬编码期望值比对),把「字节级快照锁」从人工纪律变成 CI 门禁。**注意**:期望值需在 T4-13 冻结后填入,故本单分两步——先在 v1.4.0 上落机制并验证能抓出注毒(可先行于第一波),再随 T4-13 更新期望值。
- **验收标准**:定向注毒自证——对快照做一处「计数守恒」的字节修改(如改一句 description),断言必红;恢复后转绿。四/五模块各有该断言。文档纪律更新为「升版须同步快照 + 更新校验和」。
- **依赖**:无(机制可先落);期望值随 T4-13。
- **规模**S
#### T4-15 后端集成测试滚动补齐与 CI(横切单)
- **仓库**patbond-api
- **描述**:随 B 组滚动补齐 Testcontainers 集成测试与契约一致性测试(机制复用既有三层结构:纯单测 / 契约一致性 / Testcontainers postgres:18 + 一处 MinIO 容器)。**本迭代专项矩阵**:① 状态机穷举(五态两两迁移的合法与非法各一格);② 队列并发(多 Worker 领取不重叠、`SKIP LOCKED` 有效性);③ 租约过期回收(含 `started_at` 清零后仍满足 CHECK);④ 幂等三层(应用层键、DB 唯一约束、provider 请求 id);⑤ 配额边界(超限 / 幂等重试不计数 / 跨日窗口翻转)。MinIO 容器使用面因 T4-03/T4-09 扩大,记录 CI 时长;若超阈值按模块分层执行,但**不降低「提交前全绿」标准**。
- **验收标准**:每个新业务接口覆盖六类路径;Gitea Actions 全绿(以 commit status API 实查为准,沿 M2/M3 惯例——`iteration-3/29:47` 记录过「一个 agent 识破 Monitor 的假 success 事件」的教训);CI 时长记录在案。
- **依赖**:随 T4-04~T4-12 滚动。
- **规模**M(分摊在各单内)
### D 组:Flutter 客户端
#### T4-16 `creation` feature 分层与 API Client
- **仓库**patbond-flutter
- **描述**:按 `development-plan.md:96` 已预留的 `creation` feature 拆分(`lib/features/creation/`),对齐 community/pets 的既有结构(`Page → Controller → Repository → ApiClient`,约定原文 `community_controller.dart:14-20`;状态用原生 `ChangeNotifier`DI 手写进 `lib/app/app.dart:80-147`)。依 T4-13 冻结契约实现 DTO 与 Client(模型/风格目录、任务创建/详情/列表/取消)。**若 D4-2 新建模块,须新增第五条 baseUrl**:`patbondCreationApiBaseUrl` + `PATBOND_CREATION_API_BASE_URL`(照抄 `api_client.dart:8-34` 的四条现有写法),并同步 `lib/app/app.dart` 装配、四份 E2E 脚本的地址常量、`device-verification.md` 通用前置的 `flutter run` 参数说明(当前写「四个 base URL 全传」,须改为五个)。新增 `AnalyticsPageName` 枚举值(`lib/analytics/analytics_page_name.dart:7-25`;不在枚举内 `fromRouteName` 返回 null 即不上报)。**顺带收口 429**:`api_client.dart:164-167` 现有的 `ApiRateLimitException``Retry-After` 解析与承载(T4-11 交付后),并按注释 `analytics_service.dart:268-272` 的承诺一并处理埋点上传侧的 `Retry-After` 分支。
- **验收标准**DTO 映射(含五种 status、null 尺寸、null previewUrl)有单元测试;错误映射类型化(新增 `40407`/`42206`/`42900` 各有分支);`Retry-After` 解析(有头 / 无头 / 非法值)有测试;UI 无关骨架可先行。
- **依赖**:T4-13 冻结(骨架部分可提前与后端并行)。
- **规模**M
#### T4-17 create 页替换:真实目录、输入与提交
- **仓库**patbond-flutter
- **描述**:替换 `lib/features/create/create_page.dart`(563 行、零测试、全假,逐处清单见 §2.14)。保留可用的四块视觉骨架(模式分段器 / 风格横滑卡 / 进度条 / 结果编辑区),只换数据源与状态源:模型下拉与风格卡改读 T4-04 目录(删掉 `:179` 硬编码模型数组与 `demo_data.dart:206-234` 的 unsplash URL 依赖);假上传(`:55-64`)改接 `MediaUploader``purpose` 用 T4-03 新增的 AI 输入用途,单图单并发,照抄头像 sheet 的 `maxImages:1, maxConcurrentUploads:1` 用法);prompt 输入、尺寸与高清增强开关映射到真实请求字段(`prompt`/`negativePrompt`/`widthPx`/`heightPx`/`upscale`/`parameters`);提交携带客户端生成并在重试间保持的 `Idempotency-Key`(照抄发布页 `post_compose_page.dart:267``_idempotencyKey ??= _uuid.v4()` 手法)。**视频模式按 D4-5 处理**(建议:分段器保留但 video 项禁用并给「即将上线」提示,而非删除——避免 UI 大改,也与 `media_kind` 保留枚举一致)。**搭车主题债**:给主题补 `segmentedButtonTheme`,选中态改用 `app_theme.dart:199-214` 已审计色对表中的色对(`_datePickerTheme` :215-296 是落地范本),一次修复全部 6 处 `SegmentedButton` 使用点(create/home/services/pet_form×2/vaccination_form)。
- **验收标准**
- 页面不再读任何 demo 常量(`demo_data.dart``AppState.pet` 依赖清零);`grep` 断言无 `Future.delayed` 假延时残留。
- 目录加载四态(loading/empty/error/retry)齐备并有 widget 测试——**该页当前零测试,本单是从 0 建测试基线**。
- 提交校验与错误提示:非法尺寸、未选风格、未传图、超配额 429(含 `Retry-After` 文案)各有测试。
- `SegmentedButton` 选中态对比度达 WCAG AA 并在已审计色对表中登记新增行;6 处使用点视觉一致。
- **依赖**T4-16T4-04/T4-06 联调。
- **规模**L
#### T4-18 生成状态机 UI:排队、进度、失败重试与取消
- **仓库**patbond-flutter
- **描述**`development-plan.md:253` 的直接落点(「Flutter 展示排队、生成进度、失败重试和取消状态」)。实现任务状态轮询与展示:`queued`(排队中 + 队列位置或预计时间,视 T4-07 是否外露)、`running`(真实 `progress` 驱动进度条,替换 `:532-563` 现有的 `step/4` 假进度与 4 条硬编码 label)、`succeeded`(转 T4-19)、`failed``errorCode`/`errorMessage` 可读化 + 重试按钮,重试语义随 D4-8:**是新建任务还是复用同一任务**须定型)、`cancelled`。轮询策略需明确:间隔、退避、页面不可见时暂停、离页后是否继续(建议离页即停 + 回页重取,避免后台耗电);**尊重 `Retry-After`**。断网与超时的四态齐备。
- **验收标准**
- 五种状态各有 widget 测试;状态迁移驱动的 UI 变化(queued→running→succeeded / →failed)有测试。
- 轮询在页面不可见时暂停、回前台恢复,有测试(M3 曾用 widget 测试抓出「回前台不开新曝光段」的真 bug,`iteration-3/29:46`——同类风险在此复现,须专项覆盖)。
- 失败重试与取消的乐观/悲观策略明确且不假成功(离线操作提示明确,沿 M3 T3-17 的既有纪律)。
- `progress` 为真实值(非一跳 100%),有测试。
- **依赖**T4-16、T4-17T4-07/T4-08 联调。
- **规模**L
#### T4-19 结果页与一键建社区草稿
- **仓库**patbond-flutter
- **描述**:生成成功后的结果展示与去向。复用现有结果区布局(`create_page.dart:312-386`)但换真实数据:输出图用 `outputAsset.url`(预签名 GET,**缓存 key 须剥 `X-Amz-*` 签名参数**——沿用已有的 `presignedImageCacheKey` 机制,否则每次刷新重下,该机制的回归判据已写进 `device-verification.md:190` 第 2 项);按真实 `widthPx/heightPx` 渲染宽高比(T4-09 交付后不再一律 4:3);删掉硬编码位置「北京市 · 朝阳区」(`:369-374`)与硬编码文案(`:87-90`)。「发布到社区」从占位 SnackBar(`:122-129`)改为真实调用 T4-10:建草稿并跳转 `PostComposePage` 预填(图与 `generationJobId` 已挂),由用户在既有发布页完成文案与发布——**不在本页做完整发布流程**(避免与发布页的草稿/幂等/媒体逻辑重复实现)。
- **验收标准**:结果图真实渲染且宽高比正确;预签名 URL 不被持久化(纪律 R2)、过期可重取;一键建草稿 → 发布页预填 → 发布 → Feed 出现全链路真实后端;失败四态齐备;有 widget 测试。
- **依赖**T4-16、T4-18T4-09/T4-10 联调。
- **规模**M
#### T4-20 我的生成记录列表(条件单,随 D4-11 之外的独立取舍)
- **仓库**patbond-flutter
- **描述**:若纳入:接 T4-07 的列表端点做「我的作品/生成记录」页(cursor 分页四态,照抄 `lib/features/pets/health_events_page.dart:19,71-127``_ListPhase` 范本),支持按 status 过滤、点击进详情、失败任务可重试、可删除记录(随 D4-14)。**PM 倾向纳入**:它是用户排查失败任务与找回历史输出的唯一入口,缺它则一次生成失败后用户无处可查(「失败原因可追踪」在客户端侧没有落点)。若周期紧张可后置,但须在收官总结记范围缺口。
- **验收标准**:分页不丢不重(widget 测试模拟游标);四态齐备;状态与详情页同源一致。
- **依赖**T4-16、T4-18。
- **规模**S
#### T4-21 我的收藏与草稿两页(M3.5 遗留搭车)
- **仓库**patbond-flutter
- **描述**:清偿 `iteration-3.5/06:71` 遗留(「后端与仓库层均已就位,缺两个页面 + 导航」)。两页均照抄 `_ListPhase` + cursor 分页范本:**我的收藏**接 `listMyBookmarks()``community_repository.dart:52,274-283`**当前全仓零调用点**,项形态为 `FeedCard`);**我的草稿**接 `listMyPosts(status: draft)`:26-30,179-193,当前唯一调用点是发布页取 `limit:1` 恢复最新草稿)。入口替换 `lib/features/profile/profile_page.dart:49` 菜单项的 demo SnackBar:235 `showDemoMessage`);顺带把「我的作品」统计数字(`:307-310`,当前不可点)接成可点入草稿/已发布列表。**本单不做**:草稿自动保存(见 §7)。
- **验收标准**:两页分页不丢不重、四态齐备、有 widget 测试;收藏页的取消收藏与 Feed/详情状态同源一致(复用既有 `toggle_sync.dart` 乐观更新机制);profile 页不再有该项 demo SnackBar(有反向断言)。
- **依赖**:无(与 M4 契约零耦合,第一波即可并行消化)。
- **规模**M
### E 组:埋点、实验、遗留与收口
#### T4-22 埋点字典 v4AI 创作漏斗 + `eventVersion` 口径定型 + 白名单计数纠偏
- **仓库**patbond-flutter(挂接)、patbond-api(白名单扩充)、patbond-doc(字典)
- **描述**:事件定义以 Experiment Tracker 的 M4 埋点方案为准(**本单不自造字典**;沿用「结果编码进事件名」惯例与 ADR-013 纪律)。预期覆盖 `development-plan.md:330` 要求的「AI 任务成功」漏斗:创作页进入 → 输入图上传(复用既有 `post_media_upload_*` 三段还是新增独立事件,由埋点角色定)→ 任务提交成功/失败 → 生成成功/失败/取消 → 建草稿 → 发布。**隐私红线**:props 不含 prompt 明文、不含 assetId/jobId、不含精确字节数(沿 M3 红线,服务端 `EventDictionary.java:38-39,114-116` + 客户端 `analytics_service.dart:288-295` 双层过滤在位)。
搭车三项修正:① **`eventVersion` 口径定型**M3 观察项 2`iteration-3/29:57``feature-checklist.md:222`)——契约描述 `openapi.yaml:2341-2344` 可两读、服务端零取值校验(`TrackEventsRequest.java:38-39``@NotNull``@Min`,唯一取值校验在 DB 的 `V2:22`)、客户端硬编码 1`analytics_service.dart:139`);定型为「事件 schema 版本」、写入字典纪律、并决定是否补 `@Min(1)` 与契约 `minimum`。② **白名单计数纠偏**:实测 41 条而文档三处写 42(§2.10),修正 `iteration-3/22-event-whitelist-v3.md:4,14``iteration-3/29-m3-summary.md:20``feature-checklist.md:218`,并在 `EventDictionaryTest.java` 补一条**总量断言**(当前无任何 `hasSize`)永久钉住。③ 新增事件同步进 v4 字典文档与 `EventDictionary.java`,并对刻意不做的事件名(如逐帖曝光类)维持「不在白名单即 unknown」的锁死机制。
- **验收标准**:关键动作事件端到端落库(compose 实测或 curl 造真实 payload,沿 M3 的 §5c 取证惯例);白名单与字典文档条数一致且有总量断言;`eventVersion` 定型结论写入字典纪律与契约描述;三处文档计数修正;不回归既有 597 前端测试。
- **依赖**Experiment Tracker 方案;随 T4-17~T4-19 滚动挂接。
- **规模**M
#### T4-23 A/B 最小分流设施与首个实验(条件单,随 D4-11)
- **仓库**patbond-flutter、patbond-api、patbond-doc
- **描述**:若 D4-11 拍板启动首个实验,须先补 §2.9 核实的三处零实现:① **稳定分流组件**(前置 #4 的缺失半边)——`hash(userId 或 anonymousId, experimentSalt) % buckets`,建议**纯客户端确定性哈希**`anonymousId` 持久化已在 `analytics_service.dart:51,177-190` 就位),避免为一个实验搭服务端下发设施;② **Flutter 曝光封装**(前置 #5 的缺失半边)——`experiment_exposed` 事件的强类型封装(服务端字典 `EventDictionary.java:103` 已就位、props `{experimentKey, variant}`,客户端零命中),触发时机按 `iteration-3/06:145` 的既定口径「用户**实际到达**实验触点时(渲染了变体 UI),非分配时」;③ **开关与回滚**(前置 #7 的「回滚绿」实无依据)——最小实现为客户端可远程或配置关闭实验、回落对照组。首个实验对象建议取**低风险、纯展示层**的项(如 create 页风格卡的默认排序,或结果页「发布到社区」的引导文案),**不实验后端生成逻辑**(成本与状态机风险)。
- **验收标准**:同一用户多次进入分到同一变体(确定性有测试);`experiment_exposed` 落库且 props 键集与字典一致;关闭开关后全量回落对照组,有测试;实验设计与样本量按既有模板评审(**注意**:该模板文档在两仓中未定位到实体,见 §2.18,须先补或现场约定)。
- **依赖**D4-11 拍板;T4-17(触点所在页面);T4-22(字典)。
- **规模**M
#### T4-24 真机验证 M2 两项(**硬时限 2026-09-21**
- **仓库**patbond-doc(执行记录回填)
- **描述**:**本迭代唯一带外部硬时限的工单**。执行 `device-verification.md:42`(验证一:Android 事件真实落库,~10 分钟)与 `:63`(验证二:SessionTracker 30 分钟后台换会话,~45 分钟含等待),回填 `:104` 的「M2 项执行记录」。时限来自北极星首次出数日 2026-09-21(`iteration-2/06-experiment-tracking-plan.md:308-312`)与埋点角色的提醒(`iteration-3/29:52`:未完成则首批读数只能标未验收)。**今日 09-14,仅剩 7 天,本单必须置于第一波首日。**
**降级方案(做不到时)**:① 首批北极星读数(W37 队列)在数据报告中显式标注「埋点落库未经 Android 真机验收,读数仅作趋势参考」;② 把复评点推到下一个成熟窗口(约 H14,即再迟 7 天);③ 用桌面/模拟器覆盖能覆盖的部分并明确记录**不可替代的缺口**——注意 `device-verification.md:144` 已言明 Linux 桌面的 `platform=linux` 不在契约枚举内、整批 400 被拒,故**事件落库这一项桌面根本无法替代**,降级只能降到「标注未验收」,不能降到「换环境验证」。
- **验收标准**:两项的通过标准(`:59-61``:82-83`)逐条勾选并回填执行记录(日期、机型/Android 版本、psql 输出脱敏摘录、执行人);若未执行,则降级方案的三项动作全部落地并在收官总结显式记录。
- **依赖**Android 真机到位(**外部依赖,PM 无法消除**,故列 D4-13 请用户拍板)。
- **规模**S
#### T4-25 第五份 E2E 脚本:`test_e2e_m4_manual.dart`
- **仓库**patbond-flutter(脚本)、patbond-apicompose 环境)、patbond-doc(证据归档)
- **描述**:按 `releases.md:56` 的既定原则(「每个引入对外端点的迭代都应有对应的 E2E 脚本,并在此后每次发布回归」),M4 引入生成域对外端点,须新增第五份。照抄现有四份的同构骨架(纯 Dart CLI `dart run``fail()`/`check()``_passed` 计数、`Resp` 包装、通用 `call()`、脚本内现注册账号取 token、token 截断与预签名 URL 签名 `<SIGNATURE_REDACTED>` 脱敏、内联小图 fixture——**注意 `iteration-3.5/06:56` 的教训:1×1 极小 PNG 能传能下但 Flutter 解码器拒绝,夹具须 ≥16×16**)。场景对齐 M4 四条验收标准:
1. 目录读取(模型/风格,含 `enabled` 过滤与排序)
2. 传图 → 提交生成任务 → 轮询至 `succeeded` → 输出 asset 可预签名取回且 `widthPx/heightPx` 非空
3. **相同 `Idempotency-Key` 重提 → 同一 jobId、不新增行、不消耗配额**
4. **Worker 重启不丢任务**:任务处 `running``docker compose restart` 生成域容器 → 任务被重新领取并最终成功
5. 非法状态迁移被拒(对 succeeded 任务取消、对 queued 任务重复取消等)
6. provider 失败 → 重试 → 达上限 → `failed``errorCode`/`errorMessage`/`attemptCount` 可查
7. 取消 queued 与取消 running 各一
8. 超配额 → 429 + `Retry-After`
9. 一键建草稿 → `posts.generation_job_id` 落库 → 发布 → 另一账号 Feed 可见(`category='ai_creation'`
10. 他人 jobId / 未成功 jobId 建草稿被拒
另需在脚本头补上第五条 baseUrl 常量(若 D4-2 新建模块)。
- **验收标准**:全场景绿;契约偏差 0;M4 四条验收标准逐条有证据(HTTP transcript 脱敏 + psql 库层交叉核对,沿 M3 T3-21 的双源取证强度);连跑 3 轮零 flake。
- **依赖**T4-06~T4-11、T4-19。
- **规模**M
#### T4-26 五份 E2E 回归与 v0.5.0 发布(PR 流程)
- **仓库**:三仓
- **描述**:按 `releases.md:126-138` 的**新流程**发布(v0.4.0 已实测过一轮):① 三仓 CI 绿 + **五份 E2E 同环境串行回归 PASS**M1 7 + M2 11 + M3 14 + M3.5 10 + M4 约 10 = 约 52 场景);② Gitea 建 PR `dev → main`(标题写版本号,正文贴门禁证据链接);③ 等状态检查转绿(`CI / backend-test``CI / flutter-gates``releases.md:110-114`);④ 合并(**须为快进**,若显示分叉先查明原因不得用合并提交掩盖);⑤ 打 tag + 登记发布记录。版本号建议 **v0.5.0**(功能性里程碑,随 M4 收官)。
- **验收标准**:五份回归全绿并出回归报告(沿 `iteration-3.5/08-release-e2e-regression.md` 格式);PR 快进合并、历史线性、零合并提交;三仓 tag 一致;`releases.md` 追加记录含三仓 tag/哈希、门禁证据、已知遗留。
- **依赖**:T4-25 及全部交付单。
- **规模**M
#### T4-27 文档与迭代收口
- **仓库**patbond-doc
- **描述**OpenAPI v1.5.0 归档;`architecture/backend-modules.md` 更新(模块表/端口表/生成域归属与 media 写入边界);`architecture/decisions.md` 新增 **ADR-023 起**(AI 提供方、模块与 Worker 形态、队列载体、输入图必填与视频剪出、配额与限流口径——每条拍板决策一条 ADR 或合并为一条复合 ADR);`feature-checklist.md` 新增 M4 章节(当前 AI 创作**零条目**,仅 :183/:208 两处提及)并把清偿项转绿(:193 清理任务、:194 429 限流、:211 widthPx/heightPx、:222 eventVersion、:243 收藏与草稿页)、把已过期条目纠正(**:145「auth 域契约测试补齐 ⬜」实际 M3 T3-19 已交付**);`device-verification.md` 新增 M4 节(真机专属项预登记)并回填 M2 执行记录;迭代报告归档与收官总结。
**三项文档纪律修正**:① `releases.md:131` 的「E2E **双份**回归」与 :36/:56 的四份口径自相矛盾,须改为**五份**;② 把「每个引入对外端点的迭代都应有对应 E2E 脚本并在每次发布回归」这条原则**固化进 `git-workflow.md`**(该文件 :56-64 的门禁表当前无 E2E 条目,原则只存在于 `releases.md:56` 的一句叙述里);③ **`patbond-doc` 补仓库级 `.gitignore`**(当前无该文件,`site/` 仅靠用户全局 `~/.gitignore_global:183` 排除,换机器或 CI 检出即可被误提交,与 `git-workflow.md:62` 冲突)。
**`iteration-4` 目录的 mkdocs.yml 导航由文档维护者收口提交统一添加(本拆解报告不改 mkdocs.yml**——参考 iteration-3.5 的挂法(`mkdocs.yml:100-108`,节点名脱离「第 N 迭代」序列、无 `index.md` 进展看板行),插入位置在 :108 之后、:109 `- API:` 之前。
- **验收标准**`mkdocs build --strict` 零 warning;报告索引完整;三项文档纪律修正各有对应 diff;ADR 编号连续无冲突。
- **依赖**:各波交付。
- **规模**S
#### T4-28 文档站访问控制(条件单,随 D4-12)
- **仓库**:服务器侧配置 + patbond-doc`server-exposure.md` 状态更新)
- **描述**:若 D4-12 拍板纳入:给文档站加 basic auth 或 IP 白名单。登记原文 `server-exposure.md:32`:「✅ 运行;⚠️ **公开可访问,待评估是否加 basic auth 或 IP 白名单**(无凭证内容,但暴露内部架构细节)」。实现面为 nginx 配置(`sites-enabled/` 已按域名分流,:31+ 凭据经 `.env`/密码文件不入库(沿 2026-09-11 安全事件后固化的服务器侧纪律,`iteration-3.5/07`)。**本迭代的新论据**:M4 报告将暴露 AI 提供方选型、密钥管理方式、配额策略与队列实现细节,公开可读的价值收益低于信息暴露成本。
- **验收标准**:未授权访问返回 401/403;授权后文档站功能不变;`server-exposure.md:32` 状态位更新为已处置并记录核对命令;凭据不入库(`check-secrets.sh --all` 零命中)。
- **依赖**:D4-12 拍板。不占关键路径(纯运维项)。
- **规模**S
## 5. 波次划分与关键路径
沿用已验证模式:波次并行 + 迭代式契约冻结 + 同仓串行跨仓并行 + 每波 compose 实测。
### 第一波(并行开工)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| **硬时限** | **T4-24 真机 M2 两项** | **首日执行,2026-09-21 前必须闭环或启动降级**;不占技术关键路径但占人 |
| 数据与骨架 | T4-01 → T4-02 | V6 + 生成域落位,一人连续负责;**需 D4-2/D4-4/D4-6 开工前拍板** |
| 存储写能力 | T4-03 | 与 T4-01 并行;是 T4-09 的前置,**不要等到第二波才开** |
| provider 适配 | T4-05 | **需 D4-1 拍板**;未拍板时先写接口 + fixture,止损线:适配层以上不写供应商代码 |
| 目录 | T4-04 | T4-01 后即可 |
| 契约草案 | T4-13(起草态) | 四处 TODO-FREEZE 标注 |
| 门禁补强 | T4-14(机制部分) | 先在 v1.4.0 上落校验和断言并注毒自证 |
| 前端遗留 | T4-21 我的收藏与草稿 | 与 M4 契约零耦合,第一波并行消化 |
| 清理任务 | T4-12 | 与主线解耦,可插任意空档 |
| UI 设计 | 生成状态五态与结果页设计稿 | 供 T4-17~T4-19,不占关键路径 |
### 第二波(后端纵切,契约收敛)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| 后端主线 | T4-06 → T4-08 / T4-0706 后两线并行) | T4-06 是全部生成单的前置 |
| 输出落地 | T4-09 | 依 T4-03 + T4-08 |
| 草稿打通 | T4-10 | 依 T4-01 外键 + succeeded 任务 |
| 限流 | T4-11 | 依 T4-06;**契约首个响应头,须早于冻结定型** |
| 前端骨架 | T4-16(不依赖契约部分) | Repository/DTO 骨架先行 |
| 测试滚动 | T4-15 | 即测即绿即提交 |
**波末闸门:T4-13 契约冻结 v1.5.0**(条件:T4-06/07 状态形态 + T4-09 输出形态 + T4-10 权限语义 + T4-11 限流与 `Retry-After` 形态四处全部定型;四/五份快照同步升版 + 守卫测试期望值整体更新 + T4-14 校验和期望值填入)。**不冻结不放行第三波两端联调。**
### 第三波(冻结契约下两端并行)
| 并行线 | 工单 | 说明 |
| --- | --- | --- |
| 前端主线 | T4-16(完成)→ T4-17 → T4-18 → T4-19;条件单 T4-20 | 创作页先通,状态机与结果页才有意义 |
| 埋点/实验 | T4-22;条件单 T4-23 | 随页面落地滚动挂接 |
| 后端旁路 | 契约测试补齐、队列并发压测、provider 故障注入矩阵 | 不占关键路径 |
### 第四波(收官)
T4-25 第五份 E2E → T4-26 五份回归与 v0.5.0 发布 → T4-27 文档收口;条件单 T4-28 可并行。
### 关键路径
```text
[D4-1/D4-2/D4-3/D4-4 拍板]
→ T4-01(M) → T4-03(M) → T4-05(M) → T4-06(L) → T4-08(L) → T4-09(M)
→ [T4-13 契约冻结闸门]
→ T4-16(M) → T4-17(L) → T4-18(L) → T4-19(M) → T4-25(M) → T4-26(M)
```
四个 L 工单串在关键路径上(T4-06 / T4-08 / T4-17 / T4-18),**后端队列侧与客户端状态机侧各占其二**——与 M3 的「媒体双端占其二」结构同型。
**压缩手段**
1. **D4-1~D4-4 置顶开工前裁决**(四项都在关键路径起点,其中 D4-1 阻塞 T4-05、D4-4 阻塞 V6 定稿)。
2. **T4-03 前移到第一波**:它不依赖任何生成域代码,却是 T4-09 的硬前置;若拖到第二波会把 T4-09 挤到冻结闸门之后。
3. **T4-05 fixture 优先**fixture provider 让 T4-06/T4-08 不必等真实供应商接通即可开发与测试。
4. **T4-13 草案与 T4-16 骨架前移**M2/M3 两度验证有效)。
5. **旁路化**T4-07、T4-10、T4-11、T4-12、T4-20、T4-21、T4-22 均不在关键路径上,可灵活填空档。
6. **T4-24 首日执行**:它只占 ~1 小时机时但有 7 天外部时限,越早越好。
## 6. 需要用户拍板的决策清单
以下决策 PM 只给建议,**不替用户拍板**。**D4-1 是头号**(阻塞关键路径起点且正典自认未决:`development-plan.md:358,365`);**D4-1~D4-4 建议开工前裁决**(四项都影响 V6 定稿或关键路径起点)。
| # | 决策事项 | 影响 | PM 建议(仅供参考) |
| --- | --- | --- | --- |
| **D4-1** | **AI 提供方选型**(正典三度登记为未决:`development-plan.md:358`「AI 提供方……尚未确定」、:365 待确认事项)。候选:**A. fixture / 本地 stub provider**(可配置延时与进度、返回内置图、可注入四类故障;零密钥零成本零外部依赖,CI 与 E2E 唯一可用);**B. 云 AI API**(通义万相 / 即梦 / Replicate / OpenAI 等:能出真图,但引入账号、密钥、按次成本、网络不可达风险与 CI 外部依赖);**C. 自托管 SD / ComfyUI****已核实不可行**:本工作机为 AMD Radeon Vega 集显、无 `nvidia-smi` 即无 CUDA;生产侧腾讯云轻量服务器是否有 GPU 未取证,按 ADR-016 的同类背景推断为无) | 阻塞 T4-05(关键路径起点)、T4-06/T4-08 的开发与测试;决定 provider 抽象的形态(同步返回 vs 轮询 vs 回调)、密钥管理、CI 可跑性、以及验收标准「不重复扣费」中「扣费」是真金钱还是配额 | **A 起步 + 适配层,B 作为末期可选加挂**:M4 的四条验收标准(状态流转合法 / Worker 重启不丢 / 幂等不重复 / 失败可追踪)**全部与图好不好看无关**,fixture 能 100% 取证且能注入真实供应商难以复现的故障;同时把云 API 的接入点留在适配层,若用户愿意提供密钥与预算,第三波末加挂一个云 provider 做一次「真图」验证并入报告。**不建议 C**(无 GPU) |
| **D4-2** | **模块归属与 Worker 形态**:① 生成域——**新建 `patbond-creation`:8085,七容器)** vs **并入 `patbond-user`**(后者已持 Flyway、已持唯一的 `ObjectStorage` 写入方、已有唯一的 `@EnableScheduling` 先例);② Worker 形态——**与 API 同进程 `@Scheduled`**(省容器)vs **同一 jar 不同 profile 起独立容器**(八容器,「重启不丢任务」的取证更干净、长任务不与在线请求抢线程) | 决定 T4-02/T4-03 骨架、compose 容器数(6/7/8)、CI 时长、客户端是否需要第五条 baseUrl(连带四份 E2E 脚本与真机清单的参数说明)、以及 ADR-023 的内容 | **①新建 `patbond-creation`:8085**:沿 ADR-009/ADR-017 两次先例(数据所有权独立、模块边界即未来服务边界,`backend-modules.md:68-72`);且 Worker 是长任务,与在线 API 同模块会互相影响。**媒体写入不下放**——生成输出经 T4-03 新增的 `POST /internal/media/assets``patbond-user` 落盘,**ADR-017「media 上传流程实现在 patbond-user」的边界不破**。**②同进程 `@Scheduled` + 配置开关**`patbond.creation.worker.enabled`):DB 队列 + 租约天然支持多实例,需要拆时改配置即可拆;「重启不丢任务」用重启该容器取证同样成立。七容器已是双人团队的运维上限,不建议直接上八 |
| **D4-3** | **队列载体****A. DB 队列**`FOR UPDATE SKIP LOCKED` + 目标模型已设计好的 `lease_owner`/`lease_expires_at`/`next_attempt_at`/`priority` 四列 + 两条 partial 索引)vs **B. 引入 RabbitMQ**`development-plan.md:205` 原文「RabbitMQ 在异步任务阶段启用」,选 A 即是对正典的建议性偏离)vs **C. 引入 Redis** | 决定是否新增第 7/8 个容器与新依赖;决定 T4-08 的实现复杂度;影响 `patbond-common` 纪律(`development-plan.md:79`「不应让所有服务被动引入 RabbitMQ、Feign 等依赖」) | **ADB 队列)**,理由三条:① 目标模型的 `generation_jobs` 已把租约、退避、优先级、幂等、provider 请求去重**全部设计在表里**,两条 partial 索引(`patbond_postgresql.sql:711-715`)就是为 `SKIP LOCKED` 队列写的——选 B 等于把已定稿的设计弃用一半;② 零新增基础设施(现状无 MQ 无 Redis,§2.4),运维面不增;③ 单库事务内「领任务 + 改状态」天然原子,比「MQ 消息 + DB 状态」的双写一致性问题简单一个数量级。**偏离正典须落 ADR-023 并注明**:M6「事务 Outbox 发布器」阶段若真需要 MQ,届时再引入,届时 DB 队列可作为 outbox 的对照实现 |
| **D4-4** | **生成输入是否必须有图**(决定 V6 能否照抄目标模型):目标模型 `generation_jobs.input_asset_id uuid **NOT NULL**``patbond_postgresql.sql:610`)——**即评审定稿的模型不支持纯文生图**。选项:**A. 沿用 NOT NULL,图生图先行****B. 改为 nullable 以支持文生图**(V6 偏离目标模型,须写入迁移说明) | 决定 V6 是否偏离目标模型;决定 create 页整个交互(A 则「先传宠物照片」是硬门槛,现有假上传卡直接转真实;B 则需要「有图/无图」两条分支 UI);决定配额与滥用面(A 天然收窄) | **A(沿用 NOT NULL,图生图先行)**:① 产品定位是宠物 App,核心价值是「用**我的宠物**照片生成」,纯文生图与定位弱相关且与通用 AI 画图工具直接竞争;② 现有 create 页的上传卡(`create_page.dart:159-168`)本就是必经步骤,改造成本最低;③ NOT NULL 让每次生成都有一张已归属校验的输入资产,滥用面与成本可控。若产品坚持文生图,改 nullable 的成本本身很小(V6 一处),但连带 UI 分支与滥用防护为 +1M |
| **D4-5** | **媒体形态****A. 仅 image**`media_kind` 列与枚举保留,种子只入 image)vs **B. image + video 同期** | 视频涉及转码、封面帧、时长、存储量级台阶;`duration_ms` 列已在模型里;现有 create 页已有「AI 视频」分段项(`create_page.dart:138-157`)与 video 时长 ChoiceChip:191-196);媒体上传契约的 `kind` enum 当前是 `[image]``openapi.yaml:3454`,注明 video/document 为向后新增预留) | 决定 T4-04 种子、T4-09 是否需时长探测、T4-17 分段器处置、存储成本量级 | **A(仅 image**:视频是台阶式复杂度(转码 + 封面帧 + 时长 + 带宽——注意 ADR-016 已把「单机公网带宽」列为接受的限制,视频会立刻击穿它)。**分段器建议保留但 video 项禁用 + 「即将上线」提示**,而非删除:既与保留的 `media_kind` 枚举一致,也避免 UI 大改。视频随 M5/M6 的带宽与存储决策一起重评 |
| **D4-6** | **`model_version_snapshot` 的来源矛盾**(目标模型内部自相矛盾,V6 前必须裁决):`generation_jobs.model_version_snapshot varchar(64) **NOT NULL**`,但 `generation_models`:556-573**无任何 version 列**,种子把它硬编码为 `'1.0'`:1514)。选项:**A. V6 给 `generation_models``version varchar(64) NOT NULL DEFAULT '1.0'`**(最小增列偏离);**B. 把 snapshot 列改为 nullable**(弱化可追踪性);**C. 让 snapshot 存 `provider_model_name`**(语义重复,两列同值) | 决定 V6 的一处偏离;影响「失败原因可追踪」与「历史任务可复现」的强度 | **A(给目录加 version 列)**:snapshot 四列的全部价值就是「目录改了不影响历史任务的可复现性」,B 会让这条价值在最关键的模型版本维度上失效;C 造成两列恒同值的冗余。A 的成本是 V6 里一行加列 + 一行迁移说明。**偏离须写入迁移文件头注释**(沿 V5:1-33 写法) |
| **D4-7** | **配额与限流口径**(决定「不重复扣费」中「扣费」的定义):目标模型**没有任何计费/额度表**,故 M4 的「扣费」只能理解为「消耗配额」。待定:① 每用户并发上限(建议 1);② 每用户每日次数上限(建议 20);③ 计数落在哪(**建议直接 `COUNT` `generation_jobs` 行,不新建表**);④ 429 的 `Retry-After` 取值口径(下一次可提交的秒数) | 决定 T4-06/T4-11 实现;决定 T4-25 的第 3、8 场景;直接决定成本上限(若 D4-1 选云 API,日限 × 单价 = 每日成本天花板) | **并发 1 + 日限 20 + 直接 COUNT 不建表**:① 并发 1 让排队语义对用户可解释、也让 Worker 并发度调优不受用户维度干扰;② 20 次/日对个人用户足够宽松、对成本足够安全;③ 不建表的关键理由——**幂等键已保证重复请求不新建行,所以「按行数计配额」天然满足『相同幂等请求不重复扣费』**,这是最省事又最正确的实现;④ `Retry-After` 取「最早一个 running 任务的预计完成时间」或「次日零点」的秒数,取小者。**四个数字均请用户确认,尤其若 D4-1 选 B(云 API),日限直接等于每日成本上限** |
| **D4-8** | **生成任务与草稿的语义定型**(四个子项):① 「一键建草稿」是**用户点按钮建**还是**成功后自动建**;② `category='ai_creation'` 是否**强制要求带 `generationJobId`**;③ `generationJob``Post`/`FeedCard` 读侧**外露到什么程度**(完全不露 / 只露 id / 露模型与风格名);④ 失败任务的「重试」是**新建任务**还是**复用同一任务**;⑤ 对已终态任务重复取消是**幂等 200** 还是 **422** | 决定 T4-07/T4-10/T4-18/T4-19 的实现与契约形态;③ 直接决定契约新增字段的规模;④ 影响幂等键语义(新建任务须换 key,否则命中旧任务) | ①**用户点按钮建**(自动建会产生草稿垃圾,且用户可能只想看看不想发);②**强制带 `generationJobId`**(否则用户可伪造 AI 分类,读侧 `ai_creation` 就失去可信度);③**只露 id + 模型与风格的展示名**(够做「由 Patbond-V1 · 治愈动画 生成」的角标,不露 prompt——prompt 可能含隐私);④**新建任务**(复用同一行会破坏 `attempt_count <= max_attempts` 与 snapshot 语义;客户端须生成新 `Idempotency-Key`,并在 UI 明确「重试会消耗一次配额」);⑤**幂等 200 no-op**(沿 `UpdatePostRequest.status` 对已发布帖重复提交为幂等 no-op 的既有先例 `openapi.yaml:3701-3707` |
| **D4-9** | **`widthPx`/`heightPx` 恒 null 的处置**(M3 观察项 2,跨两个迭代未决):**A. 服务端在 complete 时探测尺寸**(需 T4-03 顺带加对象读能力,读图片头部字节解析 jpeg/png/webp);**B. 只改契约描述**(把 `openapi.yaml:3536` 的「complete 后回填」改为「仅 AI 输出回填」,用户上传路径承认恒 null);**C. 契约新增 complete 请求体由客户端申报尺寸**(客户端已解码过图,成本最低,但客户端申报值不可信) | 决定单图帖能否按真实宽高比渲染(现一律回落 4:3);决定 T4-09 的规模;A 会让 T4-03 多一个方法 | **A(服务端探测)**:① M4 本就要给 `ObjectStorage` 加写能力,顺带加读只是一个方法,边际成本 S;② AI 输出路径无论如何都要写真实尺寸(尺寸是生成请求参数,服务端已知),若用户上传路径仍恒 null,就会出现「AI 图有宽高比、用户图没有」的分裂体验;③ C 的客户端申报值不可信(可被篡改,且与 `ck_media_dimensions` 的库层校验形成两个事实源)。**探测失败时回落 null 且不阻塞 `ready`**`MediaAsset.required` 不含尺寸字段,向后兼容) |
| **D4-10** | **契约版本与快照锁补强**:① 版本号 **v1.5.0**(若确认对 v1.4.0 为纯增量);② 是否顺手把快照锁从「计数守恒」补强为**真校验和断言**(§2.13CI 当前只锁 version + 3 计数 + tag 分组,md5 比对是人工步骤,任何计数守恒的字节改动都能溜过) | ① 决定四/五份快照文件名与守卫测试期望值;② T4-14 是否成立 | ①**v1.5.0,且维持「对既有集成方纯增量」的承诺**:核对全部变更(新增生成域路径、两处 enum 增值、新增字段、一处 description 修订、新增错误码与 429 响应)——**无字段删改、无类型变更、无必填收紧**,纯增量成立,可继续写入 `info.description`。②**纳入(T4-14,S)**:M4 契约面显著扩大(新增域 + 可能第五份快照),且已有一次真实教训——契约测试累计抓修 3 处真问题(`iteration-3/29:44`),其中「校验器对 `nullable + allOf` 静默跳过」正是这类「看不见的漂移」 |
| **D4-11** | **A/B 首个实验是否本迭代启动**(ADR-012 原定「M4 首实验」,`decisions.md:130`):**A. 启动,但极小面**(补分流哈希 + Flutter 曝光封装 + 最小开关三件,实验对象取纯展示层项);**B. 后置到 M5/M6**(等监控设施选型落地一并做) | 决定 T4-23 是否成立(M);决定 ADR-012 的承诺是否兑现 | **A,但请用户知情前提被高估了**:§2.9 核实后前置实为 **5 绿 3 半**#4 分流组件、#5 Flutter 曝光封装、#7 feature flag 三者**代码零实现**),文档写的「6 绿 1 半」偏乐观。启动首个实验需先补这三件(约 1M),建议用**纯客户端确定性哈希**(`anonymousId` 持久化已就位)避免为一个实验搭服务端下发设施;实验对象取 create 页风格卡默认排序或结果页引导文案这类**纯展示层**项,**不实验后端生成逻辑**。若用户认为 M4 周期已满,选 B 亦合理,但须在收官总结显式记录 ADR-012 承诺顺延,并把「6 绿 1 半」的口径更正为「5 绿 3 半」 |
| **D4-12** | **文档站是否加访问控制**`server-exposure.md:32` 登记的待评估项):**A. 本迭代纳入(S 独立工单 T4-28)**;**B. 继续挂起** | 纯运维项,不占关键路径;影响内部架构信息的暴露面 | **A(纳入,S**:本迭代的新论据是 M4 报告将写入 AI 提供方选型、密钥管理方式、配额策略与队列实现细节——公开可读的收益低于暴露成本;且 2026-09-11 安全事件后服务器侧纪律已固化,趁热做成本最低。实现为 nginx basic auth 或 IP 白名单,凭据不入库。若用户偏好继续挂起亦无技术风险,但建议至少把「M4 迭代报告」这一目录先行限制 |
| **D4-13** | **真机 M2 两项的 09-21 硬时限如何应对**(外部依赖,PM 无法消除):**A. 7 天内投入 Android 真机**(借用或采购)并首日执行 T4-24;**B. 接受降级**——首批北极星读数标注「未经真机验收」+ 复评点顺延至下一成熟窗口 | 决定 2026-09-21 首批北极星读数(W37 队列)是否可作为正式基线;连带影响 A/B 前置 #1「数据质量验收」的成色(该项判绿但拦路项正是真机) | **A(投入真机)**:① 两项合计仅约 1 小时机时(`device-verification.md:42` ~10 分钟 + `:63` ~45 分钟含等待),投入产出比极高;② **桌面无法替代**——`device-verification.md:144` 已言明 Linux 桌面 `platform=linux` 不在契约枚举内、整批 400 被拒,故「事件落库」这一项只能在 Android 上验;③ 这两项是 10 项挂起中**唯一带外部时限**的,其余 8 项无时限可继续挂起。若确实无设备,B 的三项降级动作(标注 / 顺延 / 记录不可替代缺口)须全部落地 |
| **D4-14** | **AI 资产的用途命名、归属与生命周期**:① 新 `purpose` 枚举值命名(建议 `ai_input` / `ai_output`vs 复用 `post_image`);② 输出 asset 的 `owner_user_id` 是否为请求用户(建议是);③ 失败/取消任务的**输入图**是否清理、生成记录是否可删、删除记录是否连带删对象;④ 生成图被建成帖子后,删帖是否影响该资产 | 决定 T4-03 的配置扩充、T4-09 的写入字段、T4-12 的清理范围、T4-20 的删除能力;直接影响长期存储成本(每次生成 ≥1 输入 + 1 输出) | ①**新增 `ai_input` / `ai_output` 两个值**(不复用 `post_image`:用途是审计与清理策略的依据,混用后无法区分「用户主动发的图」与「生成中间产物」)——纯配置变更无需迁移(`MediaProperties.java:66` + `application.yml:45`,ADR-022 已验证同一路径);②**是**(便于按用户清理与配额审计);③**输入图随任务终态保留、不主动清理**(重试与申诉都需要它;靠 T4-12 只清理未确认的 uploading 孤儿);**生成记录允许软删但首版不删对象**(对象清理属 M6 存储治理);④**删帖不影响资产**(`posts.generation_job_id``ON DELETE SET NULL``post_media` 与 assets 的引用关系独立,沿既有语义不动) |
**决策依赖关系提示**D4-1 → T4-05D4-2 → T4-02/T4-03②/T4-16 第五条 baseUrlD4-3 → T4-08**D4-4 + D4-6 → V6 定稿(T4-01 的开工前提)**D4-5 → T4-04 种子 + T4-17 分段器;D4-7 → T4-06/T4-11D4-8 → T4-07/T4-10/T4-18/T4-19 + 契约;D4-9 → T4-03/T4-09D4-10 → T4-13/T4-14D4-11 → T4-23 成立与否;D4-12 → T4-28 成立与否;D4-13 → T4-24 的执行 vs 降级;D4-14 → T4-03 配置 + T4-09 + T4-12 + T4-20。
## 7. 刻意不做的事项及理由
以下均为**主动裁剪**,不是遗漏。逐条给出理由,供收官总结引用与产品知情。
| # | 不做的事 | 理由 |
| --- | --- | --- |
| 1 | **视频生成**(随 D4-5) | 台阶式复杂度:转码 + 封面帧 + 时长探测 + 存储与带宽量级。**ADR-016 已把「单机公网带宽」列为接受的限制**(自托管 MinIO 在腾讯云轻量服务器),视频会立刻击穿它。`media_kind` 列与枚举保留,UI 分段项保留但禁用 |
| 2 | **纯文生图**(随 D4-4) | 评审定稿的目标模型 `input_asset_id NOT NULL``patbond_postgresql.sql:610`)本就不支持;且与「用我的宠物照片生成」的产品定位弱相关。改 nullable 的技术成本很小,连带的 UI 分支与滥用防护不小 |
| 3 | **真实计费与支付** | 目标模型**零计费/额度表**。「不重复扣费」以「不重复消耗配额」取证(D4-7),配额直接 `COUNT` 任务行。支付属独立产品决策,不在 M0~M6 任何里程碑原文中 |
| 4 | **引入 RabbitMQ / Redis**(随 D4-3 | `generation_jobs` 已把租约、退避、优先级、幂等、provider 去重全部设计在表里 + 两条为 `SKIP LOCKED` 而建的 partial 索引;引入 MQ 反而带来「消息 + DB 状态」双写一致性问题。`development-plan.md:205` 的 RabbitMQ 承诺顺延到 M6「事务 Outbox」阶段,须落 ADR 记录偏离 |
| 5 | **生成内容审核、敏感词、举报、水印** | 无运营后台(M3 D3-7 已定「`hidden`/`archived` 保留字段不开放端点」)。**但须显式声明敞口**:AI 生图的 UGC 风险高于普通图文(可生成不宜内容且难以事后归因),数据库侧可手工 `hidden` 应急;审核与水印入 backlog,随 M6 或运营后台一起设计 |
| 6 | **生成完成的推送通知** | M6 原文「通知、事务 Outbox 发布器和失败重试」(`development-plan.md:271`)。M4 内客户端靠轮询(T4-18),离页即停、回页重取 |
| 7 | **完整可观测性栈**(指标、Trace、告警、准实时护栏监控) | M6 原文「限流、审计、结构化日志、指标、Trace 和告警」(:272);A/B 前置 #7 的监控半边也已明确「留 M4」实指「依赖监控设施选型」。M4 只做 `development-plan.md:329` 的**最小兑现**:队列积压与 Worker 失败率 **SQL 可查 + 结构化日志留痕**,不建看板不接告警 |
| 8 | **全站通用限流** | T4-11 只做生成域限流(成本刚需)。全站限流属 M6 :272 范围。**注意这意味着 M3 遗留的 429 只被部分清偿**——埋点上报端点(`POST /api/v1/events`)仍无限流,须在 feature-checklist 里如实标注为部分完成 |
| 9 | **草稿自动保存** | T4-21 只做「我的草稿」列表页。自动保存(防抖 Timer + 冲突处理)是独立交互设计问题,且现有埋点枚举已明确「自动保存不埋」(`post_analytics.dart:30-41` 只有 `manual`/`on_exit`)。继续挂起 |
| 10 | **多模型并行生成 / 一次出多图 / 生图质量 A-B 对比** | 目标模型一个任务对一个 `output_asset_id`(单值列),多图需改模型(新增输出表或数组列)。属产品功能扩展,不在 M4 原文 |
| 11 | **逐帖曝光与服务端排序实验日志** | ADR-020 已否决逐卡曝光并把它记为「M4+ backlog 待服务端下发日志」(`decisions.md:169`)。服务端日志下发依赖 M6 的日志设施,本迭代不启 |
| 12 | **话题、关注列表、作者主页** | ADR-018 裁剪项,与 AI 创作零耦合。继续挂起(详见 §8) |
| 13 | **access token 黑名单、`/internal` 改 mTLS** | 跨迭代技术债。**但须记录权重上升**:T4-03 新增 `POST /internal/media/assets`(且承载对象写入),`/internal` 面在本迭代扩大,mTLS 的优先级应在 M6 加固时上调 |
| 14 | **月份网格选择器、大图下滑关闭手势、`SegmentedButton` 之外的主题债** | 与 M4 零功能耦合。大图下滑关闭需引入 `photo_view` 新依赖,不为一个手势引入依赖;月份网格的实测痛点已被年份网格 + 手输 + 「今天」覆盖(`iteration-3.5/06:72` |
| 15 | **真机 M3 四项与 M3.5 四项** | 无设备且无时限(只有 M2 两项带 09-21 硬时限)。M4 会再新增真机专属登记项,届时一并执行更经济 |
## 8. 历史遗留处置:搭车 vs 挂起
清点范围为 M3`iteration-3/29-m3-summary.md:52-62`)与 M3.5`iteration-3.5/06-wave2-closure.md:69-76`)的全部遗留,加上本次审计新发现的文档/门禁缺口。
### 8.1 搭车进 M411 项)
| 遗留项 | 判定理由 | 插入位置 |
| --- | --- | --- |
| **`widthPx`/`heightPx` 恒 null**M3 观察项 2) | AI 输出的尺寸是**生成请求参数,服务端必然已知**,不写就自相矛盾;若只写 AI 路径会造成「AI 图有宽高比、用户图没有」的分裂。且 T4-03 本就要给 `ObjectStorage` 加方法,顺带加读只是边际 S。**核实纠正:不是缺列**(V1:217-218 早已存在),规模 S 非 M | **T4-09**(随 D4-9 |
| **429 限流 + 客户端 `Retry-After` 分支** | AI 生成有真实成本,配额与限流是**刚需而非可选**;客户端 `api_client.dart:164-167` 已有 429 粗分支、`analytics_service.dart:268-272` 的注释明写在等后端落地 | **T4-11**(后端)+ **T4-16**(客户端收口)。**注意只清偿生成域**,埋点端点限流仍挂起 |
| **uploading 超时未确认 asset 清理任务** | M4 本就要建 `@Scheduled` 调度基座;且**每次生成都要先传一张输入图**,孤儿资产量被放大,此项从「可选清理」升为「成本控制项」 | **T4-12** |
| **`eventVersion` 口径未定型** | 字典 v4 本就要动 `EventDictionary` 与契约描述,顺手定型边际成本近零 | **T4-22** |
| **「我的收藏与草稿」两页** | 后端与仓库层**全就绪、零调用点**(`listMyBookmarks` 全仓无调用),与 M4 契约零耦合,可在第一波并行消化,不占关键路径 | **T4-21** |
| **真机 M2 两项** | **唯一带外部硬时限的遗留**2026-09-21),仅约 1 小时机时 | **T4-24**(第一波首日,随 D4-13 |
| **`SegmentedButton` 粉底(M2 主题债)** | **本次审计改判为搭车**:该控件的使用点之一正是 M4 要重做的 create 页模式分段器(`create_page.dart:138`),根因已定位(主题缺 `segmentedButtonTheme``app_theme.dart:94-196`)、已审计色对表与落地范本俱在(:199-214、:215-296),一次修复 6 处使用点,边际成本 S | **T4-17** |
| **A/B 前置 #4/#5/#7 的缺失半边** | ADR-012 承诺「M4 首实验」;且文档「6 绿 1 半」经核实实为「5 绿 3 半」,若不补则实验无法启动 | **T4-23**(条件单,随 D4-11 |
| **契约快照锁强度不足**(本次新发现) | CI 只锁计数不锁字节;M4 契约面显著扩大(新增域 + 可能第五份快照),此时补机制成本最低 | **T4-14** |
| **埋点白名单计数 42→41 纠偏 + 缺总量断言**(本次新发现) | 字典 v4 本就要动白名单;补一条总量断言即永久钉住,边际成本近零 | **T4-22** |
| **三项文档纪律缺口**(本次新发现):`releases.md:131` 仍写「E2E 双份」与 :36/:56 的四份矛盾;E2E 回归原则从未固化进 `git-workflow.md``patbond-doc` 无仓库级 `.gitignore``site/` 仅靠全局 gitignore 排除) | M4 本就要把回归清单改为五份并新增第五份脚本,三项一并修正;`.gitignore` 是一行 | **T4-27** |
### 8.2 继续挂起(10 项)
| 遗留项 | 挂起理由 | 建议落点 |
| --- | --- | --- |
| **真机 M3 四项 + M3.5 四项**(共 8 项) | 无设备且**无外部时限**;M4 会再新增真机专属登记项,届时一并执行更经济。步骤已在 `device-verification.md:110-168`、:172-217 备齐 | 设备到位即插入,不阻塞任何工单(约 1 天) |
| **完整草稿列表的自动保存** | 独立交互设计问题;现有埋点枚举已明确「自动保存不埋」。T4-21 只交付列表页 | M5 或独立客户端体验单 |
| **月份网格选择器** | 实测痛点已被年份网格 + 手输 + 「今天」覆盖(`iteration-3.5/06:72` | 独立客户端体验单 |
| **大图「下滑关闭」手势** | 需引入 `photo_view` 新依赖,不为一个手势引入依赖 | 独立客户端体验单(与依赖评估一并做) |
| **话题功能**ADR-018 剪出) | 与 AI 创作零耦合。**注意一处历史推测已被本次审计削弱**:M3 曾建议「topics 端点随 AI 创作分类需求一起做(`ai_creation` category 天然关联)」(`iteration-3/01:285`),但实际 `ai_creation` 只是 `category` 的第三个枚举值、与 `topics` 表无任何字段关联,**该关联不成立**,不构成搭车理由 | M5+ 或独立社区增量单 |
| **关注列表 / 作者主页**ADR-018 裁剪) | 与 AI 创作零耦合 | M5+ |
| **逐帖 Feed 曝光** | ADR-020 已否决,明确「待 M4+ 服务端下发日志」,而日志设施属 M6 | M6 后重评 |
| **access token 黑名单** | 跨迭代技术债,无 M4 耦合 | M6 加固 |
| **`/internal` 改 mTLS** | 跨迭代技术债。**权重上升须记录**:T4-03 新增 `/internal/media/assets` 且承载对象写入,`/internal` 面在本迭代扩大 | M6 加固(优先级上调) |
| **全站通用限流(生成域之外)** | T4-11 只做生成域;埋点上报端点仍无限流 | M6 :272 |
## 9. 风险清单
| # | 风险 | 影响 | 缓解措施 |
| --- | --- | --- | --- |
| R1 | **AI 提供方未决且正典自认未决**`development-plan.md:358,365`):若拖延,T4-05 空转,连带 T4-06/T4-08 无法测试;若选云 API,还叠加密钥、成本、网络不可达与 CI 外部依赖四重不确定 | 关键路径起点空转 | D4-1 置顶开工前裁决;**未拍板期间先写适配层接口 + fixture 实现**,止损线明确:适配层以上不写任何供应商特定代码;fixture 无论如何都要做(CI 与 E2E 唯一可用 provider |
| R2 | **异步基础设施从零起步**(§2.4:无 MQ、无 Redis、无线程池、`@Scheduled` 唯一先例是删会话行):租约、退避、并发领取、多实例安全全是新代码,且 `ck_generation_jobs_state` 的五态字段组合约束严格(回收时必须清 `started_at``progress`,否则库层直接拒绝) | T4-08(L)估算失准直接拖垮迭代;状态机 bug 密集区 | 选 DB 队列(D4-3)让实现收敛在一条 SQL 模式内;**T4-15 的状态机穷举矩阵作为 DoD 硬项**(每条合法迁移成功 + 每条非法迁移被应用层拒且被库层 CHECK 兜底,两层各有断言);`started_at` 清零的取舍写进代码注释与迁移说明 |
| R3 | **「输出写入媒体表」被一句话低估**(§2.5):服务端目前根本不能写对象存储,且 ADR-017 把 media 写入边界钉在 `patbond-user` | T4-03 若拖到第二波会把 T4-09 挤到契约冻结之后,连锁推迟第三波 | T4-03 前移到第一波(它不依赖任何生成域代码);用 `/internal/media/assets` 保住 ADR-017 边界不破;MinIO Testcontainer 覆盖服务端 put 全链路 |
| R4 | **契约冻结面比 M3 更宽**:四处待定型点(五态响应形态、429 + `Retry-After` 这一契约史上首个响应头、`generationJob` 外露程度、新 purpose 命名),且若 D4-2 新建模块则快照从四份变五份、守卫测试期望值需五处同步更新 | 冻结延迟连锁推迟第三波两端联调 | 四处待定型点第一波即在草案中显式 `TODO-FREEZE` 并限期第二波中期收敛;闸门纪律不放松;T4-14 的校验和断言让「漏同步一份快照」在 CI 立即暴露 |
| R5 | **AI 生成 UGC 的内容风险高于普通图文**:可生成不宜内容,且无审核、无水印、无举报、无运营后台 | 内容风险敞口(虽 MVP 用户面小) | §7 第 5 条已显式声明敞口并要求产品知情;`hidden` 字段可手工应急;prompt 不入埋点明文(隐私);审核与水印入 backlog 并在收官总结列明 |
| R6 | **成本失控**(仅在 D4-1 选云 API 时成立):无配额则单用户可无限提交 | 真金钱损失 | D4-7 的日限即每日成本天花板,**必须与 D4-1 同时拍板**T4-11 的配额是 T4-06 的准入条件而非可选后置项;fixture provider 让开发与 CI 阶段零成本 |
| R7 | **create 页从零建测试基线**(§2.14563 行、`grep "CreatePage" test` 零命中) | T4-17/T4-18 是「改代码 + 建测试」双份工作量;假延时改真轮询是状态 bug 高发区 | 保留四块视觉骨架只换数据源(降低视觉返工,M3 的 R10 不复现);轮询的「页面不可见暂停 / 回前台恢复」列为 DoD 硬项——M3 曾用 widget 测试抓出「回前台不开新曝光段」的同类真 bug(`iteration-3/29:46` |
| R8 | **09-21 硬时限只剩 7 天**且依赖外部设备(PM 无法消除) | 首批北极星读数(W37)只能标未验收,连带 A/B 前置 #1 的成色 | D4-13 请用户即刻拍板;T4-24 排在第一波首日;降级方案三项动作已写入工单,且明确**桌面无法替代**`platform=linux` 不在契约枚举、整批 400 |
| R9 | **容器数与 CI 时长继续增长**:六 → 七(或八)容器;MinIO 容器使用面因 T4-03/T4-09 扩大;测试数从 379/597 继续上量 | 门禁反馈变慢被绕过 | D4-2 建议同进程 Worker 以省一个容器;T4-15 记录每波 CI 时长,超阈值按模块分层执行,**不降低「提交前全绿」标准** |
| R10 | **文档结论与代码实况的系统性偏差**(本次审计一次性发现 8 处,见 §11):白名单计数、快照锁强度、A/B 前置成色、真机项数、E2E 份数口径…… 说明「转述被当成事实」不是 M3.5 的偶发事故 | 规模预估失准、承诺无法兑现、门禁虚假安全感 | 本报告全部结论标注核实文件与行号;**把「可机验断言」作为纠偏机制**(T4-22 补白名单总量断言、T4-14 补契约校验和断言——两者都是把人工纪律变成 CI 门禁);收官总结须逐条回填修正后的口径 |
| R11 | **未提交/未推送风险**(历次惯例项) | 工作量全损 | 每波每单交付即提交即推送(ADR-011/ADR-021:只推 devmain 走 PR);PM 每波核对三仓 `git status` 与远端同步 |
## 10. 质量要求(对全部工单生效)
- 遵守开发计划第 10 节 DoD 八条:契约/迁移/代码一致;不依赖 demo 常量;权限、校验、幂等、并发已处理;四态齐备;日志可定位且不泄敏;干净环境可复现。
- 契约规范沿用:`camelCase`、UUID 字符串、ISO 8601 + `timestamptz`、统一信封 `{code, message, data}`、稳定错误码(生成域新码段定死:建议 `40407`/`42206`/`42900`,**永不复用或改号**)、cursor 分页(禁 OFFSET)、**生成任务创建强制 `Idempotency-Key`**`development-plan.md:173`)、`version` 乐观锁。
- **契约冻结后 api 侧快照同步升版**,四/五份 md5 与正典一致;守卫测试期望值(version + 3 计数 + tag 分组)与 T4-14 的校验和断言同步更新;`everyDeclaredResponseCellIsExercised` 新增格全部真实触发,不得豁免。
- **迁移纪律**V6 进 `patbond-user`(单迁移链);不修改已推送的 V1~V5;任何偏离目标模型之处在迁移文件头注释逐条写明理由(沿 V5:1-33 写法);存量库实证 + 全新库全量迁移双向验证。
- 集成测试一律 Testcontainers `postgres:18`ADR-006/008),媒体与生成输出测试用 MinIO 容器;每单交付 `JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test` 全绿、`dart format --set-exit-if-changed` + `flutter analyze` + `flutter test` 全绿、`mkdocs build --strict` 零 warning(门禁表 `git-workflow.md:56-64`**门禁不绿不提交**)。
- 每个业务接口覆盖六类路径:成功 / 参数错 / 资源不存在 / 无权限 / 并发冲突 / 幂等重试。
- 所有网络页面四态(loading/empty/error/retry)齐备 + 离线提示;图片位另加占位/失败态;预签名 URL **不得持久化**、缓存 key 须剥 `X-Amz-*`
- **凭证纪律**:不提交任何密码、token、对象存储密钥、**AI provider 密钥**`.env` / `.sample` 模式);两层 `check-secrets.sh` + CI 兜底;埋点不含 prompt 明文、不含 assetId/jobId、不含精确字节数。
- **本迭代不实现**预约、通知推送、运营后台的任何接口或页面;`posts.region_id` 仅预留;范围外需求记 backlog。
## 11. 工单统计
- **工单总数:28**(A 数据与地基 5 + B 后端生成纵切 7 + C 契约与测试 3 + D Flutter 6 + E 埋点遗留收口 7),其中 **3 个条件单**T4-20 我的生成记录、T4-23 A/B 首实验、T4-28 文档站访问控制)
- **规模分布**(核心 25 单):S × 5T4-04 / T4-12 / T4-14 / T4-24 / T4-27)、M × 16、L × 4T4-06 / T4-08 / T4-17 / T4-18);条件单另计 S × 2、M × 1
- **波次**:4 波(第一波并行开工 10 线 / 第二波后端纵切 6 线 + 契约冻结闸门 / 第三波两端并行 3 线 / 第四波收官 3 单)
- **关键路径**D4-1~D4-4 拍板 → T4-01 → T4-03 → T4-05 → T4-06 → T4-08 → T4-09 → **T4-13 冻结闸门** → T4-16 → T4-17 → T4-18 → T4-19 → T4-25 → T4-26;L × 4 在链上,后端队列与客户端状态机各占其二
- **待拍板决策:14 项**D4-1~D4-14);**D4-1 头号**(阻塞关键路径起点),**D4-1~D4-4 建议开工前裁决**D4-4 与 D4-6 是 V6 定稿的前提)
- **遗留处置**:搭车 11 项、继续挂起 10 项
- **刻意不做**:15 项,逐条附理由
### 11.1 本报告推翻或修正的既有文档结论(8 处)
| # | 既有文档结论 | 核实后的实况 | 影响 |
| --- | --- | --- | --- |
| 1 | 「事件白名单 42 条」(`iteration-3/22:4,14``iteration-3/29:20``feature-checklist.md:218` | **41 条**`EventDictionary.java:42-104` 实测 `Map.entry` 41 个;根因是 `experiment_exposed` 被重复计数(设计源文档 `iteration-3/06:12,149` 自己写的是 19 个新事件,22 + 19 = 41)。**无任何门禁能拦**(`EventDictionaryTest` 无总量断言) | 三处文档纠正 + 补总量断言(T4-22) |
| 2 | 「四模块**字节级快照锁 CI**」 | **CI 里没有任何 md5/checksum 校验**`ci.yml` 只有三步(secret scan / 装 JDK / `mvnw clean test`);md5 比对只是人工冻结步骤。CI 实际锁的是「version + 3 计数 + tag 分组」,任何**计数守恒的字节修改**都能溜过 | 新增 T4-14(S)把纪律变门禁 |
| 3 | 「A/B 前置 **6 绿 1 半**M4 可启首个实验」(`iteration-3/29:68` | **实为 5 绿 3 半**#4 分流哈希组件、#5 Flutter 曝光封装、#7 feature flag / 社区发布开关**三者代码零实现**(后两者 grep 全仓零命中,#7 的「回滚绿」无任何依据,连 feature-checklist 都未登记) | D4-11 需在知情前提下拍板;T4-23 规模上调 |
| 4 | 「真机挂起**四项**」/ 主会话口径「共 8 项(M3.5 两项)」 | **共 10 项**M2 2 + M3 4 + **M3.5 4**`device-verification.md:176/:190/:200/:209`M3.5 收口报告 `06:75` 自己写的也是「4 项 + 1 备注」)。三个执行记录全为「待补」 | §8.2 按 10 项登记;T4-24 只抢 M2 两项 |
| 5 | 「`widthPx/heightPx` 恒 null」被隐含理解为能力缺口 | **列早已存在**(V1:217-218);真实缺口是「服务端无尺寸探测」+「契约声明了不存在的回填入口」(complete 端点 `openapi.yaml:1256-1299` **无 requestBody**)。**这是 M3.5「转述不可采信」教训的同型复现** | 规模由 M 降为 S,搭车 T4-09 |
| 6 | 「M4 需设计 AI 创作数据模型」(隐含预期) | **完整设计早已评审定稿**`docs/database/patbond_postgresql.sql:556-716` 含三表 40 列、全套租约/退避/优先级/幂等列、11 条索引(两条为 `SKIP LOCKED` 队列而建)、三个触发器、开发种子。且**零跨 schema 外键需裁剪**(V3 剪 4 条、V5 剪 2 条的先例在 M4 不重演,因 identity/pet_health/media 三个 schema 均已存在) | T4-01 由 L 降为 M;D4-3 有了「不引入 MQ」的硬证据 |
| 7 | 「输出写入媒体表」(`development-plan.md:252` 一句话) | **服务端目前根本不能写对象存储**`ObjectStorage.java:15-37` 只有 `ensureBucket`/`presignPut`/`stat`/`presignGet``putObject` 仅用于构造预签名。需新增写方法 + 实现 + 未配置桩 + 跨模块落库通路(受 ADR-017 边界约束) | 新增独立工单 T4-03(M),并前移到第一波 |
| 8 | 「回归清单已改为四份」与「发布流程」 | 口径**自相矛盾且未固化**:`releases.md:36/:56` 写四份,同一文件 :131 的纪律条目仍写「E2E **双份**回归」;且「每个引入对外端点的迭代都应有 E2E 脚本」这条原则**从未写进 `git-workflow.md`**(该文件门禁表无 E2E 条目)。另 `feature-checklist.md:145`「auth 域契约测试补齐 ⬜」已过期(M3 T3-19 已交付) | 三项文档修正并入 T4-27;第五份脚本单列 T4-25 |
**附带修正两处口径(非推翻)**:① patbond-api 测试数 **379**surefire 运行数,`releases.md:17,35`)与 **381**(注解静态计数,62 个测试类)的差异属参数化展开口径,不是回归;② patbond-flutter 是 **597 通过 + 2 skip**(环境门控冒烟),且不含 `integration_test/` 四份桌面实测与仓库根四份 E2E 脚本。
### 11.2 尚未取证、需用户或后续角色补齐的三项
1. **AI 提供方的可用性与价格**:已核实本工作机无 CUDA(AMD Vega 集显、无 `nvidia-smi`)故自托管不可行;**生产服务器是否有 GPU、云 API 的额度与单价均未取证**,需用户确认或授权调研(阻塞 D4-1)。
2. **A/B 实验设计模板与样本量规则文档**(前置 #3/#6,文档判绿):两仓中**未定位到文档实体**,仅在 `iteration-3/06` 表格里被声明为已交付(影响 T4-23 的验收标准可执行性)。
3. **北极星出数与对账 SQL 无可执行载体**SQL 仅以 Markdown 代码块存在(`iteration-2/06:200-229`、:364-470),三仓 `scripts/` 下只有 `check-secrets.sh``hooks/`。2026-09-21 首次出数须人工执行,建议后续沉淀为脚本(不在 M4 范围,记 backlog)。