Files
lixi a611acb358
CI / docs-build (push) Successful in 32s
docs: M3 第二波收口——报告 15~20 入档挂导航
- 15~17 社区后端纵切三单(帖子/Feed 作者链路/评论互动关注,226→310)
- 18 契约冻结 v1.3.0(31 路径/43 操作,26 项修正照单全收)
- 19 快照同步与全仓契约矩阵(173 格零漂移,修 allOf 校验盲区,→325)
- 20 收口总表:定型语义汇总(第三波接入依据)与质量事件记录

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-09 11:38:42 +08:00

126 lines
13 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.
# M3 第二波帖子生命周期施工报告(T3-04:草稿/编辑/发布/删除)
> 作者:Senior Developer(后端)
> 日期:2026-09-09
> 工单:T3-04(帖子生命周期,第二波关键路径首单)
> 代码基线:patbond-api `263cd88`226 测试全绿)→ 交付 `101ac0f`(251 测试全绿)
> 结论先行:**帖子域五端点(创建/详情/编辑与发布/软删/我的列表)全落地 patbond-community;权限与错误语义定型(T3-10 冻结输入之一):403/40301 只发给「可见但无权」,一切不可见合并 404/40403 防枚举,hidden/archived 对作者同样 404;创建型幂等按 ADR-019 落 `uq_posts_author_idempotency` + 规范化 request_hash 实证;asset 校验取「同库只读 media.assets」(ADR-017 同一先例);与契约草案偏差 6 项逐条记录;全套 251 测试全绿(+25)。**
---
## 1. 提交清单
按工单一逻辑提交,已推送 `origin/dev`
| 提交 | 内容 |
| --- | --- |
| `101ac0f` | T3-04:帖子生命周期五端点 + 幂等/乐观锁/可见性矩阵集成测试(含 §5 两处附带修正) |
## 2. 端点与错误/权限语义定型表(T3-10 契约冻结输入)
### 2.1 端点清单(全部在 patbond-community :8084,强制 Bearer 鉴权)
| 端点 | 成功 | 语义要点 |
| --- | --- | --- |
| `POST /api/v1/posts` | 201 + Post | 创建草稿或直接发布(`status: published` 时服务端写 publishedAt);`Idempotency-Key` 必带;纯文字帖合法(D3-4,图片不必填) |
| `GET /api/v1/posts/{postId}` | 200 + Post | published 对全部登录用户开放;draft 仅作者;响应含 likedByMe/bookmarkedByMe |
| `PATCH /api/v1/posts/{postId}` | 200 + Post(新 version | 部分更新 + version 乐观锁,仅作者;发布 = `status: published` 状态迁移,无独立端点;media 出现即整组替换 |
| `DELETE /api/v1/posts/{postId}` | 200 + VoidEnvelope | 软删 `deleted_at`,仅作者;删除后一切读路径 404 |
| `GET /api/v1/me/posts` | 200 + `{items,nextCursor,hasMore}` | 作者视角含草稿;`(created_at DESC, id DESC)``ix_posts_author_created`keyset 游标;`status` 过滤可选(draft\|published |
### 2.2 权限矩阵定型(有测试逐格锚定)
| 帖子状态 \ 调用者 | 作者读 | 他人读 | 作者写(PATCH/DELETE | 他人写 |
| --- | --- | --- | --- | --- |
| draft | 200 | **404/40403** | 200 | **404/40403**(不可见,非 403 |
| published | 200 | 200 | 200 | **403/40301** |
| hidden / archived(运营态,D3-7 | **404/40403(作者同样)** | 404/40403 | 404/40403 | 404/40403 |
| 软删 / 不存在 | 404/40403 | 404/40403 | 404/40403 | 404/40403 |
定型原则:**403/40301 只发给对资源「可见」的调用者**(不泄露新信息);一切不可见情形(不存在/软删/hidden/archived/他人 draft)响应逐字节一致(防枚举)。hidden 对作者也不露——M3 无任何端点能产生或解除 hidden,契约 status 枚举保持 `[draft, published]` 两值,不为运营态开读侧口子(草案预设「不露」的定型,读侧同样适用)。
### 2.3 错误码定型(本单启用 4 码,均按草案取值,无新码位)
| 业务码 | HTTP | 稳定名 | 本单触发场景(全部有测试) |
| --- | --- | --- | --- |
| 40301 | 403 | POST_ACCESS_DENIED | 非作者改/删他人**已发布**帖 |
| 40403 | 404 | POST_NOT_FOUND | §2.2 全部不可见情形合并 |
| 40905 | 409 | IDEMPOTENCY_PAYLOAD_MISMATCH | 同 Idempotency-Key 不同规范化 payload |
| 42203 | 422 | MEDIA_NOT_READY | 引用本人 uploading/failed 态 asset |
复用既有码(行为实证):40000(content 缺失/超长、category=ai_creation、status 非法值或非法迁移、Idempotency-Key 缺失/空白/超 128、position 不连续/重复、isCover 多于一、assetId 重复、空白 title、limit 越界、游标非法、非 UUID 路径参数、version 缺失)、40101(无/坏 token)、40401petId 引用不可见宠物,沿 pets 域防枚举语义:他人宠物与不存在同响应)、40405(asset 不存在/非本人/deleted 合并,T3-03 已入 ErrorCode)、40902version 过期)。
### 2.4 发布与幂等语义定型
- **发布**`PATCH {status: "published"}` 是唯一开放迁移(draft→published),publishedAt 恰写一次,`ck_posts_publish_state` 库层兜底;**对已发布帖重复提交 `status: published` 为幂等 no-op200version 照常 +1),不是 400**——同态提交不是迁移,弱网重试友好;`published→draft` 与 hidden/archived 目标值被请求枚举拒为 400/40000。发布时内容非空由构造保证(content 全程必填 1~10000,无「空草稿」可发布)。
- **创建型幂等(ADR-019 实证)**:`Idempotency-Key` 必带(1~128,trim 后计),落 `uq_posts_author_idempotency``ON CONFLICT DO NOTHING` + 回读比对 request_hash——同键同 hash 返回首帖(同样 201,库中恰一行);同键异 hash 409/40905;**键按作者隔离**(跨用户同键各自成帖,有测试);并发同键重试由唯一约束收敛,输家回读赢家行。
- **request_hash 规范化定型**hash 对象是**规范化后的创建命令**title/content/caption trim、category/status 缺省展开、media position/isCover 解析完成后的规范串 SHA-256,32 字节合 `ck_posts_idempotency`),非请求原始字节——语义相同、仅格式不同(空白、缺省写全)的重试仍命中首帖(有测试)。
- **幂等重试撞已删首帖**(草案未覆盖的边界,本单定型):同键同 hash 但首帖已被删 → 404/40403(重试询问的资源已消亡,沿防枚举合并;不复活、不另建)。
### 2.5 软删语义定型(D3-7
`deleted_at` 是全域唯一删除判定基准(一切读路径过滤)。已发布帖软删时 status 同步归档为 `archived` 以满足 `ck_posts_publish_state`published 行不得带 deleted_at),草稿保持原 status——归档后的 status 值纯属内部记账,对外恒 404。重复删除与「删不存在的帖」同响应 404/40403。删除不提供恢复端点(M3 无回收站)。
### 2.6 media 挂接定型
- position:**全给或全不给**——全给须恰为 0..n-1 不重复;全不给按数组序。混合 400/40000。
- isCover:至多一个 true`uq_post_media_cover` 库层兜底);全 false 时**服务端把 position 0 行落库置 is_cover=true**(比草案「展示层取 position 0」更强:库内恒有唯一封面行,T3-05 取封面免特判)。
- PATCH media **整组替换**(草案预设定型):字段出现即删旧插新;`[]` 清空为纯文字帖;缺席不动。
- ≤9 图(D3-4);caption trim 后 ≤300;同帖 assetId 不重复(`UNIQUE (post_id, asset_id)` 兜底)。
## 3. asset 校验取舍说明(13 号报告联调协议的引用侧落地)
**定型:同库只读 `media.assets``MediaAssetGateway`JdbcClient 单查询),不调 user 内部接口。**理由:
1. ADR-017 同一先例——作者公开信息即为「community 跨 schema 只读 identity」,asset 校验同构;拆库时两者一起切内部批量接口,同一演进逻辑。
2. `post_media.asset_id → media.assets` 的外键本就要求同库,网络接口不消除该耦合,只添故障面与延迟。
3. 13 号报告交接明言本模块「只做 asset 只读校验」;media 状态机的一切**写**操作仍归 patbond-user,本模块零写入。
校验语义(每项有测试):不存在 / 非本人 / `status='deleted'` → 404/40405(防枚举合并,与 media 域自身语义一致);本人所有但 uploading/failed → 422/42203。
**读取侧 URL**`media[].url` 为预签名 GET(T3-03 定型「私有桶 + 签名读」,TTL 同 `download-ttl` 配置),由 community 侧 `MediaUrlSigner` **本地 SigV4 计算**生成——预签名不联网,本服务不与对象存储建立任何连接。配置与 user 共用同组环境变量(`PATBOND_MINIO_PUBLIC_ENDPOINT/ACCESS_KEY/SECRET_KEY`compose 已为 community 服务注入,无需 depends_on minio);未配置时服务照常启动、`url` 为 null(与 JWT 公钥未配置同一降级先例)。
## 4. 与契约草案(openapi-community-draft.yaml)偏差清单
| # | 草案 | 实现定型 | 理由 / 冻结动作 |
| --- | --- | --- | --- |
| 1 | `Post.author` 为 AuthorSummaryrequired | **占位 `authorId`(裸 UUID 字符串)** | 工单口径:作者公开资料随 T3-05 的 /internal 批量接口落地;**冻结前须由 T3-05 回填 AuthorSummary**,本单不预造假数据(R10 |
| 2 | `Post.status` 对作者是否露 hidden 留白(草案预设不露) | 定型**不露**hidden/archived 对作者读写一律 404/40403 | M3 无端点能产生 hidden,枚举保持两值;运营台账属 M4+ |
| 3 | 「其余迁移 400/40000」 | **published→published 为幂等 no-op200**,非 400 | 同态提交不是迁移;弱网重发 PATCH 不应报错。400 保留给真非法目标值(draft/hidden/archived |
| 4 | 幂等重试语义未覆盖「首帖已删」 | 同键同 hash 撞已删首帖 → **404/40403** | §2.4;冻结时在 `IdempotencyKeyRequiredHeader` 描述补一句 |
| 5 | 「request_hash(请求体规范化 SHA-256)」未定规范化细则 | 规范化 = trim + 缺省展开 + media 解析后的规范串(§2.4) | 冻结时把「语义等价即命中」写入头参数描述 |
| 6 | `PostMediaItem.url` required | 保持事实 required(生产恒配置),但**对象存储未配置时为 null** | 降级路径与 user 模块同规;冻结时在 url 描述注明「未配置降级」或维持 required + 运维前提,建议后者 |
另两处为**草案预设的确认**(非偏差):PATCH media 整组替换成立(草案决策 5 删除「草案态」标注即可);isCover 全 false 取 position 0 成立(实现落库置真,见 §2.6)。
## 5. 测试数变化:226 → 251+25,0 回归)
| 模块 | 基线 | 交付 | 新增内容 |
| --- | --- | --- | --- |
| patbond-common | 3 | 3 | ErrorCode 增 4 码(40301/40403/40905/42203),无行为变化 |
| patbond-user | 88 | 88 | — |
| patbond-auth | 39 | 39 | — |
| patbond-pet | 89 | 89 | — |
| patbond-community | 7 | 32 | 帖子域 25 例(postgres:18 Testcontainers 真库,V1..V5 全链) |
| **合计** | **226** | **251** | `JAVA_HOME=<你的 JDK17 路径> ./mvnw clean test` 一次通过,BUILD SUCCESS |
新增 25 例按工单六类路径 + 专项覆盖:
- `PostLifecycleIntegrationTest`(14):全形态创建(草稿/直接发布/挂宠物)、六类路径——成功/参数错(content 缺失、ai_creation、hidden、空白 title、幂等键缺失/空白/超长)/不存在(随机 UUID、畸形 UUID)/无权限(403/404 边界四格)/并发冲突(**真双线程并发 PATCH,恰一个 200 一个 40902**,外加串行过期 version)/幂等重试(见下);**草稿可见性矩阵**(作者/他人 × draft/published/hidden/软删逐格);软删墓碑落库实证(archived + deleted_at);我的列表 keyset 翻页不丢不重 + status 过滤 + 三种非法入参;likedByMe/bookmarkedByMe 视角实证。
- `PostIdempotencyIntegrationTest`(5,幂等专项):同键同 hash(库中恰一行)/同键异 hash 40905/**跨用户同键**各自成帖/语义等价异格式仍命中/重试撞已删首帖 404。
- `PostMediaAttachIntegrationTest`(6):数组序 + 封面缺省、显式 position/isCover、他人与不存在 asset 合并 40405、uploading/failed 42203、四种形态违规 40000、PATCH 整组替换(换图/缺席不动/清空)。预签名 GET URL 形态在位断言(指向 public-endpoint、含 X-Amz-Signature)——签名是本地计算,测试注入假凭证即可,**无需 MinIO 容器**。
契约一致性测试按工单暂不加,冻结后统一入 patbond-community 契约矩阵(T3-10/T3-11)。
附带修正两处:
1. 骨架期 `BearerAuthIntegrationTest.validTokenPassesTheFilter` 的探针路径由 `/api/v1/posts`(现已是真实路由)改为未映射路径,测试意图不变。
2. `check-secrets.sh --all`CI 兜底门禁)的 KEY-ASSIGN 规则会把配置类 setter 的「字段 = 同名形参」自赋值误报为凭证字面量——patbond-user `MediaProperties` 两处属基线既有误报,新增 `CommunityMediaProperties` 同形态再中两处。按脚本处置指引第 2 条最小化解:四处 setter 形参改名 `value`(行为零变化,`@ConfigurationProperties` 绑定按 setter 名不按形参名),不单方面改三仓同构的规则表;是否给规则加自赋值豁免留待三仓同步时定。修正后 `--all` 全仓通过。
## 6. 遗留与交接
- **T3-10 冻结回填**:§2 四张定型表 + §4 偏差 6 项即帖子域冻结输入;偏差 #1AuthorSummary)等 T3-05 落地后一并回填。
- **T3-05/06/07 衔接**:可见性谓词(`status='published' AND deleted_at IS NULL`)与「不可见一律 40403」语义直接复用;`MediaUrlSigner`/`MediaAssetGateway` 即 Feed 封面签名与校验的现成件;likedByMe 批量查询模式已在列表路径验证。
- **P9 共享设施**:UuidV7/游标/幂等件已是第三份复制,下沉 common 的拍板仍悬置。
- 405(方法不匹配路由)目前落通用 500——全部四个服务同现状,属横切收口项,不在本单发明新语义。