Files
lixi d2867826d3
CI / docs-build (push) Successful in 55s
docs: M3 开工分析 8 份报告入档 + ADR-016~021 拍板决策
- 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>
2026-09-08 16:02:53 +08:00

265 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |