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

121 KiB
Raw Blame History

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.mddocs/architecture/decisions.mdADR-001~022)、docs/database/patbond_postgresql.sqlcreation 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 节 M4development-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 拆分已预留 creationdevelopment-plan.md:96 明列 auth/pets/community/creation/marketplace
  • 单迁移链backend-modules.md:41「Flyway 迁移链唯一持有者……其他模块不得携带 Flyway」——V6 必须进 patbond-user,与模块归属决策(D4-2)解耦。
  • 结构性变更须经 ADRbackend-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_orderUNIQUE (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.usersV1:60)、pet_health.petsV3:35)、media.assetsV1: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 uuidnullable),索引 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-41fixedDelayString = "${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/429patbond-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.javaS3Client 实际只用于 headBucket/createBucket(:66-79) 与 headObject(:102)putObject 仅出现在预签名构造里(:83-86 presigner.presignPutObject(...))。

即:现有媒体链路是「客户端直传,服务端只签名与校验」。AI 输出的字节由 provider 返回、必须由服务端落盘,因此 M4 必须:

  1. ObjectStorage 上新增写方法 + S3ObjectStorage 实现 + MediaStorageConfig.java:33-59UnconfiguredObjectStorage 补桩;
  2. 解决「谁来写 media.assets 行」——ADR-017decisions.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:3536description: 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-58insertPost(...) 签名与 :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:103props {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.shhooks/)。

含义:「M4 可启首个实验」这一结论的前提比文档描述的弱。若要在 M4 启动实验,需先补分流 + 曝光 + 开关三件(工单 T4-23,条件单,随 D4-11)。

2.10 埋点白名单实测 41 条,文档三处写 42experiment_exposed 被重复计数)

  • 权威定义在代码常量patbond-user/.../analytics/EventDictionary.java:42-104grep -c 'Map.entry("' = 41。无字典表(V4 只 seed breedsvaccine_catalog),DB 侧仅正则约束 V2:21
  • 文档写 42 的三处:iteration-3/22-event-whitelist-v3.md:4,14iteration-3/29-m3-summary.md:20feature-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.mdT4-27)。
  • 现有四份脚本实为纯 Dart CLIdart run,非 flutter test,不计入 597):test_e2e_manual.dartM17 场景)、test_e2e_m2_manual.dart11)、test_e2e_m3_manual.dart14)、test_e2e_m35_manual.dart10);四份同构骨架(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:7703-backend-profile-avatar.md:287「字节级复制正典 + md5 逐一比对」= 人工步骤)。
  • CI 实际锁住的是每模块一条守卫测试的「版本 + 三个计数 + tag 分组」:AuthContractConformanceTest.java:399-407MediaContractConformanceTest.java:246-251、pet ContractConformanceTest.java:755-760CommunityContractConformanceTest.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.dart563 行,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 卡 + 渐变遮罩 + 选中描边,数据源 creationStylesdemo
进度条 :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 风险在此不复现)。

顺带命中一项主题债:该页 :138SegmentedButton 选中态粉底根因是主题里没有 segmentedButtonThemelib/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-20Page/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 Clientdio;四条 baseUrl 由 --dart-define 注入(lib/core/network/api_client.dart:8-34)。若 D4-2 新建模块,须新增第五条 PATBOND_CREATION_API_BASE_URL,并同步四份 E2E 脚本的硬编码常量与真机清单的 flutter run 参数说明。
  • 429 已有粗分支、无 Retry-Afterapi_client.dart:164-167status == 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-144purpose 由调用方注入(:154-161),已被发布页(9 图并发 2)与头像 sheet(1 图并发 1)两处复用。

