Files
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

28 KiB
Raw Permalink Blame History

05 M3.5 第二波:资料页真实化 + 编辑页 + 宠物头像 + 首页问候语

作者:Frontend DeveloperFlutter 日期:2026-09-11 工单:T3.5-08(资料页真实化 + 编辑页)+ T3.5-09(宠物头像接线)+ T3.5-10(首页问候语)三单合并(同仓串行) 输入:patbond-flutter dev@7d5c84d526 测试基线);冻结契约 openapi v1.4.0doc main@5f02909);ADR-02203 号定型表;04 号冻结报告 提交:a4a97c0T3.5-08)→ eff3526T3.5-09)→ 6945436T3.5-10),均已推 origin/dev 结论先行:三单全部落地。测试 526 → 597(+71)全绿,flutter analyze 0 问题,dart format 无 diffcheck-secrets.sh --all exit 0。compose 六容器桌面实测整条链路走通并逐步截图**(注册 → 登录 → 资料真实化 → 设昵称 → 传用户头像 → Feed 作者名同步 → 宠物头像),像素级确认头像真的画出来。⚠️ 过程中修掉一个先于本单存在的错线/api/v1/me 挂在 auth(:8081) 而该端点由 user(:8082) 提供,此前无人消费 me() 故一直没暴露(§5.1)。另发现一处服务端刻意的 60s 滞后(Feed 作者名,非缺陷)已写进真机清单备注(§4.4)。**


1. 三单交付内容

1.1 T3.5-08 资料页真实化 + 编辑页

位置 变化
lib/core/models/patch_field.dart 新增 PatchField<T>PATCH 三态的类型载体(absent / clear / value
lib/features/auth/auth_models.dart UserProfile 6 字段(+nickname +avatarUrl+ displayName / hasAvatar新增 UpdateMeRequest(两个三态字段 + isEmpty
lib/features/auth/auth_repository.dart updateMeuserApi 线路/me 归 user 服务,见 §5.1
lib/features/community/community_models.dart MediaPurpose 三值(+user_avatar +pet_avatar);新增 CommunityStats
lib/features/community/community_repository.dart getMyCommunityStats()
lib/features/community/media_uploader.dart purpose 参数化(缺省 postImage,发布页行为不变)
lib/core/widgets/avatar_upload_sheet.dart 新增:复用 MediaUploader 六态编排的单图头像上传 sheet + 两个构造口 typedef
lib/features/profile/profile_controller.dart 新增/me 主链路四态 + 统计块独立三态 + save + reset
lib/features/profile/profile_display.dart 新增:错误文案分层 + validateNickname(按码点)
lib/features/profile/profile_edit_page.dart 新增:昵称输入 + 头像上传 + 昵称/头像各自的清除入口
lib/features/profile/profile_page.dart 176 行全 demo → 真实数据四态
lib/app/app.dart / main_shell_page.dart ProfileController 单例装配 + 头像上传构造口注入 + 登出 reset

头部三项自此全部来自服务端:展示名(/me)、头像(/me 的现签 avatarUrl)、四个数字(/me/community-stats 的获赞与作品 + follow-stats 的粉丝与关注)。demo 的「萌宠新手(豆豆家长)」/「Patbond 社区创作达人」/「24 / 1.8k / 2」全部退役,widget 测试反向钉住这几串文案不再出现。

「关注数」补齐为两个数字(关注我 / 我关注)而非只取一个:follow-stats 一次调用同时给出 followerCountfollowingCount,只显示一半反而要用户猜是哪一半。

数字不做 1.8k 式压缩:获赞总数要能与帖子详情的 likeCount 逐一对上(同一口径、含自赞,是 03 号 §2.5 的定型),压缩会让「对不上」变成常态——这条也进了真机清单第 4 项。

1.2 T3.5-09 宠物头像接线

位置 变化
lib/features/pets/pet_models.dart Pet.avatarUrl 入模型;UpdatePetRequest.avatarAssetId 三态(该 schema 唯一)
lib/features/pets/pet_display.dart petAvatarSaveErrorMessage40405 / 42203 / 40300 / 40902 分层)
lib/features/pets/pet_detail_page.dart 头像展示真实 URL;铅笔角标接上传;「更换 / 移除」二选一;权限 WRITE 档
lib/features/pets/pets_page.dart 列表卡展示真实头像 + 透传上传构造口
lib/core/widgets/pet_avatar.dart 文档更新(M2 的「不做上传」注释改为 T3.5-09 的实情)
  • 铅笔角标自此有功能。此前它渲染着但点下去是打开资料表单(表单里没有头像字段),从用户视角等于「渲染了但没用」——工单的描述与实情一致。
  • 权限按 WRITE 档呈现owner + caregiver 有入口,viewer 没有),与资料编辑的 MANAGE 档刻意不同档。
  • 纯头像 PATCH 只发 version + avatarAssetId,一个资料字段都不带。这不是节省字节:服务端按「本次请求触及了哪些字段」定档,夹带任一资料字段就会把档位抬到 MANAGEcaregiver 立刻 40303 号 §2.4 的「防夹带」在客户端这一侧的对应义务)。widget 测试对此有 payload.keys.length == 2 的直接断言。
  • 40902 冲突不静默重放:提示「档案已被更新,请重新操作」+ 重取档案拿新 version,让用户决定是否重来。头像是用户可见的覆盖操作,自动生效两次比失败更糟。

