Files
patbond-doc/docs/development/device-verification.md
T
lixi f9b1358b37
CI / docs-build (push) Successful in 1m59s
docs: 2026-09-11 安全事件复盘 + 新建服务器暴露面清单 + M3.5 报告 03/04/05 入档
安全事件(已闭环,服务恢复):
- 根因链:Gitea 3000 对公网开放 → 外部调用 /api/internal/manager/add-logger
  注入 gitconfig 的 uploadpack.packObjectsHook → 指向不存在的脚本 →
  upload-pack 发 NAK 后无法产出 pack → 全仓 HTTPS clone 失败(CI 全挂)
- 攻击未达成代码执行(hook 目标脚本不存在);三仓 ref 与本地逐一核对未被篡改;
  无系统层入侵(无陌生 key/crontab/挖矿进程/陌生登录)
- 新建常设「服务器暴露面清单」:补上服务器侧「决策变了环境没跟上」的核对机制
  (Nacos 在 ADR-002 移除后仍暴露公网近两个月)
- CI Runner 手册排障表增三条:CI 秒失败先在本机复现 checkout、跨仓比 CI
  须核对时间戳、clone 坏而 push 正常时查 packObjectsHook 注入

M3.5 交付报告:03 后端资料与头像(api 334→379)、04 契约冻结 v1.4.0
(31→32 路径、矩阵 173→181 格、11 格红转绿)、05 资料页与头像 UI(flutter 526→597)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 17:45:35 +08:00