2.16 收藏与草稿两页:后端与仓库层全就绪,只缺页面

  • listMyBookmarks()GET /api/v1/me/bookmarkscommunity_repository.dart:52、实现 :274-283全仓无调用点
  • listMyPosts()GET /api/v1/me/posts?status=community_repository.dart:26-30、:179-193;唯一调用点是发布页恢复最新一条草稿(post_compose_page.dart:196-199limit: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 SnackBarlib/features/profile/profile_page.dart:49 菜单项 + :235 showDemoMessage,注释 :43-46 已自认「后端能力已就位,列表页本单未做」。
  • 故此项规模确为 M(两页 + 导航 + 测试,照抄 _ListPhase 范本),与 M4 契约零耦合,可在第一波并行消化。

2.17 基线数字的口径澄清

指标 数字 口径说明
patbond-api 测试 379surefire 运行数) / 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:173media_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,最高 V5flyway_schema_history 单一,归属 patbond-user patbond-user/src/main/resources/application.yml:11-12pet/community 仅 test 作用域带 Flywaypatbond-pet/pom.xml:79-100patbond-community/pom.xml:91-113);auth 无库
ADR 最高 ADR-022M4 新决策自 ADR-023 docs/architecture/decisions.md:184-195
错误码 27 个,最大段位见 ErrorCode.java 下一可用:404 段 40407、403 段 40302、422 段 42206429 段全新

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 无仓库级 .gitignoresite/ 目前仅靠用户全局 ~/.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
  • Fluttercreation 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-docarchitecture/backend-modules.md 与 ADR-023 随收口)
  • 描述:按 D4-2 拍板结果落位生成域。PM 建议方案:沿 ADR-009/ADR-017 先例新建 Maven 模块 patbond-creation(:8085),复用 JWT 资源侧校验与当前用户解析(照抄 patbond-communitysecurity/config/SecurityConfig.java 结构),只读写 creation schematest 作用域引入 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-apipatbond-user/.../media/),patbond-doc(媒体链路说明补章)
  • 描述本迭代最易被低估的一单,见 §2.5。现有 ObjectStoragepatbond-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-59UnconfiguredObjectStorage 补桩(保持未配置即 500 的既有语义);② 新增 /internal 媒体登记端点(建议 POST /internal/media/assets),一次调用完成「写对象 + 插入 media.assets 行并直接置 ready」,供生成域落盘输出——这样 ADR-017「media 上传流程实现在 patbond-user」的边界不被打破;③ MediaProperties.allowedPurposesMediaProperties.java:66,当前 post_image,user_avatar,pet_avatarYAML application.yml:45)与 mime 白名单按 D4-14 扩充新用途(纯配置变更,media.assets.purpose 无 CHECK 约束,无需迁移——ADR-022 已验证同一路径)。若 D4-2 选择并入 user,②可退化为内部服务方法调用。
  • 验收标准
    • MinIO Testcontainer 全链路:服务端 put → media.assetsstatus='ready'ready_at 非空 → 预签名 GET 可取回同一字节(sha256 比对)。
    • /internal/media/assetsX-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/modelsGET /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)。风格响应含 previewUrlpreview_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/40000previewUrl 为 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/jobsIdempotency-Key 必带development-plan.md:173 强制;沿 POST /api/v1/posts 的既有形态 openapi.yaml:1320IdempotencyKeyRequiredHeader :1928-1944,同键异 payload → 409/40905)。落 UNIQUE (user_id, idempotency_key) + request_hash(规范化请求体的 sha256,32 字节,ck_generation_jobs_idempotency 校验)。校验链:模型/风格存在且 enabledmedia_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=0started_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 白名单过滤)、DELETEPOST .../cancel(取消,语义随 D4-8 子项)。取消的状态机后果必须逐条对齐 ck_generation_jobs_state:667-692):cancelled 要求 completed_at IS NOT NULLlease_* 全 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.8ck_generation_jobs_state 的 queued 分支要求 started_at IS NULL AND progress = 0,故回收时必须把 started_atprogress 清零——首次尝试的开始时间会丢失,须在代码注释与迁移说明中记录该取舍。
    • 重试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(或直接令租约过期)后,任务被重新领取并最终 succeededT4-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.assetspurpose 随 D4-14、owner_user_id = 请求用户、storage_type='object'status='ready' + ready_at),回填 generation_jobs.output_asset_id 并置 succeededck_generation_jobs_state 要求 succeeded 时 progress=100started_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.assetsstatus='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-apipatbond-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.javaFeedCardResponse 按 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 可见全链路走真实 PostgreSQLposts.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-apipatbond-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-apipatbond-user),patbond-docfeature-checklist 转绿)
  • 描述M3 T3-13 已有方案未实现(iteration-3/29:58feature-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_createdV1: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-docdocs/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 的声明形态(契约首个响应头)、③ generationJobPost/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/generationJobMediaAsset.widthPx description 修订(:3536);新增错误码 40407/42206/42900info.description 错误码表(:35-62,现 27 个码);新增 429 响应与 Retry-After 头。 同步义务(缺一 CI 必红):四份守卫测试的期望值 1.4.0 / 32 / 45 / 75 必须整体更新(AuthContractConformanceTest.java:399-407MediaContractConformanceTest.java:246-251、pet ContractConformanceTest.java:755-760CommunityContractConformanceTest.java:522-527),四份 OpenApiContract.javaRESOURCE 常量(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;状态用原生 ChangeNotifierDI 手写进 lib/app/app.dart:80-147)。依 T4-13 冻结契约实现 DTO 与 Client(模型/风格目录、任务创建/详情/列表/取消)。若 D4-2 新建模块,须新增第五条 baseUrlpatbondCreationApiBaseUrl + 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 即不上报)。顺带收口 429api_client.dart:164-167 现有的 ApiRateLimitExceptionRetry-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)改接 MediaUploaderpurpose 用 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.dartAppState.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)、failederrorCode/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 观察项 2iteration-3/29:57feature-checklist.md:222)——契约描述 openapi.yaml:2341-2344 可两读、服务端零取值校验(TrackEventsRequest.java:38-39@NotNull@Min,唯一取值校验在 DB 的 V2:22)、客户端硬编码 1analytics_service.dart:139);定型为「事件 schema 版本」、写入字典纪律、并决定是否补 @Min(1) 与契约 minimum。② 白名单计数纠偏:实测 41 条而文档三处写 42(§2.10),修正 iteration-3/22-event-whitelist-v3.md:4,14iteration-3/29-m3-summary.md:20feature-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 runfail()/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 重启不丢任务:任务处 runningdocker compose restart 生成域容器 → 任务被重新领取并最终成功
    5. 非法状态迁移被拒(对 succeeded 任务取消、对 queued 任务重复取消等)
    6. provider 失败 → 重试 → 达上限 → failederrorCode/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 同环境串行回归 PASSM1 7 + M2 11 + M3 14 + M3.5 10 + M4 约 10 = 约 52 场景);② Gitea 建 PR dev → main(标题写版本号,正文贴门禁证据链接);③ 等状态检查转绿(CI / backend-testCI / flutter-gatesreleases.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-docserver-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 可并行。

关键路径

[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/assetspatbond-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. 引入 RabbitMQdevelopment-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. 仅 imagemedia_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 分段器处置、存储成本量级
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_modelsversion 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;③ generationJobPost/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 且不阻塞 readyMediaAsset.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-14S: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-24B. 接受降级——首批北极星读数标注「未经真机验收」+ 复评点顺延至下一成熟窗口 决定 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_outputvs 复用 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:45ADR-022 已验证同一路径);②(便于按用户清理与配额审计);③输入图随任务终态保留、不主动清理(重试与申诉都需要它;靠 T4-12 只清理未确认的 uploading 孤儿);生成记录允许软删但首版不删对象(对象清理属 M6 存储治理);④删帖不影响资产posts.generation_job_idON DELETE SET NULLpost_media 与 assets 的引用关系独立,沿既有语义不动)

