- 28 E2E 烟囱:14/14 场景、契约偏差 0、M3 四条验收标准逐条取证 (两次不同 limit 全量翻页有序 id 逐位相等、删除前后差集验证、psql 库层互证) - 29 收官总结:终态对照开工基线(测试 191/272→334/502、契约 18→31 路径冻结、 ADR 001~021、四容器→六容器 + MinIO)、遗留清单与 M4 方向输入 - iteration-3/index.md 进展看板补建;feature-checklist 新增 M3 第 10~12 节 - 另登记 2 项 E2E 观察项:widthPx/heightPx 恒 null、eventVersion 口径未定型 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,757 @@
|
||||
# 28 M3 收官:E2E 烟囱测试报告(T3-21)
|
||||
|
||||
- 执行人:Frontend Developer
|
||||
- 日期:2026-09-10
|
||||
- 环境:patbond-flutter (dev 分支) + patbond-api(docker compose 六容器编排,**代码零改动**)
|
||||
- 测试脚本:`patbond-flutter/test_e2e_m3_manual.dart`(commit `0e87413`,已推送 origin/dev)
|
||||
- 参照模式:iteration-2/28 号 M2 收官报告(格式与取证标准沿用)
|
||||
- 冻结契约:`patbond-doc/docs/api/openapi.yaml` **v1.3.0**(community / media 域为准)
|
||||
|
||||
---
|
||||
|
||||
## 0. 执行概要
|
||||
|
||||
### 测试目标
|
||||
|
||||
M3 第四波收官(工单 T3-21):在 compose 真实后端上跑通社区完整链路烟囱并收集证据——
|
||||
注册两账号 → 两步上传直传 MinIO → 草稿发布 → 另一客户端 Feed 可见 → 预签名 GET 字节往返 →
|
||||
点赞/收藏/评论/关注全互动面 → 游标分页全量翻页 → 软删出 Feed → 防枚举 → v3 埋点落库 →
|
||||
幂等重放,**并对 M3 四条验收标准逐条取证**。
|
||||
|
||||
### 测试结果
|
||||
|
||||
**✓ 14/14 场景全部通过**(首次运行一次通过;同环境复跑再次 14/14,第二轮 Feed 全量
|
||||
91 条 / 13 页,证明分页断言不依赖固定数据规模)
|
||||
|
||||
- Docker Compose **六容器**健康运行(postgres + minio + auth:8081 + user:8082 +
|
||||
pet:8083 + community:8084)
|
||||
- 契约一致性:HTTP 状态码、业务错误码、信封结构、字段形态、分页语义、幂等语义与
|
||||
冻结契约 v1.3.0 完全一致——**契约偏差数:0 个**
|
||||
- Flutter 门禁三命令全绿:`dart format`(144 files, 0 changed)/ `flutter analyze`
|
||||
(No issues)/ `flutter test`(**502 passed**, 2 skipped)
|
||||
- 数据库证据齐备:`community` 五表 + `media.assets` + `platform.product_events`
|
||||
psql 逐项查证一致;Feed 谓词行数与脚本全量翻页条数**交叉核对相等**(65 = 65)
|
||||
|
||||
### M3 四条验收标准
|
||||
|
||||
| # | 验收标准 | 结论 |
|
||||
| --- | --- | --- |
|
||||
| ① | 发布后可在另一客户端看到 | ✓ 通过(场景 4) |
|
||||
| ② | 重复点赞不重复计数 | ✓ 通过(场景 6) |
|
||||
| ③ | 分页不丢失不重复 | ✓ 通过(场景 10) |
|
||||
| ④ | 删除或隐藏内容不可继续出现在公共 Feed | ✓ 通过(场景 11) |
|
||||
|
||||
逐条证据见 §5。
|
||||
|
||||
### ⚠️ 真机四项挂起(显著标注:**真机待补验,本报告不含其证据**)
|
||||
|
||||
真机不可用(设备未到位),`docs/development/device-verification.md`「M3 预登记」四项
|
||||
按既定方案 A 挂起。桌面/脚本侧**不可替代**的原因已逐项记录在清单里:
|
||||
|
||||
| # | 挂起项 | 桌面/脚本不可替代的原因 |
|
||||
| --- | --- | --- |
|
||||
| 1 | **媒体上传弱网表现** | 蜂窝/弱网限速、飞行模式掐断、HEIC 与拍摄方向、凭据过期挂起 >10min——Linux 桌面无 image_picker / flutter_image_compress 平台实现(用替身) |
|
||||
| 2 | **乐观更新真机手感** | 240ms 弹性动画帧率、快速连点合并、断网零动画跳变、减弱动态开关,均为真机帧率与手感范畴 |
|
||||
| 3 | **Feed 图片加载** | 局域网/蜂窝对 MinIO 可达性差异、滚动缓存命中、TTL 过期重取,需真实网络与真机内存缓存 |
|
||||
| 4 | **社区事件落库(客户端链路)** | Linux 桌面 `platform=linux` 不在契约枚举内,整批 400 被拒(既有预期)——**本报告以脚本直连 `/api/v1/events`(platform=android 模拟真机值)替代验证服务端链路**;真机端 AnalyticsClient → 持久化队列 → 冲刷的端上链路待真机补验 |
|
||||
|
||||
另:M2 遗留的两项真机挂起(Android 事件落库观察、SessionTracker 30min 会话超时手测)
|
||||
同样未闭环,仍在清单内。
|
||||
|
||||
### 脱敏声明
|
||||
|
||||
- 全部 access token 截断至前 20 字符 + `<REDACTED>`
|
||||
- **全部预签名 URL(PUT 直传与 GET 读取)的签名 query 整体替换为 `<SIGNATURE_REDACTED>`**,
|
||||
仅保留 host + 对象路径
|
||||
- 密码不出现在任何输出;`.env` 内容、MinIO 根凭据、`PATBOND_INTERNAL_TOKEN` 均未引用
|
||||
- 幂等键值以 `<KEY-1>` 占位打印
|
||||
- psql 证据中 user_id / event_id 截断为前 8 位前缀
|
||||
|
||||
---
|
||||
|
||||
## 1. 后端启动与健康检查
|
||||
|
||||
### 1.1 构建与启动(patbond-api 代码零改动)
|
||||
|
||||
> 命令中的 `<工作区>` 为你本机存放三仓的父目录(文档不写死本机路径)。
|
||||
|
||||
```bash
|
||||
cd <工作区>/patbond-api
|
||||
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package
|
||||
# BUILD SUCCESS(exit 0)
|
||||
|
||||
docker compose up -d --build
|
||||
# Container patbond-minio-1 Healthy
|
||||
# Container patbond-postgres-1 Healthy
|
||||
# Container patbond-user-1 Started
|
||||
# Container patbond-auth-1 Started
|
||||
# Container patbond-community-1 Started
|
||||
# Container patbond-pet-1 Started
|
||||
```
|
||||
|
||||
### 1.2 容器健康状态(六容器)
|
||||
|
||||
```text
|
||||
NAMES STATUS PORTS
|
||||
patbond-pet-1 Up 10 minutes 0.0.0.0:8083->8083/tcp
|
||||
patbond-auth-1 Up 10 minutes 0.0.0.0:8081->8081/tcp
|
||||
patbond-community-1 Up 10 minutes 0.0.0.0:8084->8084/tcp
|
||||
patbond-user-1 Up 10 minutes 0.0.0.0:8082->8082/tcp
|
||||
patbond-postgres-1 Up 10 minutes (healthy) 5432/tcp
|
||||
patbond-minio-1 Up 10 minutes (healthy) 0.0.0.0:9000->9000/tcp
|
||||
```
|
||||
|
||||
### 1.3 服务就绪验证
|
||||
|
||||
```text
|
||||
docker logs patbond-user-1 | grep Started → Started UserApplication in 12.526 seconds
|
||||
docker logs patbond-auth-1 | grep Started → Started AuthApplication in 9.371 seconds
|
||||
docker logs patbond-pet-1 | grep Started → Started PetApplication in 8.645 seconds
|
||||
docker logs patbond-community-1 | grep Started → Started CommunityApplication in 10.999 seconds
|
||||
|
||||
# Flyway(迁移链由 user 服务统一执行)
|
||||
o.f.core.internal.command.DbValidate : Successfully validated 5 migrations
|
||||
o.f.core.internal.command.DbMigrate : Current version of schema "public": 5
|
||||
o.f.core.internal.command.DbMigrate : Schema "public" is up to date.
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9000/minio/health/live # 200
|
||||
curl -s http://127.0.0.1:8084/api/v1/feed
|
||||
# {"code":40101,"message":"token 无效或过期","data":null} ← 无 token 预期 401
|
||||
curl -s http://127.0.0.1:8082/api/v1/me
|
||||
# {"code":40101,"message":"token 无效或过期","data":null}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 测试脚本
|
||||
|
||||
`test_e2e_m3_manual.dart`(纯 dart HttpClient 脚本,无 Flutter 运行时依赖,与 M1 版
|
||||
`test_e2e_manual.dart`、M2 版 `test_e2e_m2_manual.dart` 并列放**仓库根目录**,
|
||||
**不在 `test/` 目录**、不被 `flutter test` 收集)。
|
||||
|
||||
- 随机生成账号 `e2e_m3_a_<timestamp>` / `e2e_m3_b_<timestamp>` 避免冲突
|
||||
- 固定测试图内嵌为常量:1×1 JPEG,344 字节,
|
||||
sha256 `32142d9c…6973ce8`(`sha256sum` 实测值硬编码,不引入 crypto 依赖)
|
||||
- 分页断言不依赖数据集规模:对同一 Feed 做**两种页大小的全量翻页并逐位比对**
|
||||
- 运行方式:`docker compose up -d` 后在 patbond-flutter 目录 `dart run test_e2e_m3_manual.dart`
|
||||
|
||||
本次取证运行(第一轮)关键标识:
|
||||
|
||||
```text
|
||||
E2E_USER_ID_A=01a08a1c-d609-7804-9d57-79f3eec0294e
|
||||
E2E_USER_ID_B=01a08a1c-d720-744e-a0da-589efb1bc1bf
|
||||
E2E_POST_ID=01a08a1c-da21-7f9b-a23f-e26ac5b8a474
|
||||
E2E_ASSET_ID=01a08a1c-d7a6-7099-a638-f4d8d61eeb67
|
||||
E2E_DELETED_POST_ID=01a08a1c-df1c-79a9-95bc-af1ce14fe2cd
|
||||
E2E_DRAFT_POST_ID=01a08a1c-df99-7f81-82d9-aa51a5cbf11a
|
||||
E2E_SESSION_ID=8857898d-0ce0-4a57-bda6-bccac98b57b6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. E2E 烟囱测试执行记录(14 场景)
|
||||
|
||||
### 3.1 场景 1:注册账号 A、B(:8081)
|
||||
|
||||
```text
|
||||
[1/14] 注册账号 A、B(:8081)
|
||||
POST /api/v1/auth/register (A) → 200
|
||||
✓ A 注册成功
|
||||
userId(A): 01a08a1c-d609-7804-9d57-79f3eec0294e
|
||||
accessToken(A): eyJhbGciOiJSUzI1NiJ9...<REDACTED>
|
||||
POST /api/v1/auth/register (B) → 200
|
||||
✓ B 注册成功
|
||||
userId(B): 01a08a1c-d720-744e-a0da-589efb1bc1bf
|
||||
accessToken(B): eyJhbGciOiJSUzI1NiJ9...<REDACTED>
|
||||
✓ A/B 为两个独立账号(模拟两客户端)
|
||||
```
|
||||
|
||||
两账号即验收标准 ① 的「两个客户端」——A 为发布方,B 为消费方,全程用各自 token。
|
||||
|
||||
### 3.2 场景 2:两步上传——createUpload → 预签名 PUT 直传 MinIO → confirm ready
|
||||
|
||||
```text
|
||||
[2/14] A 两步上传图片:createUpload(:8082)→ 预签名 PUT → confirm
|
||||
POST /api/v1/media/uploads → 201
|
||||
✓ asset 登记成功(201),返回预签名直传凭据
|
||||
assetId: 01a08a1c-d7a6-7099-a638-f4d8d61eeb67
|
||||
uploadUrl: http://127.0.0.1:9000/patbond-media/post_image/2026/09/01a08a1c-…eb67?<SIGNATURE_REDACTED>
|
||||
method: PUT / expiresAt: 2026-09-10T07:09:01.176666130Z
|
||||
requiredHeaders: {Content-Type: image/jpeg}
|
||||
✓ requiredHeaders 恒且仅一键 {Content-Type: image/jpeg}
|
||||
✓ 预签名 URL 携带 SigV4 query 签名族(直传不经应用服务器)
|
||||
PUT <presigned>(344 字节,原样携带 requiredHeaders) → 200
|
||||
✓ 直传 MinIO 成功(存储侧接受签名)
|
||||
POST /api/v1/media/uploads/{assetId}/complete → 200
|
||||
✓ uploading→ready(byteSize=344,widthPx=null heightPx=null,readyAt 已写,url 现签非空)
|
||||
asset.url: http://127.0.0.1:9000/patbond-media/post_image/2026/09/01a08a1c-…eb67?<SIGNATURE_REDACTED>
|
||||
POST .../complete(重复确认)→ 200
|
||||
✓ 已 ready 重复 complete 幂等 200 同一 asset(现签新 GET URL)
|
||||
```
|
||||
|
||||
契约验证:`kind/purpose/mimeType/byteSize` 白名单通过;objectKey 由服务端生成
|
||||
(`post_image/2026/09/<assetId>`,不含任何用户输入);`expiresAt` = 签发 + 10min TTL;
|
||||
直传 URL 直指 MinIO:9000(不经应用服务器);已 ready 重复 complete 幂等 200。
|
||||
|
||||
> **观察项**:`widthPx/heightPx` 确认为 `null`——契约 schema 标注 `nullable: true`
|
||||
> 故响应形态合规,但同处描述写「complete 后回填」,实现侧**未做宽高探测**
|
||||
> (`patbond-user/.../media/` 无 ImageIO 类调用)。详见 §7 观察项 1。
|
||||
|
||||
### 3.3 场景 3:创建草稿(Idempotency-Key 必带)→ 引用 ready asset → PATCH 发布
|
||||
|
||||
```text
|
||||
[3/14] A 创建草稿(:8084,引用 ready asset)→ PATCH 发布
|
||||
POST /api/v1/posts (status=draft, Idempotency-Key 已带) → 201
|
||||
✓ 草稿创建成功(201)
|
||||
postId: 01a08a1c-da21-7f9b-a23f-e26ac5b8a474 / status: draft / version: 0 / publishedAt: null
|
||||
✓ 草稿态 status=draft 且 publishedAt=null(发布时才恰写一次)
|
||||
✓ media 挂接 1 图:position=0(按数组序)、isCover 由服务端置真(库内恒有唯一封面)
|
||||
PATCH /api/v1/posts/{postId} (draft→published, version=0) → 200
|
||||
✓ 发布成功:status=published,publishedAt 已写,version 0→1
|
||||
publishedAt: 2026-09-10T06:59:02.144899Z
|
||||
media[0].url: http://127.0.0.1:9000/patbond-media/…?<SIGNATURE_REDACTED>
|
||||
```
|
||||
|
||||
契约验证:两步发布定型(createPost(draft) → PATCH published);`position` 全不给时
|
||||
按数组序落 0;`isCover` 全 false 时服务端将 position 0 行置为封面(库内恒有唯一封面行);
|
||||
`publishedAt` 发布时恰写一次;`version` 提交比对通过后 +1。
|
||||
|
||||
### 3.4 场景 4:【验收①】A 的帖在 B 的 Feed 可见,FeedCard 字段完整
|
||||
|
||||
```text
|
||||
[4/14] 【验收①】B 拉 /api/v1/feed → A 的帖首位可见,FeedCard 字段完整
|
||||
GET /api/v1/feed?limit=5 (B 的 token) → 200
|
||||
✓ Feed 返回 200
|
||||
✓ A 刚发布的帖在 B 的 Feed 首位(published_at DESC)
|
||||
FeedCard: title=M3 烟囱主贴 / mediaCount=1 / like=0 comment=0 bookmark=0
|
||||
author: userId=01a08a1c-d609-7804-9d57-79f3eec0294e nickname=e2e_m3_a_1789023540425
|
||||
✓ AuthorSummary 归因 A 且 nickname 键在(空昵称已由服务端回退 username)
|
||||
✓ AuthorSummary 不露 bio / username
|
||||
✓ FeedCard 必填齐备:contentPreview 原样透传(<200 码点)、mediaCount=1、
|
||||
三计数为 0、B 视角 likedByMe/bookmarkedByMe=false、publishedAt 非空
|
||||
✓ coverImage = 唯一 is_cover 行(assetId 命中,url 现签非空)
|
||||
✓ FeedCard 裁剪生效:不带 content 全文 / media 整组 / version
|
||||
```
|
||||
|
||||
契约验证(报告 16 定型的 FeedCard 形态逐项):
|
||||
|
||||
| 断言面 | 证据 |
|
||||
| --- | --- |
|
||||
| 必填 11 键齐备 | id / author / category / contentPreview / mediaCount / 三计数 / likedByMe / bookmarkedByMe / publishedAt 全在且取值正确 |
|
||||
| **裁剪生效** | `content` / `media` 整组 / `version` / `petId` / `visibility` / created-updated 时间戳对**均不出现** |
|
||||
| AuthorSummary 隐私 | 含 userId + nickname(空昵称已服务端回退为 username);**不露 bio、不露 username 键** |
|
||||
| coverImage | 非 null,`assetId` 命中场景 2 的 asset,`isCover=true`,`url` 现签非空 |
|
||||
| 视角字段 | B 视角 `likedByMe=false`、`bookmarkedByMe=false`(此时 B 尚未互动) |
|
||||
|
||||
### 3.5 场景 5:预签名 GET 取回图片字节与上传一致
|
||||
|
||||
```text
|
||||
[5/14] 预签名 GET 取回图片字节 → 与上传字节逐字节比对
|
||||
GET http://127.0.0.1:9000/patbond-media/post_image/2026/09/01a08a1c-…eb67?<SIGNATURE_REDACTED>
|
||||
GET <presigned> → 200(344 字节)
|
||||
✓ 预签名 GET 取回 200
|
||||
✓ 取回 344 字节与上传逐字节一致(内容往返无损)
|
||||
GET <同一对象但去掉签名> → 403
|
||||
✓ 桶保持私有:无签名直访被存储侧拒绝(403)
|
||||
```
|
||||
|
||||
契约验证:读取一律预签名 GET;**内容往返逐字节无损**;桶保持私有——去掉签名 query
|
||||
后同一对象 403(契约「无签名直访被拒」原文兑现)。
|
||||
|
||||
### 3.6 场景 6:【验收②】B 重复点赞不重复计数
|
||||
|
||||
```text
|
||||
[6/14] 【验收②】B 连续 3 次 PUT like → likeCount 恰为 1;DELETE → 0;再 DELETE 幂等
|
||||
PUT /like(第 1 次)→ 200 {"liked":true,"likeCount":1}
|
||||
PUT /like(第 2 次)→ 200 {"liked":true,"likeCount":1}
|
||||
PUT /like(第 3 次)→ 200 {"liked":true,"likeCount":1}
|
||||
✓ 三次均返回权威终态 {liked:true, likeCount:1}(非 409)
|
||||
✓ 详情读回 likeCount=1(3 次 PUT 仅实际插入一次才 +1)
|
||||
DELETE /like → 200 {"liked":false,"likeCount":0}
|
||||
✓ 取消点赞返回 {liked:false, likeCount:0}
|
||||
DELETE /like(重复)→ 200 {"liked":false,"likeCount":0}
|
||||
✓ 取消不存在的点赞不报错不减计数(DELETE 语义幂等)
|
||||
✓ 再次点赞恢复 likeCount=1(供后续卡片计数观察)
|
||||
```
|
||||
|
||||
契约验证:主键 (post_id, user_id) 即幂等键;重复 PUT 返回 **200 权威终态而非 409**;
|
||||
仅实际插入才 `like_count` 同事务 +1;DELETE 语义幂等(取消不存在不报错不减计数)。
|
||||
`community.post_likes` 表最终恰 1 行(§4)。
|
||||
|
||||
### 3.7 场景 7:B 收藏 + `/me/bookmarks` 含该帖;取消后不含
|
||||
|
||||
```text
|
||||
[7/14] B 收藏 → GET /api/v1/me/bookmarks 含该帖;取消收藏后不含
|
||||
PUT /bookmark → 200 {"bookmarked":true,"bookmarkCount":1}
|
||||
✓ 收藏返回权威终态 {bookmarked:true, bookmarkCount:1}
|
||||
GET /api/v1/me/bookmarks → 200
|
||||
✓ 收藏列表含该帖(共 1 条,项形态 = FeedCard)
|
||||
✓ 收藏项 bookmarkedByMe/likedByMe 为 B 视角,publishedAt 恒非空
|
||||
DELETE /bookmark → 200 {"bookmarked":false,"bookmarkCount":0}
|
||||
✓ 取消收藏返回 {bookmarked:false, bookmarkCount:0}
|
||||
✓ 取消收藏后列表不含该帖(剩 0 条)
|
||||
```
|
||||
|
||||
契约验证:收藏与点赞同构(PUT/DELETE 语义幂等 + 权威终态);`/me/bookmarks` 项形态
|
||||
= FeedCard,视角字段为调用者(B)视角,`publishedAt` 恒非空不变式成立。
|
||||
|
||||
### 3.8 场景 8:评论——B 可删自己的,A(帖主)删 B 的被拒
|
||||
|
||||
```text
|
||||
[8/14] B 评论 ×2 → A 拉列表可见;B 删自己评论成功;A(帖主)删 B 的评论被拒
|
||||
POST /comments (B, #1) → 201
|
||||
✓ 评论作者归因 B、postId 一致、非回复 replyToUser=null、无 updatedAt(M3 无编辑)
|
||||
POST /comments (B, #2, replyToUserId=A) → 201
|
||||
✓ @ 回复目标解出 AuthorSummary(单层平铺,无 parentCommentId)
|
||||
GET /comments (A 的 token) → 200
|
||||
✓ A 拉评论列表可见 B 的两条(created_at DESC,#2 在前)
|
||||
✓ commentCount 同事务 +1 累计为 2
|
||||
DELETE /comments/{c1} (B 删自己的) → 200
|
||||
✓ B 删自己的评论成功(软删 status→deleted)
|
||||
DELETE /comments/{c2} (A 删 B 的) → 403 / code 40301
|
||||
✓ A(帖主)删 B 的评论被拒 403/40301(无权限执行该操作)——仅评论作者可删(D3-7 拍板)
|
||||
✓ 删后列表仅剩 #2(仅 visible 评论),越权目标未被删除
|
||||
✓ commentCount 同事务 -1 回到 1
|
||||
```
|
||||
|
||||
契约验证:**「仅评论作者可删、帖主不可删他人评论」的 D3-7 拍板语义真链路兑现**——
|
||||
帖主 A 对可见评论的删除请求得到 403/40301,且被越权目标的评论**未被删除**(删后列表
|
||||
仍含 c2、`community.comments` 中 c2 保持 `visible`);`comment_count` 同事务 +1/−1
|
||||
双向核对(0→2→1);`replyToUser` 为 AuthorSummary(单层平铺无 parentCommentId);
|
||||
Comment 不带 `updatedAt`(M3 无评论编辑)。
|
||||
|
||||
### 3.9 场景 9:关注与 follow-stats;自关注 42204;自取关 200 no-op
|
||||
|
||||
```text
|
||||
[9/14] B 关注 A(PUT)+ follow-stats 计数;自关注 42204;自取关 200 no-op
|
||||
PUT /users/{A}/follow (B) → 200 {"following":true,"followerCount":1}
|
||||
PUT /users/{A}/follow(重复)→ 200 {"following":true,"followerCount":1}
|
||||
✓ 重复关注幂等 200,粉丝数仍为 1(主键 (follower,followee) 即幂等键)
|
||||
GET /users/{A}/follow-stats (B 视角) → 200
|
||||
{"followerCount":1,"followingCount":0,"followedByMe":true}
|
||||
GET /users/{A}/follow-stats (A 查自己) → 200
|
||||
{"followerCount":1,"followingCount":0,"followedByMe":false}
|
||||
✓ A 查自己 followedByMe 恒 false(计数一致)
|
||||
✓ B 的计数:关注 1 / 粉丝 0(实时 COUNT,无冗余计数列)
|
||||
PUT /users/{B}/follow (B 自关注) → 422 / code 42204
|
||||
✓ 自关注被拒 422/42204(不能关注自己)——库层 ck_user_follows_self 兜底
|
||||
DELETE /users/{B}/follow (B 自取关) → 200 {"following":false,"followerCount":0}
|
||||
✓ 自取关 200 幂等 no-op(关系行不可能存在,权威 false 即事实;42204 只在 PUT)
|
||||
```
|
||||
|
||||
契约验证:**42204 只在 PUT、自取关走 DELETE 的 200 幂等 no-op** 这条非对称语义
|
||||
(报告 17 定型)真链路兑现;`followedByMe` 查自己恒 false;followerCount/followingCount
|
||||
为实时 COUNT,A/B 双向视角互证。
|
||||
|
||||
### 3.10 场景 10:【验收③】Feed 游标分页不丢不重
|
||||
|
||||
```text
|
||||
[10/14] 【验收③】A 批量发 25 帖 → 双粒度全量翻页比对
|
||||
POST /api/v1/posts ×25 (status=published) → 全部 201
|
||||
✓ 25 帖全部创建成功且 id 互不相同
|
||||
逐页翻到底(limit=7)→ 10 页,共 65 条
|
||||
✓ 细粒度翻页零重复(65 条全唯一)
|
||||
✓ 翻页确实跨多页(10 页 > 1,游标真被使用)
|
||||
逐页翻到底(limit=100)→ 1 页,共 65 条
|
||||
✓ 粗粒度翻页零重复(65 条全唯一)
|
||||
✓ 两种页大小全量结果**逐位一致**(顺序与集合都相同)→ 翻页不丢不重
|
||||
✓ 25 帖 + 主贴全部恰好出现一次(无遗漏)
|
||||
✓ 主贴(场景 3 发布)亦在全量结果内
|
||||
Feed 全量条数(含既有数据):65;本轮新增 26 条
|
||||
```
|
||||
|
||||
**取证方法论**:本场景刻意不采用「断言 Feed 总数等于本轮发帖数」的脆弱写法(compose
|
||||
卷内有前几波留下的既有帖),而用三重强断言:
|
||||
|
||||
1. **零重复**:`limit=7` 逐页翻到底共 65 条,去重后仍 65 条
|
||||
2. **零遗漏**:本轮 25 帖 + 主贴的 26 个 id 在全量结果中**各恰好出现一次**
|
||||
3. **粒度不变性**:同一 Feed 分别以 `limit=7`(10 页)与 `limit=100`(1 页)全量翻完,
|
||||
两份有序 id 列表**逐位相等**——若 keyset 游标在页边界丢行或重行,两种粒度必然分叉
|
||||
|
||||
另有末页不变式随行断言:`hasMore=false` 时 `nextCursor` 恒为 null;`hasMore=true` 时
|
||||
`nextCursor` 必非 null(否则脚本立即 fail 而非静默挂死)。
|
||||
|
||||
**psql 交叉核对**(脚本无库访问,独立第三方证据):
|
||||
|
||||
```text
|
||||
patbond=# select count(*) as feed_predicate_rows from community.posts
|
||||
where status='published' and visibility='public' and deleted_at is null;
|
||||
feed_predicate_rows
|
||||
---------------------
|
||||
65 ← 与脚本全量翻页 65 条相等
|
||||
|
||||
patbond=# select count(*) as posts_by_A_published from community.posts
|
||||
where author_user_id='01a08a1c-d609-…294e'
|
||||
and status='published' and visibility='public' and deleted_at is null;
|
||||
posts_by_a_published
|
||||
----------------------
|
||||
26 ← 25 批量帖 + 1 主贴
|
||||
```
|
||||
|
||||
第二轮复跑同一断言在 91 条 / 13 页规模下再次通过——**断言与数据规模解耦**。
|
||||
|
||||
### 3.11 场景 11:【验收④】软删帖不再出现在公共 Feed
|
||||
|
||||
```text
|
||||
[11/14] 【验收④】A 软删一帖 → B 的 Feed 不再含该帖,直接 GET 404/40403
|
||||
✓ 待删帖创建并发布成功(201)
|
||||
doomedPostId: 01a08a1c-df1c-79a9-95bc-af1ce14fe2cd
|
||||
✓ 删除前:该帖在 B 的 Feed 首位(全量 66 条)
|
||||
DELETE /api/v1/posts/{doomedId} (A 软删) → 200
|
||||
✓ 软删成功(deleted_at 写入)
|
||||
B 全量翻 Feed(删除后)→ 65 条
|
||||
✓ 删除后 B 的 Feed 全量不含该帖,且总数恰少 1(其余帖不受影响)
|
||||
GET /api/v1/posts/{doomedId} (B 直接访问) → 404 / code 40403
|
||||
✓ B 直接 GET 已删帖 404/40403(帖子不存在)
|
||||
✓ 作者 A 自己 GET 已删帖同样 404/40403(响应体与 B 逐字节一致)
|
||||
✓ 重复删除与删不存在的帖同响应 404/40403(防枚举合并)
|
||||
✓ 已删帖的互动面同样 404/40403(删除后一切路径关闭)
|
||||
```
|
||||
|
||||
**关键取证强度**:不是「翻第一页没看见」,而是**删除前后各做一次全量翻页**——
|
||||
66 → 65 条,差集恰为该帖一条,证明「消失」不是被挤到后页而是真正出了谓词,且
|
||||
其余 65 帖一条不少(删除操作无副作用)。四条读/写路径同时关闭:Feed(不含)、
|
||||
详情(B 与作者 A 均 404/40403 且响应体逐字节一致)、重复删除(404/40403)、
|
||||
互动面(PUT like → 404/40403)。
|
||||
|
||||
### 3.12 场景 12:防枚举一致性——草稿 vs 随机 UUID
|
||||
|
||||
```text
|
||||
[12/14] 防枚举:B 访问 A 的草稿 与 访问随机 UUID → 响应体逐字节一致
|
||||
A 的草稿 id: 01a08a1c-df99-7f81-82d9-aa51a5cbf11a
|
||||
随机 UUID: ddc96d28-ced4-4646-aba9-fa1349585b54
|
||||
详情 GET /posts/{草稿} → 404 / code 40403 ✓
|
||||
详情 GET /posts/{随机} → 404 / code 40403 ✓
|
||||
评论 GET /posts/{草稿}/comments → 404 / code 40403 ✓
|
||||
评论 GET /posts/{随机}/comments → 404 / code 40403 ✓
|
||||
✓ 四路响应体完全一致(防枚举):{"code":40403,"message":"帖子不存在","data":null}
|
||||
✓ 互动面 = 帖子公开面:草稿点赞同一 404/40403 响应体
|
||||
✓ **作者本人**对自己草稿的互动亦 404/40403(互动面恒为公开面)
|
||||
✓ 草稿对作者本人详情仍可见(draft 仅作者可见)
|
||||
✓ /me/posts?status=draft 含该草稿(作者视角)
|
||||
✓ 草稿不在公共 Feed(谓词只放行 published+public+未删)
|
||||
```
|
||||
|
||||
契约验证:**随机探测 UUID 与真实存在的他人草稿逐字节同响应**,攻击者无法通过响应
|
||||
差异区分 id 是否命中真实记录;「互动面 = 帖子公开面」定型语义完整——**含作者本人对
|
||||
自己草稿的互动亦 404/40403**(这是易被实现漏掉的一侧,本次直接取证);同时反证
|
||||
草稿并未「被藏起来」:作者本人详情可读、`/me/posts?status=draft` 可见。
|
||||
|
||||
### 3.13 场景 13:埋点——v3 社区事件上报与落库
|
||||
|
||||
```text
|
||||
[13/14] POST /api/v1/events(:8082)上报 v3 社区事件(platform=android 模拟真机值)
|
||||
eventId: a599a7e0-… (feed_viewed)
|
||||
eventId: 23bd78e5-… (post_media_upload_succeeded)
|
||||
eventId: 2a3f4d95-… (post_publish_succeeded)
|
||||
eventId: 2c4e81fc-… (post_liked)
|
||||
eventId: 3888d902-… (post_favorited)
|
||||
eventId: 79ec3981-… (comment_create_succeeded)
|
||||
eventId: 73dd7633-… (user_followed)
|
||||
eventId: 96918b41-… (page_viewed)
|
||||
POST /api/v1/events (8 条) → 202
|
||||
✓ 8/8 逐条 accepted(accepted=8, duplicated=0, rejected=0)
|
||||
POST /api/v1/events(post_liked 混入白名单外 postId)→ 202
|
||||
✓ 白名单外键剥离后事件仍 accepted(隐私红线 ingest 侧兜底,落库无 postId)
|
||||
POST /api/v1/events(字典外 post_impression)→ 202
|
||||
✓ 字典外事件整条 rejected(reason=unknown_event_name),批次仍 202
|
||||
E2E_SESSION_ID=8857898d-0ce0-4a57-bda6-bccac98b57b6
|
||||
```
|
||||
|
||||
**落库查证(docker exec psql)**:
|
||||
|
||||
```text
|
||||
patbond=# SELECT event_name, event_version, platform,
|
||||
left(user_id::text,8) AS user_id_prefix,
|
||||
left(event_id::text,8) AS event_id_prefix, props
|
||||
FROM platform.product_events
|
||||
WHERE session_id = '8857898d-0ce0-4a57-bda6-bccac98b57b6'
|
||||
ORDER BY event_name;
|
||||
|
||||
event_name | ev | platform | user_id | event_id | props
|
||||
-----------------------------+----+----------+----------+----------+--------------------------------------------------
|
||||
comment_create_succeeded | 3 | android | 01a08a1c | 79ec3981 | {"isReply": true, "durationMs": 720,
|
||||
| | | | | "textLengthBucket": "lt_200"}
|
||||
feed_viewed | 3 | android | 01a08a1c | a599a7e0 | {"feedTab": "recommend", "durationMs": 8600,
|
||||
| | | | | "refreshCount": 1, "loadMoreCount": 3,
|
||||
| | | | | "impressionCount": 27}
|
||||
page_viewed | 3 | android | 01a08a1c | 96918b41 | {"pageName": "post_detail", "referrer": "home"}
|
||||
post_favorited | 3 | android | 01a08a1c | 3888d902 | {"source": "detail"}
|
||||
post_liked | 3 | android | 01a08a1c | 2c4e81fc | {"source": "feed"}
|
||||
post_liked | 3 | android | 01a08a1c | 9884703f | {"source": "feed"} ← 混入的 postId 已剥离
|
||||
post_media_upload_succeeded | 3 | android | 01a08a1c | 23bd78e5 | {"mediaType": "image", "durationMs": 940,
|
||||
| | | | | "sizeBucket": "lt_512kb"}
|
||||
post_publish_succeeded | 3 | android | 01a08a1c | 2a3f4d95 | {"fromDraft": true, "durationMs": 1560,
|
||||
| | | | | "mediaCount": 1, "topicCount": 0,
|
||||
| | | | | "textLengthBucket": "lt_200"}
|
||||
user_followed | 3 | android | 01a08a1c | 73dd7633 | {"source": "detail"}
|
||||
(9 rows)
|
||||
```
|
||||
|
||||
三条链路同时取证:
|
||||
|
||||
1. **正常落库**:8 条 v3 社区事件(feed / 媒体 / 发布漏斗 / 互动 / 关注 / page_viewed)
|
||||
全部落 `platform.product_events`,props 键集与字典 v3 白名单(报告 22)逐键一致,
|
||||
`platform=android`、`user_id` 归因 B
|
||||
2. **隐私红线 ingest 侧兜底**:`post_liked` 混入白名单外 `postId` → 事件 accepted 但
|
||||
**落库 props 仅 `{"source":"feed"}`,postId 已被剥离**(第 6 行,event_id `9884703f`)
|
||||
3. **字典边界锁死**:被否决的逐卡曝光事件 `post_impression` 整条
|
||||
`rejected / reason=unknown_event_name`,且**未出现在落库结果的 9 行中**
|
||||
|
||||
(真机端上链路见 §0 挂起项 4。)
|
||||
|
||||
### 3.14 场景 14:幂等重放
|
||||
|
||||
```text
|
||||
[14/14] 幂等重放:同 Idempotency-Key 同 hash 返回原帖;异 hash → 409/40905
|
||||
POST /api/v1/posts (Idempotency-Key=<KEY-1>, 首发) → 201
|
||||
postId: 01a08a1c-e056-70b1-b974-53d0c269fe62 / version: 0
|
||||
POST /api/v1/posts (同 KEY-1, 同 hash, 重发) → 201
|
||||
✓ 同键同 hash 返回首次结果(id/version 一致),不产生第二帖
|
||||
✓ 我的草稿列表恰 2 条(私密草稿 + 重放帖),重放帖只出现一次(无重复落库)
|
||||
POST /api/v1/posts (同 KEY-1, **异 hash**) → 409 / code 40905
|
||||
✓ 同键异 hash 被拒 409/40905(幂等键已用于不同请求)
|
||||
POST /api/v1/posts(**不带** Idempotency-Key)→ 400 / code 40000
|
||||
✓ Idempotency-Key 缺失被拒 400/40000(参数校验失败)——该头必带
|
||||
POST /api/v1/posts(B 用同一 KEY-1 值)→ 201
|
||||
✓ 幂等键按作者隔离:B 用同一键值创建出新帖(id 不同)
|
||||
```
|
||||
|
||||
契约验证:同键重试返回首次创建的资源且**同样 201**(契约原文);`id`/`version` 逐项
|
||||
一致;**通过 `/me/posts` 反查确认库内未产生第二行**(不只是响应看起来一样);
|
||||
同键异 hash → 409/40905;该头缺失 → 400/40000;**键按作者隔离**(B 用同一键值
|
||||
创建出独立新帖,跨用户不串号)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库查询证据(community / media schema)
|
||||
|
||||
```text
|
||||
patbond=# select left(id::text,8) as post_id, status, visibility, title,
|
||||
like_count, comment_count, bookmark_count, version,
|
||||
(published_at is not null) as pub, (deleted_at is not null) as del
|
||||
from community.posts where author_user_id='01a08a1c-…294e' and id in (…);
|
||||
|
||||
post_id | status | visibility | title | like | cmt | bkm | ver | pub | del
|
||||
----------+-----------+------------+---------------+------+-----+-----+-----+-----+-----
|
||||
01a08a1c | published | public | M3 烟囱主贴 | 1 | 1 | 0 | 1 | t | f
|
||||
01a08a1c | archived | public | M3 待删帖 | 0 | 0 | 0 | 1 | t | t
|
||||
01a08a1c | draft | public | M3 私密草稿 | 0 | 0 | 0 | 0 | f | f
|
||||
01a08a1c | draft | public | M3 幂等重放帖 | 0 | 0 | 0 | 0 | f | f
|
||||
(4 rows)
|
||||
|
||||
patbond=# select left(id::text,8) as asset_id, kind, purpose, mime_type, byte_size,
|
||||
status, (ready_at is not null) as ready, left(object_key,44) as object_key,
|
||||
(sha256 is not null) as sha256_stored from media.assets where id='01a08a1c-…eb67';
|
||||
|
||||
asset_id | kind | purpose | mime_type | byte_size | status | ready | object_key | sha256_stored
|
||||
----------+-------+------------+------------+-----------+--------+-------+-------------------------------+---------------
|
||||
01a08a1c | image | post_image | image/jpeg | 344 | ready | t | post_image/2026/09/01a08a1c-… | t
|
||||
|
||||
patbond=# select left(post_id::text,8) as post_id, position, is_cover, caption
|
||||
from community.post_media where post_id='01a08a1c-…a474';
|
||||
|
||||
post_id | position | is_cover | caption
|
||||
----------+----------+----------+------------
|
||||
01a08a1c | 0 | t | 烟囱测试图
|
||||
(1 row)
|
||||
|
||||
patbond=# select left(post_id::text,8) as post_id, left(user_id::text,8) as user_id,
|
||||
created_at from community.post_likes where post_id='01a08a1c-…a474';
|
||||
|
||||
post_id | user_id | created_at
|
||||
----------+----------+-------------------------------
|
||||
01a08a1c | 01a08a1c | 2026-09-10 06:59:02.332316+00
|
||||
(1 row) ← 3 次 PUT 只留 1 行(验收②库层互证)
|
||||
|
||||
patbond=# select left(id::text,8) as comment_id, left(author_user_id::text,8) as author,
|
||||
status, (reply_to_user_id is not null) as is_reply, left(content,24) as content
|
||||
from community.comments where post_id='01a08a1c-…a474' order by created_at;
|
||||
|
||||
comment_id | author | status | is_reply | content
|
||||
------------+----------+---------+----------+------------------------------
|
||||
01a08a1c | 01a08a1c | deleted | f | B 的第一条评论(将被 B 自己删除)
|
||||
01a08a1c | 01a08a1c | visible | t | B 的第二条评论(@A 回复… ← A 越权删除未生效
|
||||
(2 rows)
|
||||
|
||||
patbond=# select left(follower_user_id::text,8) as follower,
|
||||
left(followee_user_id::text,8) as followee
|
||||
from community.user_follows where followee_user_id='01a08a1c-…294e';
|
||||
|
||||
follower | followee
|
||||
----------+----------
|
||||
01a08a1c | 01a08a1c
|
||||
(1 row) ← 两次 PUT 只留 1 行(关注幂等库层互证)
|
||||
|
||||
patbond=# select count(*) as bookmarks from community.post_bookmarks
|
||||
where post_id='01a08a1c-…a474';
|
||||
bookmarks
|
||||
-----------
|
||||
0 ← 取消收藏后关系行已移除
|
||||
```
|
||||
|
||||
验证点:
|
||||
|
||||
- `post_likes` / `user_follows` 各恰 1 行 → 重复 PUT 的幂等性在**库层**得到印证
|
||||
(不只是响应终态一致)
|
||||
- `comments` 中 c1 为 `deleted`(B 自删生效)、c2 为 `visible`(A 越权删除**未生效**)
|
||||
→ 「仅评论作者可删」拍板语义库层互证
|
||||
- `post_media` 恒有唯一 `is_cover=t` 行;`media.assets` 为 `ready` 且 `sha256` 已照存
|
||||
- **软删的内部记账**:被软删的已发布帖 `deleted_at` 非空且 `status` 被泊为 `archived`
|
||||
——这是 `ck_posts_publish_state` 约束下的实现内部记账(`PostRepository.softDelete`
|
||||
注释已写明 D3-7 定型),**任何响应都不会携带 `archived`**(场景 11 的四路 404/40403
|
||||
即为佐证),故与契约「status 枚举保持两值」不冲突
|
||||
|
||||
---
|
||||
|
||||
## 5. M3 四条验收标准逐条对照
|
||||
|
||||
| # | 验收标准 | 证据 | 结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| ① | **发布后可在另一客户端看到** | 场景 3 A 两步发布 → 场景 4 **B 的 token** 拉 `/api/v1/feed`,A 的帖在首位,FeedCard 11 个必填键逐项断言(含 AuthorSummary 归因 A、coverImage 命中 assetId、mediaCount=1)、裁剪字段确认缺席、B 视角互动布尔为 false;§4 psql 证实 `published`/`deleted_at IS NULL` | ✓ 通过 |
|
||||
| ② | **重复点赞不重复计数** | 场景 6 连续 3 次 PUT like,**三次响应均为权威终态 `{liked:true,likeCount:1}`(200 非 409)**;详情读回 likeCount=1;DELETE→0;再 DELETE 幂等仍 0;§4 `community.post_likes` 全表恰 1 行 | ✓ 通过 |
|
||||
| ③ | **分页不丢失不重复** | 场景 10 A 批量发 25 帖,同一 Feed 以 `limit=7`(10 页)与 `limit=100`(1 页)**两次全量翻页,有序 id 列表逐位相等**;65 条零重复;本轮 26 个新 id 各恰好出现一次;末页 `nextCursor=null` 不变式;psql `feed_predicate_rows=65` 交叉核对相等;第二轮复跑在 91 条/13 页规模再次通过 | ✓ 通过 |
|
||||
| ④ | **删除或隐藏内容不可继续出现在公共 Feed** | 场景 11 **删除前后各做一次全量翻页**(66→65,差集恰为该帖,其余一条不少);B 直接 GET 404/40403;**作者 A 自查同样 404/40403 且响应体与 B 逐字节一致**;重复删除 404/40403;已删帖互动面 404/40403。隐藏(hidden/archived)态 M3 无端点可产生,其「对作者亦不露」由同一 `isVisible` 谓词与场景 12 的草稿路径共同覆盖 | ✓ 通过 |
|
||||
|
||||
> 验收标准 ④ 的「隐藏」半边说明:契约 v1.3.0 明确 **M3 无端点能产生或解除运营态**
|
||||
> (hidden/archived),故烟囱层面无法从 API 造出 hidden 帖。其不可见性由与软删共用的
|
||||
> 同一可见性谓词保证,后端契约测试已覆盖(报告 15/20 的 40403 矩阵),且本次场景 11
|
||||
> 证实了被泊为 `archived` 的软删行确实不出 Feed 也不出详情。
|
||||
|
||||
---
|
||||
|
||||
## 6. 契约偏差声明
|
||||
|
||||
**契约偏差数:0 个。**
|
||||
|
||||
本次烟囱对照冻结契约 openapi **v1.3.0**(报告 18 冻结)逐场景核验,全部一致、无需修复项:
|
||||
|
||||
| 核验面 | 本次实测覆盖 |
|
||||
| --- | --- |
|
||||
| HTTP 状态码 | 200 / 201 / 202 / 400 / 403 / 404 / 409 / 422 |
|
||||
| 业务错误码 | 40000 / 40301 / 40403 / 40905 / 42204 |
|
||||
| 信封结构 | `{code, message, data}` 全路径一致;VoidEnvelope 的 delete 200 |
|
||||
| 分页正典形态 | `{items, nextCursor, hasMore}`;末页 `nextCursor` 恒 null;feed / bookmarks / comments / me-posts 四处一致 |
|
||||
| 幂等语义 | PUT/DELETE 语义幂等 + 权威终态;Idempotency-Key 必带 / 同键同 hash / 同键异 hash / 按作者隔离;complete 重复确认 |
|
||||
| 乐观锁 | PATCH `version` 必带,提交比对通过后 +1(0→1) |
|
||||
| 防枚举 | 草稿 vs 随机 UUID vs 已删帖,跨 5 条路径响应体逐字节一致 |
|
||||
| 媒体两步上传 | requiredHeaders 键集、SigV4 query 签名、TTL、objectKey 服务端生成、私有桶无签名 403 |
|
||||
| 埋点逐条结果 | accepted / rejected + reason 枚举;白名单外键剥离;字典外整条拒 |
|
||||
| FeedCard 裁剪 | 必填 11 键在、裁剪 6 类字段缺席、AuthorSummary 不露 bio/username |
|
||||
|
||||
---
|
||||
|
||||
## 7. 观察项(非契约偏差,供 M4 拍板)
|
||||
|
||||
以下两项**不构成契约偏差**(响应形态、状态码、错误码均合规),但属实现与文档措辞的
|
||||
落差 / 口径未定型,显式登记以免被「0 偏差」掩盖:
|
||||
|
||||
### 观察项 1:`widthPx/heightPx` 实测恒为 `null`,契约描述写「complete 后回填」
|
||||
|
||||
- **实测**:场景 2 confirm 后 `widthPx=null, heightPx=null`(§3.2)
|
||||
- **契约**:`MediaAsset.widthPx` 标注 `nullable: true` → **响应形态合规**;但同字段
|
||||
description 为「complete 后回填,可空」,暗示会回填
|
||||
- **实现**:`patbond-user` 的 media 包无任何图片尺寸探测(无 ImageIO 类调用),
|
||||
两列永不写值
|
||||
- **用户可见后果**:`post_detail_page.dart` 的单图渲染在 `widthPx/heightPx` 为 null 时
|
||||
回落固定 `4/3` 宽高比(客户端已正确处理 null,无崩溃),即 **M3 单图帖一律按 4:3 展示,
|
||||
不呈现真实宽高比**
|
||||
- **建议**:M4 二选一并同步落文——(a)complete 时做尺寸探测回填;
|
||||
(b)契约 description 改为「预留字段,M3 不回填」
|
||||
|
||||
### 观察项 2:`eventVersion` 口径未定型(客户端恒发 1,两版 E2E 脚本各发 2 / 3)
|
||||
|
||||
- **契约**:`TrackedEvent.eventVersion` 描述「事件 schema 版本(字典 v1 全部为 1)」
|
||||
——可读作「每个事件自身的 schema 版本」,也可读作「事件字典版本」;服务端不校验取值
|
||||
- **客户端**:`lib/analytics/analytics_service.dart:139` 对**所有**事件硬编码
|
||||
`'eventVersion': 1`
|
||||
- **脚本**:M2 版脚本发 2、本 M3 版脚本发 3(沿 M2 先例按「字典版本」读法),
|
||||
故 §3.13 落库的 `event_version=3` **不是客户端真实取值**
|
||||
- **后果**:若下游分析以 `event_version` 区分字典世代,客户端上报的数据会全部落在 1
|
||||
- **建议**:M4 明确口径并统一三方(契约描述、客户端、脚本);若采「每事件 schema 版本」
|
||||
读法,则客户端恒 1 是对的,两版 E2E 脚本应改为 1,本报告的落库证据需按新口径复采
|
||||
|
||||
---
|
||||
|
||||
## 8. Flutter 门禁验证(三命令随行取证)
|
||||
|
||||
### 8.1 格式化检查
|
||||
|
||||
```bash
|
||||
dart format --output=none --set-exit-if-changed lib test
|
||||
# Formatted 144 files (0 changed) in 0.57 seconds.
|
||||
# EXIT: 0
|
||||
```
|
||||
|
||||
### 8.2 静态分析
|
||||
|
||||
```bash
|
||||
flutter analyze
|
||||
# Analyzing patbond-flutter...
|
||||
# No issues found! (ran in 1.1s)
|
||||
```
|
||||
|
||||
(含根目录三个 E2E 脚本在内全仓 0 issues;三脚本头部 `ignore_for_file: avoid_print`。
|
||||
新脚本刻意不引入 `crypto`/`collection` 包依赖——sha256 用实测常量、列表比较自带
|
||||
7 行实现——以免触发 `depend_on_referenced_packages`。)
|
||||
|
||||
### 8.3 单元/组件测试
|
||||
|
||||
```bash
|
||||
flutter test
|
||||
# 00:28 +502 ~2: All tests passed!
|
||||
```
|
||||
|
||||
**✓ 502 个测试全部通过**(2 skipped 为既有跳过项;E2E 脚本在仓库根目录,
|
||||
不被 `flutter test` 收集)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 环境清理
|
||||
|
||||
```bash
|
||||
cd <工作区>/patbond-api && docker compose down
|
||||
# Container patbond-community-1 / patbond-pet-1 / patbond-auth-1 /
|
||||
# patbond-user-1 / patbond-minio-1 / patbond-postgres-1 Removed
|
||||
# Network patbond_default Removed
|
||||
```
|
||||
|
||||
> 卷(`pgdata` / `minio-data`)保留,故本次数据留在本机 compose 卷内;需要干净环境时
|
||||
> `docker compose down -v` 清卷(协作规则:测试数据不入库,只在本机卷里)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 工作仓库状态
|
||||
|
||||
- **patbond-flutter dev**:`0e87413` `test: M3 E2E 烟囱脚本(T3-21 收官)` 已推送 origin/dev
|
||||
- **patbond-api**:**代码零改动**(仅 compose 起停 + 只读 psql 查证)
|
||||
- **patbond-doc**:本报告(28 号),提交与 mkdocs 导航由 M3 收官文档收口工单统一处理
|
||||
|
||||
---
|
||||
|
||||
## 11. 遗留清单
|
||||
|
||||
1. **真机四项待补验**(见 §0 显著标注):媒体上传弱网表现、乐观更新真机手感、
|
||||
Feed 图片加载、社区事件落库(客户端链路)——步骤与通过标准已在
|
||||
`docs/development/device-verification.md`「M3 预登记」备齐,设备到位后按方案 A
|
||||
补验并在该文件「执行记录(M3)」追加证据。M2 遗留两项(Android 事件落库观察、
|
||||
SessionTracker 30min 手测)同样未闭环。
|
||||
2. **观察项两条**(§7):`widthPx/heightPx` 不回填、`eventVersion` 口径未定型,
|
||||
建议在 M3 收官总结中登记为 M4 待拍板。
|
||||
3. **本次烟囱未覆盖的契约面**(均有后端契约测试覆盖,非缺口):
|
||||
- hidden/archived 运营态的产生路径(M3 无端点,见 §5 说明)
|
||||
- media 的 422/42203(引用非 ready asset)与 42205(对象未上传即确认)失败分支
|
||||
- 429 Retry-After 分支(后端限流未落地)
|
||||
- `position` 全给/混合的 400 校验、9 图上限、caption 300 上限等参数边界
|
||||
- user 服务故障时 AuthorSummary 退 id-only 的降级路径(需注入故障)
|
||||
4. **E2E 脚本 CI 化**:三版脚本(M1/M2/M3)仍为手动验收工具,建议 M4 纳入 CI 定期
|
||||
回归(compose 起停 + 脚本执行,失败即红),与前两迭代建议一致。
|
||||
|
||||
---
|
||||
|
||||
**Frontend Developer**
|
||||
日期:2026-09-10
|
||||
验收状态:**PASSED**(14/14 场景,契约偏差 0,M3 四条验收标准全部通过;
|
||||
观察项 2 条待拍板;真机四项挂起待补验)
|
||||
@@ -0,0 +1,76 @@
|
||||
# 29 M3 收官总结:社区
|
||||
|
||||
**迭代周期**:2026-09-08 ~ 2026-09-10(开工分析 + 四波交付)
|
||||
**验收结论**:**PASSED**——E2E 烟囱 14/14、契约偏差 0、M3 四条验收标准逐条取证通过;真机四项按既定方案挂起待设备
|
||||
|
||||
---
|
||||
|
||||
## 0. 终态对照开工基线(07 号基线快照)
|
||||
|
||||
| 维度 | 开工基线(2026-09-08) | 收官终态(2026-09-10) |
|
||||
| --- | --- | --- |
|
||||
| patbond-api 测试 | 191 | **334**(+143) |
|
||||
| patbond-flutter 测试 | 272 | **502**(+230) |
|
||||
| openapi.yaml | v1.2.0,18 路径/24 操作/45 schema | **v1.3.0 冻结**,31 路径/43 操作/72 schema |
|
||||
| 契约一致性矩阵 | 43 格(pets+auth 部分) | **173 格,43/43 操作,零漂移** |
|
||||
| Flyway | V1~V4 | V1~V5(community 8 表 + pg_trgm) |
|
||||
| 后端模块/端口 | common/auth:8081/user:8082/pet:8083 | + **patbond-community:8084** |
|
||||
| 部署形态 | 四容器 | **六容器**(+MinIO 对象存储) |
|
||||
| 媒体能力 | 有表无代码 | **两步上传闭环**(预签名直传 + 私有桶签名读) |
|
||||
| 事件白名单 | 22 事件 | **42 事件**(+19 community 域 + experiment_exposed) |
|
||||
| ADR | 001~015 | **001~021** |
|
||||
| 社区功能 | Flutter demo 数据 | 三页全真实后端(home/详情/发布),demo 消亡 |
|
||||
| 凭证防泄漏 | 无 | **9 规则两层检查三仓在线** |
|
||||
|
||||
## 1. 交付主线回顾
|
||||
|
||||
- **开工分析**(报告 01~08):8 角色并行;Reality Checker 给出其设立以来**首个 CERTIFIED 无条件放行**(M2 收官声称全部亲验命中、E2E 冷启动复跑 11/11);Evidence 证据链 100%;ADR-016~021 拍板
|
||||
- **第一波**(09~14):V5 迁移(剪 2 条跨 schema FK) + community 骨架 + **MinIO 媒体闭环** + 埋点队列三项加固 + 凭证防泄漏三仓 + auth 契约测试补齐
|
||||
- **第二波**(15~20):社区后端纵切 16 端点(帖子/Feed/评论/互动/关注) + **契约冻结 v1.3.0** + 快照同步与矩阵扩展(零漂移)
|
||||
- **第三波**(21~27):Flutter 五单接入(数据层/媒体上传/Feed/详情互动/发布页) + 字典 v3;社区 demo 三页消亡
|
||||
- **第四波**(28~29):E2E 烟囱 14 场景收官取证 + 文档收口
|
||||
|
||||
## 2. M3 四条验收标准证据索引(28 号报告)
|
||||
|
||||
| 标准 | 结论 | 取证强度 |
|
||||
| --- | --- | --- |
|
||||
| 发布后另一客户端可见 | ✓ | B token 拉 Feed 首位即 A 帖;FeedCard 11 必填键逐项断言 + 6 类裁剪字段确认缺席;另有 T3-17 双 App 实例真 UI 实测 |
|
||||
| 重复点赞不重复计数 | ✓ | 3 次 PUT 均返权威终态 `{liked:true,likeCount:1}`;psql `post_likes` 恰 1 行库层互证;后端另有 4 线程真并发测试 |
|
||||
| 分页不丢不重 | ✓ | 同一 Feed 以 limit=7(10 页)与 limit=100(1 页)两次全量翻页,**有序 id 列表逐位相等**;psql 谓词行数交叉核对 |
|
||||
| 删除内容不出公共 Feed | ✓ | 删除前后各全量翻页(66→65,差集恰为该帖);B 与作者 A 直读均 404/40403 逐字节一致 |
|
||||
|
||||
## 3. 质量机制的兑现
|
||||
|
||||
- **契约测试累计抓修 3 处真实问题**:events 的 reason 字段误序列化 null(第一波 auth 矩阵)、校验器对 `nullable + allOf` 静默跳过的盲区(第二波矩阵扩展)、CreatePetRequest.sex 必填漂移(M2 期)
|
||||
- **compose 实测抓出跨服务接线缺陷**:T3-13 发现 media 端点在 user:8082 而 T3-12 误挂 community:8084(真链路必 404),单测无法覆盖此类装配错误
|
||||
- **widget 测试抓出两处真 bug**:Feed 尾部失败态被滚动自动重试冲掉、回前台不开新曝光段
|
||||
- **多源核验**:一个 agent 识破 Monitor 的假 success 事件(时间戳晚于实时时钟),坚持以 Gitea API 多次直查为准
|
||||
- **量级测算否决设计**:逐卡 Feed 曝光被埋点角色以「7~14 个月击穿分区阈值 + 接收端无限流背压」否决,改聚合 `feed_viewed`,并在后端字典层把 `post_impression` 等 7 事件锁死为 unknown
|
||||
|
||||
## 4. 遗留与 M4 建议
|
||||
|
||||
**真机挂起四项**(步骤已在 [真机验证清单](../../device-verification.md) 备齐):媒体上传弱网、乐观更新手感、Feed 图片加载、社区事件落库。**时限提醒**:埋点角色建议 2026-09-21(北极星首次出数日)前完成 M2 两项,否则首批读数只能标未验收。
|
||||
|
||||
**M3 范围内遗留**:
|
||||
1. 完整草稿列表与自动保存(26 号 §7);大图「下滑关闭」手势待 photo_view 复评
|
||||
2. `widthPx/heightPx` 恒 null(28 号观察项 1):契约描述称 confirm 后回填,实现无尺寸探测——单图帖一律回落 4:3,不呈现真实宽高比;补实现或改契约描述二选一
|
||||
3. `eventVersion` 口径未定型(28 号观察项 2):契约描述可两读、服务端不校验、客户端硬编码 1;需定型为「事件 schema 版本」并写入字典纪律
|
||||
4. uploading 超时未确认 asset 的清理定时任务(13 号已有方案未实现)
|
||||
5. 429 限流未实现,连带客户端 Retry-After 精细分支挂起(承自 09 号出入清单)
|
||||
6. 话题功能(ADR-018 剪出)、关注列表/作者主页(裁剪项)
|
||||
|
||||
**跨迭代遗留**(承自 M1/M2,未变化):access token 黑名单、`/internal` 改 mTLS。
|
||||
|
||||
**M4 方向输入**(开发计划 M4:AI 创作):
|
||||
- 模型/风格目录、生成任务创建/查询/取消、Worker 队列消费(租约/重试/幂等键)、输出写媒体表后一键建社区草稿——**媒体链路与社区草稿两端已在 M3 就位**,M4 可直接复用
|
||||
- V5 已为 `posts.generation_job_id` 留裸列,M4 迁移补回该外键即可打通 AI 产出→社区发布
|
||||
- create 页的 AI 生成模拟(M3 刻意零改动)是 M4 的替换目标
|
||||
- A/B 前置 8 项:M3 末 6 项全绿 + 1 项部分绿(feature flag 随社区发布开关落地),M4 可启动首个实验;北极星「7 日回访记录率」复评点为 H7 读数
|
||||
|
||||
## 5. 收官提交索引
|
||||
|
||||
| 仓库 | 收官 HEAD | 测试 |
|
||||
| --- | --- | --- |
|
||||
| patbond-api | dev@8089c06 | 334 |
|
||||
| patbond-flutter | dev@0e87413 | 502 |
|
||||
| patbond-doc | 本收口提交 | strict 通过 |
|
||||
@@ -0,0 +1,58 @@
|
||||
# 第三迭代进展看板
|
||||
|
||||
> 目标:M3 社区——图片媒体上传闭环 + 帖子草稿/发布/删除 + 公共 Feed 游标分页 + 单层评论 + 点赞/收藏幂等 + 关注最小接口 + Flutter 三页替换 demo 与乐观更新回滚,依据[开发实施计划](../../development-plan.md) M3 节。
|
||||
> 更新日期:2026-09-10(**M3 收官,验收 PASSED**)。本页是团队共享的进度事实来源。
|
||||
|
||||
## 当前状态一览
|
||||
|
||||
| 状态 | 内容 |
|
||||
| --- | --- |
|
||||
| ✅ 第一波 | V5 community 迁移 + patbond-community 骨架 + **MinIO 媒体闭环**(ADR-016/017)+ 埋点队列三项加固 + 凭证防泄漏三仓 + auth 契约测试 |
|
||||
| ✅ 第二波 | 社区后端 16 端点纵切 + **契约冻结 v1.3.0** + 快照同步与契约矩阵 173 格零漂移 |
|
||||
| ✅ 第三波 | Flutter 五单接入(数据层/媒体上传/Feed/详情互动/发布页)+ 字典 v3;社区 demo 三页消亡 |
|
||||
| ✅ 第四波 | E2E 烟囱 **14/14**、契约偏差 **0**、M3 四条验收标准逐条取证(报告 28);收官总结见报告 29 |
|
||||
| ⚠️ 遗留 | 真机四项挂起(清单已备齐步骤)、widthPx/heightPx 恒 null、eventVersion 口径未定型、uploading 清理任务、429 限流——完整清单见报告 29 §4 |
|
||||
|
||||
## 测试与契约演进
|
||||
|
||||
| 时点 | patbond-api | patbond-flutter | openapi.yaml |
|
||||
| --- | --- | --- | --- |
|
||||
| M3 开工基线 | 191 | 272 | v1.2.0(18 路径) |
|
||||
| 第一波收口 | 226 | 286 | v1.2.0 |
|
||||
| 第二波收口 | 325 | 286 | **v1.3.0 冻结**(31 路径/43 操作/72 schema) |
|
||||
| 第三波收口 | 334 | 502 | v1.3.0(矩阵 173 格零漂移) |
|
||||
| **收官** | **334** | **502**(+E2E 脚本) | v1.3.0(E2E 逐场景核验偏差 0) |
|
||||
|
||||
## 已完成(附提交)
|
||||
|
||||
**开工分析(报告 01~08)**:8 角色并行评估;Reality Checker 首个 **CERTIFIED** 无条件放行;对象存储选型经用户拍板定为自托管 MinIO 起步(ADR-016,预留迁云);ADR-016~021 入档(`patbond-doc@d286782`)。
|
||||
|
||||
**第一波(报告 09~14)**
|
||||
|
||||
- V5 community 8 表 + pg_trgm,剪 2 条跨 schema FK(`posts.generation_job_id`→M4、`posts.region_id`→M5 补回)(`patbond-api@a97814a`)。
|
||||
- patbond-community:8084 骨架,骨架期即接 RS256 校验(`3c671fc`)。
|
||||
- **MinIO 媒体闭环**:ObjectStorage 适配层 + 两步上传(预签名 PUT 直传 → confirm ready)+ 私有桶预签名 GET(`10a43f8`)。
|
||||
- auth 域契约测试补齐,首轮抓修 events `reason` 序列化漂移(`263cd88`)。
|
||||
- 埋点队列三项:30s 定时冲刷 / 指数退避 / anonymousId 持久化(`patbond-flutter@4d40c38`)。
|
||||
- 凭证防泄漏 9 规则两层检查三仓落地(`api@8330885`/`flutter@66f983d`/`doc@8e1fe2f`)。
|
||||
|
||||
**第二波(报告 15~20)**
|
||||
|
||||
- 帖子生命周期 5 端点 + 幂等 + 防枚举 40403(`101ac0f`);Feed + AuthorSummary + `/internal` 批量资料 + Feign 降级(`40bac85`/`99a3c1f`);评论/互动/关注 11 端点 + 真并发幂等 + 计数同事务(`19e8cba`/`7f1dd33`)。
|
||||
- **契约冻结 v1.3.0**:26 项草案修正照单全收(`patbond-doc@f848476`);四模块快照同步 + community 64 格 + media 8 格矩阵,修 `nullable+allOf` 校验盲区(`0569585`)。
|
||||
|
||||
**第三波(报告 21~27)**
|
||||
|
||||
- community 数据层 19 操作 + **ToggleSync** 乐观更新状态机(`19bd8c1`);MediaUploader 六态 + 孤儿防护(`1441f01`,并抓修 media 端点错挂服务的缺陷);Feed 四态 + 曝光聚合 + SignedNetworkImage(`8aac8c5`);详情页 + 互动跨页一致 + 三层视觉抑制(`92524da`/`f873acf`);发布页两步发布 + 草稿两路径 + 漏斗埋点(`9892b65`)。
|
||||
- 字典 v3 白名单 22→42 事件,7 个被否决事件锁死(`api@8089c06`)。
|
||||
|
||||
**第四波(报告 28~29)**
|
||||
|
||||
- E2E 烟囱脚本 `test_e2e_m3_manual.dart`,14 场景一次通过、复跑再次通过(`flutter@0e87413`)。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [后端模块结构与职责](../../../architecture/backend-modules.md)
|
||||
- [技术决策记录](../../../architecture/decisions.md)(ADR-016~021 为 M3 决策)
|
||||
- [真机验证清单](../../device-verification.md)(M3 四项步骤已备齐)
|
||||
- 契约:`docs/api/openapi.yaml` v1.3.0(冻结纪律见报告 18)
|
||||
Reference in New Issue
Block a user