223 lines
20 KiB
Markdown
Raw 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.
# 真机验证清单(常设)
> **定位**:跨迭代常设文档——凡「只能在真机/模拟器上验证」的事项都登记在此,按迭代分节;每项含操作步骤、通过标准与执行记录。真机到位或发版前照单执行。
> **维护约定**:各迭代收官时把真机专属验证项登记进来;完成后填执行记录并同步 [功能完成清单](feature-checklist.md) 对应条目状态。
> 原位置为 iteration-2/30 号报告,2026-09-08 提升为常设文档(M3 起亦有真机项)。
## 通用前置准备
**设备**Android 真机(推荐)或 Android 模拟器。桌面/Web 不可用——没有真实的移动端后台生命周期(`paused` 不触发),且 platform 值不在契约枚举内会被服务端整批拒绝。
**后端**(工作机上,进入你本地检出的 patbond-api 仓库目录执行):
```bash
cd <你的工作区>/patbond-api
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package
docker compose up -d --build
docker compose ps # 全部容器 Uppostgres healthy
```
**装机运行**(进入你本地检出的 patbond-flutter 仓库目录):
```bash
# 模拟器:宿主机地址用 10.0.2.2
flutter run -d <设备ID> \
--dart-define=PATBOND_API_BASE_URL=http://10.0.2.2:8081 \
--dart-define=PATBOND_USER_API_BASE_URL=http://10.0.2.2:8082 \
--dart-define=PATBOND_PET_API_BASE_URL=http://10.0.2.2:8083
# 真机:换成工作机局域网 IP(真机与工作机须同一网络)
# --dart-define=PATBOND_API_BASE_URL=http://<局域网IP>:8081 (其余同理)
```
> 注意:所有 base URL 都要传,漏传的会落到默认 127.0.0.1(指向手机自身)。M3 起若新增服务端口(如 community :8084),相应补 `PATBOND_COMMUNITY_API_BASE_URL`。
---
# M2 挂起项(2026-09-08 登记,待执行)
> 来源:报告 iteration-2/10 §2.1(验收 6)与 iteration-2/12 §3.2;方案 A 挂起决议见 iteration-2/29 §4。
> **时限提醒**iteration-3/06):建议在 **2026-09-21(北极星首次出数日)前完成**,否则首批读数只能标「未验收」。
## 验证一:Android 事件真实落库(~10 分钟)
**目的**:确认埋点链路在真实移动端(platform=android)端到端落库——桌面端已验证全链路仅差 platform 枚举这一步。
**步骤**
1. app 内注册新账号(用户名任意、手机号 11 位、密码 ≥8 位含字母数字),登录进入主页
2. 操作产生事件:切几个 Tab、建一只宠物档案、记一条体重
3. **把 app 退到后台**(Home 键,触发离开前台冲刷),等 5 秒
4. 工作机查库:
```bash
docker exec patbond-postgres-1 psql -U patbond -d patbond -c \
"SELECT event_name, platform, session_id, client_ts
FROM platform.product_events ORDER BY client_ts DESC LIMIT 20;"
```
**通过标准**
- [ ] 有行返回,`platform` 列为 `android`
- [ ] 事件覆盖 ≥3 类(如 page_viewed、pet_create_started/succeeded、health_record_create_succeeded
- [ ] 本轮所有事件共享同一个 `session_id`UUIDv7 格式)
## 验证二:SessionTracker 30 分钟后台换会话(~45 分钟,含等待)
**目的**:验证 10 号报告 §2.1 验收 6——退后台超 30 分钟回前台应更换 sessionId,不超过则沿用。
**步骤**(接验证一,同一次登录、不杀进程):
1. 回前台随便操作一下(记一条体重)
2. **退后台等 5 分钟** → 回前台操作(再记一条体重或切 Tab)
3. **退后台等 35 分钟** → 回前台操作一次
4. 再退一次后台(触发冲刷),等 5 秒后查库:
```bash
docker exec patbond-postgres-1 psql -U patbond -d patbond -c \
"SELECT DISTINCT session_id, min(client_ts) AS first_seen
FROM platform.product_events
WHERE user_id = (SELECT id FROM identity.users WHERE username = '<你的测试用户名>')
GROUP BY session_id ORDER BY first_seen;"
```
**通过标准**
- [ ] 恰好 **2 个** session_id(第 2 步的 5 分钟不换会话、第 3 步的 35 分钟换新)
- [ ] 两个会话的 first_seen 时间差 ≈ 40 分钟(与操作节奏吻合)
**巡检 SQL 兜底**(06 号 §5.1 口径,防「每事件一个 sessionId」缺陷复发):
```bash
docker exec patbond-postgres-1 psql -U patbond -d patbond -c \
"SELECT count(DISTINCT session_id)::float / count(*) AS ratio
FROM platform.product_events;"
# ratio 应远小于 0.9> 0.9 说明 sessionId 生成有问题,告警
```
## 收尾
```bash
cd <你的工作区>/patbond-api && docker compose down
# 测试数据不入库(协作规则 3):本清单产生的数据都在 compose 卷里,
# 需要干净环境时 docker compose down -v 清卷即可
```
两项都过后:填写下方执行记录 + [功能完成清单](feature-checklist.md) 第 9 节「Android 真机落库验证 + SessionTracker 30min 手测」由 🟡 改 ✅。若有任何一项不过,按惯例开缺陷单修复后复测。
### M2 项执行记录
_(待真机到位后填写:日期、设备型号/Android 版本、两项结果、psql 输出摘录(脱敏)、执行人)_
---
# M3 预登记(社区,随迭代交付补全)
以下为 M3 交付过程中预计产生的真机专属验证项,**各工单收口时在此补全具体步骤与通过标准**:
1. **媒体上传弱网表现**T3-13 收口补全,2026-09-09):真机蜂窝/弱 Wi-Fi 下选图→压缩→预签名直传→确认全链路;中断重试不产生孤儿 asset。
**前置**:通用前置准备的后端六容器在位;`PATBOND_MINIO_PUBLIC_ENDPOINT` 必须配置为手机可达地址(工作机局域网 IP:9000,.env 覆盖后重启 compose)——预签名直传 URL 直指 MinIO,漏配则手机端 PUT 必然连不上;`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL`),media 上传走 user 服务 :8082`PATBOND_USER_API_BASE_URL`)。入口:发布页(T3-17 落地后)九宫格选图。
**步骤与通过标准**
- (a)**蜂窝正常网**:相册多选 3 张 12MP 大图 → 逐格出现进度环且百分比递增(非一跳 100%)→ 全部转 ready;后端 `media.assets` 对应 3 行 `status='ready'`。压缩耗时中端机单张 ≤2s(超出记录机型上报)。
- (b)**弱网中断重试**:开发者选项限速或电梯/地库弱网,上传中开飞行模式掐断直传 → 该格转失败态(红色蒙层 + 重试通栏),其余图不受影响;恢复网络点格内重试 → 转 ready。
- (c)**孤儿不引用**:在(b)失败态与上传中态各尝试一次发布 → 发布钮 gating 拦截(全部 ready 前不可提交);发帖成功后 psql 核对 `community.post_media` 引用的 assetId 全部 `status='ready'`,且不含(b)中断产生的旧 assetId(该行保持 `uploading`,属服务端超时清理范围,不算失败)。
- (d)**凭据过期**:选一张图后挂起 App >10 分钟再恢复触发重试 → 客户端自动换新凭据完成上传(用户无感知,不弹「签名过期」类错误)。
- e**HEIC/方向**iPhone 传输的 HEIC 图与横拍竖拍各一张 → 压缩层统一出 jpeg 且方向正确(服务端 mime 白名单不收 HEIC,此项只能真机验证原生编解码)。
2. **乐观更新真机手感**T3-15/16 收口补全,2026-09-09):点赞/收藏快速连点的合并与回滚动画在真机帧率下的表现;Feed 卡片与详情页跨页状态一致。
**前置**:通用前置准备的后端六容器在位;`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL=http://<局域网IP>:8084`)。数据:Feed 内至少一条他人发布的帖子(可按 iteration-3 24 号报告 §5a)种子方式造)。
**步骤与通过标准**
- (a)**激活动画帧率**:Feed 卡片与详情页各点赞一次 → 图标同帧翻转 + 240ms 弹性缩放(1→1.25→1)+ 计数即时 ±1;中低端机无可见掉帧或延迟出现的「二次跳动」。取消点赞仅颜色渐出、无缩放。
- (b)**快速连点合并**:同一帖 1 秒内连点点赞 5~6 次 → 视觉每次即时翻转;抓包或服务端访问日志核对该帖 like 端点请求 ≤2 个(单飞 + 最终意图补发);停点后终态与最后一次点击一致,计数与 `GET /api/v1/posts/{id}` 权威值相符。
- (c)**断网回滚**:开飞行模式后点赞 → 图标即时翻转,数秒内**零动画直接跳回**原状态(不得出现「心已灭计数未减」的中间帧或回弹动画)+ SnackBar「操作失败,请重试」恰一条;恢复网络重点 → 正常收敛。
- (d)**跨页一致**:Feed 卡片点赞 → 进详情页应已是激活态;详情页取消收藏 → 返回 Feed 卡片同步取消(同一 ToggleSync 实例,无需刷新)。
- (e)**减弱动态**:系统开启「移除/减弱动画」后点赞 → 状态瞬变、无缩放动画,功能不受影响。
3. **Feed 图片加载**T3-14 收口补全,2026-09-09):真机上滚动 Feed 的图片加载/缓存/占位表现;MinIO 经局域网/公网访问 URL 的可达性差异。
**前置**:通用前置准备的后端六容器在位;`PATBOND_MINIO_PUBLIC_ENDPOINT` 必须配置为手机可达地址(工作机局域网 IP:9000,.env 覆盖后重启 compose)——Feed 卡片封面 URL 是服务端现签的预签名 GET、直指 MinIO,漏配则真机图片全部走失败兜底(`surfaceTint` 底 + pets 图标);`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL=http://<局域网IP>:8084`)。数据:桌面/工作机先按 iteration-3 24 号报告 §5(a)的种子方式发 ≥26 帖(含单图/多图),保证两页以上可翻。
**步骤与通过标准**
- (a)**首屏与占位**:登录进 Feed → 图片卡先出 `surfaceTint` 加载块(无白闪/布局跳动),随后出图;多图卡右下「+N」角标可读(ink 80% 胶囊白字)。
- (b)**滚动加载**:连续滚到列表底再回顶 → 中低端机不掉帧卡死;回滚经过已看过的图**不重新转圈**(缓存 key 已剥签名参数,同图不同签名命中同一内存缓存——若出现「每次刷新同图重新下载」即为缓存 key 回归,判失败)。
- (c)**下拉刷新后的缓存命中**:下拉刷新(服务端对同一批图重新现签、URL 必然变化)→ 已展示过的封面应即时出图不过转圈;抓包或 MinIO 访问日志核对同对象未重复 GET。
- d**过期 URL 重取**Feed 停留 >1 小时(预签名 TTL)后滚到未加载过的卡 → 旧 URL 过期图走失败兜底属预期,下拉刷新取新签 URL 后恢复出图,无崩溃。
- (e)**可达性差异**:Wi-Fi(局域网 IP)与蜂窝(若 MinIO 未公网暴露)各滚一遍——蜂窝下连不上 MinIO 时应稳定显示失败兜底图标而非无限转圈;记录两种网络的首图出图耗时。
4. **社区事件落库**T3-17 收口补全,2026-09-10):community 域 v3 事件(platform=android)落库观察(沿 M2 验证一的方法,事件名换 v3 增量)。**桌面端不可替代**:Linux 桌面的 `platform=linux` 不在契约枚举内,整批 400 被拒(`analytics_service.dart` 既有预期行为),故 v3 事件的**落库**只能在 Android 上验证;键集与形态的落库正确性已在工作机以 curl 造真实 payload 验证(iteration-3/26 §5c 发布/媒体 8 事件、iteration-3/25 §5c 互动 8 事件)。
**前置**:通用前置准备的后端六容器在位;`flutter run` 时四个 base URL 全传(含 `PATBOND_COMMUNITY_API_BASE_URL`);`PATBOND_MINIO_PUBLIC_ENDPOINT` 配为手机可达地址(媒体三段需真实直传)。可与第 1、2 项同一轮操作合并执行。
**步骤**:登录 → 首页 Feed 滚两屏并下拉刷新一次 → 进一条帖详情点赞/收藏/评论一次 → 返回 → 创作 Tab「发布动态」→ 输入正文 + 选 2 张图 → 「存草稿」一次 → 「发布」→ 回 Feed 确认新帖 → **退到后台等 5 秒**(触发冲刷)→ 工作机查库:
```bash
docker exec patbond-postgres-1 psql -U patbond -d patbond -c \
"SELECT event_name, platform, props FROM platform.product_events
WHERE event_name LIKE 'post\_%' OR event_name LIKE 'feed\_%'
OR event_name LIKE 'comment\_%' OR event_name LIKE 'user\_%'
ORDER BY client_ts DESC LIMIT 40;"
```
**通过标准**
- [ ] (a)**发布漏斗成链**:`post_create_started`(entryPoint=create_tab) → `post_draft_saved`(trigger=manual, mediaCount=2) → `post_publish_succeeded`(fromDraft=true、mediaCount=2、topicCount=0、textLengthBucket、durationMs>0) 三条齐全且 `platform=android`;无 `post_publish_failed`(顺利路径)。
- [ ] (b)**媒体三段逐文件成对**:`post_media_upload_started` / `_succeeded` 各 **2** 条(每张图一条),`sizeBucket` 同一张图的 started/succeeded 取值一致,`durationMs` 为真实上传耗时(非 0);中断重试的那张(与第 1 项(b)合并执行时)另有 `post_media_upload_failed`(failureReason=network_error, attemptSeq=1) + 重试后 started 的 `attemptSeq` 递进。
- [ ] c**隐私红线**:上述 props 中**不含** postId / assetId / commentId / 文件名 / 本地路径 / URL / 精确字数(`textLength`/ 精确字节数(`byteSize`)——出现任一即验收失败(红线 1/2/4)。
- [ ] d**互动与 Feed**`post_liked`/`post_favorited`(source=feed 或 post_detail)、`comment_create_succeeded`、`feed_viewed`(离开 Feed 时一条,impressionCount>0、refreshCount=1)落库;**无** `post_impression`/`post_viewed`(字典锁死为 unknown,若出现即客户端违规)。
- [ ] e**页名归一化**`page_viewed` 出现 `pageName='post_form'`(发布页)与 `'post_detail'`,且 pageName/referrer 中**不含 UUID**。
- [ ] (f)拒绝计数为 0:查 app 日志无 `Analytics batch permanently rejected`,或服务端响应 `rejected=0`(有 rejected 说明事件名/键集与字典不符,属回归)。
## 执行记录(M3
_(待补)_
---
# M3.5 预登记(用户资料与头像,2026-09-11 登记)
M3.5 第二波(T3.5-08/09/10)交付后新增的真机专属项。**桌面已覆盖的不重复登记**:注册/登录 → 资料页真实化 → 设昵称 → 传用户头像 → Feed 作者名同步 → 宠物头像上传的整条链路已在 Linux 桌面对 compose 真后端跑通并逐步截图(`integration_test/profile_avatar_live_test.dart`,见 iteration-3.5/05 号报告 §4);下列四项是**桌面替代不了**的部分。
1. **头像上传弱网表现**(T3.5-08/09 收口登记):真机蜂窝/弱 Wi-Fi 下「选图 → 压缩 → 预签名直传 → confirm → PATCH 挂载」全链路。
**前置**:通用前置准备的后端六容器在位;`PATBOND_MINIO_PUBLIC_ENDPOINT` 必须配置为手机可达地址(工作机局域网 IP:9000,.env 覆盖后重启 compose)——头像直传与预签名读都直指 MinIO,漏配则手机端必失败;`flutter run` 四个 base URL 全传。入口:我的资料 → 编辑资料 → 更换头像;档案 → 宠物详情 → 点头像。
**与第 1 项(媒体上传弱网)的差异**:头像走的是**单图、单并发**的 `MediaUploader``maxImages: 1`、`maxConcurrentUploads: 1`),且交付口是「预览后点『使用这张』才落 assetId」,不是九宫格的批量 gating。故槽位调度与孤儿防护的表现需单独看。
**步骤与通过标准**
- [ ] (a)**正常网真机相册**:从相册选一张 12MP 竖拍照 → sheet 内出线性进度条与递增百分比(非一跳 100%)→ 出圆形预览 → 点「使用这张」→ 回编辑页显示「已选择新头像」→ 保存后资料页头像**真的画出图**(不是爪印/人形占位)。此项同时验证原生压缩层(桌面实测用的是透传替身,真机才走 flutter_image_compress)。
- [ ] (b)**弱网中断重试**:上传中开飞行模式掐断 → sheet 转失败态并给「重试 + 重新选择」两个按钮 → 恢复网络点「重试」→ 转就绪可确认。中断产生的旧 asset 保持 `uploading`(属服务端超时清理范围,不算失败);核对 `identity.users.avatar_asset_id` / `pet_health.pets.avatar_asset_id` **未**指向中断的那个 assetId。
- [ ] (c)**压缩后仍超限**:选一张超大原图(若压缩后仍 >10 MiB)→ 失败态**只给「重新选择」、不给「重试」**(重试同一张必然再失败)。
- [ ] (d)**凭据过期**:选图后把 App 挂起 >10 分钟再回前台触发重试 → 自动换新凭据完成上传,用户无感知,不弹「签名过期」类错误。
- [ ] (e)**中途退出不留引用**:上传中直接关掉 sheet → 无 SnackBar 报错、资料/宠物头像不变;`post_media_upload_failed(failureReason=cancelled)` 一条(头像沿用同一套媒体埋点)。
- [ ] f**HEIC 与方向**iPhone 传输的 HEIC 与横拍各一张 → 压缩层统一出 jpeg 且方向正确(服务端 mime 白名单不收 HEIC,只能真机验原生编解码)。
2. **头像缓存表现**T3.5-08/09 收口登记):预签名 URL 每次响应现签,缓存 key 已剥 `X-Amz-*`;真机需确认同一张头像不会反复下载、过期后能重取。
**前置**:同第 1 项。数据:本人已设头像、至少一只宠物已设头像、Feed 内有本人发布的帖(作者头像与资料页头像同一对象)。
**步骤与通过标准**
- [ ] (a)**跨页命中**:资料页 → 首页 Feed(作者头像)→ 档案列表 → 宠物详情,来回切三轮 → 已展示过的头像**不再转圈**;抓包或 MinIO 访问日志核对同一 object key 未重复 GET(缓存 key 剥签名参数生效;若「每次进页面同图重下」即为 `presignedImageCacheKey` 回归,判失败)。
- [ ] b)**下拉刷新后仍命中**:Feed 下拉刷新(服务端重新现签、URL 必变)→ 作者头像即时出图不转圈。
- [ ] c**过期后重取**:停留 >1 小时(预签名 TTL 默认 1h)后进未加载过的页 → 旧 URL 过期走占位属预期;下拉刷新/重进页面取新签 URL 后恢复出图,无崩溃、不缓存坏图。
- [ ] (d)**清除头像后不留残影**:编辑页「清除头像」保存 → 资料页立刻回人形占位,Feed 作者头像在服务端作者缓存过期后(≤60s,见下方备注)也回占位;重启 App 后仍是占位(URL 不得被持久化,纪律 R2)。
3. **caregiver 账号改宠物头像**T3.5-09 收口登记;ADR-022 D3.5-3 的 WRITE 档实证):桌面实测只跑了 owner 路径,caregiver 需第二个账号 + 一条协作关系。
**前置**:两个账号 Aowner/ Bcaregiver),B 对 A 的宠物有 caregiver 角色(关系授予入口尚未开放,按 `pet_health` 的协作表直接造数据,收口时补 SQL)。
**步骤与通过标准**
- [ ] (a)B 打开该宠物详情 → **头像铅笔角标在**(WRITE 档),但「编辑资料」入口**不在**(MANAGE 档,仅 owner)。
- [ ] (b)B 上传头像 → 成功;A 侧刷新详情看到同一张。
- [ ] c)viewer 角色的第三个账号 C → 头像不可点、无角标。
4. **获赞数与帖子点赞数对账**(T3.5-08 收口登记):资料页「获赞」= 本人已发布未删帖的 `like_count` 之和(**含自赞**,与帖子详情同口径)。
**步骤与通过标准**
- [ ] (a)自己给自己的帖点赞 → 帖子详情 likeCount +1,资料页「获赞」也 +1(两处数字必须能对上;对不上说明口径分叉)。
- [ ] (b)他人点赞 2 次不同帖 → 资料页「获赞」为各帖 likeCount 之和。
- [ ] (c)软删一篇被赞的帖 → 「获赞」与「我的作品」同时回落(删帖即撤回其数字)。
- [ ] (d)存一篇草稿 → 「我的作品」**不**变(草稿尚非作品)。
> **备注(服务端刻意的滞后,不是缺陷)**:改昵称/换头像后,**Feed 与帖子详情里的作者名与作者头像**最多滞后 60 秒才更新——community 侧 `AuthorProfileGateway` 把 `/internal/users/profiles` 的结果放在 60s TTL 的进程内缓存里(`patbond.author-profile.cache-ttl`,M3 T3-05)。资料页与首页问候语读的是 `/me`,**没有这层缓存,立即生效**。真机执行第 2、4 项时若看到「资料页已变、Feed 还是旧名」,先等过 60 秒再判定。
## 执行记录(M3.5
_(待补)_