决策依赖关系提示D4-1 → T4-05D4-2 → T4-02/T4-03②/T4-16 第五条 baseUrlD4-3 → T4-08D4-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 NULLpatbond_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 挂起

清点范围为 M3iteration-3/29-m3-summary.md:52-62)与 M3.5iteration-3.5/06-wave2-closure.md:69-76)的全部遗留,加上本次审计新发现的文档/门禁缺口。

8.1 搭车进 M411 项)

遗留项 判定理由 插入位置
widthPx/heightPx 恒 nullM3 观察项 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),根因已定位(主题缺 segmentedButtonThemeapp_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.mdpatbond-doc 无仓库级 .gitignoresite/ 仅靠全局 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_atprogress,否则库层直接拒绝) 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-Keydevelopment-plan.md:173)、version 乐观锁。
  • 契约冻结后 api 侧快照同步升版,四/五份 md5 与正典一致;守卫测试期望值(version + 3 计数 + tag 分组)与 T4-14 的校验和断言同步更新;everyDeclaredResponseCellIsExercised 新增格全部真实触发,不得豁免。
  • 迁移纪律V6 进 patbond-user(单迁移链);不修改已推送的 V1~V5;任何偏离目标模型之处在迁移文件头注释逐条写明理由(沿 V5:1-33 写法);存量库实证 + 全新库全量迁移双向验证。
  • 集成测试一律 Testcontainers postgres:18ADR-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-1D4-14);D4-1 头号(阻塞关键路径起点),**D4-1D4-4 建议开工前裁决**D4-4 与 D4-6 是 V6 定稿的前提)
  • 遗留处置:搭车 11 项、继续挂起 10 项
  • 刻意不做15 项,逐条附理由

11.1 本报告推翻或修正的既有文档结论(8 处)

# 既有文档结论 核实后的实况 影响
1 「事件白名单 42 条」(iteration-3/22:4,14iteration-3/29:20feature-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-14S)把纪律变门禁
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 4device-verification.md:176/:190/:200/:209M3.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/presignGetputObject 仅用于构造预签名。需新增写方法 + 实现 + 未配置桩 + 跨模块落库通路(受 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 测试数 379surefire 运行数,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.shhooks/。2026-09-21 首次出数须人工执行,建议后续沉淀为脚本(不在 M4 范围,记 backlog)。