docs: M3 开工分析 8 份报告入档 + ADR-016~021 拍板决策
CI / docs-build (push) Successful in 55s

- 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:
2026-09-08 16:02:53 +08:00
parent e68b6553ca
commit d2867826d3
10 changed files with 2072 additions and 0 deletions
@@ -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 FKposts → creation.generation_jobsM4 补回)并补 `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:18pet 模块生产 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 官方镜像含 contribTestcontainers 与 compose 均无障碍)。M3 范围没有搜索需求,该索引理论上可裁;但扩展 + 索引成本极低、剪了就偏离目标模型,建议照建(P7)。
## 2. 改动面评估
| 改动面 | 内容 | 量级 |
| --- | --- | --- |
| 新模块 | `patbond-community`:8084):feed/posts/comments/likes/bookmarks/follows/topics 约 7 组资源 | 大(M3 主体) |
| media | 上传流程(预签名签发 + complete 确认 + 清理任务),归 patbond-userP2 | 中 |
| 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 内最省事,但 M4generation_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()` 去掉,应用侧 UUIDv7users/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:自托管 MinIOcompose 内) | 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 不同 payloadrequest_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 重建**(无物化 FeedMVP 拉模型)。
- 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 |