1.3 T3.5-10 首页问候语

  • 「下午好,豆豆」→ 下午好,<nickname ?? username> 👋。数据源是与资料页同一个 ProfileController:一次 /me 供两个消费点,改昵称后两处一起变,无需手动刷新(widget 测试直接验这条)。
  • 资料未到手时退化为不称名的「下午好 👋」,不编造假名(不回落 demo 宠物名、不显示占位串)。
  • 其余首页 demo 一律未动ADR-022 决策 D3.5-1),并在代码里逐项标注为「刻意保留的 demo 占位」+ 去向:
保留项 标注位置 去向
天气条 / 地区选择 HomePage 类文档 + _WeatherStatusBar 需接外部天气服务(含 key 与配额管理)
圈子「柴犬圈 / 猫咪圈 / 救助站」 _StoryRow 实为话题ADR-018 已剪出
促销卡「新用户首单立减 ¥20」 _PromoCard M5 服务域(优惠/订单能力不存在)
搜索框与「本地服务」段 HomePage 类文档 契约无检索端点;服务商为 demo 常量,同属 M5
问候卡右侧大图 _PetGreetingCard.petAvatar 需要「当前宠物」概念,尚不存在,不在 ADR-022 范围内

home_greeting_test.dart 里有一个用例反向钉住「保留项仍在」——避免后续有人以「顺手清理 demo」为名越出拍板范围(真要动得先改 ADR)。


2. 展示名回退与 PATCH 三态:客户端实现要点

2.1 展示名回退做在展示层,而且只做一层

规则displayName => nickname ?? username,实现在 UserProfile 的 getter 上,两个消费点(资料页头部、首页问候语)共用。

为什么不让服务端回退(03 号 §2.1 的定型,客户端这一侧的对应义务):/me 是本人的编辑态。若服务端回退,编辑页会把 llx 预填进昵称输入框,用户会以为自己设过昵称;下一次保存就把这个纯展示约定固化成真实数据/internal/users/profiles 的 SQL 回退链从此再也不触发。所以:

视角 回退在哪 客户端做什么
本人(资料页 / 问候语) 客户端展示层 nickname ?? username
他人(Feed 作者名 / 评论) 服务端 SQLCOALESCEM3 T3-05 什么都不做——AuthorSummary.nickname 直接上屏,不拼装

编辑页预填只用 DB 原值_initial.nickname ?? '',空则空串),并把「现在别人看到的是用户名」写在 helperText 里(未设置,当前展示为用户名「llx」)而不是写进输入框。这是三态之外第二条容易写错的地方,widget 测试与桌面实测各钉一次。

