Files
lixi fcac68daf2
CI / docs-build (push) Successful in 51s
docs: M2 收官——E2E 烟囱报告与收官总结入档,验收 PASSED
- 28 E2E 烟囱:11/11 场景全绿、契约偏差 0、四条验收标准全过(真机两项方案 A 挂起)
- 29 收官总结:终态对照开工基线(测试 82/34→191/272、契约 5→18 路径冻结、
  ADR 001~015、生产埋点从零到贯通)、遗留清单与 M3 方向输入
- 看板终态更新

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 14:47:29 +08:00

408 lines
17 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.
# 28 M2 收官:E2E 烟囱测试报告(T2-18)
- 执行人:Frontend Developer
- 日期:2026-09-08
- 环境:patbond-flutter (dev 分支) + patbond-api (docker compose 编排,代码零改动)
- 测试脚本:`patbond-flutter/test_e2e_m2_manual.dart`commit `720865b`,已推送 origin/dev
- 参照模式:iteration-1/18 号收官报告(格式与取证标准沿用)
---
## 0. 执行概要
### 测试目标
M2 第四波收官(工单 T2-18):在 compose 真实后端上跑通 M2 完整链路烟囱并收集证据——
登录 → 建档 → 记体重 → 登记疫苗 → 记健康事件 → 摘要数值核对 → 第二账号访问被拒 →
第二设备同账号全量读回,外加埋点落库与乐观锁冲突两条链路。
### 测试结果
**✓ 11/11 场景全部通过**(单次运行一次通过;格式化后复跑再次 11/11)
- Docker Compose 四容器健康运行(postgres + auth:8081 + user:8082 + pet:8083
- 契约一致性:响应字段、错误码、HTTP 状态码与冻结契约 openapi v1.2.0 完全一致
- **契约偏差数:0 个**
- Flutter 门禁三命令全绿:`dart format`0 changed/ `flutter analyze`No issues/
`flutter test`**272 passed**
- 数据库证据齐备:`pet_health` 六表 + `platform.product_events` psql 查证一致
### ⚠️ 真机挂起项(显著标注:真机待补验)
真机不可用(用户确认),以下两项按既定方案 A 挂起,**本报告不含其证据**,
待真机可用后补验:
| # | 挂起项 | 说明 |
| --- | --- | --- |
| 1 | **Android 真机事件落库观察** | 本报告以脚本直连 `/api/v1/events`platform=android 模拟真机值)替代验证服务端链路;真机端 AnalyticsClient → 持久化队列 → 上报的端上链路待真机补验 |
| 2 | **SessionTracker 30min 会话超时手测** | 前后台切换超时重建 sessionId 的真机手测;单元测试已覆盖规则(T2-15),真机行为待补验 |
### 脱敏声明
全部 token 截断至前 20 字符 + `<REDACTED>`;密码不出现在任何输出;`.env` 内容未引用。
---
## 1. 后端启动与健康检查
### 1.1 构建与启动(patbond-api 代码零改动)
```bash
cd patbond-api
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package
# BUILD SUCCESS
docker compose up -d --build
# Container patbond-postgres-1 Healthy
# Container patbond-auth-1 Started
# Container patbond-user-1 Started
# Container patbond-pet-1 Started
```
### 1.2 容器健康状态
```text
NAMES STATUS PORTS
patbond-pet-1 Up 12 seconds 0.0.0.0:8083->8083/tcp
patbond-auth-1 Up 12 seconds 0.0.0.0:8081->8081/tcp
patbond-user-1 Up 12 seconds 0.0.0.0:8082->8082/tcp
patbond-postgres-1 Up 15 seconds (healthy) 5432/tcp
```
### 1.3 服务就绪验证
```bash
docker logs patbond-pet-1 | grep Started
# Started PetApplication in 6.286 seconds
curl -s http://127.0.0.1:8083/api/v1/pets
# {"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. E2E 烟囱测试执行记录(11 场景)
### 2.1 测试脚本
`test_e2e_m2_manual.dart`(纯 dart HttpClient 脚本,无 Flutter 运行时依赖,
与第一迭代 `test_e2e_manual.dart` 并列放仓库根目录,**不在 test/ 目录**、
不进 `flutter test`)。随机生成账号 `e2e_m2_a_<timestamp>` / `e2e_m2_b_<timestamp>`
避免冲突。运行方式:`docker compose up -d``dart run test_e2e_m2_manual.dart`
本次取证运行:账号 A `e2e_m2_a_1788849543120`petId `01a07fbd-dcad-756a-a844-fe5323aa0713`
### 2.2 场景 1:注册账号 A → 登录
```text
[1/11] 注册账号 A → 登录
POST /api/v1/auth/register → 200
✓ 注册成功
userId(A): 01a07fbd-d92f-7ee7-a52c-d33c7fa80071
accessToken: eyJhbGciOiJSUzI1NiJ9...<REDACTED>
POST /api/v1/auth/login → 200
✓ 登录成功(设备 1 会话)
```
### 2.3 场景 2:建档(含品种)→ 列表/详情读回核对
```text
[2/11] 建档(POST /pets,含品种)→ 列表/详情读回核对
GET /api/v1/breeds?species=dog → 200
✓ 品种目录返回 16 条
选用品种: 中华田园犬 (3a545503-a8ad-484a-8bbf-a38d08c6dcea)
POST /api/v1/pets → 201
✓ 建档成功(201
petId: 01a07fbd-dcad-756a-a844-fe5323aa0713
myRole: owner / version: 0 / breedDisplayName: 中华田园犬
✓ 创建者角色为 owner
✓ 品种展示名解出一致
GET /api/v1/pets → 200
✓ 列表读回 1 只宠物且 id 一致
GET /api/v1/pets/{petId} → 200
✓ 详情读回核对通过(name/species/breedId/status
```
契约验证:201 + PetEnvelope、`myRole=owner`(创建者自动 primary owner)、
`breedDisplayName` 由字典解出、petId 为 UUIDv7(前缀 `01a07fbd`)。
### 2.4 场景 3:记体重 ×2 → cursor 分页读回
```text
[3/11] 记体重 ×2 → 列表 cursor 分页读回
POST /weights (8.20kg, 2026-09-06T06:39:04.429336Z) → 201
POST /weights (8.45kg, 2026-09-07T06:39:04.429336Z) → 201
GET /weights?limit=1 → 200
✓ 第一页:最新体重 8.45kg 在前,hasMore=truenextCursor 非空
GET /weights?limit=1&cursor=... → 200
✓ 第二页:8.20kghasMore=falsenextCursor=null
```
契约验证:分页正典形态 `{items, nextCursor, hasMore}``measured_at DESC` 排序;
末页 `nextCursor` 恒为 null。
### 2.5 场景 4:登记疫苗(scheduled)→ 标记完成(乐观锁)
```text
[4/11] 登记疫苗(scheduled)→ 标记完成(PATCH + version
GET /api/v1/vaccine-catalog?species=dog → 200
✓ 疫苗目录返回 6 条
选用疫苗: 犬二联疫苗 (e07d9a48-a8d3-4b6a-a14b-03a42fe85589)
POST /vaccinations (scheduled, plannedOn=2026-09-08) → 201
vaccinationId: 01a07fbd-ddc2-7dc8-aa2e-768a51785dc7 / version: 0
PATCH /vaccinations/{id} (→completed, version=0) → 200
✓ 标记完成成功,version 0→1administeredOn/nextDueOn 回读一致
```
契约验证:`scheduled → completed` 状态机合法迁移;`version` 提交比对通过后 +1
`vaccineName` 由目录解出;`administeredOn=2026-09-08``nextDueOn=2027-09-08` 原样回读。
### 2.6 场景 5:记健康事件(金额整数分)→ 时间线读回
```text
[5/11] 记健康事件(amountCents 整数分)→ 时间线读回
POST /health-events (medical, amountCents=12500) → 201
✓ amountCents=12500 原样回读,createdByUserId=token subject
healthEventId: 01a07fbd-de0a-7532-8d71-023452ba21ad
GET /health-events → 200
✓ 时间线读回 1 条且字段一致
```
契约验证:金额整数分传输无精度损耗;`createdByUserId` 取自验签 token
(等于账号 A userId),不收请求体。
### 2.7 场景 6:创建提醒 → 标记完成(completedAt 校验)
```text
[6/11] 创建提醒 → 标记完成(completedAt 校验)
POST /care-reminders (deworming, dueAt=2026-10-08T06:39:04.429336Z) → 201
✓ 提醒创建成功,恒为 pending 且 completedAt=null
reminderId: 01a07fbd-de48-779f-87f6-11135ae93be7
PATCH /care-reminders/{id} (→completed) → 200
✓ 标记完成成功,completedAt=2026-09-08T06:39:04.429336Z(客户端提交时刻回读)
```
契约验证:创建恒为 `pending`(不收 status);`completedAt` 由客户端提交、
非空当且仅当 `status=completed`
### 2.8 场景 7:摘要四项聚合逐项断言
```text
[7/11] GET /summary?tz=Asia/Shanghai 四项聚合逐项断言
GET /summary → 200
✓ 最新体重 = 8.45kg(第二条写入,measured_at DESC 首行)
✓ 疫苗进度 = 1/1scheduled→completed 后)
✓ 下次接种 = completed 行的 nextDueOn2027-09-08source=nextDue
✓ 当月花费 = 12500 分,month=2026-09timezone 回显 Asia/Shanghai
```
四项聚合与前述写入逐项一致(口径 = iteration-2 报告 18 §3 定型表):
| 聚合项 | 前述写入 | 摘要返回 | 结论 |
| --- | --- | --- | --- |
| latestWeight | 8.45kgmeasured_at 最新) | weightKg=8.45 | ✓ |
| vaccinationProgress | 1 条 completed / 1 条已登记 | completedDoses=1, totalDoses=1 | ✓ |
| nextVaccination | completed 行 nextDueOn=2027-09-08 | dueOn=2027-09-08, source=nextDue, vaccinationId 命中 | ✓ |
| monthlyExpense | amountCents=12500(当月事件) | amountCents=12500, month=2026-09, timezone=Asia/Shanghai | ✓ |
### 2.9 场景 8:权限拒绝——账号 B 访问 A 的宠物四路(防枚举)
```text
[8/11] 注册账号 B → 用 B 的 token 访问 A 的宠物四路(防枚举核对)
POST /api/v1/auth/register (B) → 200
详情 GET /pets/{id} → 404 / code 40401 ✓
体重 GET /pets/{id}/weights → 404 / code 40401 ✓
疫苗 GET /pets/{id}/vaccinations → 404 / code 40401 ✓
摘要 GET /pets/{id}/summary → 404 / code 40401 ✓
✓ 四路响应体完全一致(防枚举):{"code":40401,"message":"宠物不存在","data":null}
✓ B 的宠物列表为空(列表天然隔离)
```
契约验证:无关系调用者与「宠物不存在」响应逐字节一致,随机探测 UUID 无法区分
是否命中真实记录(防枚举语义)。
### 2.10 场景 9:跨设备读取——账号 A 重新登录全量读回
```text
[9/11] 账号 A 重新登录(模拟第二设备新会话)→ 全量数据读回
POST /api/v1/auth/login (设备 2) → 200
✓ 新会话 token 与设备 1 不同(独立 token family
✓ 宠物列表:1 只(旺财M2
✓ 体重记录:2 条
✓ 疫苗记录:1 条(completed
✓ 健康事件:1 条
✓ 提醒:1 条(completedcompletedAt=2026-09-08T06:39:04.429336Z
```
设备 1 写入的全部五类数据在设备 2 新会话完整读回,服务端为唯一事实源。
### 2.11 场景 10:埋点链路——v2 事件上报与落库
```text
[10/11] POST /api/v1/events 上报 v2 事件(platform=android 模拟真机值)
eventId: 0c673914-... (pet_create_succeeded)
eventId: dd59a83a-... (health_record_create_succeeded, recordType=weight)
eventId: 3993beb4-... (health_record_create_succeeded, recordType=vaccine)
eventId: 87a4a82b-... (page_viewed)
POST /api/v1/events (4 条) → 202
✓ 4/4 逐条 acceptedaccepted=4, duplicated=0, rejected=0
```
**落库查证(docker exec psql**
```text
patbond=# SELECT event_name, event_version, platform, user_id,
left(event_id::text,8) AS event_id_prefix, props
FROM platform.product_events
WHERE session_id = '4d8375a7-ec77-45bf-90d6-1b7eff73a1ff'
ORDER BY event_name;
event_name | event_version | platform | user_id | event_id_prefix | props
--------------------------------+---------------+----------+--------------------------------------+-----------------+-------------------------------------------------------
health_record_create_succeeded | 2 | android | 01a07fbd-d92f-7ee7-a52c-d33c7fa80071 | dd59a83a | {"durationMs": 640, "recordType": "weight"}
health_record_create_succeeded | 2 | android | 01a07fbd-d92f-7ee7-a52c-d33c7fa80071 | 3993beb4 | {"durationMs": 820, "recordType": "vaccine"}
page_viewed | 2 | android | 01a07fbd-d92f-7ee7-a52c-d33c7fa80071 | 87a4a82b | {"pageName": "pet_detail", "referrer": "pet_list"}
pet_create_succeeded | 2 | android | 01a07fbd-d92f-7ee7-a52c-d33c7fa80071 | 0c673914 | {"species": "dog", "petIndex": 1, "durationMs": 1200}
(4 rows)
```
4 条 v2 事件全部落 `platform.product_events`eventId、props 白名单键、
platform=android、user_id 归因逐项一致。(真机端上链路见 §0 挂起项 1。)
### 2.12 场景 11:乐观锁冲突明确性
```text
[11/11] 两次 PATCH 宠物档案提交同一 version → 第二次 409/40902
PATCH /pets/{id} (version=0, 第一次) → 200
✓ 第一次 PATCH 成功,version 0→1
PATCH /pets/{id} (同一过期 version=0, 第二次/设备 2) → 409
✓ 第二次被明确拒绝:409/40902(数据已被修改,请刷新后重试),先写者数据保留
✓ 读回确认先写者数据保留(personality=沉稳)
```
契约验证:不静默覆盖;先写者胜出;后写者得到明确的 409/40902 与可行动 message。
Flutter 端对 40902 的「明确提示 + 取新 version 重提」交互已有 widget 测试覆盖,
`test/features/pets/pet_form_page_test.dart`。)
---
## 3. 数据库查询证据(pet_health schema
```text
patbond=# SELECT name, species, status, version, personality FROM pet_health.pets WHERE id = '01a07fbd-...0713';
name | species | status | version | personality
--------+---------+--------+---------+-------------
旺财M2 | dog | active | 1 | 沉稳
patbond=# SELECT role, is_primary FROM pet_health.pet_owners WHERE pet_id = ...;
role | is_primary
-------+------------
owner | t
patbond=# SELECT weight_kg, measured_at FROM pet_health.pet_weight_records WHERE pet_id = ... ORDER BY measured_at DESC;
weight_kg | measured_at
-----------+-------------------------------
8.45 | 2026-09-07 06:39:04.429336+00
8.20 | 2026-09-06 06:39:04.429336+00
patbond=# SELECT status, dose_no, administered_on, next_due_on, version FROM pet_health.pet_vaccinations WHERE pet_id = ...;
status | dose_no | administered_on | next_due_on | version
-----------+---------+-----------------+-------------+---------
completed | 1 | 2026-09-08 | 2027-09-08 | 1
patbond=# SELECT event_type, title, amount_cents FROM pet_health.health_events WHERE pet_id = ...;
event_type | title | amount_cents
------------+-------------+--------------
medical | M2 烟囱体检 | 12500
patbond=# SELECT reminder_type, status, completed_at FROM pet_health.care_reminders WHERE pet_id = ...;
reminder_type | status | completed_at
---------------+-----------+-------------------------------
deworming | completed | 2026-09-08 06:39:04.429336+00
```
验证点:全部数据持久化落库;`pets.version=1`(一次成功 PATCH 后)与 40902 拒绝语义
互证;`amount_cents` 整数分无损;`completed_at` 与 API 回读一致。
---
## 4. Flutter 门禁验证(三命令随行取证)
### 4.1 格式化检查
```bash
dart format --output=none --set-exit-if-changed lib test
# Formatted 97 files (0 changed) in 0.39 seconds.
# EXIT: 0
```
### 4.2 静态分析
```bash
flutter analyze
# Analyzing patbond-flutter...
# No issues found! (ran in 0.9s)
```
(含根目录两个 E2E 脚本在内全仓 0 issues;两脚本头部 `ignore_for_file: avoid_print`。)
### 4.3 单元/组件测试
```bash
flutter test
# 00:17 +272: All tests passed!
```
**✓ 272 个测试全部通过**(E2E 脚本在仓库根目录,不被 `flutter test` 收集)。
---
## 5. M2 四条验收标准逐条对照
| # | 验收标准 | 证据 | 结论 |
| --- | --- | --- | --- |
| 1 | **跨设备数据一致**:同账号第二设备读到全部数据 | 场景 9:设备 2 新会话读回宠物/体重×2/疫苗/事件/提醒全量一致;§3 psql 证实服务端持久化 | ✓ 通过 |
| 2 | **无权限访问被拒**:他人宠物不可见 | 场景 8:账号 B 四路全部 404/40401 且响应体逐字节一致(防枚举);B 列表为空 | ✓ 通过 |
| 3 | **并发冲突明确**:不静默覆盖 | 场景 4(疫苗 version 0→1+ 场景 11(同 version 二次 PATCH → 409/40902,读回证实先写者保留);前端 40902 交互有 widget 测试 | ✓ 通过 |
| 4 | **双端测试齐备** | 后端:compose 真实链路 11 场景全绿 + 契约测试基线(报告 20);前端:272 单元/组件测试全绿 + 门禁三命令 0 偏差 | ✓ 通过(真机两项挂起,见 §0) |
## 6. 契约偏差声明
**契约偏差数:0 个。**
本次烟囱对照冻结契约 openapi v1.2.0(报告 19 冻结)逐场景核验:HTTP 状态码
200/201/202/404/409)、业务错误码(40401/40902)、信封结构 `{code, message, data}`
分页正典形态、乐观锁语义、防枚举响应体、埋点逐条结果语义,全部一致,无需修复项。
---
## 7. 环境清理
```bash
cd patbond-api && docker compose down
# Container patbond-pet-1 / patbond-auth-1 / patbond-user-1 / patbond-postgres-1 Removed
# Network patbond_default Removed
```
## 8. 工作仓库状态
- patbond-flutter dev`720865b` `test: M2 E2E 烟囱脚本(T2-18 收官)` 已推送 origin/dev
- patbond-api**代码零改动**(仅 compose 起停)
- patbond-doc:本报告(28 号),提交与 mkdocs 导航由 T2-19 文档收口统一处理
## 9. 遗留清单
1. **真机待补验 ×2**(见 §0 显著标注):Android 真机事件落库观察、SessionTracker
30min 会话超时手测——真机可用后按方案 A 补验并追加证据。
2. caregiver/viewer 角色的 403/40300 路径本次未走(M2 无邀请入口,T2-10 已用
测试数据直构场景覆盖,见报告 20),烟囱层面留待 M3 邀请流程落地后自然覆盖。
3. E2E 脚本可在 M3 纳入 CI 定期回归(当前为手动验收工具,与第一迭代建议一致)。
---
**Frontend Developer**
日期:2026-09-08
验收状态:**PASSED**11/11 场景,契约偏差 0,M2 四条验收标准全部通过;真机两项挂起待补验)