Files
patbond-doc/docs/architecture/decisions.md
T
lixi 39cf422f89
CI / docs-build (push) Successful in 46s
docs: ADR-023 M4 架构与关键决策成文 + ADR-018 视频后置标记取代 + 补仓内 .gitignore
ADR-023 收录 M4 全部跨角色决策(13 节),来源为开工分析八份报告与逐项
讨论拍板。要点:

- 范围:视频与图片同批交付,**取代 ADR-018 的「视频后置」**(该条已加
  supersede 标记,其余裁剪继续有效)。schema 侧本无障碍——目标模型自始
  按 media_kind IN ('image','video') 建模;成本集中在契约与客户端。
  供应商:视频 Seedance、图片 gpt-image-2;生产服务器无 GPU 故一律云 API。
- 服务划分:新建 Java 模块 patbond-creation(:8085) 持有队列与客户端端点;
  patbond-ai 为独立 Python 仓、只调服务商 API、不访问数据库。
- 队列:Postgres FOR UPDATE SKIP LOCKED + 租约,不引入 Redis/MQ
  (偏离 development-plan.md:205,判例同 ADR-002 移除 Nacos)。
- Java↔Python:统一异步、REST 非 gRPC、内部契约独立 openapi-internal.yaml
  不进快照锁、轮询为保证回调仅预留字段、服务端轮询节律与租约 TTL 约束。
  产物走 presignPut 直传——**据此更正开工报告的「服务端零对象写能力=缺失
  地基」推论**:缺 putObject 是事实,但服务端需要的是签发上传凭证,
  presignPut/presignGet 早已存在;附带使 Python 零对象存储凭证。
- 鉴权:服务身份 JWT 独立密钥对,禁止转发用户 token。事实依据:现有
  JwtVerifier 仅验签名、不校验 aud/iss,且用户 token 无 aud claim,
  复用密钥对会使四个模块把服务票当合法用户 token 接受。
- 数据模型:V6 两处偏离(input_asset_id 可空、新增 first_attempt_at)
  逐条记录原因;V7 目录种子待官方文档,不编造取值。
- 对象存储:**决策挂起,现有本地实现一行不动**。已核实 media.assets 的
  storage_type IN ('object','external') 使两种存储可共存、渐进迁移,
  任一选择均无需 DDL;另登记「六容器栈当前仅在开发机运行」与「MinIO 9000
  对真机不可达」两项上线前置。
- 埋点:eventVersion 定型为每事件 props schema 版本 + bump 规则;服务端
  指标以 generation_jobs 为权威不建第二管道;毒丸批次放大器须修;
  A/B 前置重置 5 绿 3 半、首个实验为 A/A;北极星改事件驱动触发条件。
- 契约 v1.5.0 纯增量、429 拆两码、AI 来源须由服务端从 generation_job_id
  派生(不得由客户端声明或由 purpose 推断,否则可被任意图片伪造)。
- 客户端:自适应轮询非 SSE、超时按 media_kind 由服务端下发、不承诺推送、
  不押注百分比进度、第六个 UI 态僵死/超时。
- 遗留:widthPx/heightPx 两侧同批必修、SegmentedButton 降 P2(实测
  7.19:1 达标)、收藏与草稿两页搭车、草稿类目静默降级须与放开写侧同提交修。
- 门禁:必需上下文已改 (pull_request) 并复核生效;patbond-ai 纳入发布语义。
- 版本号:v0.5.0。

另补本仓 .gitignore:site/ 此前仅靠机器级 ~/.gitignore_global 屏蔽,
换机器或进 CI 时一次 git add -A 会提交上百个构建产物(ADR-023 §12)。

验证:secret-scan exit 0;mkdocs build --strict exit 0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:43:49 +08:00

37 KiB
Raw Blame History

架构与技术决策记录(ADR)

状态:Accepted 决策日期:2026-09-03 决策人:产品负责人 背景:项目正式启动第一版(第一迭代「真实登录纵切」),基于 iteration-1-reports/ 六份开工前分析报告确认以下决策。

ADR-001 升级 Spring Boot 3 后再开发业务代码

决策:在编写第一迭代业务代码之前,先将 patbond-api 从 Spring Boot 2.7 升级到 Spring Boot 3(JDK 17 基线),升级工作时间盒为 1-2 天。

理由:

  • 当前业务代码不足 10 个类,迁移成本处于历史最低点;随业务增长成本只会上升。
  • 本迭代要新引入的 JWT、Flyway(PostgreSQL 16+)、Testcontainers、springdoc 在 Boot 2.7 下均为降级选型,按 2.7 编写等于主动制造返工。
  • Spring Boot 2.7 / Spring Cloud 2021 已进入旧技术代际(开发计划第 11 节风险 8)。

影响:javax.* → jakarta.* 命名空间迁移;依赖版本随 Boot 3 BOM 统一管理。

ADR-002 MVP 阶段移除 Nacos,微服务化阶段再引入服务发现

决策:MVP 阶段从 patbond-api 移除 Nacos(服务发现与配置中心)。patbond-auth 调用 patbond-user 改为通过配置注入的静态地址:

@FeignClient(name = "patbond-user", url = "${patbond.user-service.url}")

