# 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.0(32 路径 / 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-577(partial `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`),22(v2)+ 19 = 41;22 号报告把 `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`(M1,7 场景)、`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) - 埋点字典 v4(AI 创作漏斗)+ `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 V6:creation 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:18(Testcontainers)上 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-api,patbond-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 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-api,patbond-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-01;T4-02。 - **规模**:S #### T4-05 provider 适配层与首个 provider 实现 - **仓库**:patbond-api,patbond-doc(provider 接入说明 + 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-api,patbond-doc(Worker 运行手册 + 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-05;T4-03(输出落盘经 T4-09);D4-2、D4-3 拍板。 - **规模**:L #### T4-09 生成输出落媒体表与真实尺寸回填(含 `widthPx/heightPx` 遗留清偿) - **仓库**:patbond-api,patbond-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-08;D4-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-09(succeeded 任务);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-06;D4-7 拍板具体数值。 - **规模**:M #### T4-12 uploading 超时未确认 asset 清理定时任务(清偿 M3 遗留) - **仓库**:patbond-api(`patbond-user`),patbond-doc(feature-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-16;T4-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-17;T4-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-18;T4-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 埋点字典 v4:AI 创作漏斗 + `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-api(compose 环境)、patbond-doc(证据归档) - **描述**:按 `releases.md:56` 的既定原则(「每个引入对外端点的迭代都应有对应的 E2E 脚本,并在此后每次发布回归」),M4 引入生成域对外端点,须新增第五份。照抄现有四份的同构骨架(纯 Dart CLI `dart run`、`fail()`/`check()`、`_passed` 计数、`Resp` 包装、通用 `call()`、脚本内现注册账号取 token、token 截断与预签名 URL 签名 `` 脱敏、内联小图 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-07(06 后两线并行) | 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 等依赖」) | **A(DB 队列)**,理由三条:① 目标模型的 `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.13:CI 当前只锁 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-05;D4-2 → T4-02/T4-03②/T4-16 第五条 baseUrl;D4-3 → T4-08;**D4-4 + D4-6 → V6 定稿(T4-01 的开工前提)**;D4-5 → T4-04 种子 + T4-17 分段器;D4-7 → T4-06/T4-11;D4-8 → T4-07/T4-10/T4-18/T4-19 + 契约;D4-9 → T4-03/T4-09;D4-10 → T4-13/T4-14;D4-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 搭车进 M4(11 项) | 遗留项 | 判定理由 | 插入位置 | | --- | --- | --- | | **`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.14:563 行、`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:只推 dev,main 走 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 × 5(T4-04 / T4-12 / T4-14 / T4-24 / T4-27)、M × 16、L × 4(T4-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)。