- 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>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 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)、40401(petId 引用不可见宠物,沿 pets 域防枚举语义:他人宠物与不存在同响应)、40405(asset 不存在/非本人/deleted 合并,T3-03 已入 ErrorCode)、40902(version 过期)。
|
||||
|
||||
### 2.4 发布与幂等语义定型
|
||||
|
||||
- **发布**:`PATCH {status: "published"}` 是唯一开放迁移(draft→published),publishedAt 恰写一次,`ck_posts_publish_state` 库层兜底;**对已发布帖重复提交 `status: published` 为幂等 no-op(200,version 照常 +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` 为 AuthorSummary(required) | **占位 `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-op(200)**,非 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 项即帖子域冻结输入;偏差 #1(AuthorSummary)等 T3-05 落地后一并回填。
|
||||
- **T3-05/06/07 衔接**:可见性谓词(`status='published' AND deleted_at IS NULL`)与「不可见一律 40403」语义直接复用;`MediaUrlSigner`/`MediaAssetGateway` 即 Feed 封面签名与校验的现成件;likedByMe 批量查询模式已在列表路径验证。
|
||||
- **P9 共享设施**:UuidV7/游标/幂等件已是第三份复制,下沉 common 的拍板仍悬置。
|
||||
- 405(方法不匹配路由)目前落通用 500——全部四个服务同现状,属横切收口项,不在本单发明新语义。
|
||||
Reference in New Issue
Block a user