Files
patbond-doc/docs/development/iterations/iteration-3/15-post-lifecycle-report.md
T
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

13 KiB
Raw Blame History

M3 第二波帖子生命周期施工报告(T3-04:草稿/编辑/发布/删除)

作者:Senior Developer(后端) 日期:2026-09-09 工单:T3-04(帖子生命周期,第二波关键路径首单) 代码基线:patbond-api 263cd88226 测试全绿)→ 交付 101ac0f251 测试全绿) 结论先行:帖子域五端点(创建/详情/编辑与发布/软删/我的列表)全落地 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_createdkeyset 游标;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_idempotencyON 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_statepublished 行不得带 deleted_at),草稿保持原 status——归档后的 status 值纯属内部记账,对外恒 404。重复删除与「删不存在的帖」同响应 404/40403。删除不提供恢复端点(M3 无回收站)。

2.6 media 挂接定型

  • position全给或全不给——全给须恰为 0..n-1 不重复;全不给按数组序。混合 400/40000。
  • isCover:至多一个 trueuq_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.assetsMediaAssetGatewayJdbcClient 单查询),不调 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。

读取侧 URLmedia[].url 为预签名 GET(T3-03 定型「私有桶 + 签名读」,TTL 同 download-ttl 配置),由 community 侧 MediaUrlSigner 本地 SigV4 计算生成——预签名不联网,本服务不与对象存储建立任何连接。配置与 user 共用同组环境变量(PATBOND_MINIO_PUBLIC_ENDPOINT/ACCESS_KEY/SECRET_KEYcompose 已为 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 --allCI 兜底门禁)的 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——全部四个服务同现状,属横切收口项,不在本单发明新语义。