理由:

  • MVP 只有 auth/user 两个服务,服务发现解决的是「服务多、地址动态」的问题,当前并不存在。
  • 移除后干净检出即可启动,消除本地开发和 CI 对 Nacos 实例的硬依赖。
  • 升级 Boot 3 后原 Spring Cloud Alibaba 2021 版本线不再兼容,移除可避免本迭代被版本配套拖住。

回补路线(重要):本决策是阶段性的,不是永久放弃。在后续真正拆分微服务(多服务、多实例、地址动态)时,重新引入服务发现:

  1. 选用与当时 Spring Boot / Spring Cloud 版本配套的 Spring Cloud Alibaba 版本线(Boot 3 对应 2022/2023+,不得沿用 2021 坐标)。
  2. 加回 discovery 依赖与注册配置,删除 Feign 的 url 属性即可切回服务发现,调用方接口代码无需改动。
  3. 届时同步评估配置中心是否一并启用。

影响:开发计划第 5.1、5.2、5.3 节中涉及 Nacos 的环境要求与启动步骤在 MVP 阶段不再适用,以本决策为准。

ADR-003 Token 有效期与多设备策略

决策:

  • Access token 有效期 15 分钟。
  • Refresh token 有效期 30 天,每次刷新即轮换(rotation),旧 token 立即失效。
  • 允许多设备并行会话;退出仅撤销当前会话。
  • 上述值实现为配置项,不硬编码。

ADR-004 首版登录方式仅账号 + 密码

决策:第一版仅支持用户名 + 密码注册登录。不实现手机号登录、短信验证码和第三方登录,但凭证数据模型(identity.user_credentials)保留扩展能力,登录页 UI 预留不渲染的扩展区。

ADR-005 品牌色以 iOS 设计手稿为正典