2.2 PATCH 三态:类型承载,不靠约定

Dart 的 String? 只有两态,无法区分「不改」与「清空」。若把「不改」也编码成 null用户只改昵称就会连头像一起被清掉(服务端把显式 null 当清空指令执行)。故三态由类型承载:

PatchField<String>.absent()      // 键不出现  → 不改
PatchField<String>.clear()       // 键出现为 null → 清空
PatchField<String>.value('小柴')  // 键出现有值 → 设置

序列化只有一条路径 PatchField.writeTo(json, key)——「absent 不落键」这条纪律只实现一次,各请求 DTO 不自己拼 map,避免某处漏写 isPresent 判断。

编辑页维护「三态意图」而不是「当前值」。它不做「读当前表单值 → 整体提交」,而是与进页时的服务端快照比对后产出三态:

用户动作 nickname avatarAssetId
什么都没碰 absent absent空 patch → 直接短路不发请求
只改昵称 value absent(键不出现)
点「清除昵称」 clear absent
只传新头像 absent value
点「清除头像」 absent clear
改昵称 + 传头像 value value(一次 PATCH 改两样)
输入与原昵称相同 absent(视作未改,保存钮禁用) absent
昵称输入框留空/纯空白 absent(意为「不改」,不是清空) absent

最后一行是刻意的:清空只走「清除昵称」这一个显式入口。服务端对纯空白答 400/40000 而非隐式清空(03 号 §2.2),客户端与之对齐——空输入框判为「不改」,于是「用户误删了输入框内容」不会变成「删掉我的昵称」。

三处配套:

  • 空 patch 前置短路ProfileController.save 与编辑页各判一次 request.isEmpty,不去撞服务端刻意留的 400。
  • 昵称校验按码点trimmed.runes.length > 32 而非 String.length。32 个 emoji 的合法昵称 UTF-16 长度是 64,按 String.length 校验会误拒数据库存得下的昵称PostgreSQL char_length 数码点)。测试里 '🐕' * 32 这一格专门证明这点。
  • btrim 先行:先 trim() 再判长度,与 ck_users_nickname 同序。

UpdatePetRequest 同理,但只有 avatarAssetId 是三态,其余字段保持 M2 两态语义(它们的 CHECK 约束本就不允许空值,「清空」无意义)——差异刻意限定在有清空需求的字段上,并写进了类文档。

2.3 头像:只写不读 assetId

  • 「有头像」一律判 avatarUrl != null。响应里没有 avatarAssetId,代码里也没有任何地方去找它。
  • avatarUrl 不入任何本地存储(纪律 R2)。编辑页上传成功到保存之间没有可用 URL(不缓存 complete 响应里的那个),改为以「已选择新头像」占位说明,保存后由服务端回显现签 URL。
  • 展示统一走 RemoteImageSignedNetworkImage(缓存 key 剥 X-Amz-*),同对象的不同签名命中同一内存缓存。
  • 无图一律本地占位(资料页人形、宠物爪印),不显示破图——与服务端「非 ready 的 asset 直接给 null 而不是签一个必 404 的 URL」(03 号 §2.7)配对。

2.4 上传编排:复用而非重写

AvatarUploadSheet 只做呈现,状态机、凭据过期换新、失败可重试、孤儿防护全部沿用 MediaUploader(新增 purpose 参数,maxImages: 1maxConcurrentUploads: 1)。六态映射:

阶段 呈现
picking 「正在打开相册…」+ 转圈
queued / compressing 「正在处理图片…」
uploading 线性进度条 + 「上传中 N%」
confirming 进度定格 100% + 「正在确认…」
ready 圆形预览 + 「使用这张」 + 「重新选择」
failed 原因文案 + 「重试」(仅可重试时)+ 「重新选择」

两处刻意的取舍:ready 后仍要用户点「使用这张」(上传成功 ≠ 用户满意这张图;头像是长期可见的身份标识,不该剥夺预览确认);不可重试的失败只给「重新选择」(压缩后仍超 10 MB,重试同一张必然再失败,给重试钮是误导)。

