- 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>
17 KiB
28 M2 收官:E2E 烟囱测试报告(T2-18)
- 执行人:Frontend Developer
- 日期:2026-09-08
- 环境:patbond-flutter (dev 分支) + patbond-api (docker compose 编排,代码零改动)
- 测试脚本:
patbond-flutter/test_e2e_m2_manual.dart(commit720865b,已推送 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_eventspsql 查证一致
⚠️ 真机挂起项(显著标注:真机待补验)
真机不可用(用户确认),以下两项按既定方案 A 挂起,本报告不含其证据, 待真机可用后补验:
| # | 挂起项 | 说明 |
|---|---|---|
| 1 | Android 真机事件落库观察 | 本报告以脚本直连 /api/v1/events(platform=android 模拟真机值)替代验证服务端链路;真机端 AnalyticsClient → 持久化队列 → 上报的端上链路待真机补验 |
| 2 | SessionTracker 30min 会话超时手测 | 前后台切换超时重建 sessionId 的真机手测;单元测试已覆盖规则(T2-15),真机行为待补验 |
脱敏声明
全部 token 截断至前 20 字符 + <REDACTED>;密码不出现在任何输出;.env 内容未引用。
1. 后端启动与健康检查
1.1 构建与启动(patbond-api 代码零改动)
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 容器健康状态
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 服务就绪验证
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 → 登录
[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:建档(含品种)→ 列表/详情读回核对
[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 分页读回
[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=true,nextCursor 非空
GET /weights?limit=1&cursor=... → 200
✓ 第二页:8.20kg,hasMore=false,nextCursor=null
契约验证:分页正典形态 {items, nextCursor, hasMore};measured_at DESC 排序;
末页 nextCursor 恒为 null。
2.5 场景 4:登记疫苗(scheduled)→ 标记完成(乐观锁)
[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→1,administeredOn/nextDueOn 回读一致
契约验证:scheduled → completed 状态机合法迁移;version 提交比对通过后 +1;
vaccineName 由目录解出;administeredOn=2026-09-08、nextDueOn=2027-09-08 原样回读。
2.6 场景 5:记健康事件(金额整数分)→ 时间线读回
[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 校验)
[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:摘要四项聚合逐项断言
[7/11] GET /summary?tz=Asia/Shanghai 四项聚合逐项断言
GET /summary → 200
✓ 最新体重 = 8.45kg(第二条写入,measured_at DESC 首行)
✓ 疫苗进度 = 1/1(scheduled→completed 后)
✓ 下次接种 = completed 行的 nextDueOn(2027-09-08,source=nextDue)
✓ 当月花费 = 12500 分,month=2026-09,timezone 回显 Asia/Shanghai
四项聚合与前述写入逐项一致(口径 = iteration-2 报告 18 §3 定型表):
| 聚合项 | 前述写入 | 摘要返回 | 结论 |
|---|---|---|---|
| latestWeight | 8.45kg(measured_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 的宠物四路(防枚举)
[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 重新登录全量读回
[9/11] 账号 A 重新登录(模拟第二设备新会话)→ 全量数据读回
POST /api/v1/auth/login (设备 2) → 200
✓ 新会话 token 与设备 1 不同(独立 token family)
✓ 宠物列表:1 只(旺财M2)
✓ 体重记录:2 条
✓ 疫苗记录:1 条(completed)
✓ 健康事件:1 条
✓ 提醒:1 条(completed,completedAt=2026-09-08T06:39:04.429336Z)
设备 1 写入的全部五类数据在设备 2 新会话完整读回,服务端为唯一事实源。
2.11 场景 10:埋点链路——v2 事件上报与落库
[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 逐条 accepted(accepted=4, duplicated=0, rejected=0)
落库查证(docker exec psql):
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:乐观锁冲突明确性
[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)
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 格式化检查
dart format --output=none --set-exit-if-changed lib test
# Formatted 97 files (0 changed) in 0.39 seconds.
# EXIT: 0
4.2 静态分析
flutter analyze
# Analyzing patbond-flutter...
# No issues found! (ran in 0.9s)
(含根目录两个 E2E 脚本在内全仓 0 issues;两脚本头部 ignore_for_file: avoid_print。)
4.3 单元/组件测试
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. 环境清理
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:
720865btest: M2 E2E 烟囱脚本(T2-18 收官)已推送 origin/dev - patbond-api:代码零改动(仅 compose 起停)
- patbond-doc:本报告(28 号),提交与 mkdocs 导航由 T2-19 文档收口统一处理
9. 遗留清单
- 真机待补验 ×2(见 §0 显著标注):Android 真机事件落库观察、SessionTracker 30min 会话超时手测——真机可用后按方案 A 补验并追加证据。
- caregiver/viewer 角色的 403/40300 路径本次未走(M2 无邀请入口,T2-10 已用 测试数据直构场景覆盖,见报告 20),烟囱层面留待 M3 邀请流程落地后自然覆盖。
- E2E 脚本可在 M3 纳入 CI 定期回归(当前为手动验收工具,与第一迭代建议一致)。
Frontend Developer 日期:2026-09-08 验收状态:PASSED(11/11 场景,契约偏差 0,M2 四条验收标准全部通过;真机两项挂起待补验)