决策:产品视觉以 AI宠物_iOS_UI设计稿.html(珊瑚橙主色 #FF6F4C、奶油底、暖色系)为品牌正典。patbond-flutter 当前的靛蓝 #4F46E5 主题属早期脚手架占位实现,正式启动后迁移到手稿色系。

补充规范(详见 iteration-1-reports/04-ui-login-design-spec.md):

  • 主题以语义 token 组织(primary / canvas / ink / error 等),集中在单一主题文件。
  • 新增 error 色 #D0342C(设计稿与现有主题均缺失)。
  • 实心按钮使用加深的 primaryStrong #D6431A 以满足 WCAG AA 对比度;原珊瑚橙 #FF6F4C 用于装饰与非文字承载场景。

ADR-006 测试与交付容器化策略

决策(2026-09-04):

  • 自动化测试中的数据库一律通过 Testcontainers 使用 Docker 临时容器(postgres:18,版本基线见 ADR-008),不依赖开发者本机数据库;每次测试在干净实例上执行全量 Flyway 迁移。
  • 最终交付将提供 Docker 镜像/编排包(对应开发计划 M6 的容器镜像项)。
  • 开发者本人手动测试/联调时可使用自己本机的 PostgreSQL,连接信息经环境变量注入,不入库。

理由:测试可复现、与目标版本(PostgreSQL 16)对齐、不受本机数据库账号/版本差异影响;交付形态与测试基础设施统一到 Docker。

ADR-007 部署形态:容器化应用 + 分阶段数据库策略

决策(2026-09-04):

  • 应用(auth、user 及后续模块)以 Docker 容器交付部署,容器保持无状态:文件走对象存储、会话在数据库,任何状态不落容器本地。
  • MVP 阶段:数据库使用 Docker 容器运行 PostgreSQL(数据挂载 volume 持久化),与应用同机以 docker compose 编排;配套每日 pg_dump 备份到独立存储,并演练过恢复流程。
  • 规模化阶段:当用户量与数据重要性上升后,数据库迁移至云托管数据库(RDS 类)或独立数据库服务器,应用容器不动。
  • 数据库连接信息始终经环境变量注入(PATBOND_DB_URL 等),保证迁移数据库时应用层零改动。

理由:双人团队运维预算有限,容器数据库 + volume + 备份在 MVP 阶段完全够用,且与 Testcontainers 测试、Docker 交付包(ADR-006)同一体系;把高可用、故障转移等重运维在需要时外包给云托管,是成本与可靠性的最优路径。守住「应用无状态」这一条纪律,数据库放哪都可随时更换。

注意:数据库大版本升级即使在 Docker 中也需要数据迁移(pg_upgrade 或 dump/restore),属 ADR 级决策,不随镜像标签随意变更;小版本安全更新随镜像自动跟进。

ADR-008 PostgreSQL 版本基线定为 18

决策(2026-09-04):数据库版本基线从开发计划最初的「PostgreSQL 16+」明确定为 PostgreSQL 18。Testcontainers 测试镜像、未来的 compose 编排与交付镜像统一锁 postgres:18。

理由:

  • 项目尚无生产数据,大版本选择处于零成本窗口;一旦有数据,大版本升级即为一次真实迁移(见 ADR-007 注意事项)。
  • PostgreSQL 18 已是发布满一年的稳定版本,与团队本机开发库(18.6)一致,消除本机与基线的版本偏差。
  • 不违背开发计划「16+」的原始约束。

验证:切换当日 ./mvnw clean test 全量 37 测试在 postgres:18(18.6)容器上通过,Flyway V1 baseline 迁移执行无兼容问题。

影响:后续大版本变更须以新 ADR 决策并附全量测试验证;数据库特性使用以 18 为可用上限参考。

ADR-009 M2 宠物健康档案新建 patbond-pet 模块

决策(2026-09-07):宠物与健康档案域在 patbond-api 内新建独立 Maven 模块 patbond-pet 承载,不并入 patbond-user。

背景:开工评估中 PM(iteration-2/01)建议新建模块,后端评估(iteration-2/02)建议 user 内独立包。用户裁定采用新建模块方案,为后续微服务化(ADR-002 预留方向)保持模块边界清晰。

ADR-010 照片/附件剪出 M2

决策(2026-09-07):宠物头像上传、疫苗证书与健康事件附件等 media 能力不进入 M2。头像 M2 阶段使用占位/预设方案。

理由:对象存储供应商未定(第一迭代 D4 遗留);后端 media 仅有表结构、上传流程零代码(iteration-2/02 评估)。待对象存储选型拍板后另立迭代实现。

ADR-011 分支策略:dev 为日常主干,master 为发布分支

决策(2026-09-07):

  • 日常开发一律只推 dev 分支。
  • master 保留作为发布分支:正式版本发布时由 dev 合并至 master。
  • 契约先行提交顺序沿用 iteration-2/08 规范:doc(openapi 独立提交)→ api → flutter;波次收尾三仓 commit + push + CI 绿才算闭环。

备注:iteration-2/08 建议的「Flyway 迁移、契约破坏性变更、依赖升级、两人并行期四类改动走短命分支 + PR 合入 dev」与本决策兼容,作为推荐实践保留,PR 目标分支为 dev。

ADR-012 M2 北极星指标与产品假设

决策(2026-09-07):采纳 iteration-2/06 定稿:

  • 北极星 = 7 日回访记录率(分母:当 ISO 周产生生命周期首条 health_record_create_succeeded 的去重用户;分子:其中在首记日之后第 1–7 个 UTC 自然日内再次创建成功者;不含首记当日;首记日 +8 天出数)。
  • 产品假设 H1–H4 及其判定阈值按 06 号报告冻结,上线前不再调整判定线。
  • M2 不启动 A/B;按 06 号报告 8 项前置条件推进,目标 M3 末全绿、M4 首实验。

ADR-013 废弃 health_record_action 保留位

决策(2026-09-07):从后端 EventDictionary 白名单直接移除 health_record_action(客户端零引用,废弃零成本)。M2 事件按字典 v2(iteration-2/06)以具体事件落地。

备注:后续如出现新埋点需求,按 v1「结果编码进事件名」惯例新增具体事件,不复活通用 actionType 设计(多套指标共享分母、枚举扩充相互污染)。

ADR-014 设计债 DEBT-1 随 M2 偿还

决策(2026-09-07):TagPill 文字对比债(DEBT-1)随 M2 偿还,采用 iteration-2/05 的深变体映射方案(一行映射表 + 可选 inkColor 参数,既有调用零参数回归)。DEBT-2(muted 次级文字对比不足)M2 内按 05 号报告以既有正典色 inkSoft 局部规避,全局翻修另立决策。

ADR-015 照护人邀请流程后置出 M2

决策(2026-09-07):owner/caregiver/viewer 权限模型与校验进入 M2,但照护人邀请/绑定流程后置到后续迭代;M2 权限校验以测试数据覆盖三角色场景验证。

ADR-016 对象存储:自托管 MinIO 起步,预留迁云

决策(2026-09-08):M3 媒体存储采用自托管 MinIO(部署在现有腾讯云服务器,随 compose 编排),S3 兼容 API + 预签名直传;代码经存储适配层隔离供应商,本地开发与 Testcontainers 用同一 MinIO 镜像,三环境零分叉。

背景:现有腾讯云服务器仅含本地盘、未购对象存储(用户确认);后端评估(iteration-3/02)指出 Feed 图片下行将受限于单机公网带宽——此约束接受为当前限制并作为迁移触发条件:当图片下行带宽成为可感知瓶颈或预算允许时,迁移至云对象存储(COS 类,S3 API 兼容、适配层保证仅换配置与凭证)。本地磁盘直存方案违反 ADR-007 无状态容器纪律,排除。

ADR-017 社区模块归属与作者信息取数

决策(2026-09-08):

  • 社区域新建 Maven 模块 patbond-community(:8084),沿 ADR-009 先例(新模块 + 共库 + patbond-user 单迁移链)。
  • media 上传流程实现在 patbond-user(横切基础能力、与 V1 media schema 同源,避免业务模块被反向依赖)。
  • Feed/评论的作者公开信息(昵称/头像)由 community 模块跨 schema 只读 identity 域取数(同库零网络开销);微服务化拆库时改为内部批量接口,与单迁移链同一演进逻辑。

ADR-018 M3 范围裁剪

决策(2026-09-08):M3 MVP = 图片媒体上传闭环 + 帖子草稿/发布/删除 + 公共 Feed 游标分页 + 单层评论 + 点赞/收藏幂等 + Flutter 三页(home/create/post_detail)替换 demo 与乐观更新回滚。关注做最小数据接口(follow/unfollow + 数量);话题首版剪出;评论仅单层不做楼中楼。视频后置。

⚠️ 「视频后置」一项已被 ADR-023 取代(2026-09-18):M4 视频与图片同批交付。本条其余裁剪(话题首版剪出、评论单层、区域系统属 M5)继续有效。

ADR-019 写接口幂等形态按域选择

决策(2026-09-08):二元状态互动(点赞/收藏/关注)用 PUT/DELETE 语义幂等(重复调用同终态,无键管理);创建型写入(发帖/评论/媒体登记)用表内幂等列(request_hash)。M2 的 Idempotency-Key 键派生机制在 pets 域维持不变,不回改。

ADR-020 M3 埋点与实验决策

决策(2026-09-08):

  • Feed 曝光采用聚合 feed_viewed(浏览段聚合),否决逐卡曝光(量级测算 7~14 个月击穿分区阈值且接收端无限流背压,见 iteration-3/06);逐帖曝光留 backlog 待 M4+ 排序实验走服务端日志。
  • 事件字典 v3 增量 19 事件 + experiment_exposed 提前进字典(A/B 前置 #5 顺带变绿)。
  • 北极星保持「7 日回访记录率」不变,复评点 = M3 收官 + H7 读数。
  • 埋点队列三项遗留(30s 定时冲刷、上传退避、anonymousId 持久化)升为 M3 第一波必做。

ADR-021 Git 工作流修订(修订 ADR-011)

决策(2026-09-08):

  • ADR-011 中「master」统一更正为 main(远端实际分支名;master 从未存在于远端)。
  • PR 触发条件由「改动类别」改为「情形」:仅两人并行同仓期间与首次发布后影响 main 的变更强制走 PR;其余直推 dev + CI 绿(M2 全程直推零风险事件的机制归因见 iteration-3/08)。
  • M3 末执行首次 dev→main 发布(8 步 checklist 见 iteration-3/08);patbond-api 远端 main 与 dev 历史不相干,届时经 Gitea 平台删除重建 main,禁止 force push 缝合。
  • 对象存储凭证(MinIO AK/SK)防泄漏:CI 兜底 grep 在第一波、先于凭证进开发机落地。
  • E2E 烟囱不进 push 门禁,保持波次手动 + 可选 workflow_dispatch。

ADR-022 M3.5 体验补齐的范围与关键决策

决策(2026-09-10,用户实测反馈后拍板):

  • 首页 demo 裁剪范围:本迭代仅做问候语真实化(改用真实昵称)。天气与位置(需接外部服务,含 key 与配额管理)、圈子入口(实为话题,ADR-018 已剪出)、促销卡(属 M5 服务域)三项留待对应里程碑;保留期间须在代码注释与迭代报告显式标注为「刻意保留的 demo 占位」,避免后续实测重复反馈。
  • 昵称不设唯一约束:identity.users.nickname 维持现状(仅 ck_users_nickname 的 btrim + 1~32 长度校验),允许重名,靠 userId 区分(社区产品常规做法)。加唯一约束需迁移,且会破坏本迭代零迁移前提。
  • 注册流程不加昵称输入:沿 ADR-004 的最小注册面,注册仍只收用户名/手机号/密码;昵称在资料页设置,未设置时展示层回退 username(回退逻辑已在 /internal/users/profiles 的 SQL 层实现,M3 T3-05 交付)。
  • 宠物头像的写权限为 WRITE 档(owner + caregiver 均可改,ADR-015 三档权限模型下):头像属日常照护信息,与体重/疫苗记录同档;viewer 只读。
  • 本迭代零 Flyway 迁移:开工审计确认所需列均已存在——identity.users.nickname 与 avatar_asset_id(V1)、pet_health.pets.avatar_asset_id(V3)、community.posts.like_count 等冗余列(V5);media.assets.purpose 无 CHECK 约束、白名单为配置项 MediaProperties.allowedPurposes,新增 user_avatar/pet_avatar 只改配置与契约枚举。下一个 Flyway 版本号 V6 留给后续真正需要建表的迭代。
  • 获赞总数走读侧实时聚合(SUM(like_count) over 本人未删帖),不引入新冗余列:写侧维护成本高于读侧聚合收益,且数据量级远未到瓶颈。端点为新增的 GET /api/v1/me/community-stats,不并入既有 follow-stats(后者主体是「某用户的关注数」,混入「我的获赞」会造成主体歧义)。

版本号:本迭代交付按 v0.4.0 发布(新增端点与字段属功能增量,非纯补丁)。

ADR-023 M4 AI 创作的架构、边界与关键决策

决策(2026-09-18,开工分析八份报告 + 与用户逐项讨论后拍板)。本 ADR 覆盖 M4 全部跨角色决策;单角色内的细节决策留在各自报告中。

1. 范围:视频与图片同批

  • 视频与图片同批交付,取代 ADR-018 的「视频后置」。数据库侧本无障碍:目标模型的 creation 三表自始按 media_kind IN ('image','video') 建模、目录按 (id, media_kind) 复合唯一键约束,media.assets.kind 的 CHECK 亦已含 'video'、duration_ms 已在。
  • 因此视频的成本集中在契约与客户端,不在 schema:契约 kind 枚举加 video、Flutter 需引入播放器依赖(现为零)、缩略图与时长呈现(MediaType.video 已备未启用)。iteration-4/03 R13 评级 🟠 中高。
  • 供应商:AI 视频 = 火山引擎 Seedance;AI 图片 = gpt-image-2。生产服务器为腾讯云轻量、无 GPU,自托管模型不可行,故一律走云 API。
  • patbond-ai 先完成两类 provider 的接口适配,客户端视频路径随契约扩展跟上。
  • iteration-4/05 的目录呈现为数据驱动(目录返回哪些 media_kind 就渲染哪些分组),故契约扩到 video 时 UI 自动长出第二段,无需重画。

2. 服务划分:patbond-ai 为独立 Python 仓,Java 持有队列

  • 新建 Java 模块 patbond-creation(:8085),前缀 /api/v1/creation/**,承载客户端端点、Worker、Python 客户端与 generation_jobs 读写。沿 ADR-009 / ADR-017 两次先例(新模块 + 共库 + patbond-user 单迁移链)。
  • patbond-ai 为独立仓、Python 实现,只负责调用 AI 供应商 API。不访问数据库:任务状态的唯一事实来源是 Java 侧的 creation.generation_jobs。
  • 理由(为何不让 Python 直接消费队列):目标模型已把租约、退避、优先级、幂等、provider 去重全部设计在 Java 侧表中(lease_owner / lease_expires_at / next_attempt_at / attempt_count / max_attempts / priority 六列 + 两条与出队/回收语句逐列对齐的偏索引)。让 Python 也轮询该表等于用两种语言实现同一套租约逻辑,并使事实来源分裂。
  • 迁移链归属不变:V6 仍放 patbond-user。这是既有约定而非新决策——V3(pet_health)与 V5(community)的业务代码在各自模块,迁移文件全部在 patbond-user。
  • 共享物:patbond-creation 需复制 3 件(JWT 资源侧校验、UuidV7、RequestHashes)。存储适配层不复制,见 §4。是否上提 patbond-common 作为独立前置工单评估,退路是各抄一份。

3. 队列载体:Postgres FOR UPDATE SKIP LOCKED + 租约,不引入 broker

  • 不引入 Redis、不引入 MQ,偏离 development-plan.md:205 的 RabbitMQ 设想。
  • 三条理由:① 目标模型已把租约队列设计完(含现成领取 SQL),换中间件等于让这批列作废、推翻已评审模型;② 实测 docker-compose.yml 仅 postgres/minio/auth/user/pet/community,无 Redis 无 MQ,引入即净增容器与运维面——与 ADR-002 移除 Nacos 同一判例;③ 任务状态真值保持单一(Postgres),无 dual-write,「Worker 重启不丢任务」由租约回收器天然满足。
  • 缺口:目标模型给了索引但没给回收器代码,租约超时回收器为 M4 新增工单。

4. Java↔Python 边界

  • 统一异步,无论上游同步还是异步。Seedance 类为异步(提交 + 查询),gpt-image 类为同步长连接;该差异不得渗透到客户端契约与 job 表——generation_jobs 状态机本就是异步形状,iteration-4/05 的等待态设计亦只有一条路径。Python 收到 submit 立即返回任务 id,同步 provider 在 Python 内部以后台任务承载长连接。
  • 收益:Java 侧无长连接占线程(轻量服务器关键);provider_request_id 立即可得,uq_generation_jobs_provider_request 唯一索引才真正生效;超时不再产生「是否已扣费」的歧义——同 id 重新轮询即可,而非重发。
  • 双重扣费的残余风险如实登记:Java 将自身 idempotency_key 透传 Python,Python 转发为上游幂等头。上游不支持幂等头时风险窗口无法消除,仅由 max_attempts 兜住并记录,不假装已解决。各供应商是否支持须逐一核对官方文档。
  • 传输为 REST(JSON),不用 gRPC。理由不是性能——瓶颈是上游数秒至数分钟的出图出片,序列化差异无意义。理由是契约体系:本项目全部契约纪律建在 OpenAPI 上(快照、计数断言、契约矩阵、逐迭代冻结),引入 .proto 等于第二套无门禁契约体系;而本迭代开工审计恰好证明「没有门禁的契约必然漂移」(四模块快照锁实际不存在,白名单 42/41 之误四份文档一路不可见)。需要类型安全时从 OpenAPI 生成 Python 客户端,保持单一来源。
  • 内部契约独立成文:patbond-ai 仓内单独一份 openapi-internal.yaml,独立版本号,不进四模块快照锁,也不写入面向客户端的 openapi.yaml——否则内部适配端点会污染公开契约的路径/操作计数。
  • 产物落盘走 presigned PUT:Java 经 patbond-user 新增的 /internal/media/** 取得预签名上传凭证,交给 Python 直传对象存储,再由 Java 核实并建 media.assets 行。这正是客户端现用的两步上传,Python 站到客户端位置。
    • 由此服务端无需新增 putObject/getObject 原语:ObjectStorage 已有 presignPut 与 presignGet。开工报告中「服务端零对象写能力 = M4 缺失地基」的推论据此更正——缺 putObject 是事实,但服务端需要的是签发上传凭证,该能力早已存在。
    • 附带收益:Python 零对象存储凭证,少一处凭证面。MediaProperties 已区分 endpoint(容器内)与 publicEndpoint(客户端可达),预签名 URL 的 host 分发本就分离。
  • 轮询为保证,回调为优化:绝不可只靠回调(网络抖动、任一侧重启、乱序都会使任务永久卡在 running)。M4 只做轮询:submit 请求体预留可选 callbackUrl 字段但不启用,将来启用无需契约变更。若日后启用,回调鉴权用 Java 下发的一次性 job 绑定能力票(Python 原样带回),使 Python 侧无需任何私钥。
  • 服务端轮询节律(Java → Python,Docker 网络内):前 60 秒 3s、60s~5min 10s、5min 后 30s。租约 TTL 必须大于单次轮询间隔(设 60s,每次轮询顺带续租),否则会出现「任务仍在跑但租约过期被他人抢占」。ix_generation_jobs_running 即为「捞出待轮询的 running 任务」而建。

5. 鉴权:服务身份 JWT,独立密钥对

  • Java→Python 签发服务身份 JWT,使用独立密钥对;禁止转发用户 JWT(Worker 异步执行时用户 token 可能已过期,且不应让 Python 持有用户凭证)。
  • Claim 约定:iss=patbond-api、aud=patbond-ai、sub=service:creation-worker、typ=service、短 exp(5 分钟)、带 jti。Python 侧强制校验 aud 与 typ。
  • 选独立密钥对的事实依据:现有 JwtVerifier 仅验签名,不校验 aud 也不校验 iss,而 JwtSigner 签发的用户 token 不含 aud claim(仅 sub/iss/sessionId/exp)。若服务票复用同一密钥对,user/pet/community(很快加 creation)四个模块都会把它当合法用户 token 接受——sub 非 UUID 大概会在后续解析时失败,但那是意外挡住而非设计挡住。独立密钥对无需改动任何已发布的验证器。
  • 两套内部鉴权并存的说明:Java↔Java 的既有内部通道继续用 InternalAuthFilter 的共享 token(header 携带,未配置则 /internal/** 全拒;该 token 在 2026-09-11 安全事件中轮换过)。Python 那一跳改用 JWT 是有意选择更强机制(有效期、aud、jti,无长期共享秘密)。本迭代不改造既有 Java↔Java 通道,避免范围蔓延。
  • 数据最小化:请求体不传 userId,除非 Python 确有需要。Python 完成工作只需 jobId + 生成参数 + 预签名上传 URL。

6. 数据模型:V6 两处偏离,V7 待供应商文档

  • V6(creation schema 三表 + 14 索引 + 3 触发器 + 补回 posts.generation_job_id 外键)已落地,零跨 schema 外键需裁剪——首个不必砍外键的 baseline 迁移。
  • 偏离一:input_asset_id 改可空(目标模型 NOT NULL)。参考图为选填,主流生图与生视频均同时支持文生与参考生,由用户是否上传参考图选择路径;原 NOT NULL 会使文生图无法表达。未发明 requires_input 标记(不超出已评审模型),「某模型是否必须有输入」由应用层查目录判断。
  • 偏离二:新增 first_attempt_at(目标模型无此列)。iteration-4/06 §4.2 的指标口径要求分离排队时长与纯执行耗时,而目标模型无法表达——其 ck_generation_jobs_state 强制 queued 分支 started_at IS NULL,故失败重排队必然擦除首次尝试时间。该列仅首次进入 running 时写入、永不覆盖,并刻意排除在 queued 分支的置空要求之外;ck_generation_jobs_first_attempt 钉住其与 attempt_count 的双向不变式。
  • V7 目录种子暂缓:种子需填 provider_code / provider_model_name / 版本快照等真实取值,须待 Seedance 与 gpt-image-2 官方文档到位后再写,否则即为编造数据。沿 V3/V4「结构与种子分离」先例。
  • posts.region_id 仍为裸列,区域系统属 M5(ADR-018 该项仍有效)。

7. 对象存储:决策挂起,现有实现不动

  • 本地 MinIO 与云对象存储之间的选择本迭代不作决定,理由是分布式、磁盘容量、备份恢复等考量尚未收敛。现有本地存储代码一行不动、不删除。
  • 已核实的两点供决策参考:
    • media.assets 已同时支持两种模型:storage_type 的 CHECK 为 IN ('object','external'),object 用 bucket+object_key,external 用 external_url,ck_media_location 强制二选一。两种存储可共存于同一张表,允许存量资产留 object、新资产走 external 的渐进迁移,任一选择均无需 DDL 迁移。
    • 现状是宿主机本地磁盘:MinIO 数据落在 Docker 命名卷 minio-data,无任何云对象存储。且 server-exposure.md §2 显示生产服务器只运行 gitea/nginx/文档站/act_runner/dockerd/sshd,六容器栈(含 MinIO)当前仅在开发机运行。
  • 由此挂起两项:① M4 是否将六容器栈部署到服务器(不部署则交付仍为「本机可验、真机不可达」);② MinIO 的 9000 端口既不在防火墙放行范围(仅 22/80/443)也无 nginx 路由,预签名 URL 目前对真机不可达——这构成客户端媒体链路的上线前置,也是「媒体上传弱网」真机验证长期挂起的深层原因之一。注意 M4 的 AI 产物上传不受此影响:Python 在容器网络内走内部 endpoint。

8. 埋点与实验

  • eventVersion 定型为「每个事件自身的 props schema 版本」,否决「字典世代」读法。决定性依据是可修复性不对称:此读法下客户端现行硬编码 1 本就正确(零改动零回填);「字典世代」读法下全部历史行皆错且不可修复(server_ts 无法反推事件当初属于哪代字典)。配套 bump 规则:新增可选属性不 bump;删属性、改语义单位、改既有枚举含义、改触发时机必须 bump,新旧版本在字典中并列为两条。
  • 落地时白名单 key 改 (name, version) 二元组,并新增逐条拒绝原因 unknown_event_version——必须置于逐条校验路径,不得放在 DTO 校验,否则重犯整批连坐错误(见下条)。
  • 服务端生成侧不建第二条埋点管道:以 creation.generation_jobs 事实表为服务端指标权威,经 creationTaskId 与客户端事件拼接。platform.product_events 的 anonymous_id/session_id NOT NULL 与 platform IN ('android','ios') CHECK 使 Worker 事件无法入表,放宽这两个 NOT NULL 是不可逆的反向操作。
  • 客户端埋点整批丢弃缺陷须修:TrackEventsRequest 的 @Valid 级联至列表元素,任一元素校验失败即整请求 400,客户端进而判为永久拒绝并整批删段。这是与平台无关的毒丸批次放大器(任一条事件的任一字段可毒死整批 50 条),桌面 platform=linux 100% 丢事件只是其最显眼的表现。analytics_service.dart 注释所称「逐条 rejected、不影响客户端」与实现相反,且该错误表述已转述进 5 处文档。枚举排除 linux 是设计意图,不动枚举;修客户端本地短路 + 调试覆盖开关。
  • A/B 前置状态重置为「5 绿 3 半」(原记「6 绿 1 半」):分流哈希、Flutter 曝光封装、feature flag 三者代码零实现,须作为前置工单而非既有资产。按旧口径排期会在实验启动日才发现要先写三个组件。
  • M4 首个「实验」为 A/A 基建验证空跑(零产品风险,≥200 曝光即可暴露分流失衡与曝光漏报);真实 A/B 设计冻结存档,放行条件挂流量而非挂日期。
  • 北极星首次出数改为事件驱动触发条件(原 2026-09-21 日期承诺作废,其 W37 入队窗口已于 09-13 关闭,无补救可能):T0 = 首个满足「≥1 名真实移动端用户产生生命周期首条 health_record_create_succeeded」的 ISO 周,首次出数 = T0 + 8 天后的首个周一。明确否决用桌面 override 或 curl 造数凑读数。另须把北极星 SQL 落成仓库内可执行载体——它目前只存在于报告正文。
  • 补一个断言埋点白名单条数的测试,列为 M4 前置:白名单实际 41 条而四份文档记 42(experiment_exposed 被重复计数),漏了整整一个迭代未被发现,根因正是 EventDictionaryTest 零条数断言。

9. 契约

  • openapi.yaml v1.4.0 → v1.5.0,纯增量:零删除、零重命名、零必填收紧。「一键建草稿」复用既有 POST /api/v1/posts,不新增路径。
  • 429 与配额拆为两个错误码,不共用一码:配额耗尽等的是次日(呈现为 sheet 并给出出口),频率过快等的是数秒(呈现为 SnackBar);共用一码必然使一半场景的文案与出口错误。契约当前完全没有 429、Retry-After 或任何响应头机制(components.headers 为空),M4 为首次引入,同时兑付客户端 Retry-After 死分支的历史挂账。
  • 两处配置扩展,零迁移:MediaProperties.allowedMimeTypes 加视频类型(现仅 jpeg/png/webp)、allowedPurposes 加 AI 产物 purpose。media.assets.purpose 为裸 varchar 无 CHECK。
  • 须一并修正契约内部既有矛盾:openapi.yaml:1228 称「purpose 仅 post_image」与 :3460 的三值枚举冲突(M3.5 加值时漏改端点描述)。M4 再加值若不同时修,即为第三次漂移。
  • 「AI 创作」标签不得由客户端声明或由 purpose 推断:社区侧 PostService.validateAssets 只校验归属与 ready、不校验 purpose,故仅凭 purpose 或客户端标记可被任意图片伪造。AI 来源须由服务端从 generation_job_id 派生。

10. 客户端

  • 进度反馈用自适应轮询(1s×3 → 2s×5 → 3s,上限 5s),不做 SSE/WebSocket。决定性理由:现有网络层全部围绕「信封解包 + 401/40101 单飞刷新重放一次」构建,SSE 两者皆用不上,等于旁路整条既建鉴权链、形成第二套鉴权路径——而 M3.5 端口事故的根因正是「跨模块调用绕开既有接线纪律」。且任何推送方案都必须有「查状态」GET 兜底,有了它轮询已免费到手。
    • AI 对话类若进 M5 需重新评估流式,本条不构成对流式的一般性否决。
  • 硬超时按 media_kind 分档,且由服务端在任务响应中下发(如 estimatedTimeoutSec)而非客户端硬编码:图片 5 分钟对视频不足,硬编码两个数会使换 provider 就要发版。
  • 绝不承诺推送:客户端无任何推送能力(无 WS/SSE/FCM)。等待页写「完成后通知你」会使用户锁屏等待而永不到达;正确表述为「回到 App 就能看到结果」。
  • 不押注百分比进度:ck_generation_jobs_progress 与状态机使 queued 时 progress 恒 0、running 时无下限约束,押注百分比的后果是最常见观感变成「0% 卡住」。主形态为阶段文字 + 不确定进度环,progress>0 才叠确定型数字;展示已用时而非 ETA(已用时是事实,ETA 是承诺,M4 给不出可信 ETA)。
  • 须定义服务端五个 status 之外的第六个 UI 态:僵死/超时(running 超阈值),且它不是错误态。

11. 遗留搭车与优先级调整

  • widthPx/heightPx 列 M4 必修,且必须两侧同批——只修一侧用户零感知。该债的三段事实:详情页客户端已正确消费(有 clamp,服务端一给即生效);Feed 卡片是客户端硬编码 4:3 缺陷(维度可用却被忽略);客户端根本无法提供维度(CreateMediaUploadRequest 契约无该字段)。M4 严重度上升的实质不是「更严重」而是**「从看不见变成看得见」:结果页按真实构图渲染、发到 Feed 被裁成 4:3,是同一次会话内的直接前后对照(9:16 竖图裁掉约 58%)。而 AI 输出路径不需要图片解码**,Worker 写产物时维度就在手里。
  • SegmentedButton 粉底降为 P2 品牌一致性债(原记 P1 无障碍债):实测 secondaryContainer #FFDAD2 + onSecondaryContainer #5D4038 对比度 7.19:1 达标。仍搭车修(根因与 M3.5-01 修 DatePicker 完全同构,主题层无 segmentedButtonTheme 亦无 chipTheme,一处补全修好 6 个使用点),但不为它拖 M4 范围。
  • 「我的收藏与草稿」两页搭车:后端与客户端仓库方法均已就绪且在测试覆盖内,只缺 2 个页面,为本次盘点投入产出比最高项。
  • 必须与放开写侧同一提交修掉的数据损坏路径:post_compose_page.dart 将恢复的 AI 草稿类目静默降级为 general。现无害(写侧被封),M4 放开写侧后会把类目改错并写回服务端。
  • ai_creation 与话题无关:它只是 category 枚举值,与 topics 表零字段关联,M3 所提「话题随 ai_creation 一起做」的搭车理由不成立,话题继续挂起。

12. 门禁、安全与发布

  • 必需状态检查上下文改为 (pull_request)(已执行并复核生效):原为 CI / backend-test (push),而 ci.yml 的 push 触发器限定 branches: [dev],造成双重后果——门禁实际由「推 dev」满足使 PR 侧检查红也能合并,且任何非 dev 分支 → main 的 PR 永久无法合并(其 head 永不产生 (push) 状态),使 hotfix 短命分支纪律实际失效。改动为替换而非追加(追加不解除死锁)。建议以一次空 PR 验证,勿以正式发布为试验场。
  • M4 继续 dev 直推:push 触发器只覆盖 dev,推 feat/** 不跑任何 CI,开分支等于自断反馈。仅 spike/*(探完不合并)与 refactor/*(rebase 后 ff-only 合回)开短命分支,且均不得直接开 PR 到 main。
  • check-secrets.sh 已补 AI 服务商密钥形态(三仓同构):新增 AK-ANTHROPIC(sk-ant-)与 AK-OPENAI(sk-/sk-proj-/sk-svcacct-),KEY-ASSIGN 扩至 api[-_]?(key|secret|token) 与 auth[-_]?token。修复前 5 种真实形态全部漏网。须先于任何 AI 凭证落地——现已完成。
  • patbond-ai 纳入发布语义:参与四仓 tag 同步、CI 门禁、防泄漏脚本与 E2E 回归。它是唯一将存放 AI 凭证的仓,不得成为唯一没有防泄漏门禁的仓。
  • 契约跨仓漂移无 CI 兜底:api 的 CI 只读 api 仓内文件、只验「快照↔守卫↔矩阵」内部自洽,完全不校验与 doc 正典的字节一致性。升版须按「doc 升正典单独提交 → 记录 md5 → api 侧一次性改完全部快照/常量/计数断言/矩阵 → 四道校验后单个原子提交」执行;最危险的反模式是手改快照而非从正典 cp——CI 不会发现。
  • 发布流程须固化进 git-workflow.md:现正典是一份标题仍写「草案」的迭代报告,8 步中 4 步已过期,全部修正只以散文形式存在于 releases.md。E2E 份数只在一处写死,其余位置表述为「全份」并链接过去。M4 引入对外端点,故须新写第五份 E2E 脚本,发布回归变为五份。
  • patbond-doc 须补 .gitignore:site/ 目前仅靠机器级全局忽略屏蔽,换机器或进 CI 时一次 git add -A 会提交上百个构建产物。
  • 三仓 core.hooksPath 均为 unset,防泄漏第一层从未生效,须逐仓启用。

13. 版本号

本迭代交付按 v0.5.0 发布(新增模块、新增端点与新增 schema,属功能增量)。