purpose 由调用页给定并透传到 createUpload,测试直接断言 user_avatar / pet_avatar ——用途即服务端引用侧的类型检查,给错会被答 404/40405。


3. 权限与四态

3.1 宠物头像的按字段分档(客户端呈现侧)

角色 头像入口(WRITE 资料编辑入口(MANAGE
owner 角标 + 可点
caregiver 角标 + 可点
viewer

widget 测试三格全覆盖。第四格:未装配上传能力(构造口为 null)时 owner 也不渲染入口——意为「本次构建没有上传能力」,而不是「有入口但点了没反应」;生产装配(app.dart)恒注入,既有不关心头像的 widget 测试因此无需改动。

3.2 四态口径

页面 loading ready error 空态
资料页主链路 居中转圈 头部 + 菜单 横幅 + 重试 无独立空态,见下
资料页统计块 转圈占位 四个数字 「统计加载失败 + 重试」 一排 0
宠物详情头像 沿用详情页四态 真实头像 爪印占位

资料页没有独立空态是定型而非遗漏:任何已认证用户都有资料,/me/community-stats 契约上「永不 404、空数据返回 0」。所以「新用户什么都没有」的形态就是 ready 态里的一排 0,不是另一个页面态。widget 测试 expect(find.text('0'), findsNWidgets(4)) 钉住这一格。

统计块的三态是独立的/me 成功而统计失败时只降级这一块,不把整页打成 error——昵称和头像已经拿到了,为两个数字丢掉整页是过度反应。两块统计同失败共用一个「重试」(两个数字并列在同一张卡上,只有一半是数字、另一半是「—」比整块失败更费解)。

已有资料副本时刷新失败保留副本(停在 ready),沿宠物详情页「有副本即不打断阅读」的既有取舍。


4. compose 桌面实测(逐步记录)

4.1 环境与命令

# 后端六容器
cd <你的工作区>/patbond-api
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package   # BUILD SUCCESS
docker compose up -d --build
docker compose ps    # postgres/minio healthy + auth/user/pet/community up(六容器)

# 桌面真链路(默认跳过,不进常规测试套件)
cd <你的工作区>/patbond-flutter
PATBOND_PROFILE_LIVE=1 flutter test integration_test/profile_avatar_live_test.dart -d linux
# 逐步截图落到 build/profile-live/

cd <你的工作区>/patbond-api && docker compose down   # 用完即拆

实测脚本沿 M3.5 第一批的 client_ux_live_test.dart 先例:驱动真实 AppLinux GTK 渲染 + 真实 HTTP + 真实 MinIO),把整棵 App 包一层 RepaintBoundarytoImage() 直出真实渲染像素。只有选图与压缩两层是桌面替身Linux 无 image_picker / flutter_image_compress 原生实现);createUpload → 预签名 PUT 直传 → confirmPATCH 四段全是生产实现。

4.2 逐步结果

