- iteration-3 报告 01-08(PM 拆解/后端/Flutter/RC 首个 CERTIFIED/UI/埋点/证据基线/Git) - ADR-016 自托管 MinIO 起步预留迁云(用户确认现无云存储)、ADR-017 patbond-community :8084 + media 归 user + 作者信息跨 schema 只读、ADR-018 范围裁剪(话题剪出/单层评论)、 ADR-019 幂等按域(PUT/DELETE + request_hash)、ADR-020 聚合 feed_viewed/字典 v3/ 北极星不变/队列三项升第一波、ADR-021 Git 修订(main 更正/PR 情形触发/首发布重建 main/ 防泄漏 grep 先行) - mkdocs 挂「第三迭代」导航,build --strict 通过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -142,3 +142,41 @@
|
||||
## 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-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。
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
# Patbond 第三迭代任务分解(M3 社区)
|
||||
|
||||
> 作者:Senior Project Manager
|
||||
> 日期:2026-09-08
|
||||
> 依据:`docs/development/development-plan.md`(第 7 节 M3、第 4/6 节规范、第 9/10 节质量门禁与 DoD)、`iterations/iteration-2/29-m2-summary.md`(M2 收官与遗留)、`docs/architecture/backend-modules.md`、`docs/architecture/decisions.md`(ADR-001~015)、`docs/database/patbond_postgresql.sql`(`community` schema 7 表 + `media.assets`)、`docs/api/openapi.yaml` v1.2.0(18 路径,冻结中)
|
||||
> 编号约定:本迭代工单以 `T3-` 前缀编号,避免与 T1/T2 冲突。
|
||||
> 范围声明:严格限定为 M3 社区。AI 创作(M4)、本地服务(M5)、通知推送(M6)不在本迭代范围;`posts.generation_job_id` 等 M4 挂钩字段仅作预留,不开放写入。范围外需求一律记 backlog。
|
||||
|
||||
---
|
||||
|
||||
## 1. 范围界定与依据
|
||||
|
||||
### 1.1 开发计划 M3 原文(正典依据)
|
||||
|
||||
开发计划第 7 节 M3 定义(引用原文):
|
||||
|
||||
- 目标:"完成真实动态发布和互动闭环。"
|
||||
- "实现 Feed、帖子详情、草稿/发布、媒体、评论、点赞、收藏、关注和话题。"
|
||||
- "Feed 使用游标分页;点赞、收藏使用幂等写入。"
|
||||
- "Flutter 替换本地帖子,并实现刷新、分页、失败重试和乐观更新回滚。"
|
||||
- 验收标准:"发布后可在另一客户端看到;重复点赞不重复计数;分页不丢失、不重复;删除或隐藏内容不可继续出现在公共 Feed。"
|
||||
|
||||
四条验收标准与工单的映射:跨客户端可见 → T3-21(E2E);重复点赞不重复计数 → T3-06;分页不丢失不重复 → T3-05 + T3-11 专项测试;删除/隐藏不出公共 Feed → T3-04/T3-05 语义 + T3-21 取证。
|
||||
|
||||
### 1.2 开工前的关键事实(PM 逐项核实)
|
||||
|
||||
1. **media 域是本迭代最大前置**:`media.assets` 表结构 V1 已建,但上传流程**零代码**(ADR-010 剪出 M2),且**对象存储供应商至今未拍板**(第一迭代 D4 → M2 D2-1 两度遗留)。社区帖子以图片为主要形态(demo 每帖有 `mainImage`),媒体不通则发帖闭环不成立。对象存储选型是本迭代头号拍板项(D3-1)。
|
||||
2. **community 数据模型已定稿评审**:7 张表(posts、post_media、comments、post_likes、post_bookmarks、user_follows、topics + post_topics 关联)。要点:
|
||||
- `posts` 自带 `idempotency_key + request_hash` 唯一约束、`version` 乐观锁、`like_count/comment_count/bookmark_count` 计数列、`status`(draft/published/hidden/archived)与 `visibility`(public/followers/private);Feed 索引 `(published_at DESC, id DESC) WHERE status='published' AND visibility='public'` 已就绪。
|
||||
- **评论刻意设计为单层平铺**(DDL 注释原文:"Comments are deliberately one flat level. reply_to_user_id supports @ replies without parent_comment_id")——"评论层级"不是开放问题,模型已裁决,仅需确认沿用(D3-5)。
|
||||
- `post_likes`/`post_bookmarks` 复合主键 `(post_id, user_id)` 天然支撑幂等写入。
|
||||
3. **Flutter 待替换对象明确**:`lib/features/home/home_page.dart`(首页 Feed + `_PostCard`)、`lib/features/create/create_page.dart`(创作页)、`lib/features/post/post_detail_page.dart`(详情 + 评论),数据挂在 `AppState` 的本地 `PostModel`(含 mainImage、tags、hasLiked/hasBookmarked、平铺 comments)。demo **没有**关注页与话题页——关注/话题是纯增量,不是替换项,这是裁剪空间的客观依据(D3-2/D3-3)。
|
||||
4. **隐藏依赖——作者公开资料**:Feed 卡片与评论需要作者昵称/头像,但现有契约只有 `GET /api/v1/me`,无任何"查看他人公开资料"的途径;`identity.users.avatar_asset_id` 又指向 media。获取方式(跨 schema 只读 vs Feign 调 user 内部接口 vs 嵌入响应)需拍板技术方案(D3-9),头像在 M3 至少要能随 media 域上传(否则占位)。
|
||||
5. **跨 schema 外键**:`posts.generation_job_id → creation.generation_jobs`(M4)与 `posts.region_id → platform.regions`(platform.regions 未随 V1/V2 迁移)在 V5 迁移时须裁剪为裸 uuid 列——与 M2 T2-01 裁剪 marketplace 外键同一先例。另 `topics.name` 用 `citext`、`ix_posts_content_trgm` 用 `pg_trgm`,两个扩展需随 V5 启用。
|
||||
|
||||
### 1.3 本迭代 MVP 范围(PM 建议口径,待 §4 拍板确认)
|
||||
|
||||
- **纳入**:图片媒体上传闭环(对象存储 + `POST /api/v1/media/uploads` + 状态机)、帖子草稿/编辑/发布/删除(幂等 + 乐观锁)、公共 Feed 游标分页、帖子详情、单层评论(含 @ 回复)、点赞/收藏幂等写入与计数、Flutter 三页替换真实数据 + 乐观更新回滚、M2 高优先遗留两项(auth 契约测试、埋点队列完善)。
|
||||
- **待拍板裁剪项**(默认建议见 §4):关注(D3-2,建议最小数据接口入、关注流与 followers 可见性后置)、话题(D3-3,建议首版剪出)、视频(D3-4,建议图片先行视频后置 M4+)。
|
||||
- **默认剪出**:`region_id`/`location_text_snapshot`(依赖 platform.regions,属 M5 地区体系)、`visibility='followers'`(依赖关注体系成熟)、内容审核后台与举报流程(`hidden` 字段保留为运营位,见 D3-7)、评论区通知(M6)、全文搜索(trgm 索引建了但搜索端点不在 M3 原文)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 工单列表
|
||||
|
||||
预估规模口径沿用前两迭代:S ≈ 半天内,M ≈ 1-2 天,L ≈ 3-5 天(含测试与文档)。
|
||||
|
||||
### A 组:数据与工程基础(后端)
|
||||
|
||||
#### T3-01 Flyway V5:community schema 迁移与扩展启用
|
||||
- **仓库**:patbond-api(迁移进 patbond-user,单迁移链纪律),patbond-doc(迁移说明)
|
||||
- **描述**:从 bootstrap SQL 提取 community 全部表结构为 V5;启用 `citext`、`pg_trgm` 扩展;**裁剪两条跨 schema 外键**(`posts.generation_job_id`、`posts.region_id` 保留为裸 uuid 可空列,M4/M5 迁移时补回,写入迁移说明);topics 开发种子(若 D3-3 纳入)独立为不进生产的脚本。
|
||||
- **验收标准**:
|
||||
- 全新 postgres:18(Testcontainers)上 V1→V5 全量迁移一次成功,表结构与 bootstrap SQL 一致(裁剪项除外,差异入迁移说明)。
|
||||
- `./mvnw clean test` 全绿(既有 191 测试不回归)。
|
||||
- **依赖**:无(第一波首项)。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-02 patbond-community 模块骨架与鉴权接入
|
||||
- **仓库**:patbond-api,patbond-doc(backend-modules.md 更新随收口)
|
||||
- **描述**:按 D3-6 拍板结果建立社区模块骨架(PM 建议:沿 ADR-009 先例新建 Maven 模块 `patbond-community`,:8084);复用 JWT 资源侧校验与当前用户解析;模块只读写 `community` schema(作者资料获取按 D3-9 方案);compose 编排纳入新容器。
|
||||
- **验收标准**:
|
||||
- 模块编译入构建链,`./mvnw clean test` 全绿;无 token/过期 token 返回 401 + 既有 40100 系错误码。
|
||||
- compose 起五容器(postgres + auth + user + pet + community)健康。
|
||||
- **依赖**:D3-6 拍板(可先按建议方案搭骨架,骨架期变更成本最低)。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-03 media 域最小闭环:对象存储接入与上传流程
|
||||
- **仓库**:patbond-api(模块归属随 D3-6),patbond-doc(上传流程说明)
|
||||
- **描述**:**本迭代关键路径起点,依赖 D3-1 拍板**。实现 `POST /api/v1/media/uploads`(创建 asset 记录 + 签发上传凭据,建议预签名直传)与上传完成确认端点(uploading→ready,校验 mime/尺寸/大小;失败→failed);接入拍板的对象存储(建议 MinIO 起步);读取侧签发访问 URL(或公共读桶策略,随 D3-1 定);首版仅 `kind='image'`(D3-4),单文件上限与允许 mime 白名单写入契约。清理策略(uploading 超时未确认的 asset)首版仅记录方案不实现定时任务。
|
||||
- **验收标准**:
|
||||
- 上传→确认→ready→URL 可访问全链路 compose 实测通过;非法 mime/超限被拒且错误码稳定。
|
||||
- 集成测试覆盖状态机合法/非法迁移;CI 内以 MinIO Testcontainer(或拍板方案对应容器)验证。
|
||||
- `ck_media_location`/`ck_media_ready` 等数据库约束与应用层校验一致。
|
||||
- **依赖**:D3-1 拍板;T3-02(或 media 独立模块骨架)。
|
||||
- **规模**:L
|
||||
|
||||
### B 组:后端社区纵切
|
||||
|
||||
#### T3-04 帖子生命周期:草稿/编辑/发布/删除
|
||||
- **仓库**:patbond-api
|
||||
- **描述**:`POST /api/v1/posts`(创建草稿,`Idempotency-Key` + request_hash 落 `uq_posts_author_idempotency`)、`PATCH /api/v1/posts/{postId}`(编辑,`version` 乐观锁,仅作者)、发布动作(draft→published,写 `published_at`,校验 `ck_posts_publish_state`)、删除(软删 `deleted_at`,语义随 D3-7)、`GET /api/v1/posts/{postId}` 详情、我的帖子列表(含草稿,`ix_posts_author_created` 游标)。post_media 挂接:只接受 `status='ready'` 且属于当前用户的 asset,position/is_cover 语义与 `uq_post_media_cover` 一致;发布时至少校验内容非空(图片是否必填随 D3-4 定)。category 三值白名单(general/help/ai_creation,ai_creation 仅预留不开放)。
|
||||
- **验收标准**:
|
||||
- 草稿→编辑→发布→详情→删除全链路走真实 PostgreSQL;相同 Idempotency-Key 重试不产生重复帖子。
|
||||
- 非作者编辑/删除被拒(403/404 语义契约定死);version 冲突返回既有 40902 语义。
|
||||
- 引用非 ready/非本人 asset 被拒;六类测试路径(成功/参数错/不存在/无权限/并发冲突/幂等重试)覆盖。
|
||||
- **依赖**:T3-01、T3-02、T3-03(media ready 校验)。
|
||||
- **规模**:L
|
||||
|
||||
#### T3-05 公共 Feed 游标分页与帖子卡片聚合
|
||||
- **仓库**:patbond-api
|
||||
- **描述**:`GET /api/v1/feed`(命名待契约定稿):`status='published' AND visibility='public'` 走 `ix_posts_feed`,复合游标 `(published_at, id)` 降序,**禁止 OFFSET**(第 6.1 节红线);软删/hidden/archived 一律不可见(M3 验收标准四)。响应含卡片所需全部字段:作者公开摘要(昵称/头像,取数方案按 D3-9)、封面图 URL、三计数、**当前用户 liked/bookmarked 状态**(批量查询避免 N+1)、话题标签(若 D3-3 纳入)。
|
||||
- **验收标准**:
|
||||
- 分页不丢失不重复:含"翻页间隙有新发布/有删除"两个专项集成测试;游标篡改/过期返回规范错误。
|
||||
- 删除与 hidden 帖子在下一次请求即不可见,有测试。
|
||||
- 卡片字段口径逐项写入契约描述(liked 状态、封面选取规则、计数来源)。
|
||||
- **依赖**:T3-04。
|
||||
- **规模**:L
|
||||
|
||||
#### T3-06 点赞/收藏幂等写入与计数
|
||||
- **仓库**:patbond-api
|
||||
- **描述**:点赞/收藏的施加与取消(建议 `PUT/DELETE /api/v1/posts/{postId}/like`、`.../bookmark`,PUT/DELETE 天然幂等语义);依托复合主键防重,`like_count/bookmark_count` 与关系行**同事务**原子增减;重复施加/重复取消均返回成功且计数不变(M3 验收标准二);对不可见帖子(软删/hidden/他人 private)操作返回 404。我的收藏列表(`ix_post_bookmarks_user_created` 游标分页)。
|
||||
- **验收标准**:
|
||||
- 重复点赞并发压测(同用户并发 N 次)后 like_count 恰为 1,有集成测试。
|
||||
- 取消不存在的点赞不报错不减计数;计数列与关系表对账一致性有测试。
|
||||
- **依赖**:T3-04;与 T3-05/T3-07 可并行。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-07 评论:单层平铺 + @ 回复
|
||||
- **仓库**:patbond-api
|
||||
- **描述**:按 D3-5 确认的单层模型实现 `GET/POST /api/v1/posts/{postId}/comments`(`ix_comments_post_created` 游标分页;创建带 `client_request_id` 幂等 + `reply_to_user_id` 可选 @ 回复)与评论删除(作者可删;帖主是否可删他人评论随 D3-7 定)。`comment_count` 同事务维护(删除减计数);`ck_comments_deleted` 状态一致性;评论长度 1~2000 与数据库约束一致。响应含评论作者公开摘要(同 D3-9 方案)。
|
||||
- **验收标准**:
|
||||
- 相同 client_request_id 重试不产生重复评论;对不可见帖子评论返回 404。
|
||||
- 分页正确;删除后计数与列表一致;六类测试路径覆盖。
|
||||
- **依赖**:T3-04。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-08 关注最小数据接口(条件单,随 D3-2)
|
||||
- **仓库**:patbond-api
|
||||
- **描述**:若 D3-2 拍板纳入:follow/unfollow(PUT/DELETE 幂等,`ck_user_follows_self` 禁自关注)、我的关注/粉丝列表(游标分页)、目标用户维度的关注状态查询(嵌入 D3-9 公开资料响应)。**关注 Feed tab 与 `visibility='followers'` 不在本单**(后置,见 D3-2 影响面)。
|
||||
- **验收标准**:重复 follow 幂等;自关注被拒;列表分页正确;六类测试路径覆盖。
|
||||
- **依赖**:D3-2 拍板;T3-02。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-09 话题目录与帖子挂接(条件单,随 D3-3)
|
||||
- **仓库**:patbond-api
|
||||
- **描述**:若 D3-3 拍板纳入:topics 只读目录(active 过滤)、发帖挂话题(≤N 个,上限入契约)、话题维度 Feed(`ix_post_topics_topic` + 可见性过滤)。话题创建首版仅种子数据,不开放用户建话题。
|
||||
- **验收标准**:挂接与话题 Feed 正确过滤不可见帖;目录/上限校验有测试。
|
||||
- **依赖**:D3-3 拍板;T3-04。
|
||||
- **规模**:M
|
||||
|
||||
### C 组:契约与测试
|
||||
|
||||
#### T3-10 OpenAPI v1.3.0 扩展与冻结
|
||||
- **仓库**:patbond-doc(`docs/api/openapi.yaml`),patbond-api(字节级快照同步)
|
||||
- **描述**:沿用 M2 验证过的**迭代式契约冻结**:第一波按 §1.1 与数据模型出草案(TODO-FREEZE 标注媒体凭据形态、Feed 卡片字段、公开资料形态三处待定型点)→ 随 T3-03/04/05 实现定型回填 → 拍板 → 冻结合入 + **api 侧字节级快照同步升版**(冻结纪律:升版须同步快照,缺一 CI 必红)。沿用既定规范:camelCase、UUID 字符串、统一信封、稳定错误码(community/media 域新错误码段定死)、cursor 分页形态、Idempotency-Key、version。
|
||||
- **验收标准**:契约评审通过;契约测试锁定零漂移;`mkdocs build --strict` 通过;冻结后变更须显著上报两端同步。
|
||||
- **依赖**:草案仅依赖数据模型;冻结须 T3-03 凭据形态 + T3-04 权限/错误语义 + T3-05 卡片字段定型。**冻结是第三波前端联调放行闸门。**
|
||||
- **规模**:M
|
||||
|
||||
#### T3-11 后端集成测试滚动补齐与 CI(横切单)
|
||||
- **仓库**:patbond-api
|
||||
- **描述**:随 B 组滚动补齐 Testcontainers 集成测试与契约一致性测试(机制复用 M2 T2-20);每单交付 `./mvnw clean test` 必绿。专项:Feed 分页边界矩阵(空 Feed/单页/翻页间隙增删/游标非法)、计数对账、幂等并发。MinIO 容器纳入 CI 后记录时长,超阈值评估分层。
|
||||
- **验收标准**:每个业务接口覆盖六类路径;Gitea Actions 全绿(commit status API 实查,M2 惯例);CI 时长记录在案。
|
||||
- **依赖**:随 T3-03~T3-09 滚动。
|
||||
- **规模**:M(分摊在各单内)
|
||||
|
||||
### D 组:Flutter 客户端
|
||||
|
||||
#### T3-12 community feature 分层与 API Client
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:按第 4.2 节拆出 community feature(Controller → Repository → API Client,对齐 pets feature 既有结构);依 T3-10 冻结契约实现 DTO 与 Client(帖子、Feed、评论、点赞/收藏、媒体上传,条件项随拍板);错误码解析复用既有网络层与 token 拦截。`AppState` 中 `PostModel` demo 数据链路在本组末位工单交付后移除。
|
||||
- **验收标准**:DTO 映射有单元测试;错误映射类型化;UI 无关骨架可先行。
|
||||
- **依赖**:T3-10 冻结(骨架部分可提前与后端并行)。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-13 媒体上传客户端
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:选图(image_picker 或既定方案)、客户端压缩/尺寸约束(与契约上限一致)、按 T3-03 协议两步上传(取凭据→直传→确认)、上传中/失败/重试状态、多图并发上传与顺序保持(position)。
|
||||
- **验收标准**:上传全链路 compose 实测;弱网失败可重试不产生孤儿引用(未确认 asset 不挂帖);单元/widget 测试覆盖状态机。
|
||||
- **依赖**:T3-12;T3-03 联调。
|
||||
- **规模**:L
|
||||
|
||||
#### T3-14 首页 Feed 替换真实数据
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:`home_page.dart` Feed 替换:下拉刷新、游标分页加载更多、**loading/empty/error/retry 四态**(第 9 节硬要求)、图片加载占位与失败态、卡片计数与 liked/bookmarked 状态取自服务端。demo 的 breedTag/tags 展示按契约实际字段调整(话题未纳入则该位裁剪)。
|
||||
- **验收标准**:刷新与分页不丢不重(widget 测试模拟游标);四态齐备有测试;不再读 AppState demo 帖子。
|
||||
- **依赖**:T3-12。
|
||||
- **规模**:L
|
||||
|
||||
#### T3-15 发帖与草稿流程
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:`create_page.dart` 替换:文字 + 多图(挂 T3-13)、本地暂存与服务端草稿(保存草稿/继续编辑/发布)、发布携带 Idempotency-Key(客户端生成并在重试间保持)、发布失败重试、成功后 Feed 可见引导。字段对齐契约(title 可选 120、content 1~10000、category)。
|
||||
- **验收标准**:草稿→发布→Feed 出现全链路真实后端;断网发布重试不产生重复帖;四态与校验提示齐备有测试。
|
||||
- **依赖**:T3-12、T3-13。
|
||||
- **规模**:L
|
||||
|
||||
#### T3-16 帖子详情与评论接入
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:`post_detail_page.dart` 替换:详情取数、评论游标分页、发评论(client_request_id 幂等 + @ 回复)**乐观插入**(发送即上屏置 pending 态,失败标红可重试/撤回)、删除自己的评论。已删除/隐藏帖子的详情页兜底(404 → 友好提示并从列表移除)。
|
||||
- **验收标准**:评论乐观插入失败回滚有 widget 测试;分页与 @ 回复展示正确;四态齐备。
|
||||
- **依赖**:T3-12;T3-14 后并行于 T3-15。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-17 点赞/收藏乐观更新与回滚
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:统一乐观更新工具(立即翻转 UI 与本地计数 → 请求失败回滚 + toast;快速连点合并为末态请求,防抖;响应乱序以末次请求为准);Feed 卡片、详情页、收藏列表三处状态一致(同一帖子跨页面状态同源)。我的收藏列表页接入。
|
||||
- **验收标准**:失败回滚、连点合并、跨页面一致各有 widget 测试;离线操作提示明确不假成功。
|
||||
- **依赖**:T3-12、T3-14。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-18 关注 UI 最小版(条件单,随 D3-2)
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:若 D3-2 纳入:帖子作者处关注/取关按钮(乐观更新复用 T3-17 工具)、我的关注/粉丝列表页。不做关注 Feed tab。
|
||||
- **验收标准**:关注状态跨页面一致;乐观回滚有测试。
|
||||
- **依赖**:T3-08、T3-17。
|
||||
- **规模**:S
|
||||
|
||||
### E 组:遗留、埋点与收口
|
||||
|
||||
#### T3-19 M2 高优先遗留清偿(第一波插入)
|
||||
- **仓库**:patbond-api、patbond-flutter
|
||||
- **描述**:随 D3-8 拍板,PM 建议纳入两项:① auth 域契约测试补齐(机制复用 M2 契约测试框架,S);② 埋点队列完善(30s 定时冲刷、失败退避——429 依赖后端限流未做则先覆盖网络错误退避、anonymousId 持久化;方案见 iteration-2/15 §4)。与 M3 契约零耦合,第一波并行消化。
|
||||
- **验收标准**:auth 全响应矩阵入契约测试;队列三项行为各有测试;不回归既有 272 前端测试。
|
||||
- **依赖**:D3-8 拍板。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-20 社区埋点:字典 v3 与挂接
|
||||
- **仓库**:patbond-flutter(挂接)、patbond-api(白名单扩充)、patbond-doc(字典)
|
||||
- **描述**:事件定义以 Experiment Tracker 的 M3 埋点方案为准(本单不自造字典;沿用 v1「结果编码进事件名」惯例与 ADR-013 纪律),预期覆盖发帖成功/Feed 浏览/点赞/收藏/评论等关键动作;随 D 组页面落地滚动挂接;埋点不含帖子内容明文。兼顾 ADR-012:A/B 前置 8 项目标 M3 末全绿,缺口由 Experiment Tracker 盘点。
|
||||
- **验收标准**:关键动作事件端到端落库;白名单与字典同步;有测试。
|
||||
- **依赖**:Experiment Tracker 方案;T3-14~T3-17 滚动。
|
||||
- **规模**:S
|
||||
|
||||
#### T3-21 E2E 烟囱与验收取证
|
||||
- **仓库**:patbond-flutter(用例)、patbond-api(compose 环境)、patbond-doc(证据归档)
|
||||
- **描述**:沿用 M2 收官战模式,烟囱场景对齐 M3 四条验收标准:账号 A 传图发帖 → 账号 B(另一客户端会话)Feed 可见并点赞/收藏/评论 → A 重复点赞并发验证计数 → 翻页期间新发布/删除验证分页 → A 删帖后 B 侧 Feed 与详情不可见 → 幂等重试发帖不重复。HTTP transcript 脱敏、数据库证据、门禁输出入档。
|
||||
- **验收标准**:全场景绿;契约偏差 0;M3 四条验收标准逐条有证据。
|
||||
- **依赖**:T3-05、T3-06、T3-15、T3-16、T3-17。
|
||||
- **规模**:M
|
||||
|
||||
#### T3-22 文档与迭代收口
|
||||
- **仓库**:patbond-doc
|
||||
- **描述**:OpenAPI v1.3.0 归档、backend-modules.md 更新(新模块与 media 归属)、feature-checklist 增补、迭代报告归档与收官总结。**iteration-3 目录的 mkdocs.yml 导航由文档维护者收口提交统一添加(本拆解报告不改 mkdocs.yml)**。
|
||||
- **验收标准**:`mkdocs build --strict` 通过;报告索引完整。
|
||||
- **依赖**:各波交付。
|
||||
- **规模**:S
|
||||
|
||||
---
|
||||
|
||||
## 3. 波次划分与关键路径
|
||||
|
||||
沿用已验证模式:波次并行 + 迭代式契约冻结 + 同仓串行跨仓并行 + 每波 compose 实测。
|
||||
|
||||
### 第一波(并行开工)
|
||||
|
||||
| 并行线 | 工单 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 数据与骨架 | T3-01 → T3-02 | V5 + community 骨架,一人连续负责 |
|
||||
| media 闭环 | T3-03 | **需 D3-1 开工前拍板**;未拍板时可先做 asset 元数据/状态机 + 存储接口抽象,把供应商差异隔离在适配层 |
|
||||
| 契约草案 | T3-10(起草态) | TODO-FREEZE 标注三处待定型点 |
|
||||
| 前端遗留 | T3-19 | 与 M3 契约零耦合 |
|
||||
| UI 设计 | Feed/发帖/详情四态与空态设计稿 | 供 T3-14~16,不占关键路径 |
|
||||
|
||||
### 第二波(后端纵切,契约收敛)
|
||||
|
||||
| 并行线 | 工单 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 后端主线 | T3-04 → T3-05 / T3-06 / T3-07(04 后三线并行);条件单 T3-08/T3-09 随拍板插入 | T3-04 帖子生命周期是全部互动单的前置 |
|
||||
| 前端骨架 | T3-12 分层骨架(不依赖契约部分) | Repository/状态骨架先行 |
|
||||
| 测试滚动 | T3-11 | 即测即绿即提交 |
|
||||
|
||||
**波末闸门:T3-10 契约冻结**(条件:T3-03 凭据形态 + T3-04 权限/错误语义 + T3-05 卡片字段定型;快照同步升版)。不冻结不放行第三波联调。
|
||||
|
||||
### 第三波(冻结契约下两端并行)
|
||||
|
||||
| 并行线 | 工单 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 前端主线 | T3-12(完成)→ T3-13 → T3-14 → T3-15 / T3-16 / T3-17(可两人并行);条件单 T3-18 | 媒体上传客户端先通,发帖流程才有意义 |
|
||||
| 后端旁路 | 契约测试补齐、Feed 分页专项、性能核对 | 不占关键路径 |
|
||||
| 埋点 | T3-20 | 随页面落地滚动挂接 |
|
||||
|
||||
### 第四波(收官)
|
||||
|
||||
T3-21 E2E 烟囱 → T3-22 文档收口 → 任务板更新与验收报告。
|
||||
|
||||
### 关键路径
|
||||
|
||||
```text
|
||||
[D3-1 拍板] → T3-03(L) → T3-04(L) → T3-05(L) → [T3-10 冻结] → T3-12 → T3-13(L) → T3-15(L) → T3-21
|
||||
```
|
||||
|
||||
五个 L 工单串在关键路径上,media 双端(T3-03/T3-13)占其二——媒体链路是周期决定因素。压缩手段:D3-1 置顶开工前拍板;T3-03 存储适配层先行;T3-10 草案与 T3-12 骨架前移;T3-06/07/16/17 走旁路。
|
||||
|
||||
---
|
||||
|
||||
## 4. 需要用户拍板的决策清单
|
||||
|
||||
以下决策 PM 只给建议,**不替用户拍板**。D3-1 是头号,阻塞关键路径起点;D3-1~D3-6 建议开工前裁决。
|
||||
|
||||
| # | 决策事项 | 影响 | PM 建议(仅供参考) |
|
||||
| --- | --- | --- | --- |
|
||||
| D3-1 | **对象存储选型**(第一迭代 D4 → M2 D2-1 三度上桌,本迭代无法再拖)。候选路径:**A. 自建 MinIO**(S3 兼容,compose/Testcontainers 即起,后续平滑迁云 S3 兼容服务);**B. 云厂商对象存储**(阿里 OSS/腾讯 COS/AWS S3:免运维、自带 CDN,但引入账号/密钥/成本与 CI 外部依赖,且当前无生产部署环境承接);**C. 本地磁盘/DB 临时方案**(不建议:与 `storage_type='object'` 模型冲突、无预签名能力、迁移即返工) | 阻塞 T3-03/T3-13 全部媒体链路(关键路径起点);决定上传协议(预签名直传 vs 服务端中转)、URL 签发/公共读策略、compose 与 CI 编排、M4 AI 输出存储 | **方案 A(MinIO)起步**:S3 SDK 编码,供应商差异收敛在配置层,生产化阶段(M6)再评估迁云;上传走预签名直传(服务端不过流量);读取侧首版公共读桶 + 稳定 URL,签名读后置 |
|
||||
| D3-2 | **关注是否首版**:M3 原文含"关注",但 demo 无关注 UI、四条验收标准均不涉及关注;完整关注体系 = follow 写入 + 关注 Feed + `visibility='followers'` 三层 | 全量纳入约 +1M(后端)+1M(前端)并拖长契约面;全剪则 M3 原文范围有显式缺口 | **中间态**:T3-08/T3-18 最小版纳入(follow/unfollow 幂等 + 列表 + 按钮),**关注 Feed tab 与 followers 可见性后置**(首版 visibility 固定 public,字段保留);若周期紧张可整体后置,在收官总结记范围缺口 |
|
||||
| D3-3 | **话题是否首版**:M3 原文含"话题",demo 帖面有 tags 展示但无话题页;topics 模型已就绪 | 纳入 +1M(T3-09)+ 前端话题选择/话题页;剪出则 demo tags 位需处理 | **首版剪出**,帖子先跑通"内容+图片"主干;topics 端点 M3.5/M4 随 AI 创作分类需求一起做(ai_creation category 天然关联)。demo tags 展示位首版收起 |
|
||||
| D3-4 | **媒体形态**:图片先行、视频后置?每帖图片上限?图片是否必填? | 视频涉及转码/时长/封面帧,复杂度台阶式上升;上限影响 UI 与存储 | **图片先行**(`kind='image'`,视频 M4+ 随 AI 视频输出统一考虑);每帖上限 9 图(对齐主流社区惯例);图片**非必填**(纯文字帖合法,content 本就 NOT NULL) |
|
||||
| D3-5 | **评论层级确认**:DDL 已裁决单层平铺 + `reply_to_user_id` @ 回复(注释言明不做 parent_comment_id/递归) | 若推翻需数据模型变更提案(新列 + 树查询 + UI 缩进体系,约 +1L) | **沿用单层设计**,不做二级楼中楼;@ 回复已覆盖对话场景。若产品坚持多级,另立模型变更提案排 M3.5 |
|
||||
| D3-6 | **模块归属**:① 社区域——沿 ADR-009 先例新建 `patbond-community`(:8084)vs 并入现有模块;② **media 域归属**——独立 `patbond-media`(:8085,跨域共享:用户头像/宠物照片/帖子/M4 AI 输出都写 media.assets)vs 并入 patbond-user(平台能力先例:埋点在 user)vs 并入 community(本迭代唯一消费方) | 决定 T3-02/T3-03 骨架、compose 容器数(5 或 6)、CI 时长 | 社区**新建 `patbond-community`**(ADR-009 同理:数据所有权独立、微服务化边界清晰);media **倾向独立 `patbond-media` 小模块**(M4 起至少三个域消费,塞进任何业务模块都会造成反向依赖),但六容器对双人团队运维面偏重,若求稳可先并入 patbond-user(迁移链持有者,平台能力聚合),M4 前再拆 |
|
||||
| D3-7 | **删除/隐藏语义与权限**:作者删帖(软删)与 `hidden`(运营位)的开放范围;帖主是否可删他人评论 | 影响 T3-04/T3-07 权限矩阵与 M3 验收标准四的取证口径 | 作者可删自己帖子与评论(软删);`hidden`/`archived` 字段保留但**不开放任何端点**(无运营后台,M6+);帖主删他人评论首版不做(涉治理策略,随举报体系一起设计) |
|
||||
| D3-8 | **M2 遗留纳入范围**:① auth 契约测试(S);② 埋点队列完善(M);③ T2-12 §8 三项交互(单宠直进/归档入口/sterilizedOn,本身即待产品拍板项);④ iteration-2/09 契约-实现出入 5 项(64KB 上限、429 限流等) | 纳入挤占 M3 周期;不纳入债务滚动 | ①② 纳入(T3-19,第一波,与 M3 零耦合);③ 待产品对三项交互本身拍板后另排,不进 M3 计划;④ 其中 429 限流若不做,T3-19 退避按网络错误实现并记录依赖;跨迭代项(token 黑名单、mTLS)继续挂技术债清单不进 M3 |
|
||||
| D3-9 | **作者公开资料获取方案**:Feed/评论需他人昵称头像,现无公开资料端点。候选:**A.** community 跨 schema 只读 identity.users(破"模块只读写自己 schema"纪律,需 ADR 豁免);**B.** Feign 批量调 user 内部接口(ADR-002 静态直连先例,`/internal/**` 保护范围内);**C.** user 增开公开资料端点由前端二次请求(N+1 且泄露面大) | 决定 T3-05/T3-07 响应组装方式与性能形态;亦影响 M4/M5 同类需求的先例 | **方案 B**:user 模块增 `/internal` 批量公开资料接口(仅昵称/头像 assetId),community 侧 Feign 批量取并短 TTL 进程内缓存;跨 schema 只读若被选择须补 ADR 明确豁免边界 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 遗留项插入位置汇总
|
||||
|
||||
| 遗留项(iteration-2/29 §4 口径) | 优先级 | 插入位置 |
|
||||
| --- | --- | --- |
|
||||
| media 域(ADR-010 剪出项) | 最高(M3 天然落点) | **T3-03/T3-13 主线工单**;宠物头像/疫苗证书/事件附件的**接入**不在 M3(属 pet 域回填,media 通了之后 M3.5 顺手做,本迭代只交付能力) |
|
||||
| auth 域契约测试 | 高 | **T3-19,第一波**(随 D3-8) |
|
||||
| 埋点队列完善(30s 冲刷/退避/anonymousId) | 高 | **T3-19,第一波**(随 D3-8) |
|
||||
| T2-12 §8 三项交互 | 中(待产品拍板) | 不进 M3 计划,拍板后另排(D3-8③) |
|
||||
| 照护人邀请流程(ADR-015 后置项) | 中 | 不进 M3(社区已满负荷),M3.5+ 候选 |
|
||||
| health_record_deleted 事件 | 低 | 随 pet 域删除端点设计,不进 M3 |
|
||||
| 真机补验两项(iteration-2/30) | 挂起 | 真机到位即插入,不阻塞 M3(约 0.5 天) |
|
||||
| token 黑名单、/internal mTLS | 中(跨迭代) | 技术债清单,加固阶段处理;D3-9 若选 Feign 方案,mTLS 需求权重上升,记入债项说明 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险清单
|
||||
|
||||
| # | 风险 | 影响 | 缓解措施 |
|
||||
| --- | --- | --- | --- |
|
||||
| R1 | **媒体链路全新且横跨双端**:对象存储、上传协议、状态机、CI 容器、客户端选图压缩上传全部从零;T3-03 + T3-13 占关键路径两个 L | 估算失准直接拖垮迭代周期 | D3-1 开工前拍板;存储适配层隔离供应商差异;范围钉死"图片先行 + 预签名直传 + 公共读"最小面;MinIO Testcontainer 让 CI 无外部依赖;每波 compose 实测媒体链路 |
|
||||
| R2 | **D3-1 拍板拖延**:三度遗留的决策,再拖则关键路径起点空转 | 第一波 media 线停摆 | 决策清单置顶;未拍板期间 T3-03 先行做元数据/状态机/接口抽象(明确止损线:适配层以上不写供应商代码) |
|
||||
| R3 | **乐观更新回滚复杂度**:点赞/收藏/评论三处乐观 UI,叠加快速连点、响应乱序、跨页面状态同源、离线场景 | 前端状态 bug 密集区,返工黑洞 | T3-17 先建统一乐观更新工具再铺页面;widget 测试矩阵(失败回滚/连点合并/乱序末态)作为 DoD 硬项;服务端幂等兜底(重复请求无害) |
|
||||
| R4 | **Feed 正确性与性能**:翻页间隙增删导致丢帖/重帖;liked-by-me 逐帖查询 N+1;计数列与关系表漂移 | 直接命中 M3 验收标准二、三 | 复合游标 `(published_at, id)` 严格实现(索引已就绪);liked/bookmarked 批量 IN 查询;计数同事务更新 + 对账测试;T3-11 分页专项测试矩阵 |
|
||||
| R5 | **作者资料组装成为性能与架构双坑**(D3-9):Feed 每页 20 帖若逐个查作者即 N+1 跨服务调用 | Feed 延迟高、服务间耦合失控 | D3-9 开工前拍板;无论何种方案都要求**批量**接口 + 缓存;契约测试锁定卡片字段避免前端二次拼装 |
|
||||
| R6 | **UGC 无审核机制上线**:帖子/评论/图片全开放,无敏感词、无举报、无运营后台 | 内容风险敞口(虽 MVP 阶段用户面小) | 模型已留 `hidden` 运营位(D3-7 保留字段不开放端点);数据库侧可手工 hidden 应急;举报/审核入 backlog 并在收官总结显式声明敞口,产品知情 |
|
||||
| R7 | **契约面与冻结节奏**:media 凭据、Feed 卡片、公开资料三处形态开工时未定型,比 M2 的 TODO-FREEZE 面更宽 | 冻结延迟连锁推迟第三波 | 三处待定型点第一波即在草案中显式标注并限期收敛(第二波中期);冻结闸门纪律不放松,偏差显著上报 |
|
||||
| R8 | **CI 时长与容器数增长**:五~六应用容器 + MinIO + 测试数从 191/272 继续上量 | 门禁反馈变慢被绕过 | T3-11 记录每波 CI 时长;超阈值按模块分层执行;不降低"提交前全绿"标准 |
|
||||
| R9 | **未提交/未推送风险**(第一迭代 R3 教训惯例项) | 工作量全损 | 每波每单交付即提交即推送(ADR-011:只推 dev);PM 每波核对三仓 `git status` 与远端同步 |
|
||||
| R10 | **demo 替换的 UI 落差**:`home_page.dart`/`create_page.dart` 是 demo 中视觉最重的页面,真实数据字段与 demo 卡片(breedTag、tags、精选图)不完全对齐 | "替换后不如 demo 好看"的观感回退,或前端擅自造字段 | 第一波 UI 稿先行明确真实字段下的卡片形态(含无图帖、无头像作者的降级样式);缺失字段一律走契约提案不留本地拼凑 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 质量要求(对全部工单生效)
|
||||
|
||||
- 遵守开发计划第 10 节 DoD:不依赖 Demo 常量;权限、校验、幂等、并发已处理;文档同步更新;干净环境可复现。
|
||||
- 契约规范沿用:camelCase、UUID 字符串、ISO 8601 + timestamptz、统一信封与稳定错误码、cursor 分页(Feed 禁 OFFSET)、Idempotency-Key、version 乐观锁;**契约冻结后 api 侧字节级快照同步升版**。
|
||||
- 不提交任何密码、token、对象存储密钥(`.env`/`.sample` 模式,ADR 纪律);埋点不含帖子内容明文与敏感信息。
|
||||
- 集成测试一律 Testcontainers postgres:18(ADR-006/008),媒体测试用 MinIO 容器(随 D3-1);每单交付 `./mvnw clean test`、`flutter analyze` + `flutter test` 全绿。
|
||||
- 所有网络页面四态(loading/empty/error/retry)齐备;图片位另加占位/失败态。
|
||||
- 本迭代不实现 AI 创作、预约、通知推送的任何接口或页面;`generation_job_id`/`region_id` 仅预留;范围外需求记 backlog。
|
||||
|
||||
## 8. 工单统计
|
||||
|
||||
- 工单总数:**22**(数据与工程基础 3 + 后端社区纵切 6 + 契约与测试 2 + Flutter 7 + 遗留与收口 4),其中 **3 个条件单**(T3-08/T3-09/T3-18,随 D3-2/D3-3 拍板启停)
|
||||
- 规模分布(核心 19 单):S × 2、M × 12、L × 5;条件单另计 S × 1、M × 2
|
||||
- 关键路径:D3-1 拍板 → T3-03 → T3-04 → T3-05 → 契约冻结 → T3-12 → T3-13 → T3-15 → T3-21,L × 5 在链上,media 双端占其二
|
||||
- 待拍板决策:**9 项**(D3-1~D3-9;D3-1 头号且阻塞关键路径起点,D3-1~D3-6 建议开工前裁决)
|
||||
@@ -0,0 +1,264 @@
|
||||
# Patbond 第三迭代后端技术评估(Dev)
|
||||
|
||||
- 日期:2026-09-08
|
||||
- 评估范围:patbond-api 承接 M3「社区」的改动面、模块划分、Flyway V5+ 规划、Feed/互动机制草案、对象存储选型专题、遗留项耦合
|
||||
- 代码基线:patbond-api `dev@64c9b72`(工作区干净)
|
||||
- 结论先行:**当前基线 191 个测试全绿(1 分 05 秒)**;建议新建 `patbond-community` 模块(:8084)承载社区域、media 上传流程放 patbond-user;对象存储推荐**腾讯云 COS + S3 兼容 API + 预签名直传**(本地/测试用 MinIO 容器跑同一套代码);Flyway V5 社区基线须剪 1 条跨 schema FK(posts → creation.generation_jobs,M4 补回)并补 `pg_trgm` 扩展;共 9 项待拍板。
|
||||
|
||||
## 1. 现状盘点(实际读码结论)
|
||||
|
||||
### 1.1 模块与可复用惯例
|
||||
|
||||
Maven 四模块:`patbond-common`(错误码/响应信封/内部 DTO)、`patbond-auth`(8081,无库)、`patbond-user`(8082,**唯一 Flyway 迁移链持有者**,V1~V4)、`patbond-pet`(8083,与 user 共库,ADR-009 定型的「新模块 + 共库 + 单迁移链」形态)。M2 沉淀的设施对 M3 全部直接可套用:
|
||||
|
||||
- **鉴权**:pet 模块的 `BearerAuthFilter`/`JwtVerifier`/`RsaPublicKeyLoader`(`patbond-pet/src/main/java/com/patbond/patbond/pet/security/`)是从 user 复制的第二份,RS256 本地验签、userId 进 request attribute。community 若再复制就是第三份——见待拍板 P9。
|
||||
- **游标分页**:`CursorPage<T>`({items, nextCursor, hasMore} 信封,契约 §3.5 定为全 API 分页正典)+ `EventCursor`/`WeightCursor`(base64url("epochMicros:id") 不透明游标,keyset 谓词 `(sortKey, id) < (cursor)`,同 key 平局用 id 决胜,保证不丢不重)。社区各列表照此模式各配一个游标类型即可。
|
||||
- **幂等**:`IdempotencyKeys.deriveId()`(键派生主键 + `ON CONFLICT (id) DO NOTHING`,免键表免 TTL)。注意:这是 M2 因 V3 表内没有幂等列而设计的方案;**目标模型的 community.posts/comments 表自带 `idempotency_key + request_hash` 列与唯一约束**,两种机制取一,见 P5。
|
||||
- **乐观锁**:`version` 列 + 40902 `VERSION_CONFLICT`;`updated_at` 由 V1 的 `platform.set_updated_at()` 触发器维护,version 自增留在 repository UPDATE 语句里显式可见。
|
||||
- **防枚举 404**:不可见资源一律 404(`PET_NOT_FOUND` 先例),可见但越权 403。
|
||||
- **契约锁**:v1.2.0 冻结(18 个 path),`ContractConformanceTest` + 字节级快照 `patbond-pet/src/test/resources/contract/openapi-v1.2.0.yaml`。M3 新增 path 走 M2 验证过的「草案 → 实现回填 → 拍板冻结 v1.3.0 → 快照锁」流程。
|
||||
- **测试**:Testcontainers postgres:18;pet 模块生产 classpath 无 Flyway,**测试 classpath 挂 user 的 jar + Flyway 跑全链 V1~V4**(`patbond-pet/src/test/java/com/patbond/patbond/pet/TestcontainersConfiguration.java` 注释明确此机制)——community 模块测试照抄即可拿到 V5+。
|
||||
- **CI**:Gitea Actions 单 job `./mvnw -B clean test`,新模块进 reactor 自动纳入门禁,CI 零改动。
|
||||
|
||||
### 1.2 media 域现状
|
||||
|
||||
- **表**:`media.assets` 自 V1 就有且设计完备——`storage_type`(object/external)、`bucket + object_key`(部分唯一索引 `uq_media_object`)、`mime_type/byte_size/sha256/width_px/height_px/duration_ms`、状态机 `uploading → ready/failed/deleted`(CHECK 强制 ready 必有 `ready_at`)、`ix_media_uploading_created` 部分索引(明显是给「清理超时未完成上传」预留的)。**表结构零改动即可承载 M3 上传流程**。
|
||||
- **代码**:仍是零(无 controller/service/repository,与 iteration-2/02 §1.3 评估时一致)。
|
||||
- **消费方**:pet_health 三处可空 FK 已预留(`pets.avatar_asset_id`、`pet_vaccinations.certificate_asset_id`,均 M2 未写入);`health_event_media` 表被 V3 明确剪出(注释:纯增量表,随 media 工作以后续迁移补建);社区侧 `post_media.asset_id` 是 **NOT NULL RESTRICT**——社区图片对 media 是硬依赖,绕不过去。
|
||||
- **供应商**:未定(第一迭代 D4 遗留,ADR-010 引为剪出 M2 的理由)。这是 M3 头号拍板项,专题见 §5。
|
||||
|
||||
### 1.3 目标模型社区表通读(patbond_postgresql.sql 718~875 行)
|
||||
|
||||
8 张表 + 2 个 updated_at 触发器(posts/comments)。要点:
|
||||
|
||||
| 表 | 关键设计 | M3 承接注记 |
|
||||
| --- | --- | --- |
|
||||
| `posts` | 状态机 draft/published/hidden/archived + `deleted_at` 软删;visibility public/followers/private;冗余计数 `like_count/comment_count/bookmark_count`(CHECK ≥0);`idempotency_key + request_hash`(uq 约束按 author 域隔离);`version` 乐观锁;`published_at` CHECK 与 status 联动 | Feed 部分索引 `ix_posts_feed (published_at DESC, id DESC) WHERE status='published' AND visibility='public'` 与游标排序键严格对齐 |
|
||||
| `post_media` | PK (post_id, position),`asset_id NOT NULL → media.assets RESTRICT`,封面部分唯一索引 | 硬依赖 media 流程 |
|
||||
| `comments` | **刻意平铺一层**(`reply_to_user_id` 支持 @ 回复,无 parent_comment_id 无递归);status visible/hidden/deleted 与 `deleted_at` CHECK 联动;`client_request_id + request_hash` 幂等列 | 与 M2「结构定调照目标模型」纪律一致,不要自行加嵌套 |
|
||||
| `post_likes` / `post_bookmarks` | PK (post_id, user_id),无附加列 | 天然主键幂等,`ON CONFLICT DO NOTHING` 即可,无需 Idempotency-Key |
|
||||
| `user_follows` | PK (follower, followee) + 禁自关注 CHECK | 同上 |
|
||||
| `topics` | `name citext UNIQUE`(citext 扩展 V1 已建),status active/hidden | 话题来源见 P8 |
|
||||
| `post_topics` | 纯关联表 | — |
|
||||
|
||||
### 1.4 跨 schema FK 排查(照 M2 剪 marketplace FK 的经验逐条过)
|
||||
|
||||
| FK | 目标 schema 是否已迁移 | 处置 |
|
||||
| --- | --- | --- |
|
||||
| posts.author_user_id、comments/likes/bookmarks/follows → `identity.users` | V1 有 | 保留 |
|
||||
| posts.region_id → `platform.regions` | V1 有 | 保留 |
|
||||
| posts.pet_id → `pet_health.pets` | V3 有 | 保留 |
|
||||
| post_media.asset_id → `media.assets` | V1 有 | 保留 |
|
||||
| **posts.generation_job_id → `creation.generation_jobs`** | **creation schema 属 M4,未迁移** | **必剪**:V5 保留裸可空 uuid 列,FK 由 M4 建 creation schema 的迁移补回(与 M2 剪 4 条 marketplace FK、目标模型 1156~1166 行 M5 补回同一先例) |
|
||||
|
||||
另一个非 FK 的迁移前置:`ix_posts_content_trgm`(gin, `gin_trgm_ops`)需要 **pg_trgm 扩展,V1 只建了 pgcrypto 与 citext**——V5 需 `CREATE EXTENSION IF NOT EXISTS pg_trgm`(postgres:18 官方镜像含 contrib,Testcontainers 与 compose 均无障碍)。M3 范围没有搜索需求,该索引理论上可裁;但扩展 + 索引成本极低、剪了就偏离目标模型,建议照建(P7)。
|
||||
|
||||
## 2. 改动面评估
|
||||
|
||||
| 改动面 | 内容 | 量级 |
|
||||
| --- | --- | --- |
|
||||
| 新模块 | `patbond-community`(:8084):feed/posts/comments/likes/bookmarks/follows/topics 约 7 组资源 | 大(M3 主体) |
|
||||
| media | 上传流程(预签名签发 + complete 确认 + 清理任务),归 patbond-user(P2) | 中 |
|
||||
| Flyway | V5 社区基线(剪 1 FK + pg_trgm);V6 `health_event_media` 补建(若 P6 拍板) | 中 |
|
||||
| common | `ErrorCode` 追加约 5 值;若 P9 拍板则下沉 security/support 共享件 | 小~中 |
|
||||
| 依赖 | community 模块无新依赖;media 需引对象存储 SDK(推荐 AWS SDK v2 S3 客户端,见 §5) | 小 |
|
||||
| 契约 | v1.2.0 → v1.3.0,新增约 15 个 path(草案见 §6,本评估不动 openapi.yaml) | 中 |
|
||||
| compose | 新增 community 服务(照 pet 服务块抄);若 P3 选 MinIO 另加一个有状态服务 | 小 |
|
||||
| 既有代码 | 零改动(auth/user/pet 业务代码不动) | — |
|
||||
|
||||
## 3. 模块划分建议
|
||||
|
||||
### 3.1 社区域归属【待拍板 P1】
|
||||
|
||||
- **方案 A(推荐):新建 `patbond-community` Maven 模块(:8084)**。ADR-009 已为「按域新建模块 + 共库 + user 单迁移链」拍过板并在 M2 全程验证(pet 模块 89 个测试、compose 联调、CI 均无摩擦);社区与宠物档案是平行业务域,没有理由破坏既定形态。成本在 M2 已一次性摊销:Testcontainers 跑全链、compose 服务块、CI 自动纳入都是抄作业。
|
||||
- 方案 B:并入 patbond-pet 或 patbond-user 内包。省一个服务进程,但与 ADR-009 的裁定方向相逆,且社区是后续体量最大的域,混入他模块日后必拆。
|
||||
- 推荐 A。唯一实质增量是第 4 个 JVM 进程的内存占用,单机 compose 下可接受(各服务未设堆上限的话部署时统一加 `-Xmx` 即可,属部署细节)。
|
||||
|
||||
### 3.2 media 归属【待拍板 P2】
|
||||
|
||||
- **方案 A(推荐):上传流程放 `patbond-user`**。理由:`media.assets` 在 V1 就与 identity 同批建(owner_user_id 指向 users,天然身份域相邻);user 是迁移链持有者与基础域服务,media 是横切基础能力(社区图片、宠物头像、疫苗证书、M4 生成输入输出全要用),放任何单一业务模块都会造成反向依赖;user 已有最全的安全设施与集成测试基建。community/pet 对 `media.assets` 做只读 SQL 校验(asset 存在、owner 匹配、status='ready'),沿用「共库阶段跨 schema 只读」的既有纪律(pet 读 identity.users 先例)。
|
||||
- 方案 B:独立 `patbond-media` 模块。边界最干净,但双人团队第 5 个服务的运维/联调成本,对一个「两个接口 + 一个清理任务」的域不成比例;将来真需要(如加图片处理流水线)再从 user 拆出,代价是搬包级别。
|
||||
- 方案 C:放 community。M3 内最省事,但 M4(generation_jobs 的 input/output asset)和宠物头像会反向依赖社区模块,方向错误。
|
||||
- 推荐 A。
|
||||
|
||||
### 3.3 共享设施下沉【待拍板 P9】
|
||||
|
||||
`BearerAuthFilter`/`JwtVerifier`/`RsaPublicKeyLoader`/`UuidV7`/`CursorPage`/游标编解码在 user 和 pet 已是两份复制,community + media 落地后将是三到四份。建议 M3 第一波把这组下沉到 `patbond-common`(或 common 内独立包),community 从第一行代码就用共享件;user/pet 的存量复制件可顺带切换(纯搬移,测试全绿即证等价),也可不动留待日后。反方观点:common 目前刻意保持零 Spring Web 依赖,下沉 filter 会引入 servlet 依赖——可用「common 只收 `JwtVerifier`/`UuidV7`/游标编解码等纯 Java 件,filter 仍每模块一份薄壳」的折中。推荐折中方案。
|
||||
|
||||
## 4. Flyway V5+ 规划
|
||||
|
||||
迁移链继续由 patbond-user 持有(community 生产 classpath 无 Flyway,测试经 test classpath 复用 user 链,照 pet 先例)。
|
||||
|
||||
| 版本 | 内容 | 调整点(相对目标模型原样) |
|
||||
| --- | --- | --- |
|
||||
| **V5 社区基线** | community schema + 8 表 + 索引 + posts/comments 两个 updated_at 触发器(复用 `platform.set_updated_at()`) | ① 剪 `fk posts.generation_job_id → creation.generation_jobs`(裸可空 uuid,M4 补回,迁移文件头注释写明——照 V3 剪 marketplace FK 的文档格式);② 文件头先 `CREATE EXTENSION IF NOT EXISTS pg_trgm`;③ 主键 `DEFAULT gen_random_uuid()` 去掉,应用侧 UUIDv7(users/pets 同规) |
|
||||
| **V6 health_event_media 补建**(若 P6 拍板进) | 照目标模型 521~532 行原样建表 | 无需调整(asset_id → media.assets 已可建 FK);V3 注释承诺的「随 media 工作补建」在此兑现 |
|
||||
| V7 预留 | topics 运营种子(若 P8 拍板预置制) | 生产字典数据进正式链,不进 db/dev(V4 先例) |
|
||||
|
||||
风险面:V5 无破坏性变更(纯增量 schema),对既有 V1~V4 数据零影响;`PetHealthMigrationIntegrationTest` 模式可复制一个 CommunityMigrationIntegrationTest 验证约束与索引。
|
||||
|
||||
## 5. 对象存储选型专题【待拍板 P3/P4,M3 头号拍板项】
|
||||
|
||||
### 5.1 环境事实
|
||||
|
||||
- 部署形态:单机 docker compose,应用容器无状态、文件明确走对象存储(ADR-007 原文),敏感值环境变量注入。
|
||||
- 服务器在腾讯云(`docs/development/ci-runner-setup.md` 与 api 仓 CI 注释实证:runner 位于腾讯云、用内网镜像源)。
|
||||
- 双人团队,运维预算有限(ADR-007 立论基础)。
|
||||
- 社区场景的流量特征:图片**下行读远大于上行写**(Feed 刷图),且轻量云主机的公网出口带宽通常是个位数 Mbps——这是选型的决定性约束。
|
||||
|
||||
### 5.2 候选对比
|
||||
|
||||
| 维度 | A:自托管 MinIO(compose 内) | B:腾讯云 COS(推荐) | C:本地卷过渡 |
|
||||
| --- | --- | --- | --- |
|
||||
| 现金成本 | 0 | MVP 体量下每月几元量级(存储 + 流量按量),有免费额度 | 0 |
|
||||
| 图片下行带宽 | **全部吃服务器公网出口——Feed 刷图直接顶死个位数 Mbps,是硬伤** | 走 COS 公网/CDN,服务器带宽零占用 | 同 A,且更差(经应用容器) |
|
||||
| 运维 | 多一个有状态服务:volume、备份、版本升级都要自己做 | 零运维,备份/多副本由云侧兜底 | 违反 ADR-007「状态不落容器本地」纪律 |
|
||||
| 预签名直传 | 支持,但「直传」仍落到同一台服务器,带宽上毫无收益 | 支持,客户端直连 COS,真正卸载 | 不适用 |
|
||||
| 供应商锁定 | 无(S3 API) | **低**:COS 提供 S3 兼容端点,代码层用 S3 协议即无锁定 | 无 |
|
||||
| 与测试体系 | 与 Testcontainers 同体系 | 测试不打真云——见 5.3 的 MinIO 替身方案 | — |
|
||||
|
||||
- **推荐 B:腾讯云 COS**。决定性理由是带宽:社区 Feed 的图片读流量放在自己服务器上,MVP 刚有点用户就会先死在出口带宽而不是 CPU;COS 同厂商内网上行、公网/CDN 下行,把最贵的资源(带宽)externalize,月成本在 MVP 体量下可忽略。ADR-007「重运维在需要时外包给云托管」的成本逻辑对对象存储同样成立,且比数据库更该先外包(无状态、无迁移锁定)。
|
||||
- 方案 A 不是被否定而是被降级:**MinIO 转为本地开发与集成测试的替身**(见下),生产不跑。
|
||||
- 方案 C 违反已拍板的 ADR-007,列出仅为完整性,不推荐。
|
||||
|
||||
### 5.3 落地方式:S3 协议统一,环境三态零分叉
|
||||
|
||||
代码统一用 **AWS SDK for Java v2 的 S3 客户端**(endpoint/credentials/bucket 全部 `PATBOND_S3_*` 环境变量注入,符合既有配置纪律):
|
||||
|
||||
- 生产:指向 COS 的 S3 兼容端点;
|
||||
- 本地 compose:可选加 MinIO 服务块(profile 隔离),开发者无云账号也能全流程联调;
|
||||
- 集成测试:Testcontainers 起 MinIO 容器(与 postgres:18 同模式),上传流程测试全自动、不打真云、不进 CI 密钥。
|
||||
|
||||
如此供应商锁定压到最低:将来换任何 S3 兼容存储只改环境变量。
|
||||
|
||||
### 5.4 上传流程草案【待拍板 P4:预签名直传 vs 服务端中转】
|
||||
|
||||
**推荐预签名直传**,流程:
|
||||
|
||||
1. `POST /api/v1/media/uploads`:客户端声明 `{kind, purpose, mimeType, byteSize, sha256?}` → 服务端校验白名单(mime/大小上限)→ 写 `media.assets` 行(应用侧 UUIDv7,`status='uploading'`,`bucket + object_key` 服务端生成,key 形如 `{purpose}/{yyyy/MM}/{assetId}` 不含用户输入)→ 返回 `{assetId, uploadUrl(预签名 PUT,短 TTL 约 10 分钟), headers}`。
|
||||
2. 客户端向 `uploadUrl` 直传字节流(不经应用服务器)。
|
||||
3. `POST /api/v1/media/uploads/{assetId}/complete`:服务端对对象 HEAD 校验存在性与 byte_size(有 sha256 则一并核)→ `status='ready', ready_at=now()`。失败置 `failed`。
|
||||
4. 业务引用时机:`post_media`/头像等只允许挂 `status='ready'` 且 owner 匹配的 asset,否则 422(新错误码,见 §6)。
|
||||
5. 清理:定时任务(照 `SessionCleanupJob` 模式)用 `ix_media_uploading_created` 扫超时(如 >24h)的 uploading 行,删对象 + 行置 failed——该索引 V1 就是为此预留的,全链路闭环。
|
||||
|
||||
服务端中转(multipart 上传给应用、应用转存)唯一优势是校验在字节流上同步做,但上传流量两次过应用容器、占用连接与堆,在 COS 方案下毫无必要;即便将来切 MinIO 同机部署它也只是不更差。推荐直传。
|
||||
|
||||
### 5.5 与 M2 剪出项的衔接
|
||||
|
||||
- `health_event_media` 补建:media 流程落地后表即可建(V6,见 §4),健康事件附件接口是否随 M3 接线见 P6。
|
||||
- `pets.avatar_asset_id` / `certificate_asset_id`:列早已就位,接线只是 pet 模块 PATCH 校验 + 契约增字段,量级 S;范围见 P6。
|
||||
|
||||
## 6. API 资源设计草案(供 v1.3.0 契约草案参考,本评估不动 openapi.yaml)
|
||||
|
||||
全部挂 Bearer 鉴权;列表全部 `CursorPage` 信封。
|
||||
|
||||
| 接口 | 说明 |
|
||||
| --- | --- |
|
||||
| `POST /api/v1/media/uploads`、`POST /api/v1/media/uploads/{assetId}/complete` | §5.4 上传流程(user 模块) |
|
||||
| `GET /api/v1/feed` | 公共 Feed:`(published_at, id)` 游标,谓词与 `ix_posts_feed` 部分索引对齐 |
|
||||
| `GET /api/v1/feed?scope=following` | 关注流(若 P7 拍板进):同排序键,author 限定关注集合 |
|
||||
| `POST /api/v1/posts` | 创建(`Idempotency-Key` **必带**——开发计划 6.1 强制名单含帖子);status 可 draft 或 published |
|
||||
| `GET /api/v1/posts/{postId}` | 详情:published 对可见者开放;draft/hidden 仅作者可见,他人 404 防枚举;响应含 `likedByMe/bookmarkedByMe` |
|
||||
| `PATCH /api/v1/posts/{postId}` | 编辑/发布草稿(status 迁移)/隐藏,请求体带 `version`,冲突 40902 |
|
||||
| `DELETE /api/v1/posts/{postId}` | 软删(`deleted_at`),仅作者 |
|
||||
| `GET/POST /api/v1/posts/{postId}/comments`、`DELETE /api/v1/comments/{commentId}` | 评论平铺一层 + `replyToUserId`;POST 带 `Idempotency-Key`(落 client_request_id 列);游标 `(created_at, id)` |
|
||||
| `PUT/DELETE /api/v1/posts/{postId}/like` | 点赞/取消:天然幂等(§7.1),响应回 `{liked, likeCount}` 权威态 |
|
||||
| `PUT/DELETE /api/v1/posts/{postId}/bookmark` | 收藏/取消:同上 |
|
||||
| `GET /api/v1/me/bookmarks` | 收藏列表:游标 `(bookmarks.created_at, post_id)`,与 `ix_post_bookmarks_user_created` 对齐 |
|
||||
| `PUT/DELETE /api/v1/users/{userId}/follow`、`GET /api/v1/users/{userId}/followers|following` | 关注关系;自关注 422(库层 CHECK 兜底) |
|
||||
| `GET /api/v1/users/{userId}/posts` | 作者主页:游标 `(created_at, id)`,与 `ix_posts_author_created` 对齐;本人可带 status 过滤(含 draft) |
|
||||
| `GET /api/v1/topics`、`GET /api/v1/topics/{topicId}/posts` | 话题与话题下帖子 |
|
||||
|
||||
错误码扩展草案(延续现有分段,不重编号):
|
||||
|
||||
| code | HTTP | 语义 |
|
||||
| --- | --- | --- |
|
||||
| 40301 `POST_ACCESS_DENIED` | 403 | 帖子/评论可见但无权操作(如改他人帖) |
|
||||
| 40403 `POST_NOT_FOUND` | 404 | 帖子不存在、已删或不可见(防枚举合并) |
|
||||
| 40404 `COMMENT_NOT_FOUND` | 404 | 评论不存在或已删 |
|
||||
| 40405 `MEDIA_NOT_FOUND` | 404 | asset 不存在或非本人所有(防枚举) |
|
||||
| 40905 `IDEMPOTENCY_PAYLOAD_MISMATCH` | 409 | 同 Idempotency-Key 不同 payload(request_hash 不符) |
|
||||
| 42203 `MEDIA_NOT_READY` | 422 | 引用了非 ready 状态的 asset |
|
||||
|
||||
## 7. 互动与 Feed 机制草案
|
||||
|
||||
### 7.1 点赞/收藏幂等(验收标准「重复点赞不重复计数」的实现本体)
|
||||
|
||||
无需 Idempotency-Key——`post_likes`/`post_bookmarks` 主键 (post_id, user_id) 就是幂等键:
|
||||
|
||||
```sql
|
||||
-- 同一事务内:
|
||||
INSERT INTO community.post_likes (post_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING;
|
||||
-- 仅当上句 rowsAffected = 1 才执行:
|
||||
UPDATE community.posts SET like_count = like_count + 1 WHERE id = ?;
|
||||
```
|
||||
|
||||
取消侧对称(DELETE 影响行数为 1 才 `-1`,`ck_posts_counts` CHECK ≥0 兜底)。重复 PUT/DELETE 返回 200 同一权威态而非 409——对客户端乐观更新最友好。计数列即目标模型的冗余列,读侧零 join。
|
||||
|
||||
### 7.2 帖子/评论幂等【待拍板 P5】
|
||||
|
||||
- **方案 A(推荐):用目标模型表内幂等列**。`INSERT ... ON CONFLICT (author_user_id, idempotency_key) DO NOTHING`,冲突时按 key 读回已建资源返回;`request_hash`(请求体规范化 SHA-256)不符则 40905——比 M2 的键派生主键多一层「key 复用但 payload 变了」的误用检测。列是目标模型自带的,不用白不用。
|
||||
- 方案 B:沿用 M2 `IdempotencyKeys` 键派生主键。惯例统一,但 posts 的幂等列与唯一约束就闲置了,且丢掉 payload 校验。
|
||||
- 推荐 A;两方案客户端语义相同(重试返回同一资源 id),不影响契约。
|
||||
|
||||
### 7.3 Feed 游标分页(多排序键)
|
||||
|
||||
「多排序键」= 每个列表各有固定排序键,游标携带**本列表的排序键值 + id 决胜**,端点间互不通用(游标不透明,客户端只回传):
|
||||
|
||||
| 列表 | 排序键 | 支撑索引(目标模型已备) |
|
||||
| --- | --- | --- |
|
||||
| 公共 Feed / 关注流 | `(published_at DESC, id DESC)` | `ix_posts_feed`(部分索引,谓词同查询过滤) |
|
||||
| 作者主页 | `(created_at DESC, id DESC)` | `ix_posts_author_created` |
|
||||
| 评论 | `(created_at DESC, id DESC)` | `ix_comments_post_created` |
|
||||
| 我的收藏 | `(bookmarks.created_at DESC, post_id DESC)` | `ix_post_bookmarks_user_created` |
|
||||
| 粉丝/关注列表 | `(follows.created_at DESC, user_id)` | `ix_user_follows_followee` |
|
||||
|
||||
编码沿用 pet 惯例 `base64url("epochMicros:id")`;实现上建议把 `EventCursor` 的模式提炼成一个通用编解码件(P9 下沉候选)。禁 OFFSET 由开发计划 6.1 明文规定。
|
||||
|
||||
### 7.4 删除/隐藏内容出 Feed(验收标准「不可继续出现在公共 Feed」)
|
||||
|
||||
- **读侧过滤即机制本体**:所有公共查询恒带 `status='published' AND deleted_at IS NULL AND visibility='public'`——与 `ix_posts_feed` 部分索引谓词一致,过滤免费。删除/隐藏是行状态翻转,**无需任何 Feed 重建**(无物化 Feed,MVP 拉模型)。
|
||||
- keyset 分页天然免疫中途删除:不像 OFFSET 会页移丢行,游标翻页时被删行只是不再命中谓词,**不丢不重**(验收标准「分页不丢失、不重复」由排序键唯一性 + keyset 谓词共同保证)。
|
||||
- 已删/隐藏帖详情对非作者 404(40403,防枚举);作者访问自己的 hidden/draft 正常返回(编辑场景)。
|
||||
- 评论区随帖子状态整体不可见;单条评论删除置 status='deleted',列表过滤 `status='visible'`。
|
||||
|
||||
### 7.5 客户端乐观更新回滚需要的后端保证
|
||||
|
||||
1. **写响应携带权威终态**:like/bookmark 响应必回 `{liked, likeCount}`(收藏同构),客户端以响应对账而非自行猜测计数——回滚 = 用响应值覆盖本地乐观值。
|
||||
2. **重复请求收敛**:重复 PUT like 返回 200 同态(非 409);带同 Idempotency-Key 重发帖返回同一 post id——客户端重试永不产生第二份资源,乐观插入的临时项可按 id 对账替换。
|
||||
3. **失败语义可辨**:40403(帖子已没了→客户端剔除该卡片)、40902(版本冲突→拉最新重演)、42203(图未 ready→回滚发布态提示重传)、40905(幂等 key 误用→视为 bug 上报)各自可编程区分,`{code,message,data}` 信封已保证。
|
||||
4. **无部分成功**:计数与关系行同事务(§7.1),客户端看到的 likeCount 与 liked 永远一致,不需要处理「计了数但没点上赞」的中间态。
|
||||
|
||||
## 8. M2 遗留与 M3 的耦合评估
|
||||
|
||||
- **auth 域契约测试补齐**(M2 遗留 §4-2):与 M3 社区代码**无耦合**,但 M3 要把契约升 v1.3.0 并重打快照,正是补齐 auth path 覆盖的顺手时机(`ContractConformanceTest` 机制照搬,量级 S)。建议进 M3 第一波,不做也不阻塞任何社区工单。
|
||||
- **access token 黑名单**(承自 M1):与 M3 **弱耦合,维持不进**。社区写操作的授权是「作者本人」逐请求校验(同 pet 逐请求查 pet_owners 的结构),不依赖 token 吊销;15 分钟 TTL(ADR-003)对社区场景敏感度同样够用。M3 未引入新的触发点(封号踢出属治理域,不在 M3 范围)。结论与 iteration-2/02 §6 一致,无需翻案。
|
||||
- **/internal 改 mTLS**:M3 不新增 internal 接口(media 校验走共库只读,不走服务间调用),无耦合。
|
||||
|
||||
## 9. 构建与测试基线(2026-09-08 实测)
|
||||
|
||||
命令:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`(系统默认 JDK 不可用于构建,须显式指定,与前两轮一致)。
|
||||
|
||||
| 模块 | 测试数 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| patbond-common | 3 | 通过 |
|
||||
| patbond-user | 68 | 通过(含 Testcontainers 全链迁移测试) |
|
||||
| patbond-auth | 31 | 通过 |
|
||||
| patbond-pet | 89 | 通过(含契约一致性 11 项) |
|
||||
| **合计** | **191** | **全绿,BUILD SUCCESS,总耗时 1 分 05 秒** |
|
||||
|
||||
与 M2 收官基线(dev@64c9b72,191 测试)一致,无回归。此为 M3 开工基线:M3 结束时该命令一次通过且测试数只增不减。
|
||||
|
||||
## 10. 待拍板清单
|
||||
|
||||
| # | 事项 | 选项 | 推荐 |
|
||||
| --- | --- | --- | --- |
|
||||
| P1 | 社区域归属 | 新建 patbond-community 模块 vs 并入既有模块 | 新模块 :8084(ADR-009 形态已验证,§3.1) |
|
||||
| P2 | media 归属 | patbond-user 内 vs 独立 patbond-media vs community 内 | user 内(横切基础能力 + V1 schema 同源,§3.2) |
|
||||
| P3 | **对象存储供应商**(头号拍板,宜出 ADR) | 腾讯云 COS vs 自托管 MinIO vs 本地卷 | COS(带宽决定论,§5.2);MinIO 降级为本地/测试替身 |
|
||||
| P4 | 上传流程 | 预签名直传 vs 服务端中转 | 直传(§5.4) |
|
||||
| P5 | 帖子/评论幂等机制 | 目标模型表内幂等列 + request_hash vs M2 键派生主键 | 表内幂等列(§7.2) |
|
||||
| P6 | media 接线范围 | 仅社区图片 vs 社区 + 宠物头像 vs 全量(含证书/事件附件 + V6 建 health_event_media) | 社区图片 + 宠物头像进 M3(头像量级 S、补 M2 占位方案);证书/事件附件接口推迟,V6 表是否随建看排期余量 |
|
||||
| P7 | 关注流与 visibility | following feed + followers 可见性全做 vs M3 只做 public/private、关注关系先落库 | 关注关系 + following feed 进(M3 范围明文含关注);`visibility='followers'` 语义推迟(Feed 权限矩阵复杂度的主要来源,砍它不砍表) |
|
||||
| P8 | 话题来源 | 发帖时自动 get-or-create vs 运营预置种子(V7)+ 只读 | 自动创建(citext 唯一约束天然去重,MVP 免运营流程);status='hidden' 留给治理 |
|
||||
| P9 | 共享设施下沉 | 纯 Java 件(JwtVerifier/UuidV7/游标编解码)下沉 common vs 继续每模块复制 | 下沉纯 Java 件,filter 留薄壳(§3.3) |
|
||||
@@ -0,0 +1,178 @@
|
||||
# 03 · Flutter 前端技术评估(M3:社区)
|
||||
|
||||
> 作者:Frontend Developer
|
||||
> 日期:2026-09-08
|
||||
> 依据:开发计划 §M3、M2 收官报告(iteration-2/29)、22/23/26 号报告交接约定、15 号队列报告 §4
|
||||
> 性质:开工前评估,只读分析 + 验证性测试,未改动任何生产代码。
|
||||
|
||||
## 0. 基线验证
|
||||
|
||||
```text
|
||||
$ flutter test # patbond-flutter dev@720865b @ Flutter 3.44.6 stable
|
||||
00:27 +272: All tests passed! # 272 个测试全绿,与 M2 收官记录一致
|
||||
```
|
||||
|
||||
**M3 以 272 为基线**,收官时只增不减。
|
||||
|
||||
## 1. 社区 demo 现状盘点(实际读码结论)
|
||||
|
||||
### 1.1 替换面总览
|
||||
|
||||
社区 demo 分布在四处,总计约 1700 行,其中**数据层是 100% 替换、UI 骨架大半可保留**:
|
||||
|
||||
| 文件 | 行数 | demo 面 | 可保留骨架 |
|
||||
| --- | --- | --- | --- |
|
||||
| `lib/state/app_state.dart` | 97 | `posts` demo 列表 + shared_preferences 持久化(`_postsKey`)、`updatePost`/`publishPost` | 无——`posts` 相关全部退役;`pet`/`locationWeather` demo 归首页/创作页家具,本迭代不动 |
|
||||
| `lib/features/home/home_page.dart` | 863 | Feed segment:`_PostCard` 列表直读 `appState.posts`、客户端关键词过滤、**RefreshIndicator 是 500ms 假延时**、点赞就地翻转 demo 数据;`_StoryRow` 圈子/`_PromoCard` 促销硬编码 | `_PostCard` 版式、天气条/问候卡/搜索框/segment 结构、服务 segment(M5 范围)全保留 |
|
||||
| `lib/features/post/post_detail_page.dart` | 277 | **整页数据 demo**:`toggleLike`/收藏本地翻转、`sendComment` 本地插入(作者硬编码「萌宠新手」)、关注按钮纯 `setState` 布尔、分享是演示 SnackBar | 版式(大图头、作者卡、正文卡、评论列表、底部输入条)整体保留 |
|
||||
| `lib/features/create/create_page.dart` | 547 | `publishPost` 落 AppState;700/650/500ms 假延时是 **AI 生成模拟,属 M4 范围** | M3 只接「发布 → 社区服务」半边(草稿/发布);AI 模拟原样留给 M4 |
|
||||
|
||||
- **模型不复用**:`PostModel`/`CommentModel`(`lib/models/models.dart`)是 demo 形态——`time` 是「刚刚」类字符串、无服务端 id/authorId、无 version/游标字段。照 M2 先例新建 community 模型,demo 模型随页面替换下线。
|
||||
- **埋点基础就绪**:`main_shell_page.openPost` 已带 `RouteSettings(name: postDetail)`,页名枚举已有 `post_detail`;社区事件按 `pet_analytics.dart` 同款强类型封装新建 `community_analytics.dart`。
|
||||
- **图片入口集中**:全仓远程图统一走 `widgets/common.dart` 的 `RemoteImage`(内部 `Image.network`,仅内存缓存)——媒体缓存改造成本集中一处(§4.1),但替换波及全仓图片(含 pets 头像),需全局回归。
|
||||
|
||||
一句话:**post_detail 数据层整页重写(UI 骨架保留),home 的 Feed segment 重做数据源与分页,create 只接发布半边,`AppState.posts` 退役**。
|
||||
|
||||
### 1.2 可直接复用的 M2 资产
|
||||
|
||||
- 分层模板:`PetsController`(ChangeNotifier 四态)→ `PetsRepository`(抽象 + Api 实现)→ `ApiClient`(错误信封、401/40101 单飞刷新重放、429 类型化)。
|
||||
- `CursorPage<T>` 正典信封(`{items, nextCursor, hasMore}`)与「加载更多失败保留重试」页面交互(体重/健康事件列表已验证)。
|
||||
- 分端口直连模式:`--dart-define` 注入 base url(auth :8081 / user :8082 / pet :8083),community 服务照加 `PATBOND_COMMUNITY_API_BASE_URL`(默认 :8084,以后端为准);同一 `SessionManager`/`TokenRefresher` 共享,新建一个指向 community 端口的 `ApiClient` 实例即可,**网络层零改动**。
|
||||
- Idempotency-Key 先例:pets 域四个 POST 每次逻辑提交换新键、token 刷新重放沿用同键。
|
||||
- 测试手法:`FakeRepository` + `Completer` 控时序(`test/helpers/` 先例)、四态 widget 测试。
|
||||
|
||||
## 2. community feature 分层规划
|
||||
|
||||
### 2.1 目录与分层(照 pets 模式,一处例外)
|
||||
|
||||
```
|
||||
lib/features/community/
|
||||
community_models.dart # Post / PostComment / FeedPage 等,手写 JSON
|
||||
community_exceptions.dart # 业务码 → 类型化异常映射
|
||||
community_repository.dart # 抽象接口 + ApiCommunityRepository
|
||||
feed_controller.dart # Feed 状态机(见 §2.2),Tab 级注入
|
||||
post_detail_page.dart # 重写现 features/post/(旧目录随迁移删除)
|
||||
post_analytics / media/... # 随工单拆分
|
||||
```
|
||||
|
||||
例外在**控制器职责**:`PetsController` 的 `refresh()` 一次拉全量,而 Feed 是游标累积流、且详情页/首页共享同一份帖子内存副本(点赞状态要跨页一致),所以 `FeedController` 是 Tab 级单例(`app.dart` 装配注入主壳,同 PetsController),**不做页面级 state**。评论列表则相反——只属详情页,照 26 号报告「页面级状态按页自建」纪律放详情页 State 里,不膨胀 FeedController。
|
||||
|
||||
### 2.2 Feed 状态机(对 pets 四态的两点扩展)
|
||||
|
||||
```dart
|
||||
enum FeedPhase { initial, loading, ready, error } // 首屏四态,同 pets
|
||||
enum LoadMorePhase { idle, loading, error } // 尾部加载态,新增
|
||||
|
||||
class FeedController extends ChangeNotifier {
|
||||
List<Post> _items; // 累积列表(多页内存缓存即「多页缓存」,不落盘)
|
||||
String? _nextCursor;
|
||||
bool _hasMore;
|
||||
FeedPhase _phase;
|
||||
LoadMorePhase _loadMorePhase;
|
||||
int _generation = 0; // 刷新代次,丢弃过期响应(见下)
|
||||
}
|
||||
```
|
||||
|
||||
- **下拉刷新与游标的关系**:刷新 = 丢弃游标、从头拉第一页、**成功后整体替换**累积列表(不做增量 prepend/「有新内容」提示,M3 不引入 since 语义);**刷新失败保留旧列表** + SnackBar,不清空不闪空态。刷新使 `_generation++`,在途的旧代次加载更多响应到达时直接丢弃——这是 pets 没有的并发点,必须做,否则「刷新后旧尾页追加」会产生重复/错位。
|
||||
- **加载更多**:滚动近底触发;失败置 `LoadMorePhase.error`,尾部渲染重试条(复刻体重列表交互);`hasMore=false` 渲染到底提示。
|
||||
- **详情页同步**:详情页构造注入 `FeedController` + postId,读 controller 副本渲染;进入时 `getPost(id)` 拉详情并 `_replaceInList` 回写(照 `PetsController.getPet` 先例),点赞/收藏经 controller 统一走 §3 状态机,Feed 卡片与详情天然一致。
|
||||
- **登出 reset()**:清列表回 initial,同 pets 纪律。
|
||||
- 首页现有的客户端关键词过滤在真实分页下语义不成立(只能过滤已加载页),M3 建议搜索框对 Feed segment 降级为占位/隐藏,真搜索留给后端搜索接口(范围归 PM)。
|
||||
|
||||
## 3. 乐观更新回滚设计草案(点赞/收藏)
|
||||
|
||||
M3 前端最大新课题。核心:**乐观翻转 + 快照回滚 + 单飞合并意图 + 代次守卫**,点赞/收藏共用一套 `ToggleSync` 小状态机(字段读写与端点参数化,避免复制两份)。
|
||||
|
||||
### 3.1 状态机
|
||||
|
||||
对每个 postId 维护(Map 存于 FeedController,随 reset 清空):
|
||||
|
||||
```
|
||||
inFlight: bool # 该 post 是否有请求在途(单飞)
|
||||
pendingTarget: bool? # 在途期间用户又点出的最终意图
|
||||
snapshot: (liked, likeCount) # 本轮操作链起点快照,用于回滚
|
||||
```
|
||||
|
||||
1. **点击**:立即翻转内存副本(`hasLiked` 取反、`likeCount ±1`)并 notify——反馈是同帧的。若 `inFlight`,只记 `pendingTarget` 并返回(不发新请求)。
|
||||
2. **发请求**:非在途则记快照、置 `inFlight`,按当前目标态发送。
|
||||
3. **成功**:若 `pendingTarget` 与已确认态不一致 → 以 pendingTarget 为目标**补发一次**(连续快速点击最多两个请求,中间抖动全被合并);一致则用服务端返回的权威 `likeCount` 覆盖乐观计数(吸收他人并发点赞造成的偏差),清状态。
|
||||
4. **失败**:恢复快照并 notify,SnackBar 轻提示(「点赞失败,请重试」),**不自动重试**(用户可再点,重点一次即新一轮);清状态。
|
||||
5. **守卫**:请求携带发起时的 `_generation`,响应到达时代次不符(期间发生过刷新,列表已被服务端数据整体替换)→ 丢弃该响应、不回滚不覆盖——避免用陈旧快照污染新数据。快照恢复前同样校验该 postId 仍在列表且当前态仍是本轮乐观写入的目标态。
|
||||
|
||||
### 3.2 与后端幂等的配合
|
||||
|
||||
开发计划要求「点赞、收藏使用幂等写入」。两种契约形态对客户端的影响:
|
||||
|
||||
- **语义幂等(推荐)**:`PUT /posts/{id}/like` / `DELETE /posts/{id}/like`,重复调用收敛到同一终态、服务端返回权威 `{liked, likeCount}`。客户端**无需 Idempotency-Key**(PUT/DELETE 天然可安全重放,token 刷新后的自动重放也安全),补发/重点都不会重复计数——正是验收标准「重复点赞不重复计数」的最省事实现。
|
||||
- **POST + Idempotency-Key**:若后端坚持 `POST /likes` 形态,客户端沿用 pets 先例(每轮逻辑操作换新键、刷新重放同键)。代价:toggle 语义下「点了又取消」是两个不同逻辑操作两个键,键管理与 §3.1 的意图合并叠加后复杂度明显更高。
|
||||
|
||||
跨端待拍板(§6-A),前端强烈建议前者。评论创建则相反:非幂等 POST,照 pets 四 POST 先例带 Idempotency-Key;**评论不做乐观插入**(发送中态 + 成功后插入服务端返回实体),回滚一条已渲染的评论气泡收益低、复杂度高,M3 不做。
|
||||
|
||||
### 3.3 测试清单
|
||||
|
||||
- Controller 单测:成功覆盖计数 / 失败恢复快照 / 在途连点只发一请求且完成后补发 / 补发目标与终态一致不再发 / 刷新代次不符丢弃响应 / reset 清状态。`FakeRepository` + `Completer` 控时序。
|
||||
- Widget 测试:点击图标同帧变红计数 +1;失败回滚且 SnackBar 出现;连点若干次最终态正确。
|
||||
|
||||
## 4. 媒体客户端链路草案
|
||||
|
||||
### 4.1 依赖选型(新增依赖是 M3 最大的 pubspec 变更,逐项理由)
|
||||
|
||||
| 能力 | 推荐包 | 备选与理由 |
|
||||
| --- | --- | --- |
|
||||
| 图片选择 | `image_picker`(flutter.dev 官方维护,`pickMultiImage` 支持多选) | `wechat_assets_picker` 功能强但依赖重、维护面大;M3 用系统选择器足够 |
|
||||
| 压缩 | `flutter_image_compress`(原生编解码,快;支持质量 + 尺寸重采样 + EXIF 方向自动矫正) | 纯 Dart 的 `image` 包在中端机上压一张 12MP 图秒级卡顿,排除 |
|
||||
| 展示缓存 | `cached_network_image`(磁盘缓存) | Feed 无限流 + 反复滚动下 `Image.network` 仅内存缓存不可接受。**改造点集中在 `RemoteImage` 一处**,全仓受益,但需全局回归(pets 头像等) |
|
||||
| 大图预览 | Flutter 内置 `InteractiveViewer`(零依赖,捏合缩放/平移够用) | `photo_view` 手势更全(双击缩放曲线、画廊),体验不满意再引,待拍板 §6-E |
|
||||
|
||||
压缩策略草案:长边 ≤2048 重采样 + JPEG 质量 80(Feed 场景肉眼无损、体积约降一个量级);`flutter_image_compress` 默认不保留 EXIF——**注意不要开 `keepExif`,顺带剥离 GPS 定位隐私**;`autoCorrectionAngle` 处理方向。九宫格缩略图靠 `cached_network_image` 的 resize 或后端缩略图 URL(依赖后端媒体方案给不给多尺寸,向后端提需求)。
|
||||
|
||||
### 4.2 上传进度与失败重试 UI
|
||||
|
||||
- 进度:dio 原生 `onSendProgress`,无需新依赖。
|
||||
- 创作页九宫格每张图独立小状态机:`待传 → 压缩中 → 上传中(进度环) → 成功 / 失败(蒙层 + 点按重试)`;单图失败只重传该图。
|
||||
- 发布 gating:全部图片成功(拿到 mediaId/URL)才允许提交发布;正文先行、图片后台传的「先发后补」模式 M3 不做。
|
||||
|
||||
### 4.3 预签名直传 vs 后端中转(客户端影响面对比)
|
||||
|
||||
| 维度 | 预签名直传 | 后端中转 |
|
||||
| --- | --- | --- |
|
||||
| 请求步数 | 两步:`POST /media`(取签名 URL)→ `PUT` 对象存储(+ 可能的 confirm 回调) | 一步 multipart POST |
|
||||
| 网络层 | 需**另建一个裸 Dio**:对象存储不认 Bearer、响应不是业务信封,不能走 `ApiClient`/`AuthInterceptor` | 完全复用既有 `ApiClient`(鉴权/信封/40x 映射/刷新重放全白拿) |
|
||||
| 错误处理 | 两段异构:取签名的业务错误 + 存储 PUT 的原始 HTTP 错误(含签名过期重取) | 一段,既有类型化异常分层 |
|
||||
| 客户端成本 | 多约 1 个封装 + 裸 dio + 两段错误测试 | 最小 |
|
||||
|
||||
客户端两种都可行、成本差约一天。**解耦手段:先冻结 `MediaUploader` 抽象接口**(`Future<MediaRef> upload(XFile file, {void Function(double) onProgress})`),创作页只依赖接口,后端对象存储选型拍板后填实现——媒体不阻塞创作页开工。前端不对后端选型施加约束(§6-F)。
|
||||
|
||||
## 5. M2 遗留纳入评估
|
||||
|
||||
| 遗留项 | 内容 | 建议 |
|
||||
| --- | --- | --- |
|
||||
| T2-12 §8 三项交互 | 单宠直进/切换器、归档入口(依赖 listPets 对 archived 的过滤语义契约确认)、sterilizedOn 编辑 | **随 M3 消化**:三项都是 S 级、纯 pets 域文件,与社区工单零文件冲突,适合作为波次间隙的独立小工单;归档入口需后端先明确过滤语义 |
|
||||
| 埋点队列完善(15 号 §4) | 30s 定时冲刷、指数退避(5s ×2 上限 5min)+ 429 按 Retry-After、`anonymousId`/`lastActiveAt` 持久化 | **必须随 M3 且排第一波**:社区事件量(feed 加载/点赞/发布)远超 pets,现状「4xx 整批永久丢弃 + 无定时冲刷」在高频事件下丢数风险放大;三项均不依赖社区契约,可与契约冻结完全并行。429 的 Retry-After 语义依赖后端限流落地,可先实现通用退避、Retry-After 留接线点。30s 定时器测试用 `fakeAsync`;anonymousId 落 `pb.analytics.lastActiveAt` 同款 shared_preferences 键位 |
|
||||
|
||||
另提醒数据侧:若 M3 要开 feed 曝光类事件(`post_impression`),事件量将冲击持久化队列 500 条上限,采样策略需在字典 v3 评审时一并定(§6-H)。
|
||||
|
||||
## 6. 权衡与待拍板
|
||||
|
||||
| # | 议题 | 选项 | 推荐 |
|
||||
| --- | --- | --- | --- |
|
||||
| A | 点赞/收藏幂等形态(跨端契约) | ① `PUT/DELETE /posts/{id}/like` 语义幂等;② `POST` + Idempotency-Key | **①**。客户端免键管理、重放天然安全、服务端回权威计数即满足「重复点赞不重复计数」(§3.2) |
|
||||
| B | 并发点击策略 | ① 在途忽略点击;② 单飞 + 最终意图合并(最多补发一次);③ 300ms debounce 后发 | **②**(§3.1)。①在快速「点了又取消」时 UI 与服务端脱节;③延迟真实提交、时序更难测 |
|
||||
| C | 下拉刷新语义 | ① 从头拉第一页整体替换;② 增量 prepend + 新内容提示 | **①**。②需要 since 游标语义与去重合并,M3 收益不匹配 |
|
||||
| D | Feed 只读冷启动缓存(首页 JSON 落盘先渲染) | ① 做;② 纯在线 + 四态 | **②**。M2 pets 最终拍板即纯在线(22 号:服务端唯一事实源);M3 新面已大,缓存一致性(点赞态陈旧)另添课题,留 M4+ 评估 |
|
||||
| E | 大图预览 | ① `InteractiveViewer` 内置;② `photo_view` | **①**,体验不达再升级,少一个依赖 |
|
||||
| F | 媒体上传通道 | ① 预签名直传;② 后端中转 | 前端**跟随后端选型**,两案成本差约 1 天;`MediaUploader` 接口先冻结解耦(§4.3) |
|
||||
| G | M2 遗留纳入波次 | 见 §5 | 埋点队列第一波必做;T2-12 三项作间隙工单 |
|
||||
| H | `post_impression` 曝光事件是否 M3 开报 | 归数据侧 | 若开报须定采样,且以 §5 队列完善为前置 |
|
||||
|
||||
## 7. 风险与依赖小结
|
||||
|
||||
1. **社区契约是关键路径**:openapi 尚无任何社区路径(Feed 游标信封、点赞返回体、媒体接口、评论分页);前端第一波可并行做:埋点队列三项、`FeedController`/`ToggleSync` 状态机 + 假仓实现、`RemoteImage` 缓存化改造、创作页九宫格 UI。
|
||||
2. **点赞契约形态(§6-A)影响 §3 状态机的键管理分支**,建议契约评审最先拍这一项。
|
||||
3. **M3/M4 边界**:create_page 的 AI 生成模拟必须原样保留(属 M4),M3 只替换发布落库半边——工单里写明改动边界,避免顺手清理越界。
|
||||
4. **`RemoteImage` 缓存化波及全仓图片**,改动一处但回归面全局,建议独立小工单先行合入。
|
||||
5. 关注/话题在开发计划 M3 条目内,但现状 demo 只有详情页一个孤立关注按钮、无关注流/话题页——范围裁剪归 PM 工单拆解,本评估未按全量规划。
|
||||
6. 本评估未改任何生产代码;测试基线 272 全绿已复验。
|
||||
|
||||
---
|
||||
**Frontend Developer** · 2026-09-08 · patbond-flutter dev@720865b
|
||||
@@ -0,0 +1,140 @@
|
||||
# 04 · M3 开工前现实核查(Reality Check)
|
||||
|
||||
- 核查人:Reality Checker(TestingRealityChecker)
|
||||
- 日期:2026-09-08
|
||||
- 方法:延续 iteration-2/04 的标准——**不采信任何书面转述**。所有结论分档标注:【亲验】命令自己跑、输出自己看;【UNVERIFIED】本地无法复现、明确不采信
|
||||
- 约束遵守:只读核查 + 运行测试/构建/API 查询/E2E 脚本;零代码改动、零 commit/push、未改 mkdocs.yml;compose 用后已 down
|
||||
|
||||
---
|
||||
|
||||
## 0. 裁定(先说结论)
|
||||
|
||||
**M3 开工 readiness:CERTIFIED(无条件放行)。**
|
||||
|
||||
这是本核查人首次给出 CERTIFIED,理由是证据构成与 M2 开工时有质的不同:M2 收官声称的**每一个关键数字都由本人在 2026-09-08 当天重新实跑并逐一命中**——后端 191/191、前端 272/272 + analyze 零问题、mkdocs strict 通过、契约快照 sha256 字节级一致、三仓 HEAD CI 经 Gitea API 亲查全 success、**E2E 烟囱 11/11 本人从冷启动完整复跑一遍通过**(这同时证明 M3 开工时后端 compose 通道是活的,不是「2026-09-08 时点的历史记录」)。七项核查零实质偏差;上一轮(iteration-2/04)的 5 条放行条件全部消解。
|
||||
|
||||
M2 的已知挂起项(真机两项、auth 域契约测试缺口等)**均已在文档中诚实标注为 🟡/另立工单**,不构成对 M3(社区域)开工的阻塞,列为第 §5 节「随行观察项」而非放行条件。
|
||||
|
||||
---
|
||||
|
||||
## 1. 三仓 Git 状态与远端同步 —【亲验,全部通过】
|
||||
|
||||
`git status --short --branch` + `git fetch` + `git rev-parse HEAD origin/<branch>` 逐仓实测(2026-09-08):
|
||||
|
||||
| 仓库 | 分支 | 工作树 | 本地 HEAD | 远端 HEAD | 一致 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| patbond-api | dev | 干净 | `64c9b72` | `64c9b72` | ✓ |
|
||||
| patbond-flutter | dev | 干净 | `720865b` | `720865b` | ✓ |
|
||||
| patbond-doc | main | 干净 | `e68b655` | `e68b655` | ✓ |
|
||||
|
||||
与收官声称的 `api dev@64c9b72`、`flutter dev@720865b` 完全一致。**上一轮放行条件 1(doc 仓不干净、报告长期不 commit)已消解**:本次 doc 仓干净且与远端同步,iteration-2 全部 30 份报告 + 索引已入库。
|
||||
|
||||
环境事实:工作区存在 patbond-doc 的两个克隆(`patbond-doc` 主克隆与本核查所在的 `referral` 克隆,origin 均指向 `zhaoyuxi/patbond-doc.git`),两者均干净、HEAD 同为 `e68b655`,不构成风险,但建议后续收敛为单一工作副本以免改错目录。
|
||||
|
||||
核查结束时复查:四个工作树(含 referral)`git status --porcelain` 均为 0 处未提交——本核查自身未污染任何仓库(mvn target/、site/ 均被 gitignore 覆盖)。
|
||||
|
||||
## 2. 双端测试基线实跑 —【亲验,数字逐一命中】
|
||||
|
||||
### 2.1 后端 191/191
|
||||
|
||||
命令:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`(patbond-api,本人实跑,BUILD SUCCESS,1 分 37 秒)。
|
||||
|
||||
surefire 报告逐文件解析汇总(不抄 Maven 控制台,直接数 XML):
|
||||
|
||||
| 模块 | tests | failures | errors | skipped |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| patbond-common | 3 | 0 | 0 | 0 |
|
||||
| patbond-user | 68 | 0 | 0 | 0 |
|
||||
| patbond-auth | 31 | 0 | 0 | 0 |
|
||||
| patbond-pet | 89 | 0 | 0 | 0 |
|
||||
| **合计** | **191** | **0** | **0** | **0** |
|
||||
|
||||
与声称的 191 精确一致。Testcontainers 正常(postgres:18 容器起落、4 个 Flyway 迁移在干净实例全量执行成功)。
|
||||
|
||||
小观察(非缺陷):Flyway 提示 `PostgreSQL 18.6 is newer than this version of Flyway... latest supported is 17`——当前仅为警告且全部迁移执行成功,M3 若升级 Flyway 版本可顺手消除。
|
||||
|
||||
### 2.2 前端 272/272 + analyze 零问题
|
||||
|
||||
命令:`flutter test`(patbond-flutter,本人实跑):`00:32 +272: All tests passed!`。
|
||||
命令:`flutter analyze`:`No issues found! (ran in 1.9s)`。均与声称一致。
|
||||
|
||||
## 3. 文档门禁与契约快照 —【亲验,字节级一致】
|
||||
|
||||
- `mkdocs build --strict`:通过(3.51s,EXIT=0)。
|
||||
- 契约快照 sha256 比对:
|
||||
|
||||
```
|
||||
243fe6487bfa19018bddbfdb2cece16d9f81bc9718d3404501574677a4cd689d patbond-api/patbond-pet/src/test/resources/contract/openapi-v1.2.0.yaml
|
||||
243fe6487bfa19018bddbfdb2cece16d9f81bc9718d3404501574677a4cd689d patbond-doc/docs/api/openapi.yaml
|
||||
```
|
||||
|
||||
字节级一致属实。正典 `info.version: 1.2.0`、路径数 grep 实数 **18**,与声称一致。上一轮的 D-1 缺口(events 端点游离于契约外)已不复存在——v1.2.0 含 `/api/v1/events`。
|
||||
|
||||
## 4. 三仓 HEAD 的 CI 状态 —【亲验,Gitea API 亲查】
|
||||
|
||||
`curl https://git.patbond.cn/api/v1/repos/zhaoyuxi/<repo>/commits/<sha>/status`(2026-09-08):
|
||||
|
||||
| 仓库 | commit | state | 检查项 |
|
||||
| --- | --- | --- | --- |
|
||||
| patbond-api | `64c9b72f…` | **success** | CI / backend-test |
|
||||
| patbond-flutter | `720865bc…` | **success** | CI / flutter-gates |
|
||||
| patbond-doc | `e68b6553…` | **success** | CI / docs-build |
|
||||
|
||||
29 号报告写 flutter 侧「待本提交 CI」——该悬置项现已落定为 success。
|
||||
|
||||
## 5. E2E 烟囱复跑 —【亲验,11/11 全过,通道确认存活】
|
||||
|
||||
完整冷启动复跑(非采信 2026-09-08 收官记录):
|
||||
|
||||
1. `./mvnw -DskipTests package`(EXIT=0)→ `docker compose up -d --build` → 四容器 Up、postgres healthy;
|
||||
2. `dart run test_e2e_m2_manual.dart`(patbond-flutter 仓根):**`=== M2 E2E 烟囱测试全部通过 ✓(11/11 场景)===`**,EXIT=0;
|
||||
3. `docker compose down` 已执行,栈已清理。
|
||||
|
||||
11 场景全部真实走通,抽样摘录(本人输出):场景 8 防枚举四路响应体完全一致(40401);场景 9 第二设备新会话五类数据全量读回;场景 10 v2 事件 4/4 accepted(202,含 platform=android);场景 11 乐观锁 409/40902 且先写者数据保留。
|
||||
|
||||
**这条同时回答了 M3 开工的关键问题:后端 compose 通道今天是活的。** 上一轮放行条件 3(E2E 通道 UNVERIFIED)消解。
|
||||
|
||||
## 6. 收官声称抽查(3+ 条高影响项)
|
||||
|
||||
### 6.1 契约测试确实会抓漂移 —【亲验(结构审读 + 实跑)】
|
||||
|
||||
审读 `patbond-pet/src/test/java/.../contract/ContractConformanceTest.java`(735 行),结构真实严格,不是摆设:
|
||||
|
||||
- 对 pets 域 18 操作**真实起服务发请求**(MockMvc + Testcontainers),响应体经 ContractValidator 对冻结快照严格校验(字段名/类型/必填/nullable/枚举/信封/错误码值);
|
||||
- Order(98) 快照守卫:断言版本=1.2.0、18 路径、24 操作、45 schema——doc 仓升版而忘同步快照会立即变红;
|
||||
- Order(99) 全响应矩阵门禁:契约声明的每个 (操作, 状态码) 单元格都必须被真实响应覆盖,唯一豁免 care-reminders PATCH 409(并发守卫,单线程无法确定性触发,已注释说明);
|
||||
- 本次实跑中该测试类 11/11 通过(含在 191 内)。
|
||||
|
||||
诚实标注的已知边界:auth 域 6 个 M1 操作无契约测试(注释明言「另立工单」),见 §7 观察项。
|
||||
|
||||
### 6.2 pet_health schema 表数 —【亲验,8 表属实】
|
||||
|
||||
`V3__pet_health_baseline.sql` grep 实数 8 个 CREATE TABLE:breeds、pets、pet_owners、pet_weight_records、vaccine_catalog、pet_vaccinations、health_events、care_reminders——与 29 号报告「pet_health 8 表」一致(其「六表 psql 证据」指业务数据六表,不含 breeds/vaccine_catalog 字典表,无矛盾)。
|
||||
|
||||
### 6.3 feature-checklist 与实际相符 —【亲验】
|
||||
|
||||
`docs/development/feature-checklist.md` 实有 M2 三节(§7 宠物域后端 / §8 宠物域客户端 / §9 埋点体系)。关键的是**它没有虚报**:真机落库验证 + SessionTracker 手测标 🟡 挂起、integration_test 自动化标 🟡 留第四波、三项交互细节标 🟡 待拍板——与 30 号真机补验清单相互印证,状态标注诚实。
|
||||
|
||||
### 6.4 报告与 ADR 入档 —【亲验】
|
||||
|
||||
`iteration-2/` 实有 01~30 共 30 份编号报告 + index.md + openapi-pets-draft.yaml,mkdocs strict 通过即导航无死链;`docs/architecture/decisions.md` 实有 ADR-001 至 ADR-015。与声称一致。
|
||||
|
||||
## 7. 随行观察项(非放行条件,不阻塞 M3 开工)
|
||||
|
||||
1. **真机两项挂起**(Android 真机落库验证 + SessionTracker 30min 手测,30 号清单)——按方案 A 挂起属既定决策,设备到位后 0.5 天补验;M3 若涉及移动端埋点新事件,建议合并补验。
|
||||
2. **auth 域 6 操作无契约测试**——M1 遗留、已声明另立工单;M3 新增社区域端点时应从第一天就纳入契约测试矩阵,勿再累积。
|
||||
3. **Flyway 对 PostgreSQL 18.6 的版本警告**(§2.1)——顺手升级可消除。
|
||||
4. **doc 仓双克隆**(§1)——建议收敛为单一工作副本。
|
||||
|
||||
## 8. 与上一轮(iteration-2/04)放行条件的对账
|
||||
|
||||
| 上轮放行条件 | 本次状态 |
|
||||
| --- | --- |
|
||||
| 1. doc 仓报告未提交/工作树不干净 | ✓ 消解:30 份报告入库,三仓干净同步 |
|
||||
| 2. D-1 契约缺口(events 游离) | ✓ 消解:v1.2.0 含 events,18 路径,字节级快照锁 CI |
|
||||
| 3. E2E 通道 UNVERIFIED | ✓ 消解:本人冷启动复跑 11/11 |
|
||||
| 4/5.(埋点空转与相关接线) | ✓ 消解:E2E 场景 10 实证 4/4 accepted;v2 白名单已入 191 测试基线 |
|
||||
|
||||
---
|
||||
|
||||
**结论:M2 收官声称经全量独立复验零实质偏差,M3(社区域)可以开工。** 本报告全部数字均为核查人 2026-09-08 亲跑所得。
|
||||
@@ -0,0 +1,271 @@
|
||||
# 05 · 第三迭代 社区 UI 设计规范
|
||||
|
||||
> 作者:UI Designer
|
||||
> 日期:2026-09-08
|
||||
> 迭代:Iteration 3「M3 社区」
|
||||
> 素材来源:`AI宠物_iOS_UI设计稿.html`(品牌正典,ADR-005)、`patbond-flutter/lib/core/theme/app_theme.dart`(已落地 token)、`lib/widgets/common.dart`(RemoteImage / SectionCard / TagPill DEBT-1 修复版 / EmptyState)、`lib/core/widgets/`(M2 落位的 PetAvatar / RecordTypeDot / EmptyStateIllustration)、社区 demo 现状(`lib/features/home/home_page.dart`、`lib/features/post/post_detail_page.dart`、`lib/features/create/create_page.dart`)、一迭代 04/12 号与二迭代 05 号 UI 报告(规范基线)
|
||||
> 性质:开工前设计规范;只定规格,不改代码
|
||||
|
||||
---
|
||||
|
||||
## 0. 正典设计语言提炼(社区相关)
|
||||
|
||||
### 0.1 正典「首页 · Feed」画框已给出的语言(本规范全部延续)
|
||||
|
||||
| 正典元素 | 描述 | 对应 Flutter 现状 |
|
||||
| --- | --- | --- |
|
||||
| `feed-card` | 白底卡、`border` 1px、圆角 18、图片通栏出血(卡内零 padding 贴边) | `_PostCard` 已按此实现(圆角随 Card 主题为 24) |
|
||||
| `feed-user` | 头像 28 + 名字(12/w600 ink)+ 元信息「2 小时前 · 柴犬」(10 muted) | 已实现(头像 38,元信息 bodySmall) |
|
||||
| `feed-img` | 单图通栏,`peach` 占位底 | `RemoteImage`(loading 即 surfaceTint 块)已一致 |
|
||||
| `feed-caption` | 正文 11、色 `#6B5A4A`(即 `inkSoft` 的正典出处)、行高 1.5 | 已实现(bodyMedium ink;正文色比正典更深,可接受) |
|
||||
| `feed-actions` | ❤ 数字(coral)+ 💬 数字 / 分享(muted) | ActionChip/Chip 实现,点赞红用了 `Colors.red`(脱离色板,§5.2 修订) |
|
||||
| `stories` | brandGradient 2px 渐变环头像 + 「发布」首位入口 | `_StoryRow` 已实现 |
|
||||
| `search-bar` | 白底 border 描边圆角 14 搜索条 | TextField 主题已覆盖 |
|
||||
| `chip` / `chip.active` | 胶囊筛选;选中态 coral 实底白字(2.75:1 不达 AA,二迭代 D7 已裁决弃用,改 surfaceTint + primaryDark) | ChoiceChip 主题派生 |
|
||||
| 求助帖形态 | 正典第二张 feed 卡有图无操作行,元信息带「求助专区」分区标记 | 未实现分区标记 |
|
||||
|
||||
### 0.2 社区 demo 现状评估(哪些视觉可保留)
|
||||
|
||||
| Demo 现状 | 判定 |
|
||||
| --- | --- |
|
||||
| `_PostCard` 骨架(头部行 → 图 → 正文 2 行截断 → 操作行) | **保留**,升级为共享 `PostCard` 三形态(§3.1);修订点:点赞 `Colors.red` → `error`(§5.2)、操作行触控补足 44、元信息 muted → `inkSoft`(DEBT-2) |
|
||||
| `post_detail_page` 作者卡 + `FilledButton.tonal` 关注钮、TagPill 话题、评论气泡(36 头像 + SectionCard 14)、底部固定输入条 | **保留**,评论气泡升共享 `CommentTile`(§3.4);头图 1:1 单图改为多图适配(§2.2) |
|
||||
| `create_page` 上传卡(空/上传中/已选三态)、话题 InputChip、生成完成后的发布表单(标题/正文/话题/位置/发布钮) | **表单与话题交互保留**并迁移为社区发布页骨架;AI 生成流程(风格选择、`_GenerationProgress`)属 AI 创作域,不进 M3 发布页 |
|
||||
| `home_page` Feed/服务 SegmentedButton 分段、`_StoryRow`、搜索过滤 | **保留**;M3 Feed 只在 Feed 段内扩展 |
|
||||
|
||||
### 0.3 正典未覆盖(详见 §6 待拍板清单)
|
||||
|
||||
图片九宫格(正典 feed 卡仅单图)、纯文字帖形态、帖子详情页与评论区、社区发布页(正典「AI 创作」是生成器不是发帖器)、上传进度、草稿、话题聚合页、个人主页/关注关系、骨架屏。以上均为本规范新增提案。
|
||||
|
||||
---
|
||||
|
||||
## 1. 页面族总览
|
||||
|
||||
```text
|
||||
首页 Tab(Feed 段)
|
||||
└─ P1 Feed 流(story 环 + 帖子卡列表 + 骨架屏/空态)
|
||||
├─ P2 帖子详情(媒体区 + 作者卡 + 正文 + 话题 + 操作行 + 评论区 + 底部输入条)
|
||||
├─ P3 发布页(push 全屏;媒体选择九宫格 + 正文 + 话题 + 上传进度 + 草稿)
|
||||
├─ P4 话题页(话题头 + 该话题 Feed 复用 P1 卡)
|
||||
└─ P5 个人主页(用户头 + 关注/粉丝 + TA 的帖子;PM 若裁剪关注域则按 §6 D5 降级)
|
||||
```
|
||||
|
||||
通用排版 token(延续一、二迭代规范,不新造):
|
||||
|
||||
- 页面内边距 `EdgeInsets.fromLTRB(16, 12, 16, 28)`(与现有 Tab 页一致);间距刻度 4 / 8 / 12 / 16 / 24 / 32
|
||||
- 圆角:卡片 `AppRadius.xl`(24,Card 主题默认)、输入框 `lg`(18)、九宫格单格 `sm`(12)、chip `pill`
|
||||
- 字级:分区标题 `titleLarge` 18/w800;卡内标题 `titleMedium` 15/w700;正文 `bodyMedium` 14/1.5;次级 12 **`inkSoft`**(不用 `muted`,§5.3 DEBT-2)
|
||||
- Bottom sheet 沿用既有骨架(handle + 标题行 + 内容 + 全宽提交,`viewInsets.bottom` 适配)
|
||||
- 错误三层模型沿用:字段 `errorText` / 区块 `InlineErrorBanner` + 重试 / 瞬态 SnackBar
|
||||
|
||||
---
|
||||
|
||||
## 2. 页面规范
|
||||
|
||||
### 2.1 P1 Feed 流
|
||||
|
||||
结构自上而下:天气条 + 问候卡 + 搜索条 + 分段钮(既有,不动)→ `_StoryRow`(既有)→ 帖子卡列表(`PostCard`,卡间距 16)。下拉刷新既有 `RefreshIndicator`;触底加载更多:列表尾 24 高居中 `CircularProgressIndicator`(`primary`),到底后显示「没有更多了」12 `inkSoft` 居中,上下留白 16。
|
||||
|
||||
**帖子卡三形态**(同一 `PostCard` 组件,按媒体数分支):
|
||||
|
||||
| 形态 | 媒体区 | 其余结构 |
|
||||
| --- | --- | --- |
|
||||
| 单图 | 通栏出血 `AspectRatio 4:3`(既有),竖长图裁切 `BoxFit.cover` | 头部行(PetAvatar sm32 + 名字 14/w700 + 元信息 12 inkSoft「2 小时前 · #柴犬圈」)→ 媒体 → 正文 bodyMedium ≤2 行截断 → 操作行 |
|
||||
| 多图 | `PostMediaGrid`(§3.2)嵌在水平 padding 14 内(不出血,与九宫格圆角配合) | 同上 |
|
||||
| 纯文字 | 无媒体区;正文放宽至 ≤6 行截断,字号升 15/1.6(补偿视觉重量);超行尾随「全文」`primaryStrong` 14/w600 | 同上 |
|
||||
|
||||
**操作行**(替换 demo 的 ActionChip/Chip 混排):`LikeButton`(§3.5)+ 评论钮(`chat_bubble_outline` 20 `inkSoft` + 计数 13/w600 `inkSoft`)+ 收藏钮(同规格,bookmark 图标)+ 右端分享 `ios_share_outlined` 20 `inkSoft`。每钮触控 44×44(图标 20 + padding 撑足),间距 4,行高 48,水平 padding 14。
|
||||
|
||||
**状态**:首载 = `FeedSkeleton`(§3.7)3 张;空态 = `EmptyStateIllustration`(`forum_outlined`、「还没有动态」、说明「关注的毛孩子们还没发帖,去逛逛话题吧」、CTA「发布第一条」→ P3);搜索空态沿用既有 `EmptyState`;加载失败 = `InlineErrorBanner` + 重试。
|
||||
|
||||
### 2.2 P2 帖子详情 【评论区为新增提案,待拍板】
|
||||
|
||||
demo 骨架保留,修订媒体区与评论区:
|
||||
|
||||
| 区块 | 规格 |
|
||||
| --- | --- |
|
||||
| AppBar | 既有(标题「社区动态」16/w800 + 分享 action) |
|
||||
| 媒体区 | 单图:原比例展示,高度钳制 [宽×0.75, 宽×1.33];多图:`PageView` 横滑轮播 1:1 + 底部中央页码指示(当前点 `primaryStrong` 6×6、其余 `border` 5×5,间距 6;同时右上角「2/9」角标:`ink` 实底胶囊 + 白字 12/w700,13.50:1);点击全屏大图浏览(黑底、双击缩放、下滑关闭) |
|
||||
| 作者卡 | 既有 SectionCard + `FilledButton.tonal` 关注钮保留;关注双态:未关注 = tonal(surfaceTint 底 + primaryDark 字 7.98:1)文案「+ 关注」;已关注 = `OutlinedButton`(border 描边 + `inkSoft` 字 6.59:1)文案「已关注」,点按弹确认「不再关注 TA?」 |
|
||||
| 正文 | bodyMedium 14/1.6 全文;话题 `TopicChip`(§3.6)Wrap 8/8;「发布于 …」12 `inkSoft` |
|
||||
| 操作行 | 与 P1 同一套组件(LikeButton + 评论锚点钮 + 收藏),demo 的 FilledButton.tonalIcon 形态弃用,统一卡片外裸排 |
|
||||
| 评论区 | 「评论 (N)」`titleLarge` → `CommentTile` 列表(§3.4,间距 12);空态:居中 `chat_bubble_outline` 36 `muted`(纯装饰,muted 合法)+「还没有评论,来抢沙发」12 `inkSoft`,上下留白 32;分页触底加载同 P1 |
|
||||
| 底部输入条 | demo 形态保留:`surface` 底 + 顶部 `border` 1px 分隔线(demo 缺分隔线,补上)+ TextField(isDense)+ `IconButton.filled` 发送(`primaryStrong` 底白图标 4.49:1;空文本时禁用态:`ink` 12% 底 + 38% 图标,主题既定禁用惯例);发送中按钮内 18 转圈锁尺寸 |
|
||||
|
||||
### 2.3 P3 发布页 【设计稿未覆盖,本规范为新增提案,待拍板】
|
||||
|
||||
push 全屏页(媒体多、需防误触丢稿,不用 sheet)。AppBar:左「取消」TextButton(`ink`)、标题「发布动态」、右「发布」`FilledButton`(高 40,水平 padding 20;不可发布时禁用态)。
|
||||
|
||||
```text
|
||||
┌ 媒体选择区 ──────────────────────┐
|
||||
│ [图1][图2][+] │ PostMediaGrid 编辑态(§3.2):已选图 1:1
|
||||
│ │ 预览 + 右上删除角标;「+」虚线格;最多 9 张
|
||||
└──────────────────────────────────┘
|
||||
↓ 16
|
||||
正文 TextField:multiline 6 行高起步自增,maxLength 1000,
|
||||
计数器「128/1000」12 inkSoft 右下(超限 error)
|
||||
↓ 12
|
||||
话题行:已选 TopicChip(带删除角标)+ 「+ 话题」ActionChip → 话题选择 sheet
|
||||
(搜索 + 热门话题列表;demo 的 AlertDialog 输入弃用)
|
||||
↓ 12
|
||||
位置 ListTile(demo 保留,选填)
|
||||
↓ 底部安全区上方
|
||||
「已自动保存草稿 ✓」12 inkSoft(保存动作后淡入,3s 淡出)
|
||||
```
|
||||
|
||||
- **可发布条件**:正文非空或媒体 ≥1。
|
||||
- **上传进度态**:点「发布」后媒体逐张上传,每格叠加进度覆盖层(§3.3);全部完成前「发布」钮转圈锁定;单张失败 → 该格 error 角标 + 整页顶部 `InlineErrorBanner`「第 3 张图片上传失败」+ 格内点按重试;全败/接口失败不清空内容。
|
||||
- **草稿**:内容变更后静默自动保存(防抖 2s);点「取消」且有内容 → `AlertDialog`「保留草稿?」【保留 / 不保留 / 继续编辑】;再次进入发布页时若有草稿则恢复并显示顶部提示条(surfaceTint 底圆角 sm12:「已恢复上次草稿」12 `primaryDark` + 右侧「清空」TextButton)。草稿仅本机单份,覆盖式保存。
|
||||
|
||||
### 2.4 P4 话题页 【设计稿未覆盖,本规范为新增提案,待拍板】
|
||||
|
||||
push 页。话题头(canvas 底直排,非卡片):`#柴犬圈` `headlineSmall 22 ink` + 「1234 条动态 · 56 人参与」12 `inkSoft` + 右侧「关注话题」钮(与 P2 关注双态同规格)→ 24 → 该话题 Feed(P1 的 `PostCard` 列表原样复用,含骨架/空态/加载更多)。空态文案「这个话题还没有动态,来发第一条」+ CTA → P3(预填该话题)。
|
||||
|
||||
### 2.5 P5 个人主页 / 关注关系 【设计稿未覆盖,新增提案;PM 若裁剪关注域,本页降级见 §6 D5】
|
||||
|
||||
push 页。用户头部(正典「我的」`profile-head` 语言横排):头像 64(`PetAvatar` 组件复用,无徽标)+ 昵称 `titleLarge` + ID/加入天数 12 `inkSoft`;其下统计行三等分(正典 `stat-row` 语言):动态数 / 关注数 / 粉丝数——数值 15/w800 `ink`、标签 12 `inkSoft`,关注/粉丝可点进列表页;右侧或其下「关注」钮(P2 同款双态)。之后「TA 的动态」`titleLarge` + `PostCard` 列表。
|
||||
|
||||
关注/粉丝列表页:`ListTile` 式行(头像 44 + 昵称 14/w700 + 简介 12 `inkSoft` + 尾部关注双态小钮高 36),行高 64,触控达标。
|
||||
|
||||
**若 PM 裁剪关注关系**:P5 保留头部(无关注钮、统计行只留「动态数」)+ 帖子列表;P2 作者卡关注钮整个不渲染(不留占位)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 新组件规格(7 个)
|
||||
|
||||
### 3.1 `PostCard`(`lib/core/widgets/post_card.dart`)
|
||||
|
||||
§2.1 三形态的唯一出口,`home_page` 私有 `_PostCard` 升级迁移。构成:Card 主题默认 + `InkWell` 整卡进 P2;头部行 padding 14;操作行组件化(LikeButton / 计数钮)。求助/分区帖在元信息尾追加 `TagPill(accent)` 小标(正典「求助专区」语义,7.07:1)。
|
||||
|
||||
### 3.2 `PostMediaGrid` 图片九宫格(`lib/core/widgets/post_media_grid.dart`)
|
||||
|
||||
展示态 + 编辑态一个组件(编辑态多「+」格与删除角标)。
|
||||
|
||||
- **列数规则**:1 图不走网格(由 PostCard/详情页按 §2 单图规格处理);2、4 图 → 2 列;3、5–9 图 → 3 列。全部 1:1 `BoxFit.cover`,格间距 4,单格圆角 `sm`(12),`RemoteImage` 复用(loading surfaceTint 块 / 失败 pets 图标兜底)。
|
||||
- **"+N" 折叠角标**(Feed 卡超 9 图理论不出现,接口若返回超 9 张:第 9 格叠 `ink.withAlpha(204)`(80%) scrim + 白字「+3」20/w800 居中(合成最亮白图仍 7.10:1;60% scrim 仅 3.88:1 不达标,弃用))。
|
||||
- **编辑态**:末尾「+」格——`border` 1.5px 虚线、圆角 12、居中 `add_photo_alternate_outlined` 24 `inkSoft`;满 9 张隐藏。删除角标:格右上角 22 圆、`ink` 80% 实底 + 白 close 图标 14(非文字 7.10:1),触控热区扩至 32。长按拖拽排序(可选实现,见 §6 D9)。
|
||||
- **状态**:展示格点按 → 全屏浏览(初始页为所点格);编辑格点按 → 预览/替换菜单。
|
||||
|
||||
### 3.3 `UploadProgressOverlay` 上传进度指示(同文件或 `upload_progress_overlay.dart`)
|
||||
|
||||
叠加在编辑态九宫格单格上的进度层,四态:
|
||||
|
||||
| 态 | 视觉 |
|
||||
| --- | --- |
|
||||
| 排队 | scrim `ink` 40% + 白字「等待中」12/w600(合成后底 ≈#8B7F79 亮于 40% 实际值;按 80% 局部字条处理:文字衬 `ink` 80% 胶囊底,7.10:1) |
|
||||
| 上传中 | scrim `ink` 40% + 居中白色环形进度 36(`CircularProgressIndicator` value 态,白轨 24% + 白值条;非文字对白图标准由 scrim 保底)+ 下方百分比白字 11/w700 衬 `ink` 80% 胶囊 |
|
||||
| 成功 | scrim 淡出 150ms,无残留角标 |
|
||||
| 失败 | scrim `error` 12% + 中央 `error_outline` 24 `errorDark`(6.50:1 于白底)+ 底部通栏字条 `errorDark` 实底 + 白字「重试」11/w700;整格点按重试 |
|
||||
|
||||
页级汇总:发布钮上方细线性进度 `LinearProgressIndicator`——值条 `primaryStrong`、轨道 `surfaceTint`(3.80:1 ≥ 非文字 3:1)+ 左侧「正在上传 2/5」12 `inkSoft`。
|
||||
|
||||
### 3.4 `CommentTile` 评论条目(`lib/core/widgets/comment_tile.dart`)
|
||||
|
||||
demo 气泡形态升共享:`PetAvatar sm32`(demo 36 收敛到组件尺寸档)+ 10 + 气泡(`surface` 底、`border` 1px、圆角 16、padding 12):作者名 13/w700 `ink` → 4 → 内容 bodyMedium 14/1.5 → 6 → 底行(时间 11 `inkSoft` + 右端点赞:heart 16 + 计数 11,未赞 `inkSoft`/已赞 `error`,触控 44 靠 padding 撑足)。楼中楼回复(若 PM 纳入范围):气泡内下方缩进块 `canvas` 底圆角 12 padding 10,「@昵称:内容」13,最多显 2 条 + 「查看全部 N 条回复」12 `primaryStrong`(白卡内 4.49:1)。长按气泡 → 操作 sheet(回复/复制/举报,举报为社区合规必备项)。
|
||||
|
||||
### 3.5 `LikeButton` 点赞/收藏交互钮(`lib/core/widgets/like_button.dart`)
|
||||
|
||||
点赞与收藏同一组件(图标与语义色参数化)。
|
||||
|
||||
- **静态规格**:图标 20 + 计数 13/w600,间距 4;未激活:`favorite_border` / `bookmark_border` + 计数均 `inkSoft`(6.59:1);激活:`favorite` 实心 `error`(图标非文字 4.99:1)+ 计数 `errorDark`(6.50:1);收藏激活用 `accentDark`(7.40:1,图标与字同色)。**修订**:demo 的 `Colors.red`(`#F44336`,白底 3.13:1 且脱离色板)弃用。
|
||||
- **乐观更新视觉**(配合 §4 策略):点按即刻翻转状态 + 计数 ±1;激活动画 = 图标 scale 1 → 1.25 → 1 弹性 240ms + 实心色淡入;取消动画 = 仅 120ms 颜色渐出,无缩放(降低视觉噪音)。计数变化不做滚动动画(数字直接替换,避免回滚时二次滚动)。
|
||||
- **回滚态**:失败回滚时**禁用过渡动画**,状态直接跳回 + SnackBar「操作失败,请重试」;详见 §4。
|
||||
|
||||
### 3.6 `TopicChip` 话题 chip(`lib/core/widgets/topic_chip.dart`)
|
||||
|
||||
**与 TagPill 的关系**:TagPill(DEBT-1 修复版)是静态语义标签,无点击态、字 11、padding 10/6;话题需要可点击、可删除(发布页)、更大触控,故独立组件、**视觉同族**——底色同款 8% 淡染 + 深变体字,直接复用 `TagPill._defaultInkFor` 同一映射(映射常量建议随本工单从 TagPill 提为共享导出,两组件一处取色)。
|
||||
|
||||
- **规格**:高 32(垂直方向由父容器留 6 补至 44 触控带),padding 12/0,圆角 `pill`,「#话题名」13/w600;默认色族 `primary`(8% 底 + `primaryDark` 字 8.74:1)。点按 → P4 话题页,`InkWell` pill ripple。
|
||||
- **编辑态**(发布页):尾部 16 close 图标(`primaryDark`),点删除;被预填(从话题页进入)时不可删除、色族转 `accent`。
|
||||
- demo 中 `InputChip`/`ActionChip` 话题混用形态弃用,统一本组件;「+ 话题」添加钮保留 ActionChip 形态。
|
||||
|
||||
### 3.7 `FeedSkeleton` 骨架屏(`lib/core/widgets/feed_skeleton.dart`)
|
||||
|
||||
- **单元结构**(模拟单图卡):Card 默认底内——头部行(32 圆 + 两条圆角横条 12/8 高、宽 40%/24%)→ 4:3 通栏块 → 两条正文横条(宽 90%/60%)。块色 `surfaceTint`,底为白卡(1.18:1,装饰性占位不受对比度约束)。
|
||||
- **动效**:整体不透明度 0.6 ↔ 1.0 呼吸循环 1200ms(不做横扫高光,实现轻);尊重系统「减弱动态效果」设置时静止在 1.0。
|
||||
- **用途**:P1/P4 首载 3 张;P2 评论区首载 2 个(气泡形骨架:32 圆 + 圆角 16 矩形块高 72)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 乐观更新视觉反馈与回滚闪烁抑制(点赞/收藏/关注通用)
|
||||
|
||||
1. **即时反馈**:点按瞬间本地翻转状态并播放激活/取消动画(§3.5),不等接口。
|
||||
2. **连点合并(闪烁抑制第一层)**:交互层防抖 600ms——连续点按只做本地翻转动画,仅将「最终状态」发给接口;in-flight 期间再次点按不发新请求,记录期望终态,返回后对账。
|
||||
3. **回滚静默化(第二层)**:接口失败回滚时,a) 若激活动画未播完,等播完再回滚(避免动画中途反转的抖动);b) 回滚本身零动画、直接跳变;c) 计数与状态一次性成对恢复,不出现「心已灭计数未减」的中间帧;d) 同帧只弹一条 SnackBar(多目标失败合并文案)。
|
||||
4. **对账不打扰(第三层)**:接口成功返回的权威计数若与本地乐观值不同(他人同时点赞),静默替换数字,不播任何动画。
|
||||
5. 关注钮乐观更新同策略;回滚时按钮从「已关注」直接跳回「+ 关注」+ SnackBar。
|
||||
|
||||
---
|
||||
|
||||
## 5. 色彩无障碍自查(WCAG AA,程序精算)
|
||||
|
||||
计算方法:WCAG 2.x 相对亮度公式;8% 淡底按 `withAlpha(20)`(7.84%)与白底合成;scrim 合成按最不利底(纯白图)计算。正文阈值 4.5:1,大字 3:1,非文字 3:1。
|
||||
|
||||
### 5.1 本规范用到的全部新增/关键组合
|
||||
|
||||
| 组合(用途) | 对比度 | 判定 |
|
||||
| --- | --- | --- |
|
||||
| `ink` / `surface`(正文、页码角标白字于 ink 实底 13.50 同值) | 13.50 | 达标 |
|
||||
| `inkSoft` / `surface`、`canvas`、`surfaceTint`(全部次级信息文字) | 6.59 / 6.21 / 5.58 | 达标 |
|
||||
| 白字 / `ink` 80% scrim 合成白图(+N 角标、删除角标、上传百分比胶囊) | 7.10 | 达标 |
|
||||
| 白字 / `ink` 60% scrim 合成白图 | 3.88 | **不达标,弃用**(§3.2 一律 80%) |
|
||||
| 白字 / `primaryStrong`(发送钮、发布钮) | 4.49 | 达标(一迭代已裁决按 ≈4.5 采纳) |
|
||||
| `primaryStrong` / `surface`(白卡内「全文」「查看全部回复」链接字) | 4.49 | 达标 |
|
||||
| `primaryStrong` / `canvas`(canvas 直排底上的链接字) | 4.23 | **贴线不过**——canvas 底文字链接一律改 `primaryDark`(8.88:1);`primaryStrong` 文字仅限白卡内(§5.2 新规则) |
|
||||
| `primaryStrong` 值条 / `surfaceTint` 轨道(上传线性进度,非文字) | 3.80 | 达标(≥3) |
|
||||
| `primaryDark` / primary 8% 底、`surfaceTint`(TopicChip 字、草稿恢复条、tonal 关注钮) | 8.74 / 7.98 | 达标 |
|
||||
| `accentDark` / accent 8% 底、`surface`(分区小标、收藏激活态) | 7.07 / 7.40 | 达标 |
|
||||
| `error` / `surface`(点赞激活图标,非文字) | 4.99 | 达标 |
|
||||
| `errorDark` / `surface`、error 淡底(点赞计数、上传失败字) | 6.50 / 5.78 | 达标 |
|
||||
| `Colors.red #F44336` / `surface`(demo 点赞现状) | 3.13 | 图标勉强 3:1 但脱离色板且伴随计数字不达标,**修订为 error 族**(§3.5) |
|
||||
| 页码指示点 `primaryStrong` / 白图最不利底(非文字) | 4.49 | 达标(另有 ink 胶囊「2/9」双通道兜底) |
|
||||
| 骨架块 `surfaceTint` / `surface` | 1.18 | 装饰性占位,不受约束 |
|
||||
|
||||
### 5.2 修订与新规则(本规范裁决点)
|
||||
|
||||
| 事项 | 处置 |
|
||||
| --- | --- |
|
||||
| 点赞 `Colors.red` | 全部替换为 `error`(激活图标)+ `errorDark`(伴随计数),收编进色板 |
|
||||
| `primaryStrong` 于 canvas 4.23:1 | 新规则:**`primaryStrong` 作文字色仅限 `surface` 白卡内**;canvas/surfaceTint 底文字链接与强调字用 `primaryDark`。二迭代已有页面按此规则在 M3 回归中顺手核(影响面小,见 §7) |
|
||||
| scrim 浓度 | 图片上承字 scrim 统一 `ink` 80%(withAlpha 204),禁用更浅档承载文字 |
|
||||
|
||||
### 5.3 DEBT-2(muted 色)触发场景与规避
|
||||
|
||||
M3 是**次级信息文字密度最高**的一族页面(时间戳、计数、元信息、上传状态、字数计数器满屏皆是),是 DEBT-2 的重灾区。demo 三页现全部用 `bodySmall`(默认 `muted` 3.36:1)承载这些信息,**照抄即触发**。规避方案沿二迭代 05 号 §5.4 既定路线:
|
||||
|
||||
- 本页面族**所有承载信息**的次级文字(帖子时间、评论时间、计数、「没有更多了」、上传状态、草稿提示、话题统计、字数计数器)一律显式 `inkSoft`(三底 5.58–6.59 全达标);操作行未激活图标同用 `inkSoft`。
|
||||
- `muted` 仅限:输入占位符(评论框、正文框 hint)、禁用态、纯装饰图标(评论空态大图标)。
|
||||
- 全局 `bodySmall` 默认色是否切 `inkSoft` 的议题仍挂账(二迭代 D9 遗留),M3 不做全局翻修;但 M3 新页面从落笔起就不产生新债。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与正典出入 / 待拍板清单
|
||||
|
||||
| # | 事项 | 性质 |
|
||||
| --- | --- | --- |
|
||||
| D1 | 图片九宫格 + 纯文字帖形态(正典 feed 卡仅单图有图形态);列数规则 2/4→2 列、其余→3 列 | 设计稿未覆盖,新增提案 |
|
||||
| D2 | 帖子详情评论区整套(CommentTile、楼中楼、长按操作 sheet 含举报);楼中楼是否入 M3 范围随 PM 拍板 | 设计稿未覆盖,新增提案 |
|
||||
| D3 | 发布页整页(正典只有 AI 创作生成器):push 全屏而非 sheet、上传进度四态、草稿自动保存/恢复交互 | 设计稿未覆盖,新增提案 |
|
||||
| D4 | 话题页整页 + TopicChip 独立组件(与 TagPill 同色系分工:TagPill 静态标签 / TopicChip 可交互) | 设计稿未覆盖,新增提案 |
|
||||
| D5 | 个人主页 + 关注关系(关注双态钮、关注/粉丝列表);PM 裁剪时的降级形态已备(§2.5 末段) | 设计稿未覆盖,新增提案 |
|
||||
| D6 | 点赞激活色 `Colors.red` → `error`/`errorDark`;收藏激活 → `accentDark` | 现状修订(脱离色板 + 3.13:1) |
|
||||
| D7 | 新规则:`primaryStrong` 文字仅限白卡内,canvas/tint 底改 `primaryDark`(4.23:1 实测贴线不过) | 无障碍修订 |
|
||||
| D8 | 乐观更新三层闪烁抑制策略(600ms 防抖合并 / 回滚零动画 / 对账静默),需客户端与埋点侧确认「合并后只报最终态」的事件口径 | 交互提案,跨角色确认 |
|
||||
| D9 | 九宫格编辑态长按拖拽排序:建议 M3 可选(不阻塞),砍掉不影响主流程 | 范围裁剪建议 |
|
||||
| D10 | 骨架屏引入(正典无 loading 语言;呼吸动效尊重减弱动态设置) | 设计稿未覆盖,新增提案 |
|
||||
| D11 | 详情页多图采用「轮播 + 页码」而非九宫格平铺(沉浸浏览优先);Feed 卡多图才用九宫格 | 形态裁决,待确认 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付验收对照(供开发/QA)
|
||||
|
||||
- [ ] 7 个新组件(PostCard / PostMediaGrid / UploadProgressOverlay / CommentTile / LikeButton / TopicChip / FeedSkeleton)落位 `lib/core/widgets/`;话题/标签深变体色映射全 app 仅存一份(TagPill 现映射提为共享)。
|
||||
- [ ] P1–P5 均具备 loading(骨架或转圈)/ empty / error / retry 态;错误三层模型与前两迭代一致。
|
||||
- [ ] 本规范全部文字组合按 §5.1 达 AA;图片上承字仅用 `ink` 80% scrim;`Colors.red` 在社区页面族零残留;canvas 底无 `primaryStrong` 文字。
|
||||
- [ ] 次级信息文字全部 `inkSoft`,`muted` 仅出现在占位/禁用/纯装饰(DEBT-2 不新增欠账)。
|
||||
- [ ] 点赞/收藏/关注乐观更新按 §4:连点只发终态、回滚零动画、失败必有 SnackBar;无「计数与状态不成对」的中间帧。
|
||||
- [ ] 发布页:上传单张失败可单独重试且不清空内容;取消必经草稿确认;触控目标全数 ≥44×44。
|
||||
- [ ] 骨架与激活动画在系统「减弱动态效果」开启时降级为静态/瞬变。
|
||||
|
||||
---
|
||||
**UI Designer** · 2026-09-08
|
||||
@@ -0,0 +1,467 @@
|
||||
# 第三迭代埋点与实验规划(社区)
|
||||
|
||||
> 角色:Experiment Tracker
|
||||
> 日期:2026-09-08
|
||||
> 前序:iteration-2 `06-experiment-tracking-plan.md`(字典 v2、北极星定义式、H1~H4、A/B 八项前置)、`15-analytics-persistent-queue.md`(分段持久化队列实况)、`24-event-whitelist-v2.md`、`29-m2-summary.md` §4(遗留与 M3 方向)、`30-device-verification-checklist.md`(真机补验挂起)
|
||||
> 依据:`development-plan.md` 第 4 节 community 域、第 7 节 M3 验收、第 9 节「可观测性与产品验证」;`patbond-api` `EventDictionary.java` 现行白名单(v2,21 事件);`patbond-flutter` `lib/analytics/` 现状(SessionTracker / RouteObserver / 分段队列均已落地)
|
||||
> 范围:M3 社区纵切(Feed、帖子、草稿/发布、媒体、评论、点赞、收藏、关注、话题);AI 创作、本地服务不在本轮定义
|
||||
> 性质:纯规划文档,供 M3 开发工单直接引用;不含任何代码改动
|
||||
|
||||
**速览(五个核心结论)**:
|
||||
|
||||
1. 事件字典 v3 增量 **19 个新事件**(post 域 8 + feed 域 2 + 互动 8 + 实验基建 `experiment_exposed` 1),命名沿 v1/v2 惯例,结果编码进事件名,见 §1。
|
||||
2. **Feed 曝光采用「浏览段聚合」设计,逐卡曝光事件被本角色否决**——量级重测表明:逐卡设计下 M2 的「零扩容」结论**不再成立**(1,000 DAU 约 7~14 个月击穿 5,000 万行分区阈值,且接收端限流实际未实现、快速滑动会形成无背压直写),聚合设计下**零扩容结论继续成立**,见 §2。
|
||||
3. 新增 **4 条可证伪假设 H5~H8**(发布渗透率 / 发布漏斗完成率 / 社区-记录协同 / Feed 消费深度),H1~H4 出数日历与责任人落定(判定日 10-06 / 10-13 / 10-20),见 §3。
|
||||
4. 北极星**建议 M3 保持「7 日回访记录率」不变**,社区复合指标不在本迭代引入;复评点设在 M3 收官、以 H7 读数为依据(**待拍板**),见 §4。
|
||||
5. A/B 八项前置的 M3 推进计划:**6 项本迭代变绿 + 1 项部分变绿**(#7 的 feature flag 回滚随社区发布开关顺带落地、监控留 M4),维持「M3 末基本全绿、M4 首实验」路线,见 §5。
|
||||
|
||||
---
|
||||
|
||||
## 0. 基线现状(开工前核对)
|
||||
|
||||
M2 收官把 v2 规划的绝大部分落成了现实,本节只记与 M3 规划直接相关的事实。
|
||||
|
||||
| 项 | 现状 | 出处 |
|
||||
| --- | --- | --- |
|
||||
| 后端字典 | v2 共 21 事件:auth 11 + `page_viewed` 正稿 + pet 3 + health_record 6;`health_record_action` 已移除(ADR-013) | `EventDictionary.java` 实读 |
|
||||
| 接收端 | `POST /api/v1/events` 批量 1–50、202 逐条、eventId 幂等、白名单剥离、红线拒绝;**64KB 上限与 429 限流均未实现**(契约据实未写,09 号 §出入 1/2) | 09 号报告 |
|
||||
| 存储 | `platform.product_events` 不分区,分区阈值约 5,000 万行 | v1 报告 13 §2.3 |
|
||||
| 客户端会话 | `session_tracker.dart` 已落地(冷启动/30 分钟规则);`analytics_route_observer.dart` + `page_viewed` 已挂全 | 15 号 / 24 号报告 |
|
||||
| 客户端队列 | 分段持久化(500 条 / 25 段、at-least-once、批 ≤50);冲刷触发点 3/4:满 20 条、退后台、冷启动恢复——**30 秒定时器未做** | 15 号 §2.3/§4 |
|
||||
| 队列遗留三项 | 30s 定时冲刷、退避/429(依赖后端先有限流)、anonymousId 持久化 | 29 号 §4 遗留 3 |
|
||||
| 真机补验 | Android 落库观察 + SessionTracker 30min 手测挂起(0.5 天清单在 30 号报告)——**这是 A/B 前置 #1「数据质量验收」的拦路项** | 30 号报告 |
|
||||
| pageName 实况 | 客户端枚举 13 个:字典 v2 初始 9 个 + 客户端自行补充 4 个(`create`/`pet_archive`/`services`/`post_detail`),后者**尚未同步进字典正稿** | `analytics_page_name.dart` 实读 |
|
||||
| 假设与北极星 | H1~H4 判定线已冻结(观察窗自 2026-09-08 起算);北极星 SQL 与 §6 对账 SQL 已入档待巡检 | M2 06 号 §2/§3 |
|
||||
|
||||
**开工前必须知道的一件事**:M2 06 号 §7.1 曾以「限流 60 请求/5 分钟余量十几倍」论证零扩容,但 09 号契约回填核实该限流**从未实现**——接收端目前对客户端写入没有任何背压。这不改变 M2 量级下的结论(量太小),但 M3 引入首个高频事件后,**保护必须内建在事件设计里而不能指望限流兜底**,这是 §2 裁定聚合方案的硬前提之一。
|
||||
|
||||
---
|
||||
|
||||
## 1. 事件字典 v3 增量(community 域族)
|
||||
|
||||
### 1.1 沿用原则与域划分
|
||||
|
||||
命名 `<域>_<动作>_<结果>` snake_case、结果编码进事件名(`_succeeded`/`_failed`)、单义事件不设结果后缀(沿 `health_record_viewed`/`health_record_deleted` 先例)、`eventVersion` 起始 1、公共属性十项全带、属性 camelCase。v3 新增域前缀:
|
||||
|
||||
- **`post`**:帖子生命周期(创建、媒体、草稿、发布、删除)
|
||||
- **`feed`**:Feed 消费(曝光聚合、加载失败)
|
||||
- **`comment`**:评论
|
||||
- **`user`**:关注关系(`user_followed`——关注的对象是用户,域按实体归 user;话题关注见 §1.6 缺口 3)
|
||||
- 点赞/收藏归 **`post`** 域(作用对象是帖子)
|
||||
|
||||
设计纪律沿 v2 §1.2 的教训:**不设** `community_action(actionType)` 式多路复用事件——like/unlike/favorite/unfavorite 是四个语义独立的动作,各自独立成名,任何一个的枚举扩充不污染其他指标口径。
|
||||
|
||||
### 1.2 核心裁定:Feed 曝光用「浏览段聚合」,不做逐卡事件
|
||||
|
||||
这是 v3 最重要的一个设计决策,先给结论再给依据(量级数字在 §2 展开):
|
||||
|
||||
**`feed_viewed` 定义为「一个 Feed 浏览段」的聚合事件**:用户进入 Feed 页起累计计数,**离开时(路由跳走 / 退后台)发一条**,携带该段的曝光卡片数、翻页数、刷新数与停留时长。卡片「曝光」的客户端判定:卡片可见面积 ≥ 50% 且持续 ≥ 500ms,**同一浏览段内按 postId 去重**(postId 只在客户端内存里做去重键,**绝不上报**,上报的只有计数)。
|
||||
|
||||
否决逐卡方案(每张卡片可见发一条 `post_impression(postId)`)的四条理由:
|
||||
|
||||
1. **存储击穿**(§2.2):逐卡设计使「零扩容」结论失效,M3 就要启动分区改造——为一个当前没有消费方的数据形态提前付基建成本,不成立。
|
||||
2. **无背压直写**:接收端限流未实现(§0),快速滑动可产生 5–10 卡/秒,客户端满 20 条即冲刷 ≈ 每 2–4 秒一个 HTTP 请求,无任何机制拦截这种放大。
|
||||
3. **队列容量反噬**:500 条队列按 M2 量级可容两周离线积压,逐卡设计下缩水到 2~4 天,离线场景开始真实丢数据(丢最旧整段),反而伤害其他低频高价值事件。
|
||||
4. **当前无消费方**:逐卡曝光的唯一刚需是「按帖子算曝光-点击率」供推荐排序实验用。M3 的 Feed 是游标分页的时序流、没有排序算法;等 M4+ 真做排序实验时,逐帖曝光的正确采集点是**服务端 Feed 下发日志**(server-side,天然全量、无客户端丢失率问题),而不是客户端埋点。此路线记入 backlog(§8 拍板 6),届时按需再评估分区与采样。
|
||||
|
||||
聚合方案的代价是丢失「单帖曝光→点进」归因,保留的是本迭代真正要回答的问题:**人们刷不刷、刷多深、刷完动不动手**(H8、H5 的数据源)——按需采集,不为想象中的分析囤数据。
|
||||
|
||||
### 1.3 隐私红线增量(社区内容是重灾区,在 v2 五条之上追加)
|
||||
|
||||
社区域的埋点只记**行为**不记**内容**,且社区首次引入「用户生成内容 + 用户间关系」,红线从严:
|
||||
|
||||
1. **帖子/评论正文**:任何自由文本禁止上报;文本规模用 `textLengthBucket` 枚举(`empty` / `short`(≤50) / `medium`(51–500) / `long`(>500)),不报精确字数。
|
||||
2. **内容 ID 与用户 ID**:postId、commentId、topicId、被关注/被赞用户的 userId 一律不进 props(公共属性里的 userId 是**行为主体**自己,这是既有契约;**行为客体**的任何标识不上报)。逐卡曝光被否决后,v3 全部事件无一需要内容 ID。
|
||||
3. **话题名**:话题是公开分类词但仍不上报名称(自建话题可能含用户自由文本),只报 `topicCount`;话题维度的内容分析走服务端事实表。
|
||||
4. **媒体线索**:文件名、本地路径、URL 禁止;只允许 `mediaType` 枚举与 `sizeBucket` 枚举(`lt_1mb` / `mb_1_5` / `mb_5_20` / `gte_20mb`),不报精确字节数。
|
||||
5. **pageName 归一化**(红线 5 延伸):`post_detail`、`topic_detail`、`user_profile` 等带参数路由,参数一律剥离,UUID 出现在 pageName/referrer 即验收失败。
|
||||
|
||||
红线正则本轮仍不扩(理由同 v2 §1.3);值级巡检(v2 §6.4 长度 >64 扫描)天然覆盖「正文塞进合法字段」的泄漏形态,继续每日跑。
|
||||
|
||||
### 1.4 新事件清单
|
||||
|
||||
失败枚举基底(v2 七项之上按 M3 验收新增):
|
||||
|
||||
- `content_rejected` — 内容审核/敏感词拒绝(**待拍板**:M3 是否有审核环节,无则删)
|
||||
- `media_too_large` / `unsupported_format` — 媒体上传专用
|
||||
- `not_found` 复用 — 目标帖子/评论已被删除(对应验收「删除内容不可继续出现」的客户端时序窗口)
|
||||
|
||||
#### 发布漏斗(post 域)
|
||||
|
||||
| 事件名 | 触发时机 | 专有属性 |
|
||||
| --- | --- | --- |
|
||||
| `post_create_started` | 进入发帖编辑器并产生**首次输入**(含首次选媒体),每次进入记一次 | `entryPoint`(`create_tab` / `feed` / `topic_detail` / `pet_detail`,待 UI 定稿收敛) |
|
||||
| `post_draft_saved` | 草稿保存成功响应后;**仅手动保存与离开时保存**,若产品做打字自动保存,自动保存不埋(防高频) | `trigger`(`manual` / `on_exit`)、`mediaCount` |
|
||||
| `post_publish_succeeded` | 发布接口成功响应后(**漏斗事件**,H5/H6 核心数据源) | `durationMs`(started→publish)、`mediaCount`、`topicCount`、`textLengthBucket`、`fromDraft`(bool) |
|
||||
| `post_publish_failed` | 发布失败 / 超时 / 本地校验拦截 | `failureReason`、`errorCode`、`httpStatus`、`attemptSeq` |
|
||||
| `post_deleted` | 删帖成功响应后(单事件风格,失败靠服务端错误率观测) | 无专有属性 |
|
||||
|
||||
`post_publish_failed.failureReason`:`validation_error`、`content_rejected`(待拍板)、`media_upload_incomplete`(有媒体未传完即点发布)、`rate_limited`、`network_error`、`server_error`。
|
||||
|
||||
#### 媒体上传漏斗(post 域,逐文件)
|
||||
|
||||
| 事件名 | 触发时机 | 专有属性 |
|
||||
| --- | --- | --- |
|
||||
| `post_media_upload_started` | 单个媒体文件开始上传 | `mediaType`(`image` / `video`)、`sizeBucket` |
|
||||
| `post_media_upload_succeeded` | 单文件上传成功 | `mediaType`、`sizeBucket`、`durationMs` |
|
||||
| `post_media_upload_failed` | 单文件失败 / 超时 / 用户取消 | `mediaType`、`sizeBucket`、`failureReason`、`errorCode`、`httpStatus`、`attemptSeq` |
|
||||
|
||||
逐文件(而非逐帖聚合)的理由:上传是发布漏斗预判的最大流失段(H6),失败归因需要文件粒度的 `sizeBucket × mediaType × failureReason` 交叉;量级无忧——单帖媒体数有产品上限(九宫格类,≤9),非高频。`failureReason`:`media_too_large`、`unsupported_format`、`network_error`、`server_error`、`cancelled`。
|
||||
|
||||
#### Feed 消费(feed 域)
|
||||
|
||||
| 事件名 | 触发时机 | 专有属性 |
|
||||
| --- | --- | --- |
|
||||
| `feed_viewed` | **离开 Feed**(路由跳走 / 退后台)时发一条,聚合本浏览段(§1.2 裁定) | `feedTab`(`home` / `topic` / `user_posts` / `favorites`,待 UI 定稿收敛)、`durationMs`、`impressionCount`(≥50% 可见 ≥500ms、段内按帖去重)、`loadMoreCount`(翻页次数)、`refreshCount`(下拉刷新次数) |
|
||||
| `feed_load_failed` | 刷新或翻页请求失败(M3 验收「分页不丢失不重复」的客户端观测点) | `feedTab`、`loadType`(`refresh` / `load_more`)、`failureReason`、`errorCode`、`httpStatus` |
|
||||
|
||||
实现注意:`impressionCount` 去重集合只存活于浏览段内存中,段结束即弃;`durationMs` 用前台时长(退后台暂停计时),上限截断 30 分钟(防止挂机污染 H8)。
|
||||
|
||||
帖子详情**浏览**不设 `post_viewed`——由 `page_viewed(pageName=post_detail)` 覆盖(沿 v2 `pet_viewed` 不设的同一先例,防双事件重复计数)。
|
||||
|
||||
#### 互动(post / comment / user 域)
|
||||
|
||||
| 事件名 | 触发时机 | 专有属性 |
|
||||
| --- | --- | --- |
|
||||
| `post_liked` | 点赞成功响应后 | `source`(`feed` / `post_detail`) |
|
||||
| `post_unliked` | 取消点赞成功响应后 | `source` |
|
||||
| `post_favorited` | 收藏成功响应后 | `source` |
|
||||
| `post_unfavorited` | 取消收藏成功响应后 | `source` |
|
||||
| `comment_create_succeeded` | 评论提交成功响应后 | `durationMs`、`isReply`(bool,楼中楼)、`textLengthBucket` |
|
||||
| `comment_create_failed` | 评论提交失败 | `failureReason`、`errorCode`、`httpStatus`、`attemptSeq` |
|
||||
| `user_followed` | 关注成功响应后 | `source`(`post_detail` / `feed` / `user_profile` / `follow_list`) |
|
||||
| `user_unfollowed` | 取关成功响应后 | `source` |
|
||||
|
||||
取舍说明(与 v2 同款自觉取舍,复活条件注明):
|
||||
|
||||
- **点赞/收藏/关注不埋失败**:幂等写入、单点交互,失败率靠服务端接口错误率观测(`health_record_deleted` 先例)。若乐观更新回滚率成为问题,届时以 eventVersion=2 增补 `_failed`。
|
||||
- **评论不设 `comment_create_started`**:短表单,沿 v2「编辑不设 started」先例;评论放弃率若成为问题再增补。
|
||||
- **like/unlike 分立而非 `action` 属性**:v2 §1.2 废弃 `health_record_action` 的同一逻辑——H5 的「互动用户」分母定义只引用语义单一的事件名。
|
||||
|
||||
#### 实验基建(platform 域,A/B 前置 #5 提前进字典)
|
||||
|
||||
| 事件名 | 触发时机 | 专有属性 |
|
||||
| --- | --- | --- |
|
||||
| `experiment_exposed` | 用户**实际到达**实验触点时(渲染了变体 UI),非分配时 | `experimentKey`(实验注册表枚举)、`variant` |
|
||||
|
||||
M4 首实验才启用,但字典与白名单**本迭代一次进**:M3 后端反正要动 `EventDictionary`,避免 M4 为一个事件再开一轮字典工单;客户端强类型封装同批出(可先无调用方)。这直接把 A/B 前置 #5 在 M3 变绿(§5)。
|
||||
|
||||
### 1.5 v3 增量总览(19 个新事件)
|
||||
|
||||
| # | 事件名 | 版本 | 性质 |
|
||||
| --- | --- | --- | --- |
|
||||
| 22 | `post_create_started` | 1 | 新增 |
|
||||
| 23 | `post_draft_saved` | 1 | 新增 |
|
||||
| 24 | `post_publish_succeeded` | 1 | 新增(漏斗事件) |
|
||||
| 25 | `post_publish_failed` | 1 | 新增 |
|
||||
| 26 | `post_deleted` | 1 | 新增 |
|
||||
| 27 | `post_media_upload_started` | 1 | 新增 |
|
||||
| 28 | `post_media_upload_succeeded` | 1 | 新增(漏斗事件) |
|
||||
| 29 | `post_media_upload_failed` | 1 | 新增 |
|
||||
| 30 | `feed_viewed` | 1 | 新增(聚合曝光,首个高频事件) |
|
||||
| 31 | `feed_load_failed` | 1 | 新增 |
|
||||
| 32 | `post_liked` | 1 | 新增 |
|
||||
| 33 | `post_unliked` | 1 | 新增 |
|
||||
| 34 | `post_favorited` | 1 | 新增 |
|
||||
| 35 | `post_unfavorited` | 1 | 新增 |
|
||||
| 36 | `comment_create_succeeded` | 1 | 新增 |
|
||||
| 37 | `comment_create_failed` | 1 | 新增 |
|
||||
| 38 | `user_followed` | 1 | 新增 |
|
||||
| 39 | `user_unfollowed` | 1 | 新增 |
|
||||
| 40 | `experiment_exposed` | 1 | 新增(M4 启用,字典先行) |
|
||||
|
||||
后端 `EventDictionary` 白名单增量(工单可直接抄):
|
||||
|
||||
```java
|
||||
// v3 增量 post 域(iteration-3 报告 06 §1.4)
|
||||
Map.entry("post_create_started", Set.of("entryPoint")),
|
||||
Map.entry("post_draft_saved", Set.of("trigger", "mediaCount")),
|
||||
Map.entry("post_publish_succeeded",
|
||||
Set.of("durationMs", "mediaCount", "topicCount", "textLengthBucket", "fromDraft")),
|
||||
Map.entry("post_publish_failed",
|
||||
Set.of("failureReason", "errorCode", "httpStatus", "attemptSeq")),
|
||||
Map.entry("post_deleted", Set.of()),
|
||||
Map.entry("post_media_upload_started", Set.of("mediaType", "sizeBucket")),
|
||||
Map.entry("post_media_upload_succeeded", Set.of("mediaType", "sizeBucket", "durationMs")),
|
||||
Map.entry("post_media_upload_failed",
|
||||
Set.of("mediaType", "sizeBucket", "failureReason", "errorCode", "httpStatus", "attemptSeq")),
|
||||
// v3 增量 feed 域(聚合曝光设计,§1.2 裁定)
|
||||
Map.entry("feed_viewed",
|
||||
Set.of("feedTab", "durationMs", "impressionCount", "loadMoreCount", "refreshCount")),
|
||||
Map.entry("feed_load_failed",
|
||||
Set.of("feedTab", "loadType", "failureReason", "errorCode", "httpStatus")),
|
||||
// v3 增量互动
|
||||
Map.entry("post_liked", Set.of("source")),
|
||||
Map.entry("post_unliked", Set.of("source")),
|
||||
Map.entry("post_favorited", Set.of("source")),
|
||||
Map.entry("post_unfavorited", Set.of("source")),
|
||||
Map.entry("comment_create_succeeded", Set.of("durationMs", "isReply", "textLengthBucket")),
|
||||
Map.entry("comment_create_failed",
|
||||
Set.of("failureReason", "errorCode", "httpStatus", "attemptSeq")),
|
||||
Map.entry("user_followed", Set.of("source")),
|
||||
Map.entry("user_unfollowed", Set.of("source")),
|
||||
// A/B 前置 #5:曝光事件字典先行,M4 启用(§1.4)
|
||||
Map.entry("experiment_exposed", Set.of("experimentKey", "variant"))
|
||||
```
|
||||
|
||||
Flutter 侧沿用强类型封装惯例:新建 `post_analytics.dart` / `feed_analytics.dart` / `community_interaction_analytics.dart`,枚举编译期锁死。
|
||||
|
||||
### 1.6 漏斗闭环与维度够用性复核
|
||||
|
||||
复核方法同 v2 §1.6:以 §3 假设与 M3 验收逐条反推数据源。
|
||||
|
||||
**闭环成立**:发布漏斗四段 `page_viewed(post_form) → post_create_started → post_publish_succeeded/failed`(媒体上传子漏斗嵌套其中,started→succeeded/failed 配对完整);Feed 消费闭环 `feed_viewed`(曝光量)→ `page_viewed(post_detail)`(点进)→ 互动事件。**发布漏斗的「到达→动笔」段由 pageName 新增 `post_form` 承接**(§6),与 v2 修订 1 的 `pet_form` 同构——这次在设计期就补上,不留缺口。
|
||||
|
||||
**缺口 1(接受不埋)**:逐帖曝光-点击归因——§1.2 已论证,M4+ 走服务端日志路线,backlog 登记。
|
||||
|
||||
**缺口 2(接受不埋)**:评论/帖子的浏览深度(评论区滚动)——`page_viewed(post_detail)` 足够回答「点进率」,评论区消费深度在排序实验之前无消费方。
|
||||
|
||||
**缺口 3(待拍板)**:话题关注——若 M3 UI 有「关注话题」按钮,需增补 `topic_followed/unfollowed(source)`(不报话题名,红线 3);UI 定稿前挂起(§8 拍板 5)。
|
||||
|
||||
**维度够用性**:H5 需互动/发布事件按 userId 去重(有);H6 需发布漏斗配对 + 媒体子漏斗(有);H7 需互动事件与 `health_record_create_succeeded` 的 userId + serverTs(有,跨域 join);H8 需 `feed_viewed.impressionCount/loadMoreCount`(有)。**全部假设可由 v3 字典 + community 事实表回答,判定通过。**
|
||||
|
||||
---
|
||||
|
||||
## 2. Feed 曝光量级评估与「零扩容」结论复核
|
||||
|
||||
### 2.1 v3 上线后的单用户日事件量重估
|
||||
|
||||
| 来源 | 条/DAU/日 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| M2 存量(auth + page_viewed + pet/health_record) | 15–30 | v2 §7.1 估算,实测待巡检校准 |
|
||||
| `page_viewed` 社区页面增量 | +5–10 | post_detail 点进是主要来源 |
|
||||
| `feed_viewed`(聚合) | +3–8 | 每浏览段一条 |
|
||||
| 互动(like/favorite/comment/follow 及 un-*) | +3–10 | 活跃互动者 |
|
||||
| 发布漏斗 + 媒体 + 草稿 | +0.5–3 | 发布是低频动作(H5 预估 ≤10% 用户/周) |
|
||||
| **合计** | **27–61** | **约 M2 的 2 倍** |
|
||||
|
||||
### 2.2 「零扩容」结论复核:聚合设计下成立,逐卡设计下不成立
|
||||
|
||||
**聚合设计(本方案)**:
|
||||
|
||||
- 接收端:61 条/日、满 20 条冲刷 ≈ 3–4 请求/日/用户,即便未来补 60 请求/5 分钟限流也有百倍余量。契约、批上限 50、接收逻辑**均不动**。
|
||||
- 存储:1,000 DAU × 60 条 × 365 天 ≈ **2,200 万行/年**,距 5,000 万分区阈值仍有约 2 年余量。**不分区决策继续有效。**
|
||||
- 队列:500 条 ≈ 8 天以上离线积压(vs M2 两周,可接受),**上限不调**。
|
||||
- **结论:零改动,「零扩容」结论继续成立。**
|
||||
|
||||
**逐卡设计(被否决方案,留数字供复议)**:
|
||||
|
||||
- 活跃刷 Feed 用户 2–4 段/日 × 20–60 卡 ≈ 40–240 条曝光/日,总量升至 100–250 条/DAU/日。
|
||||
- 存储:1,000 DAU 中位 ≈ 4,400 万行/年、上沿 ≈ 9,100 万行/年——**7~14 个月击穿分区阈值**,M3 就得启动分区 + 保留策略改造。
|
||||
- 突发:快速滑动 5–10 卡/秒 → 每 2–4 秒满 20 条冲刷一次 → 单用户可达 75–150 请求/5 分钟;限流未实现(§0),这是对接收端和数据库的无背压直写。
|
||||
- 队列:500 条仅容 2–4 天离线积压,挤压其他事件的 at-least-once 保障。
|
||||
- **结论:逐卡设计使 M2「零扩容」结论失效**——这就是 §1.2 裁定的量化依据。
|
||||
|
||||
### 2.3 队列遗留三项的 M3 处置(优先级重排)
|
||||
|
||||
| 遗留项(29 号 §4) | M3 处置 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| 30s 定时冲刷 | **本迭代第一波做**(P1) | 社区场景出现「长前台会话」(刷 Feed 半小时不切页),现有三触发点在这种会话里最多积压 19 条不上传;定时器同时改善当日监控的数据新鲜度。实现按 15 号 §4 既定方案。 |
|
||||
| 退避 + 429 处理 | **客户端退避本迭代做**(5xx/网络错误指数退避 + 抖动);429 分支随后端限流落地一并做 | 后端限流是 09 号出入 5 项排期评估的一部分(后端侧决策);客户端 5xx 退避不依赖它,社区量级翻倍后重试风暴的伤害面变大,先行。 |
|
||||
| anonymousId 持久化 | **本迭代做**(P2,一行级改动) | 现状每次冷启动新生成(`analytics_service.dart` 构造器 `Uuid().v4()`),登录前事件无法跨启动归并。A/B 前置 #4 的「登录前实验 anonymousId 分流」硬依赖持久化——M3 不做,M4 首实验若涉及注册/登录前触点就被卡住。 |
|
||||
|
||||
以上三项均为 `lib/analytics/` 内改动,与社区功能开发无耦合,建议与字典 v3 后端工单同批排入第一波。
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品假设:H5~H8 新增 + H1~H4 出数日历
|
||||
|
||||
### 3.0 方法约定(沿 v2 §3,两点强调)
|
||||
|
||||
判定线上线前登记并 PM 会签冻结,届满出「支持 / 证伪 / 数据不足」三态判定。**H5~H8 的观察窗自社区功能对用户可用之日(下称 T0,随 M3 发布日落定)起算**,T0 后第 1 周为尝鲜噪声期,除 H6 外剔除。社区上线会扰动 M2 假设的在途窗口,处理纪律见 §3.2。
|
||||
|
||||
### H5:社区消费者远多于生产者,但生产者渗透率决定内容池成活(发布渗透假设)
|
||||
|
||||
- **陈述**:稳定期内,周活跃用户中当周产生 ≥1 条 `post_publish_succeeded` 的比例 ≥ 5%。
|
||||
- **判定指标**:周去重 `userId`(post_publish_succeeded) / 周去重 userId(任意事件);辅助读数:互动渗透率(≥1 条 like/favorite/comment/follow 的占比)。
|
||||
- **判定线**:支持 = ≥ 5%;证伪 = 连续 3 周 < 2%;2–5% 顺延。
|
||||
- **窗口**:T0 后第 2–5 周。
|
||||
- **行动**:证伪 → 发布门槛过高或动机不足,「从健康记录一键生成帖子」类降门槛引导进 A/B 候选池;不在 Feed 排序上浪费资源(内容池不成活时排序无意义)。支持 → 内容池自生长成立,资源投向消费侧(H8)。
|
||||
|
||||
### H6:媒体上传是发布漏斗的最大流失段(漏斗诊断假设)
|
||||
|
||||
- **陈述**:发布漏斗完成率(`post_publish_succeeded` / `post_create_started`,24h 归因窗)≥ 60%,且流失集中在含媒体的发布(含媒体发布的完成率比纯文字低 ≥ 15pp)。
|
||||
- **判定指标**:漏斗配对 + `post_media_upload_failed` 按 `sizeBucket × failureReason` 分布交叉定位。
|
||||
- **判定线**:支持 = 完成率 ≥ 60% 且媒体差 ≥ 15pp;证伪 = 完成率 < 40%(漏斗整体坏,另找原因)或媒体差 < 5pp(流失不在媒体段);其余顺延。
|
||||
- **窗口**:T0 后第 1–4 周(**含噪声周**——漏斗诊断恰恰要看首批用户的失败形态,沿 H4 先例)。
|
||||
- **行动**:支持 → 上传压缩/断点续传优化排 M4 前置;证伪且完成率低 → 按 failureReason 分布重新归因(validation_error 高则查表单/文案)。
|
||||
|
||||
### H7:社区活跃提升记录回访(社区-记录协同假设,北极星拍板的数据依据)
|
||||
|
||||
- **陈述**:首记后 7 日内产生过 ≥1 次社区互动(like/favorite/comment/follow/publish 任一成功事件)的用户,其 7 日回访记录率比无互动者高 ≥ 8pp。
|
||||
- **判定指标**:北极星 SQL(M2 06 号 §2.1)按「窗口内是否有社区互动事件」分两群比较;回访事件仍只算 `health_record_create_succeeded`(社区行为只做分群、不充当回访,无循环)。
|
||||
- **判定线**:支持 = 差值 ≥ 8pp 且两群各 ≥ 100 人;证伪 = 差值 < 3pp 或倒挂;3–8pp 顺延。
|
||||
- **窗口**:T0 后 6 周(需 ≥ 2 个成熟队列)。
|
||||
- **方法论警示**:观察性对照,只证相关(活跃用户本来什么都多做,自选择偏差与 H3 同款)。支持的正确用法是把「记录完成页引导分享到社区」列为 A/B 候选,用随机化坐实;同时它是 §4 北极星复评的核心输入——**若证伪(社区与记录是两个不相干场景),复合北极星的动议应就地终结**。
|
||||
- **行动**:支持 → A/B 候选池 + 北极星复评启动;证伪 → 社区按独立场景运营,北极星保持记录型不再复议。
|
||||
|
||||
### H8:Feed 首屏之外仍有消费需求(内容供给/消费深度假设)
|
||||
|
||||
- **陈述**:稳定期内,≥ 40% 的 Feed 浏览段发生翻页(`feed_viewed.loadMoreCount` ≥ 1)。
|
||||
- **判定指标**:翻页浏览段占比;辅助读数:浏览段 `impressionCount` 中位数、`durationMs` 分布(截断 30min,§1.4)。
|
||||
- **判定线**:支持 = ≥ 40%;证伪 = 连续 3 周 < 20%;20–40% 顺延。
|
||||
- **窗口**:T0 后第 2–5 周。
|
||||
- **行动**:证伪 → 首屏即耗尽兴趣,指向内容供给不足(结合 H5 判定:若 H5 也证伪则是供给问题,运营/官方内容或 M4 AI 创作「一键发帖」提前;若 H5 支持则是分发问题);支持 → 游标分页体验(预加载、去重)投入合理,排序实验(M4+)有消费基础。
|
||||
|
||||
### 3.2 H1~H4 与北极星在 M3 期间的出数安排
|
||||
|
||||
M2 冻结的窗口自 2026-09-08 起算,判定日历与责任人如下(周节奏:**每周一**数据侧跑 M2 06 号 §6 全部对账 SQL + §2.1 北极星 SQL,本角色复核读数并记入巡检记录):
|
||||
|
||||
| 项 | 窗口 | 关键日期 | 跑数责任 | 判定责任 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 北极星首个成熟周队列 | W37 队列(09-07~09-13 首记)+8 天成熟 | **2026-09-21(周一)首次出数**,此后每周一滚动 | 数据侧 | 本角色发布(Wilson 95% CI,<50 人周合并) |
|
||||
| H4 激活链路 | 上线后 4 周(含第 1 周) | **2026-10-06 判定** | 数据侧 | 本角色 + PM 会签 |
|
||||
| H2 多宠 | 上线后 4 周末读数 | **2026-10-06 判定**(`pet.pets` 真值侧) | 数据侧 | 同上 |
|
||||
| H1 记录类型分布 | 第 2–5 周(09-15~10-12) | **2026-10-13 判定** | 数据侧(§6.2 SQL 即读数) | 同上 |
|
||||
| H3 提醒-回访 | 6 周(≥2 成熟队列) | **2026-10-20 判定** | 数据侧 | 同上 |
|
||||
|
||||
**社区上线对在途窗口的污染纪律**:若 T0(社区发布日)落在 H1/H3 窗口内,判定线**不改**(冻结纪律),但读数发布时必须按 T0 前/后拆周标注;H3 若前后两段方向不一致,判定记「数据不足-顺延」并注明混杂因素,不得挑一段下结论。H7 的对照组恰好提供了交叉检验。
|
||||
|
||||
**前置风险**:H1~H4 与北极星的一切读数都以真实事件流入库为前提。真机补验(30 号清单)未完成前,Android 端数据可信度未验证——**补验必须在 09-21 首次北极星出数前完成**,否则首批读数只能标「未验收数据,仅供方向参考」。
|
||||
|
||||
---
|
||||
|
||||
## 4. 北极星:M3 保持「7 日回访记录率」,不引入社区复合指标(待拍板)
|
||||
|
||||
社区上线后「北极星要不要变」是必答题。本角色立场:**M3 全程保持现北极星不变**,理由三条:
|
||||
|
||||
1. **基线刚建立,换指标即断线**。北极星 09-21 才出第一个成熟队列读数,M3 期间总共只会积累 4~6 个可比周。此时切换或掺入社区成分,等于永远失去「社区上线前后」这组最有价值的对照——北极星的首要职责是跨迭代可比。
|
||||
2. **新功能光环效应会系统性高估社区成分**。任何复合指标(如「7 日回访有效行为率 = 记录或发帖或互动」)在社区上线后前几周必然被尝鲜流量冲高,读数好看但不可解释,恰好违背 v2 选 A 弃 B 的原始理由(拒绝易被一次性行为冲高的指标)。
|
||||
3. **「社区是否服务于留存」本身是待验假设,不是前提**。这正是 H7 的问题。把社区写进北极星等于未经验证就宣布答案。正确顺序:H7 出数(T0+6 周)→ 若支持且 A/B 坐实,M4 起再评估复合式(候选形态:分子扩为「记录 或 发布」,互动类行为因信号太弱不入分子);若 H7 证伪,动议终结。
|
||||
|
||||
**落定为拍板项**(§8 拍板 1):M3 保持不变,复评点 = M3 收官会 + H7 读数;PM 保留否决权,否决须给出替代定义式与断线代价的处置方案。社区侧的健康度用**辅助指标层**观测(不升格):周发布渗透率(H5 口径)、周互动渗透率、Feed 翻页率(H8 口径)——三者随 §7 巡检周报发布。
|
||||
|
||||
---
|
||||
|
||||
## 5. A/B 八项前置的 M3 推进计划
|
||||
|
||||
M2 06 号 §4.2 立的路线是「M3 末全绿、M4 首实验」。逐项落定 M3 的动作与责任侧:
|
||||
|
||||
| # | 前置条件 | M3 动作 | 责任侧 | M3 末预期 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 数据质量验收 | 真机补验(30 号清单,0.5 天)→ M2 字典 v2 事件 2 周巡检达标(丢失 <5%、对账偏差 <5%、去重 <10%、serverTs 100%、无红线泄漏) | 数据 + 真机执行人 | **绿**(拦路项是真机,见 §3.2 风险) |
|
||||
| 2 | 指标基线 | 北极星 + M2 漏斗连续 ≥2 周稳定产出(09-21 起自然达成),留档均值与方差 | 数据 | **绿** |
|
||||
| 3 | 样本量规则成文 | 基线率 × MDE × α=0.05 × 功效 80% 的计算方法 + 查表 + 按实测 DAU 换算最短运行时长;以 §3 实测基线代入(不再用 M2 的假设值) | 本角色 | **绿**(M3 中交付) |
|
||||
| 4 | 稳定分流组件 | `hash(userId, experimentSalt) % buckets` 后端组件 + anonymousId 持久化(§2.3,登录前分流的前提)+ 登录后归并规则成文 | 后端(归并规则:本角色) | **绿** |
|
||||
| 5 | 曝光事件 | `experiment_exposed` 已随 v3 进字典(§1.4);Flutter 强类型封装同批出 | 后端 + Flutter | **绿**(本报告已完成设计) |
|
||||
| 6 | 实验设计模板与评审流程 | 模板(假设/主指标/护栏/提前停止规则/多重比较约定)+ 评审流程成文;与 #3 同一文档交付 | 本角色 | **绿** |
|
||||
| 7 | 护栏监控与回滚 | feature flag 开关机制随「社区功能发布开关」顺带落地(社区本就该有开关灰度);护栏**准实时监控**留 M4(依赖监控设施选型) | 后端/DevOps | **部分绿**(回滚绿、监控 M4) |
|
||||
| 8 | 隐私合规复核 | 每实验一次,常态项 | 每实验 | 常态 |
|
||||
|
||||
**结论:M3 末 6 项全绿 + #7 部分绿,M4 初补齐监控即可启动首实验。** 首实验候选池按判定结果动态排序:H3 支持 →「默认引导创建提醒」;H4 证伪 →「建宠成功页引导首条记录」;H7 支持 →「记录完成页引导分享社区」;H5 证伪 →「记录一键生成帖子」。届时按 #3 的样本量规则做可行性检验(运行 >8 周即判不可行,退回观察,止损线预登记——沿 M2 §4.3 纪律)。
|
||||
|
||||
---
|
||||
|
||||
## 6. pageName 枚举增量与 page_viewed 覆盖检查
|
||||
|
||||
### 6.1 增量清单
|
||||
|
||||
现状(§0):字典正稿 9 个 + 客户端已自行补充 4 个未同步正稿。v3 一次收编 + 社区族增量:
|
||||
|
||||
| pageName | 性质 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `create` / `pet_archive` / `services` / `post_detail` | **收编转正**(客户端已存在) | 补进字典说明,消除枚举双源 |
|
||||
| `post_form` | 新增 | 发帖编辑器——发布漏斗「到达段」承接者(§1.6),对应 v2 的 `pet_form` 教训,设计期即补 |
|
||||
| `topic_list` | 新增 | 话题列表/广场 |
|
||||
| `topic_detail` | 新增 | 话题详情(含话题内 Feed;话题 ID 剥离) |
|
||||
| `user_profile` | 新增 | **他人**主页(自己的主页仍是 `profile`,两者语义不同不合并;用户 ID 剥离) |
|
||||
| `follower_list` / `following_list` | 新增 | 粉丝/关注列表分立(关注关系的两个方向是不同页面) |
|
||||
| `favorite_list` | 新增 | 我的收藏 |
|
||||
| `draft_list` | 新增 | 草稿箱 |
|
||||
|
||||
共 **9 个新增 + 4 个收编**。Feed 本体不新增 pageName:首页 Tab 即 Feed,沿用 `home`(pageName 保持导航语义,Feed 消费的度量职责已由 `feed_viewed` 承担,避免一次改名断掉 M2 以来的 `home` 时序)。终稿在社区 UI 定稿后由 UI + 本角色对齐一次(§8 拍板 4)。
|
||||
|
||||
后端零改动提示:`page_viewed` 白名单只校验 props **键**(pageName/referrer),值级枚举由客户端编译期锁死 + 离线巡检兜底——pageName 增量**不需要动 EventDictionary**,只改 `analytics_page_name.dart` 与字典文档。
|
||||
|
||||
### 6.2 覆盖检查(社区页面族接线的验收 sanity)
|
||||
|
||||
沿 v2 §5.2/§6.3.2 框架,社区族新增三条关系式(数据侧入每日巡检):
|
||||
|
||||
1. **互动必有承载页**:产生过互动事件(like/favorite/comment/follow)的 session 必有 ≥1 条 `page_viewed`(pageName ∈ {home, post_detail, topic_detail, user_profile})。偏差 >5% = 社区页面路由漏挂。
|
||||
2. **曝光先于点进**:日 `feed_viewed.impressionCount` 总和 ≥ 日 `page_viewed(pageName=post_detail)` 条数(点进的帖子必先曝光;深链/推送入口出现前该式恒成立,破式即 `feed_viewed` 聚合逻辑漏计)。
|
||||
3. **发布必经编辑器**:日 `post_publish_succeeded + post_publish_failed` ≤ 日 `page_viewed(pageName=post_form)`(发布尝试必先到达编辑器)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 对账 SQL v3 增量(真值:community 事实表)
|
||||
|
||||
v1/v2 巡检全部继续。表名以 M3 后端 DDL 定稿为准,下文假定 `community` schema(`community.posts`、`community.comments`、`community.post_likes`、`community.post_favorites`、`community.follows`),命名不同替换即可。
|
||||
|
||||
### 7.1 发布对账(H5 真值侧)
|
||||
|
||||
`post_publish_succeeded` 事件数 vs `community.posts` 当日新建行数(排除草稿态),UTC 日界,偏差 >5% 告警——结构同 v2 §6.1,替换事件名与表名即可,不重抄。评论对账同构(`comment_create_succeeded` vs `community.comments`)。
|
||||
|
||||
### 7.2 互动净值对账(点赞/收藏的 toggle 语义专用)
|
||||
|
||||
like/unlike 是幂等 toggle,事实表存的是**净状态**,逐日计数对账不成立,改对**净增量**:
|
||||
|
||||
```sql
|
||||
-- 日 (post_liked - post_unliked) 事件净值 vs community.post_likes 当日净增行数
|
||||
WITH evt AS (
|
||||
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
|
||||
count(*) FILTER (WHERE event_name = 'post_liked')
|
||||
- count(*) FILTER (WHERE event_name = 'post_unliked') AS evt_net
|
||||
FROM platform.product_events
|
||||
WHERE event_name IN ('post_liked', 'post_unliked')
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT e.day, e.evt_net, a.api_net,
|
||||
abs(e.evt_net - a.api_net) AS diff_abs -- 相对偏差对净值无意义,看绝对差趋势
|
||||
FROM evt e
|
||||
JOIN (SELECT date_trunc('day', created_at AT TIME ZONE 'UTC') AS day,
|
||||
count(*) AS api_net -- 若删行实现取消,需改为审计表/净增视图,DDL 定稿后校准
|
||||
FROM community.post_likes GROUP BY 1) a USING (day)
|
||||
ORDER BY e.day;
|
||||
```
|
||||
|
||||
(若后端用删行实现取消点赞,`api_net` 须改从审计日志或快照差分取数——DDL 定稿后由数据侧校准,此处登记口径意图。收藏、关注同构。)
|
||||
|
||||
### 7.3 feed_viewed 自洽巡检(聚合事件的质量门)
|
||||
|
||||
聚合事件一旦逻辑有 bug,坏的是整段计数,须专设 sanity:
|
||||
|
||||
```sql
|
||||
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
|
||||
count(*) AS segments,
|
||||
count(*) FILTER (WHERE (props->>'impressionCount')::int = 0
|
||||
AND (props->>'durationMs')::int > 10000) AS zero_imp_long_stay,
|
||||
-- 停留超 10s 却零曝光 = 曝光判定逻辑失效,>1% 告警
|
||||
count(*) FILTER (WHERE (props->>'durationMs')::int > 1800000) AS over_cap
|
||||
-- durationMs 超 30min 截断上限 = 计时暂停逻辑失效,期望恒 0
|
||||
FROM platform.product_events
|
||||
WHERE event_name = 'feed_viewed'
|
||||
GROUP BY 1 ORDER BY 1;
|
||||
```
|
||||
|
||||
### 7.4 巡检节奏汇总
|
||||
|
||||
- **每日**:v1/v2 既有全部 + §7.1~7.3 + 值级泄漏扫描(v2 §6.4,社区正文是新的高风险源)。
|
||||
- **每周一**:北极星 + H 假设读数(§3.2 日历);辅助指标层三项(§4)随周报发布。
|
||||
|
||||
---
|
||||
|
||||
## 8. 待拍板清单(汇总)
|
||||
|
||||
| # | 事项 | 选项 | 本角色裁定/建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 北极星是否随社区调整 | 保持 7 日回访记录率 / 引入社区复合指标 | **建议保持**,复评点 = M3 收官 + H7 读数(论证 §4);PM 否决须给替代定义式与断线处置 |
|
||||
| 2 | H5~H8 判定线 | §3 各阈值 | T0 前 PM 会签一次,会签后冻结(同 H1~H4 纪律) |
|
||||
| 3 | `content_rejected` 枚举 | M3 是否有内容审核环节 | 有则保留,无则从枚举删(发布/评论两处) |
|
||||
| 4 | `entryPoint`/`feedTab`/pageName 终稿 | 待社区 UI 定稿收敛 | 埋点工单开工前 UI + 本角色对齐一次(含拍板 5) |
|
||||
| 5 | 话题关注事件 | UI 有「关注话题」则增补 `topic_followed/unfollowed` | 按 UI 定稿定(§1.6 缺口 3) |
|
||||
| 6 | 逐帖曝光路线 | 本迭代不做(§1.2 裁定);M4+ 排序实验若立项,走服务端 Feed 下发日志 | backlog 登记,届时同评分区与采样 |
|
||||
| 7 | 后端限流排期 | 09 号出入 5 项之一 | 建议 M3 排入(客户端 429 分支在等它,§2.3);非本角色职权,仅登记依赖 |
|
||||
| 8 | 真机补验时限 | 30 号清单 0.5 天 | **建议 09-21 前完成**(北极星首次出数的数据可信前提,§3.2 风险) |
|
||||
|
||||
---
|
||||
|
||||
## 附:M3 埋点工单拆分建议(按依赖排序)
|
||||
|
||||
1. **真机补验**(30 号清单,独立于开发,越早越好——拍板 8)。
|
||||
2. **Flutter 队列三小修**(§2.3:30s 定时器、5xx 退避、anonymousId 持久化)——`lib/analytics/` 内闭环,先于社区新事件。
|
||||
3. **后端**:`EventDictionary` v3 增量 19 事件(§1.5 代码块可直抄,含 `experiment_exposed`)+ 集成测试;可与 2 并行。
|
||||
4. **Flutter**:pageName 增量与收编(§6.1,`analytics_page_name.dart`)+ 社区页面族路由挂接。
|
||||
5. **Flutter**:社区功能开发时按 §1.4 挂接(强类型封装先行;`feed_viewed` 的浏览段聚合器建议独立类 + 单测覆盖曝光判定/去重/计时暂停/30min 截断)。
|
||||
6. **数据**:§7 对账 SQL 入巡检(7.2 口径待 DDL 定稿校准);§6.2 三条覆盖 sanity 随社区页面上线启用。
|
||||
7. **后端**:分流组件 + 归并规则(A/B 前置 #4,§5)。
|
||||
8. **本角色**:H5~H8 判定线 T0 前会签冻结(拍板 2);样本量规则 + 实验设计模板文档(前置 #3/#6)M3 中交付;每周一北极星/假设读数复核(§3.2 日历)。
|
||||
@@ -0,0 +1,225 @@
|
||||
# 07 · M3 开工前证据审计与基线快照
|
||||
|
||||
> 角色:Evidence Collector(沿用 iteration-2/07 模式:每条声称附可复现命令与输出,拒绝空口断言)
|
||||
> 审计日期:2026-09-08 · 只读审计,未改代码、未 commit、未动 mkdocs.yml
|
||||
> 与 Reality Checker 分工:本报告不复跑测试套件与 E2E(运行态归他),只管证据链完整性、档案质量、静态计数与基线快照
|
||||
|
||||
---
|
||||
|
||||
## 1. M2 证据链审计
|
||||
|
||||
### 1.1 报告在档与导航挂载:30/30 + index,完整率 100%
|
||||
|
||||
```bash
|
||||
ls docs/development/iterations/iteration-2/ | grep -c '^\([0-9]\|index\)' # → 31(01~30 + index.md)
|
||||
grep -c "iteration-2/" mkdocs.yml # → 31
|
||||
```
|
||||
|
||||
逐份核对结果:`01-pm-task-breakdown.md` ~ `30-device-verification-checklist.md` 编号连续无断号,31 个文件与 `mkdocs.yml` 第 34~64 行的 31 条导航一一对应(进展看板 + 01~30),无孤儿文件、无空挂导航。另有非报告附件 `openapi-pets-draft.yaml`(第二波契约草案存档,14 号报告引用,不要求挂导航)。
|
||||
|
||||
注意口径:29 号收官总结第 21 行写"29 份入档"——写作当时属实;30 号(真机补验清单)系收官后由 `e68b655` 追加入档并挂导航,时间线自洽,不算矛盾。
|
||||
|
||||
### 1.2 ADR 断号检查:001~015 连续,终号 015
|
||||
|
||||
```bash
|
||||
grep -oE "ADR-[0-9]+" docs/architecture/decisions.md | sort -u
|
||||
# → ADR-001 ~ ADR-015,15 个,无断号
|
||||
```
|
||||
|
||||
与 29 号总结"ADR 001~015"声称一致。
|
||||
|
||||
### 1.3 feature-checklist M2 三节(§7~§9)抽核 5 条状态声称
|
||||
|
||||
| # | 清单声称 | 实物证据(可复现) | 结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | §7 "Flyway V3 pet_health **8 表** + V4 字典种子(**28 品种/10 疫苗**)" | `grep -c "CREATE TABLE" V3__pet_health_baseline.sql` → **8**(breeds/pets/pet_owners/pet_weight_records/vaccine_catalog/pet_vaccinations/health_events/care_reminders);V4 两条 INSERT 值行数 **28**(breeds)+ **10**(vaccine_catalog) | ✅ 逐字吻合 |
|
||||
| 2 | §7 "宠物 CRUD + breeds 目录 **23 例**(六类路径 + 三角色矩阵)" | `PetCrudIntegrationTest` 14 例 + `PetPermissionIntegrationTest` 9 例 = **23**(@Test 注解计数) | ✅ 吻合 |
|
||||
| 3 | §7 "契约一致性测试(v1.2.0 **字节级快照**)" | api 侧快照文件在档且 sha256 与正典**逐字节一致**(见 §2.2);`ContractConformanceTest` 11 例在档 | ✅ 吻合 |
|
||||
| 4 | §8 "pets 数据层……**DTO 映射 62 例测试**" | 22 号报告原文第 109 行为"本单(dev@7fb9031)126(**+62**,全绿)"——62 是该工单**全量新增测试数**(含 DTO 映射、repository、异常类型化等),非纯 DTO 映射例数 | ⚠️ 数字有出处,清单转述口径漂移(见 §6-G4) |
|
||||
| 5 | §9 "事件字典 v2 白名单(pet 域 3 + health_record 域 7)……api@64c9b72" | `EventDictionary.java` 实数 pet 域 **3** + health_record 域 **7**,与 06 号 §1.5 键集逐条一致(24 号报告已逐条对照);提交 `64c9b72` 在 api 历史中定位到 | ✅ 吻合 |
|
||||
|
||||
抽核之外顺带实证:§7 各接口测试例数声称(体重 8 / 疫苗 12 / 事件 11 / 提醒 10 / 摘要 12)与对应测试类 @Test 计数**全部逐一吻合**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 契约档案审计
|
||||
|
||||
### 2.1 openapi.yaml v1.2.0 的 18 路径(逐一列出)
|
||||
|
||||
```bash
|
||||
grep -nE "^ /" docs/api/openapi.yaml # 18 行
|
||||
python3 -c "...yaml.safe_load..." # paths: 18, operations: 24, schemas: 45
|
||||
```
|
||||
|
||||
| # | 路径 | # | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `/api/v1/auth/register` | 10 | `/api/v1/pets/{petId}/weights` |
|
||||
| 2 | `/api/v1/auth/login` | 11 | `/api/v1/vaccine-catalog` |
|
||||
| 3 | `/api/v1/auth/refresh` | 12 | `/api/v1/pets/{petId}/vaccinations` |
|
||||
| 4 | `/api/v1/auth/logout` | 13 | `/api/v1/vaccinations/{vaccinationId}` |
|
||||
| 5 | `/api/v1/me` | 14 | `/api/v1/pets/{petId}/health-events` |
|
||||
| 6 | `/api/v1/events` | 15 | `/api/v1/health-events/{eventId}` |
|
||||
| 7 | `/api/v1/pets` | 16 | `/api/v1/pets/{petId}/care-reminders` |
|
||||
| 8 | `/api/v1/pets/{petId}` | 17 | `/api/v1/care-reminders/{reminderId}` |
|
||||
| 9 | `/api/v1/breeds` | 18 | `/api/v1/pets/{petId}/summary` |
|
||||
|
||||
`info.version: 1.2.0`(第 4 行)。29 号声称"18 路径/24 操作/45 schema"三个数字全部复现吻合。
|
||||
|
||||
### 2.2 api 侧快照 sha256 与正典一致(字节级)
|
||||
|
||||
```bash
|
||||
sha256sum docs/api/openapi.yaml \
|
||||
patbond-api/patbond-pet/src/test/resources/contract/openapi-v1.2.0.yaml
|
||||
# 二者均为 243fe6487bfa19018bddbfdb2cece16d9f81bc9718d3404501574677a4cd689d
|
||||
```
|
||||
|
||||
✅ 快照存在且与正典逐字节一致,契约测试的"字节级快照锁"有实物支撑。
|
||||
|
||||
### 2.3 Flyway 迁移清单:V1~V4 齐全(单链归 patbond-user)
|
||||
|
||||
```
|
||||
patbond-user/src/main/resources/db/migration/
|
||||
├── V1__identity_media_baseline.sql
|
||||
├── V2__create_platform_product_events.sql
|
||||
├── V3__pet_health_baseline.sql # pet_health schema 8 表
|
||||
└── V4__pet_health_dictionary_seed.sql # 28 品种 + 10 疫苗种子
|
||||
```
|
||||
|
||||
无断号,pet 模块自身无迁移目录,与"迁移链仍归 patbond-user 单链"(清单 §7)一致。
|
||||
|
||||
---
|
||||
|
||||
## 3. 提交完整性
|
||||
|
||||
### 3.1 三仓 status:工作区全干净,与远端零偏差
|
||||
|
||||
```bash
|
||||
git -C <repo> status -sb # 三仓均无未跟踪/未提交文件,无 ahead/behind 标记
|
||||
```
|
||||
|
||||
| 仓库 | 分支 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| patbond-doc | main…origin/main | 干净,已同步 |
|
||||
| patbond-api | dev…origin/dev | 干净,已同步 |
|
||||
| patbond-flutter | dev…origin/dev | 干净,已同步 |
|
||||
|
||||
### 3.2 29 号收官索引的关键提交逐一定位(`git log --oneline -25` + 逐哈希 `git log -1`)
|
||||
|
||||
**doc 仓**(10/10 定位到):`1891d9b`(开工 10 报告 + ADR-009~015)→ `2ceab6b`(events 契约补录)→ `6025832`/`b04e93c`(第一波收口)→ `511617b`(契约冻结 v1.2.0)→ `222990e`(第二波收口)→ `b81c050`(第三波收口)→ `23ce404`(T2-19 文档收口)→ `fcac68d`(M2 收官)→ `e68b655`(30 号追加,现 HEAD)。
|
||||
|
||||
**api 仓**(11/11 定位到):`49299fb`(V3/V4)→ `0eae1c9`(pet 骨架)→ `58576f8`(ADR-013 移除 health_record_action)→ `8fbf444`(T2-03)→ `825dde3`(T2-04)→ `4c2653c`(T2-05)→ `d8303bf`(T2-06)→ `3b27f9f`(T2-07)→ `00f7dbd`(T2-08)→ `d026f2f`(契约测试 T2-09)→ `64c9b72`(字典 v2,现 HEAD,= 29 号声称收官 HEAD)。
|
||||
|
||||
**flutter 仓**(8/8 定位到):`33b993c`(持久化队列)→ `7fb9031`(T2-11 数据层)→ `97a1f46`(T2-12)→ `5b34fa3`/`c91f18a`(T2-13)→ `e186ba3`/`ba50332`(T2-14)→ `720865b`(E2E 脚本,现 HEAD,= 29 号声称收官 HEAD)。
|
||||
|
||||
E2E 实物:`patbond-flutter/test_e2e_m2_manual.dart`(777 行)在档,脚本内场景标号 `[1/11]`~`[11/11]` 恰 11 个,与 28 号"11/11 场景"声称的场景数吻合(复跑归 Reality Checker)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 静态计数 vs 声称
|
||||
|
||||
### 4.1 后端 @Test:**191,与声称一致**
|
||||
|
||||
```bash
|
||||
grep -rE "@(Test|ParameterizedTest)\b" --include="*.java" patbond-api \
|
||||
| grep -v target | wc -l # → 191
|
||||
```
|
||||
|
||||
| 模块 | @Test 数 |
|
||||
| --- | --- |
|
||||
| patbond-common | 3 |
|
||||
| patbond-user | 68 |
|
||||
| patbond-auth | 31 |
|
||||
| patbond-pet | 89 |
|
||||
| **合计** | **191** ✅ |
|
||||
|
||||
pet 模块内分布:CRUD 14 / 权限矩阵 9 / 体重 8 / 疫苗 12 / 事件 11 / 提醒 10 / 摘要 12 / 契约一致性 11 / 健康探针 1 / 骨架 1。
|
||||
|
||||
### 4.2 前端 test/testWidgets:**272,与声称一致**
|
||||
|
||||
```bash
|
||||
grep -rE "^\s*(test|testWidgets)\(" patbond-flutter/test --include="*.dart" | wc -l # → 272
|
||||
```
|
||||
|
||||
| 目录 | 例数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| test/features/pets/ | 193 | 20 个文件(models 25、repository 22、detail_page 19、health_record_display 16 为大头) |
|
||||
| test/analytics/ | 34 | 队列/存储/路由观察者/服务/会话 5 文件 |
|
||||
| test/core/ | 21 | token_refresher 5 + 共享 widget 16 |
|
||||
| test/features/auth/ | 18 | repository 10 + 登录/注册页各 4 |
|
||||
| test/widgets/ + 根 | 6 | tag_pill 5 + widget_test 1 |
|
||||
| **合计** | **272** ✅ | |
|
||||
|
||||
> 静态注解计数与运行期用例数吻合,说明无参数化展开偏差;实际运行全绿与否归 Reality Checker 复核。
|
||||
|
||||
---
|
||||
|
||||
## 5. M3 开工基线快照(M3 收官对比基准)
|
||||
|
||||
### 5.1 三仓 HEAD(完整哈希)
|
||||
|
||||
| 仓库 | 分支 | HEAD | 末次提交 |
|
||||
| --- | --- | --- | --- |
|
||||
| patbond-doc | main | `e68b6553cadaccb3b29fbbca3d44df04473c506f` | docs: 真机补验独立操作清单(30 号,M2 挂起项) |
|
||||
| patbond-api | dev | `64c9b72fd19cec916d964e2468330ede5fddfb81` | feat: 事件字典 v2 白名单扩充 pet/health_record 域 10 事件(T2-17 后端) |
|
||||
| patbond-flutter | dev | `720865bcb93fca5fe49340b77807fb174d193b91` | test: M2 E2E 烟囱脚本(T2-18 收官) |
|
||||
|
||||
### 5.2 核心数字
|
||||
|
||||
| 维度 | 基线值(静态计数) |
|
||||
| --- | --- |
|
||||
| 后端 @Test | **191**(common 3 / user 68 / auth 31 / pet 89) |
|
||||
| 前端 test/testWidgets | **272**(pets 193 / analytics 34 / core 21 / auth 18 / 其他 6) |
|
||||
| openapi.yaml | **v1.2.0,18 路径 / 24 操作 / 45 schema**(清单见 §2.1),sha256 `243fe648…4cd689d`,api 侧快照字节级一致 |
|
||||
| Flyway | **V1~V4**(单链归 patbond-user;pet_health 8 表 + 字典种子 28 品种/10 疫苗) |
|
||||
| ADR 终号 | **ADR-015** |
|
||||
| E2E 资产 | `test_e2e_manual.dart`(M1)+ `test_e2e_m2_manual.dart`(M2,11 场景) |
|
||||
|
||||
### 5.3 模块与端口表
|
||||
|
||||
| 模块 | 端口 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| patbond-auth | :8081(`PATBOND_AUTH_PORT`) | application.yml |
|
||||
| patbond-user | :8082(`PATBOND_USER_PORT`) | application.yml;含 analytics 接收端与 Flyway 单链 |
|
||||
| patbond-pet | :8083(`PATBOND_PET_PORT`) | **仅 application.yml.sample**(本地需从 sample 复制);compose 映射 8083:8083 |
|
||||
| postgres | 容器内 :5432 | postgres:18,**不对宿主机发布端口**(compose 注释:调试临时加 15432:5432) |
|
||||
| patbond-common | — | 共享库,无端口 |
|
||||
|
||||
### 5.4 事件白名单基线(EventDictionary 实数:**共 22 事件**)
|
||||
|
||||
`patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/analytics/EventDictionary.java`:
|
||||
|
||||
- **auth 域 11**:`auth_register_started` / `auth_register_succeeded` / `auth_register_failed` / `auth_login_succeeded` / `auth_login_failed` / `auth_token_refresh_succeeded` / `auth_token_refresh_failed` / `auth_logout` / `auth_session_restore_started` / `auth_session_restore_succeeded` / `auth_session_restore_failed`
|
||||
- **通用 1**:`page_viewed`(v2 正稿)
|
||||
- **pet 域 3**:`pet_create_started` / `pet_create_succeeded` / `pet_create_failed`
|
||||
- **health_record 域 7**:`health_record_create_started` / `health_record_create_succeeded` / `health_record_create_failed` / `health_record_viewed` / `health_record_edit_succeeded` / `health_record_edit_failed` / `health_record_deleted`
|
||||
- 已废弃(ADR-013,测试锁定拒绝):`health_record_action`
|
||||
|
||||
客户端实际发射面(`grep -rhoE "'(auth_|pet_|health_record_|page_viewed)…'" lib/`):**15 个**——auth 5(register/login 成败 + logout)+ page_viewed + pet 3 + health_record 6。白名单侧多出的 7 个中,auth 6 个为服务端字典预置(token_refresh/session_restore/register_started 客户端未挂),`health_record_deleted` 留待删除端点(27 号已声明合理留白)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 证据缺口清单
|
||||
|
||||
| # | 缺口 | 出处 | 定级 |
|
||||
| --- | --- | --- | --- |
|
||||
| G1 | **"字典 v2 13 事件"口径不可复现**:27 号 §"埋点端到端贯通"写"13 个事件(pet 域 3 + health_record 域 6 + page_viewed 正稿)"——括号内实为 **10**;29 号沿用"13 事件"。从任何实数(白名单总 22 / v2 增量 10 / v2 客户端挂接 10 / 客户端发射面 15)均凑不出 13 | 27 号第 27 行、29 号第 20 行 | 低(数字笔误级,但收官总结是对外口径,M3 引用时应改写为"v2 增量 10、白名单共 22") |
|
||||
| G2 | **CI 状态声称离线不可复核**:29 号收官索引 api"CI success"、doc"strict 通过"无法在本机复现(需按 M2 建立的 Gitea commit status API 实查惯例取证);flutter 一栏写"**待本提交 CI**"且 30 号追加后**未回填终态结论**——三仓收官 CI 是否全绿目前档内无闭环证据 | 29 号 §5 | 中(M3 开工前建议补一次三仓 HEAD 的 commit status 实查并回填) |
|
||||
| G3 | **feature-checklist 头部哈希滞后**:头部"最后更新"写 flutter `ba50332`,终态 HEAD 为 `720865b`(E2E 脚本提交)。272 计数在 HEAD 仍成立,非事实错误,但对账时会引起哈希对不上 | feature-checklist.md 第 5 行 | 低 |
|
||||
| G4 | **"DTO 映射 62 例测试"转述漂移**:22 号原文的 +62 是 T2-11 工单全量新增测试数,清单 §8 转述成了"DTO 映射 62 例" | feature-checklist §8 | 低 |
|
||||
| G5 | **真机两项仍挂起**(非新缺口,登记延续):Android 事件落库观察、SessionTracker 30 分钟手测——方案 A 挂起,操作清单已独立成 30 号 | 29 号 §4、30 号 | 中(M3 期间设备到位即补,预计 0.5 天) |
|
||||
|
||||
除上述外,M2 档案的可复现声称(报告数、导航、ADR、契约三数字、快照哈希、Flyway、双端测试计数、关键提交链、E2E 场景数)**全部实证通过**:抽核与全查合计 40+ 条声称,仅 G1/G3/G4 三处口径瑕疵,无一处"声称的实物不存在"。
|
||||
|
||||
---
|
||||
|
||||
## 7. 审计结论
|
||||
|
||||
- **证据链完整率**:30/30 报告 + index 在档且挂导航(100%);ADR-001~015 无断号;关键提交 29/29 在三仓历史定位。
|
||||
- **静态计数**:后端 191、前端 272,与收官声称**逐一吻合**;契约 18/24/45 三数字与字节级快照全部复现。
|
||||
- **基线快照**:已建立(§5),M3 收官时以本节为对比基准。
|
||||
- **缺口**:5 项(G1~G5),无阻塞级;建议 M3 开工时顺手处理 G2(CI 实查回填)与 G1(口径改写)。
|
||||
|
||||
---
|
||||
|
||||
**审计执行**:Evidence Collector · 2026-09-08
|
||||
**本报告未挂导航**(不动 mkdocs.yml 为本次硬约束,待 M3 文档收口时统一挂载)
|
||||
@@ -0,0 +1,139 @@
|
||||
# 08 M3 Git 与 CI 工作流核查规划
|
||||
|
||||
- 执行人:Git Workflow Master
|
||||
- 日期:2026-09-08
|
||||
- 范围:第三迭代(M3 社区)开工前的三仓状态核查、ADR-011 PR 条款实践复盘、发布分支启用规划、对象存储凭证防泄漏、CI 增量评估。**本报告只核查与规划,未改动任何代码、工作流或 mkdocs.yml,未执行 commit/push。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 三仓当前状态核查(2026-09-08 实测)
|
||||
|
||||
| 仓库 | 分支 | 相对 origin | 工作区 | stash | 最新提交 CI 状态 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| patbond-api | dev | 同步(fetch --prune 后确认) | 干净 | 无 | **success**(`64c9b72`,CI / backend-test,run 31,5m18s) |
|
||||
| patbond-flutter | dev | 同步 | 干净 | 无 | **success**(`720865b`,CI / flutter-gates,run 37,2m12s) |
|
||||
| patbond-doc | main | 同步 | 干净 | 无 | **success**(`e68b655`,CI / docs-build,run 39,29s) |
|
||||
|
||||
CI 状态经 Gitea commit status API 逐仓核实,非转述。**未提交内容清单:无**(本报告文件本身除外,按波次规则随下一波提交)。M2 收官时的「三仓 commit + push + CI 绿」闭环纪律保持完好。
|
||||
|
||||
### 1.1 历史遗留分支的新发现(比 M2 报告掌握的更严重一档)
|
||||
|
||||
M2 报告只记录了「api 本地孤儿 `master` 上游已删」。本次为发布分支规划做了祖先关系核查,发现:
|
||||
|
||||
- **patbond-api 的 `ff876bc`(本地孤儿 master、远端 `origin/main` 共同指向的初始 README 提交)不是 dev 的祖先**——`git merge-base --is-ancestor ff876bc dev` 判定失败,dev 的根提交是 `b1252b9`(Initialize patbond microservice modules)。即 **api 远端默认分支 main 与 dev 是两条不相干历史**(unrelated histories)。这直接影响第 3 节「dev→发布分支」怎么做第一次合并。
|
||||
- patbond-flutter 的 `main`(`030b11f`)**是** dev 祖先,未来 dev→main 可干净 fast-forward。
|
||||
- M2 报告建议的「核实后删本地孤儿 master」当时的前提(`ff876bc` 已被 dev 包含)实测**不成立**,但结论不变:该提交仅是初始 README,远端 `origin/main` 仍保留它,本地 `git branch -D master` 无信息损失,可顺手做。
|
||||
|
||||
## 2. M2 工作流实践复盘:ADR-011「高风险走 PR」条款何去何从
|
||||
|
||||
### 2.1 实践事实(git log + Gitea Actions 全量核查)
|
||||
|
||||
- **PR 使用次数:0。** 三仓 M2 期间(09-07 至 09-08)无任何 merge commit,历史全程线性。
|
||||
- **四类「高风险」全部直推了**:Flyway V3/V4(`49299fb`,T2-01)、契约冻结 v1.2.0(doc `511617b`)、事件字典白名单扩充(`64c9b72`)均直推 dev/main。
|
||||
- **风险事件清点:零。** 具体证据:
|
||||
1. M2 期间三仓 CI **零失败**——Actions 全量 run 列表中的 7 次 failure 全部集中在 09-04(M1 末 CI 搭建期),且全是流水线自身配置问题(外部 action 不可达、JDK 安装方式、format 未跑),无一是业务代码直推打红 dev;
|
||||
2. **零 revert**(`--grep` 回退/回滚/revert 无命中);
|
||||
3. **迁移不可变规则守住了**:V3/V4 文件推送后零修改(`git log --follow` 各只有一次提交);
|
||||
4. **契约冻结守住了**:`openapi.yaml` 在冻结提交 `511617b` 之后零改动;
|
||||
5. 无 force push 痕迹(线性历史 + 各推送头全绿)。
|
||||
|
||||
### 2.2 为什么直推没出事——机制归因,而非运气
|
||||
|
||||
M2 的安全性不是来自 PR 的缺席碰巧无事,而是四道机制已经覆盖了 PR 想防的东西:
|
||||
|
||||
1. **Flyway 迁移**:`./mvnw clean test` 经 Testcontainers 起真库执行完整迁移链,每次 push 都等于迁移演练——PR 合入前 CI 与 push 后 CI 跑的是同一条命令,对串行开发者而言只差「红了是否已在 dev 上」,而两人+AI 模式下红 dev 的传播面就是自己。
|
||||
2. **契约破坏**:T2-09 契约一致性测试把破坏性变更变成红测试,比人工 PR review 更机械可靠。
|
||||
3. **波次收尾 compose 实测 + E2E 烟囱**兜住了集成层。
|
||||
4. **串行作业**:M2 全程实质单线程推进(AI 辅助不产生 git 并发),四类触发条件中真正指向并发风险的「两人并行期」从未发生。
|
||||
|
||||
### 2.3 结论建议(**待拍板 #1**):条款降级为「按情形触发」,不是纪律失效
|
||||
|
||||
判定:**这不是纪律失效,是条款的触发条件设计错了**——它按「改动类别」(迁移/契约/依赖)触发,而 M2 证明这些类别在串行+CI 全量门禁下并无 PR 才能拦住的残余风险。真正需要 PR 的是「情形」:
|
||||
|
||||
- **建议修订 ADR-011 备注**:Flyway 迁移、契约变更、依赖升级在串行开发期**直推 dev + CI 绿 + 波次实测**即为足够实践,不再列为 PR 推荐触发项;
|
||||
- **PR 保留为强制的仅两种情形**:
|
||||
1. **两人并行改同一仓库期间**(唯一真实的并发冲突风险源);
|
||||
2. **首次 dev→发布分支合并之后**,凡影响已发布版本的破坏性变更(不可变迁移的例外处理、已冻结契约的破坏性修订、发布分支 hotfix)——发布后爆炸半径从「自己人」扩大到「装了 App 的用户」,性质不同。
|
||||
- 三仓 ci.yml 的 `pull_request:` 触发器保留不动(零成本待命);M2 待拍板 #2 的分支保护同理**降级为「随首次发布对发布分支启用」**,dev 不开(见 3.3 checklist)。
|
||||
|
||||
这样条款从「写了但没人执行的推荐」变成「触发即无争议的强制」,规范与实践重新一致。
|
||||
|
||||
## 3. M3 发布分支启用规划
|
||||
|
||||
### 3.1 先解决命名与历史两个前置问题
|
||||
|
||||
**命名不一致(待拍板 #2)**:ADR-011 写的是「`master` 保留为发布分支」,但远端实况是:api 的 `origin/master` 已删除(默认分支为 main)、flutter/doc 默认分支均为 `main`,**三仓远端今天没有任何一个 master 分支**。建议:**统一以 `main` 为发布分支名**,修订 ADR-011 措辞(master→main),顺手删除 api 本地孤儿 master(§1.1,无信息损失)。反向方案(重建三仓 master)多一次全员改默认分支操作,无收益。
|
||||
|
||||
**api 的 main 与 dev 历史不相干(待拍板 #3)**:`origin/main`(`ff876bc`)不是 dev 祖先,首次 dev→main 无法 fast-forward,普通 merge 需要 `--allow-unrelated-histories` 且会把一条孤儿历史永久缝进发布线。三个选项:
|
||||
|
||||
| 选项 | 操作 | 评价 |
|
||||
| --- | --- | --- |
|
||||
| A(推荐) | Gitea 仓库设置将默认分支临时切到 dev → 删除远端 main → 从 dev 重建 main → 默认分支按需切回 | 零 force push、历史干净,纯平台操作 |
|
||||
| B | 一次性 `git push --force origin dev:main`,在 ADR 中记录为例外 | 结果等价,但破「不 force push 共享分支」戒律,留坏先例 |
|
||||
| C | `merge --allow-unrelated-histories` | 永久保留无意义的孤儿历史缝合点,不推荐 |
|
||||
|
||||
flutter 无此问题(main 是 dev 祖先,直接 ff);doc 仓 main 即日常分支,不参与发布分支语义。
|
||||
|
||||
### 3.2 何时启用:建议 M3 末做第一次 dev→main 发布(**待拍板 #4**)
|
||||
|
||||
理由:M2 收官已具备「E2E 烟囱脚本 + 全绿测试基线 + 冻结契约」的可发布形态,缺的只是发布动作本身;北极星指标出数(ADR-012,M3 末 A/B 前置目标全绿)需要一个稳定版本承载;再往后拖,发布流程的首次演练会和 M4 首实验挤在一起。M3 末做第一次,把流程走通比版本内容重要。
|
||||
|
||||
### 3.3 发布 checklist 草案(首次发布用,验证后固化进 git-workflow.md)
|
||||
|
||||
1. **冻结**:发布波次收尾,三仓 commit + push + CI 绿(既有纪律);
|
||||
2. **实测**:compose 全栈起,跑 M2+M3 两份 E2E 烟囱脚本,全场景 PASS,证据入波次报告;
|
||||
3. **前置一次性项**(仅首次):完成 §3.1 的命名统一与 api main 重建;
|
||||
4. **合并**:api/flutter 各执行 `git checkout main && git merge --ff-only dev && git push origin main`(此后每次发布 dev→main 都应 ff-only 可过,过不了说明 main 被绕过 dev 改动,先查明);
|
||||
5. **打标**:两仓 `git tag -a v0.3.0 -m "M3 社区"`(版本号待拍板时一并定)并 push tag;doc 仓同点位打同名 tag,三仓互为对照;
|
||||
6. **平台侧**:Gitea 为 api/flutter 的 main 开启分支保护(禁直推、合并需 CI 状态检查通过)——dev 仍不开,保持直推流;
|
||||
7. **记录**:发布说明入 doc 仓(版本、三仓 tag 哈希、E2E 证据链接、已知遗留);
|
||||
8. **发布后**:影响 main 的 hotfix 一律走短命分支 + PR(§2.3 强制情形之二正式生效)。
|
||||
|
||||
## 4. 对象存储凭证防泄漏(M2 方案未实施,重新评估)
|
||||
|
||||
### 4.1 现状核查
|
||||
|
||||
- M2 报告 §5 的两层纯 shell 方案**零实施**:三仓均无 `scripts/hooks/`,`core.hooksPath` 均未设置,ci.yml 均无检查 step。
|
||||
- M2 没出事的原因和 PR 条款同理:M2 引入的凭证(RS256 密钥对、DB 密码、internal token)全部由 `deploy/init-secrets.sh` 生成且不入库,`*.sample` 占位约定执行到位——但这套卫生依赖「凭证只在本机生成」这个前提。
|
||||
|
||||
### 4.2 M3 威胁面变化:这次不一样,建议先落第二层(**待拍板 #5**)
|
||||
|
||||
M3 的对象存储凭证(ADR-010 剪出项回归:COS/OSS/MinIO 的 AccessKey/SecretKey)与 M2 的密钥有本质区别:**它是云厂商控制台签发的长期凭证,泄漏即可被外部直接使用且常绑计费**,不是本机自生成的内部秘密。AI 辅助开发下,凭证从「配置文件」流向「示例代码/测试/报告」的路径变多,纯约定不够。重新评估结论:
|
||||
|
||||
- **第二层(CI 兜底 grep)从「可选」升为「M3 第一波、对象存储凭证进入任何开发机之前必须上线」**。各仓 ci.yml 加一个纯 shell step(零外部依赖,秒级),模式清单在 M2 方案基础上增补云凭证特征:`AKID[A-Za-z0-9]{13,}`(腾讯云)、`LTAI[A-Za-z0-9]{12,}`(阿里云)、`(access|secret)[-_]?key\s*[:=]` 后跟非占位值、40 位以上连续 base64/hex;文件名黑名单增补 `.env`、`credentials`、`*.csv`(控制台导出的密钥文件形态)。
|
||||
- 第一层(共享 pre-commit 脚本)维持推荐;若继续搁置,第二层单独上线也成立(拦「已提交的」比拦「即将提交的」在两人团队更关键——push 即触发,无 `--no-verify` 逃逸)。
|
||||
- 沿用 M2 结论:不引入 gitleaks 等外部工具;真泄漏的第一动作是**去云控制台轮换/禁用密钥**,历史清理其后——此条随实施写进 git-workflow.md。
|
||||
- 实施时顺手核对三仓 .gitignore 对 `.env` 的覆盖(api 仓 compose 依赖 `.env`,规则应已有,实施时以 `git check-ignore` 取证)。
|
||||
|
||||
## 5. CI 增量评估
|
||||
|
||||
### 5.1 新模块接入:确认零成本(同仓模块方案下)
|
||||
|
||||
patbond-api 是 maven 聚合工程(根 pom `<modules>` 现有 common/user/auth/pet 四个)。若 M3 沿 ADR-009 模式在 api 仓内新建 `patbond-community` / `patbond-media` 模块:**根 pom 加一行 `<module>`,ci.yml 零改动**,`./mvnw -B clean test` 自动覆盖新模块(含其 Testcontainers 测试)。**确认零成本,无待拍板。**
|
||||
|
||||
仅当选择独立新仓(当前无此计划)才有增量:复制既有 ci.yml(三仓模板已统一:手动 checkout + apt/镜像装工具链)+ 仓库设置启用 Actions,runner 是实例级共享的,无需新注册,估计半小时内。
|
||||
|
||||
### 5.2 E2E 烟囱进 CI:技术可行但不建议进 push 门禁(**待拍板 #6,倾向不做**)
|
||||
|
||||
可行性核查(基于 ci-runner-setup.md 与 act_runner 现状):
|
||||
|
||||
- **docker.sock 已挂进 job 容器**(Testcontainers 依赖,实测可用),job 内跑 `docker compose up` 起的是宿主 sibling 容器——技术上通。
|
||||
- 但有四项实际成本:
|
||||
1. **网络**:compose 端口发布在宿主,job 容器内 `127.0.0.1:8081-8083` 不可达,E2E 脚本的 base URL 需改造为可注入,并让 job 容器走宿主网关 IP 或直接加入 compose 网络;
|
||||
2. **工具链**:job 镜像需补 docker CLI + compose 插件;
|
||||
3. **时长**:`mvnw package` + 三个镜像 build + 全栈起 + 11 场景,估计给流水线加 5–10 分钟(现 backend-test 5m18s,翻倍以上);
|
||||
4. **跨仓**:脚本在 flutter 仓、compose 在 api 仓,任一仓的 push CI 跑它都要 clone 另一仓,触发归属含糊。
|
||||
- **建议**:push 门禁维持现状(快、单仓、职责清晰);E2E 保持 M2 已验证的「波次收尾手动跑、证据入档」模式,并作为发布 checklist 第 2 步的强制项。若要自动化,做成独立的 `workflow_dispatch` 手动触发工作流(发布前一键跑),M3 内低优先,不占开工路径。
|
||||
|
||||
## 6. 待拍板事项汇总
|
||||
|
||||
| # | 事项 | 推荐 | 见 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | ADR-011 PR 条款降级:类别触发(迁移/契约/依赖)取消,改为仅「两人并行同仓」与「首次发布后影响 main 的变更」两种情形强制 PR;分支保护随之改为只对发布分支启用 | 采纳修订 | 2.3 |
|
||||
| 2 | 发布分支统一命名为 `main`(修订 ADR-011 的 master 措辞),顺手删 api 本地孤儿 master | 采纳 | 3.1 |
|
||||
| 3 | api 远端 main 与 dev 历史不相干的一次性处理:Gitea 平台删除重建(选项 A) | 选项 A | 3.1 |
|
||||
| 4 | M3 末执行第一次 dev→main 发布,采纳 §3.3 checklist(含版本号定名) | 采纳 | 3.2/3.3 |
|
||||
| 5 | 防泄漏第二层(CI 兜底 grep + 云凭证模式增补)升为 M3 第一波必做、先于任何对象存储凭证落地;第一层 pre-commit 维持推荐 | 采纳 | 4.2 |
|
||||
| 6 | E2E 烟囱不进 push 门禁;可选做 workflow_dispatch 手动工作流(低优先) | 不进门禁 | 5.2 |
|
||||
|
||||
采纳后需要落实的改动(本报告未执行):ADR-011 修订、git-workflow.md 增补(PR 情形条款、发布流程、泄漏应急)、三仓 ci.yml 加防泄漏 step、api main 重建操作、本报告挂入 mkdocs 导航。
|
||||
@@ -62,6 +62,15 @@ nav:
|
||||
- 28 E2E 烟囱收官: development/iterations/iteration-2/28-e2e-smoke-report.md
|
||||
- 29 M2 收官总结: development/iterations/iteration-2/29-m2-summary.md
|
||||
- 30 真机补验清单: development/iterations/iteration-2/30-device-verification-checklist.md
|
||||
- 第三迭代:
|
||||
- 01 任务分解: development/iterations/iteration-3/01-pm-task-breakdown.md
|
||||
- 02 后端技术评估: development/iterations/iteration-3/02-backend-technical-assessment.md
|
||||
- 03 Flutter 技术评估: development/iterations/iteration-3/03-flutter-technical-assessment.md
|
||||
- 04 现状核实: development/iterations/iteration-3/04-reality-check.md
|
||||
- 05 社区 UI 设计规范: development/iterations/iteration-3/05-community-ui-spec.md
|
||||
- 06 埋点规划: development/iterations/iteration-3/06-experiment-tracking-plan.md
|
||||
- 07 证据基线审计: development/iterations/iteration-3/07-evidence-baseline-audit.md
|
||||
- 08 Git 工作流规划: development/iterations/iteration-3/08-git-workflow-plan.md
|
||||
- API:
|
||||
- 契约说明: api/index.md
|
||||
- 架构:
|
||||
|
||||
Reference in New Issue
Block a user