# 步骤 结果 截图
1 UI 注册(用户名/手机号/密码/确认密码;注册面不收昵称ADR-022 D3.5-5 进主壳 01-register.png
2 首页问候语(未设昵称) 「下午好,plive… 👋」——回退 username 02-home-greeting-username.png
借真实会话用 API 种一帖 + 一只宠物
2.5 UI 登出 → UI 登录 三个控制器重新预取(登出 reset 是既有纪律)
3 资料页 展示名 = 真实 username,副行 @username,四个数字 0 / 0 / 0 / 1(刚发 1 帖);demo 文案零残留 03-profile-real-username.png
4 编辑页 昵称框(未预填 username+ helperText「未设置,当前展示为用户名「plive…」」;无昵称时不给「清除昵称」入口 04-profile-edit-empty.png
5 设昵称 → 保存 「资料已更新」+ 头部改昵称 05-profile-nickname-set.png
6 更换头像 → sheet 压缩→直传→confirm 走通,出圆形预览 + 「使用这张」 06-avatar-sheet-ready.png
7 「使用这张」→ 保存 资料页头像真的画出上传的图(不是占位) 07-profile-avatar-uploaded.png
8 回首页 问候语同步变昵称(同一控制器,未手动刷新) 08-home-greeting-nickname.png
9 Feed 下拉刷新 我的帖的作者名变昵称、作者头像变新头像——服务端 /internal 回退链的实证(客户端对作者名零拼装)。⚠️ 有 60s 滞后,见 §4.4 09-feed-author-nickname.png
10 档案 → 宠物列表 → 详情 上传前爪印占位,owner 见铅笔角标 10a-…10-pet-detail-placeholder-avatar.png
11 点头像 → 上传 → 「使用这张」 「头像已更新」;详情头像画出真实图;PATCH 只带 version + avatarAssetId 11-pet-avatar-uploaded.png

脚本内的机器断言(每次运行都跑):编辑页不预填 username、作品数为服务端聚合值、Feed 作者名 = 昵称、createUpload 的 purpose 正确、详情头像 URL 带 pet_avatar 前缀且在应用进程内直取得到 200、详情头像下有 Image没有爪印兜底(有图就必须画出图)。

4.3 顺带用 HTTP 直连复核的服务端语义

结果
GET /me 初始态 nickname: nullavatarUrl: null(键恒在,不回退)
GET /me/community-stats 空数据 {receivedLikeCount: 0, publishedPostCount: 0}200 不是 404
PATCH /me 只带 nickname 200avatarUrl 保持 null(未被顺手清空)
PATCH /me 只带 avatarAssetId 200nickname 原值保留(三态「不改」生效)
PATCH /me 空 body {} 400 / 40000「请至少提交一个可更新字段:nickname 或 avatarAssetId」
用户头像预签名 GET 200,字节与上传逐字节相同
宠物头像预签名 GET(pet 侧本地 SigV4 200,字节相同
发帖后 stats publishedPostCount: 1

4.4 实测发现的两处「看起来像 bug、其实不是」

(a)Feed 作者名有 ≤60s 滞后(服务端设计)。 设完昵称立刻下拉刷新 Feed,作者名仍是旧的 username。根因不在客户端:community 侧 AuthorProfileGateway/internal/users/profiles 的结果放在 60s TTL 的进程内缓存里(patbond.author-profile.cache-ttlM3 T3-05);首屏 Feed 是设昵称之前拉的,那一次已经把「作者名 = username」写进了缓存。等过 TTL 再刷新即同步(实测确认)。

  • 资料页与首页问候语读的是 /me没有这层缓存,立即生效——所以会出现「资料页已变、Feed 还是旧名」的一分钟窗口。
  • 处置:不改动服务端缓存(60s 是合理的读侧优化,改它属后端范畴且需拍板)。已写进实测脚本注释 + 真机清单备注,避免下次实测把它当缺陷重复上报。若产品认为这一分钟不可接受,处置方向是「改昵称成功后由 user 服务发失效通知/缩短 TTL」,属后续里程碑的后端工单。

(b)1×1 的极小测试图能上传能下载,但 Flutter 解码器拒绝。 最初用 M3 e2e 那张 344 字节的 1×1 JPEG 做实测夹具,结果:服务端照收、curl 与应用进程内 HttpClient 都能取回逐字节相同的 344 字节,但 UI 一路显示爪印占位。定位到 Image.networkCodec failed to produce an image, possibly due to invalid image data——是图片本身在解码路径上被拒,不是链路问题(1×1 PNG 也一样)。换成一张 16×16 的棋盘 PNG(87 字节)后像素正常渲染。

  • 这个坑很容易被误判成「头像根本没传上去」,故写进了实测脚本的夹具注释。
  • 不影响生产:真机走相册真实照片,不会遇到 1×1。真机项第 1(a)另有「真的画出图」的通过标准兜底。

5. 顺手修掉的既有缺陷

5.1 ⚠️ /api/v1/me 端口错线(先于本单存在,本单第一个消费者才暴露)

症状:桌面实测里资料页始终停在 error 态、问候语始终不称名。

根因ApiAuthRepository 只持有一个 auth 服务(:8081)的 ApiClient,而 /api/v1/meuser 服务(:8082)的 MeController 提供(ADR-002 分端口直连,无网关)。curl http://127.0.0.1:8081/api/v1/me 实测 404

为什么一直没被发现AuthRepository.me() 自 M1 就在接口上,但此前没有任何页面消费它Splash 恢复走 restoreSessionTokenRefresher,打的是 auth 的 refresh 端点)。本单的 ProfileController 是第一个真实消费者。

处置ApiAuthRepository 加一条 userApi 线路(缺省回落主客户端,既有测试桩不受影响),me()updateMe() 走它;app.dartpatbondUserApiBaseUrl 装配。手法与 ApiCommunityRepositorymediaApimedia 端点也在 user 服务)完全同构,类文档写明了「auth 上没有 /api/v1/me 路由,走主客户端会得到 404」。

教训(值得留档):分端口直连模式下,「接口定义在哪个 Repository」与「端点部署在哪个服务」是两件事。凡是 Api*Repository 里出现跨服务端点,都应有一条独立的 ApiClient 并在类文档里写清线路。目前有此情况的两处(auth 的 /me、community 的 /media)均已显式接线。

5.2 编辑页保存钮的可用性不刷新

TextEditingController 的监听里原先只在有错误/有清除意图时 setState,于是「已经打了字但保存钮还是灰的」。改为每次输入都重建(可用性由 _hasChanges 现算)。widget 测试覆盖。

5.3 资料页首屏预取触发时机

ProfilePage.initState 里直接 refresh() 会在 IndexedStack 挂载阶段同步 notify,而同一控制器的另一个监听者(首页问候语)此时已构建完成 → 命中 Flutter「build 期间 setState」断言。改为推到帧末(addPostFrameCallback)。pets/home 各自只有一个监听者,故它们在 initState 里直取无妨——差异写进了注释。


6. 测试数变化

文件 新增 覆盖
test/core/models/patch_field_test.dart 4 三态 JSON 表现(absent 绝不落键)、absent/clear 不可由 valueOrNull 区分、encode 只作用于有值态
test/features/profile/profile_models_test.dart 14 展示名回退两路 + nickname 不被回退值污染、UpdateMeRequest 五种三态组合、昵称码点边界(32 CJK / 32 emoji / 33 拒 / btrim / 纯空白)、错误文案分层
test/features/profile/profile_controller_test.dart 10 四态、/me 失败与重试、有副本时刷新失败保留、统计独立降级 + 单独重试、空数据零值、空 patch 短路、save 回显替换、save 失败外抛、reset、follow-stats 主体是本人
test/features/profile/profile_page_test.dart 8 四态齐备、展示名回退两路、demo 文案零残留、空数据四个 0、统计块独立降级、编辑页往返、未装配上传能力时无头像入口
test/features/profile/profile_edit_page_test.dart 11 三态载荷五格(每格断言未改字段的键不出现)、同值视为未改、无昵称不预填 username、33 码点字段级错、纯空白不隐式清空、42203/40405 分层、上传接线 purpose + 载荷、一次改两样
test/core/widgets/avatar_upload_sheet_test.dart 6 打开即拉起选择器、上传中进度 + ready 预览确认、purpose 透传、可重试失败重试成功、不可重试只给重新选择、用户取消不交付 assetId
test/features/pets/pet_avatar_wiring_test.dart 13 Pet.avatarUrl 解析、UpdatePetRequest 三态、错误文案分层、权限呈现四格(owner/caregiver/viewer/未装配)、上传后 PATCH 只带两键、移除发显式 null 且 keys.length == 2、40902 重取、42203 提示、列表卡真实头像与占位回退
test/features/home/home_greeting_test.dart 5 有昵称/无昵称两路问候语、资料未到手不称名、改昵称后首页同步、刻意保留的 demo 占位仍在
合计 +71
基线 现在
flutter test 526+2 skip 597+2 skip)全绿
flutter analyze 0 0
dart format 无 diff 无 diff
check-secrets.sh --all exit 0 exit 0

既有 526 测试零回归。三处 helper 层改动(不改断言语义):FakeAuthRepository.me() 由抛 UnimplementedError 改为缺省返回「无昵称无头像」样本(该类文档本就写着「默认成功空实现」),FakeCommunityRepository 的 stats 两法给缺省零值,samplePetJsonavatarUrl: null_SwitchableRepository(smoke 测试的代理)补一个转发方法。


7. 遗留与交接

7.1 本单未做(范围裁剪,均已在代码注释登记)

  • 「我的收藏与草稿」列表页未做。后端能力早已就位(GET /me/bookmarksGET /me/posts?status=draftM3 T3-05/T3-17),客户端仓库层也有 listMyBookmarks / listMyPosts;缺的是两个列表页面 + 导航。工单原文允许「若工作量超出则本单只接资料 + 统计」,本单按此裁剪——三单合并本身已是 L + M + S,再加两页列表会挤压实测与测试的完成度。菜单入口仍走演示提示,ProfilePage.menuItems 的文档注释里写明了「后端已就位、列表页待做」。建议作为独立小工单(规模 S~M:两页都是既有 CursorPage 四态列表的同构复制(可照抄 WeightRecordsPage 的翻页骨架 + PostCard 的卡片)。
  • 资料页其余四个菜单项(预约订单 / 健康卡包 / 地址定位 / 设置与关于)保持演示提示,已标注去向(M5 服务域 / 需外部服务 / 待有实际可设项)。
  • 「恢复演示数据」按钮保留:AppState 仍承载首页天气与本地服务的演示数据,这个入口是它唯一的复位口。对话框文案已改为「首页天气与本地服务的演示内容会恢复…(不影响账号资料与宠物档案)」,不再声称会重置宠物档案(那部分自 M2 起已是服务端数据)。
  • PetSummaryResponse 未补 avatarUrl(后端本就未做,03 号 §7 已登记);bio 字段未开放(同上)。

7.2 真机清单已登记(docs/development/device-verification.md 的 M3.5 节,4 项 + 1 备注)

  1. 头像上传弱网表现(6 格):真机相册 + 原生压缩、弱网中断重试、压缩后仍超限只给重新选择、凭据过期自动换新、中途退出不留引用、HEIC 与方向。
  2. 头像缓存表现(4 格):跨页命中不重下、下拉刷新后仍命中、TTL 过期重取、清除头像后不留残影(含重启后仍是占位——URL 不得持久化)。
  3. caregiver 改宠物头像(3 格):WRITE 档实证(桌面只跑了 owner)。
  4. 获赞数与帖子点赞数对账(4 格):含自赞、多帖求和、软删回落、草稿不计。
  5. 备注:Feed 作者名/头像的 ≤60s 服务端缓存滞后(§4.4a),说明这不是缺陷,避免重复上报。

该文件按维护约定直接修改(工单授权)。

7.3 给后续波次的提醒

  • 头像上传构造口有两层:页面侧 AvatarUploaderBuilder(purpose)(页面只知道用途)+ App 侧 AvatarUploaderFactory(repository, purpose)(拿到已装配的仓库)。桌面实测与集成测试在 App 侧替换选图与压缩层,网络三段永远是生产实现。新增头像场景(如后续的封面图)照此接即可。
  • MediaPurpose 加值只需改枚举 + 服务端配置白名单(无 DB 约束,ADR-022);但引用侧会校验用途相符,purpose 给错是 404/40405 而不是 400。
  • PatchField 是通用件(在 lib/core/models/),后续任何需要「清空」语义的 PATCH 字段直接用它,不要再引入 clearXxx: true 伴生布尔或空串哨兵。
  • 本报告只写不提交;mkdocs.yml 本次未动,随波末统一挂导航入档。