v0.4.0 M3.5 体验补齐 #1

Merged
lixi merged 7 commits from dev into main 2026-09-14 11:15:05 +08:00
Contributor
No description provided.
lixi added 6 commits 2026-09-14 09:40:02 +08:00
用户 Linux 桌面实测反馈「日期选择器全英文」。根因是 MaterialApp 从一迭代
建起就没配 localizationsDelegates/supportedLocales/locale,pubspec 也没有
flutter_localizations;Flutter 在缺 delegate 时静默回退内置的
DefaultMaterialLocalizations(只有英文),于是业务自绘文案全中文、Material
内置组件全英文,同一弹窗里中英混排:

  Select date → 选择日期
  OK / Cancel → 确定 / 取消
  Enter Date → 输入日期
  Switch to input → 切换到输入模式
  Invalid format. / Out of range. → 格式无效。/ 超出范围。
  mm/dd/yyyy → yyyy/mm/dd(zh 顺序年在前)
  September 2026 → 2026年9月

处置:

- pubspec 加 flutter_localizations(sdk)与 intl(日期符号底座,显式直接
  依赖以锁定与 SDK 一致的版本)。
- 新增 lib/app/app_localization.dart 把 Global{Material,Cupertino,Widgets}
  三件套 delegate、supportedLocales、locale 收在一处常量:widget 测试若只
  pumpWidget(MaterialApp(home: ...)) 而不挂 delegate,测到的「中文」是假的,
  测试直接引用同一份常量,生产与测试不会各配一套而漂移。
- 单语言 zh-CN,不列 en:避免设备语言为英文时回退英文,再造混排。
- pets 域三份 widget 测试的 MaterialApp 同步挂上 delegate,
  tap(find.text('OK')) 相应改 '确定'——让测试与生产一致,而不是在英文兜底下测。

新增 test/app/app_localization_test.dart 守住这一行不被回删:断言根
MaterialApp 实际挂上三件套 + locale zh-CN,并从运行期 MaterialLocalizations
取回中文文案。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
用户实测已因日期选择器误录:Flutter 原生日历只给年份网格、月份必须靠 < >
逐月点,从 9 月回 4 月要点 5 次,他把当月(2026-09)的就医记录记成了
2026-04-09,进而误判「本月花费 ¥0」是聚合坏了。

新增 lib/core/widgets/app_date_picker.dart,全仓 7 处裸 showDatePicker 收口
(改造后 lib/ 下 showDatePicker 只出现在该文件内部一次):

- pickAppDate(...):calendar 首屏 + 保留头部铅笔切手输;initialDate 自动夹进
  [firstDate, lastDate] 防原生越界断言(调用方常传「当前值 ?? 今天」,而
  「到期日期」的 firstDate 就是今天,历史值可能已越界);返回值统一抹时分秒;
  中文文案一律交给 zh-CN 本地化,不硬编码,避免两处文案漂移。
- AppDateFieldTrailing(...):7 处日期行统一「今天 + 日历图标」。今天越界自动
  隐藏按钮、提交中禁用、触控 44×44、primaryStrong 白底 4.49:1。

入口模式取舍:不用 calendarOnly——它恰好会砍掉手输按钮,把「录一个已知日期」
这条唯一快路堵死;也不用 input 首屏——「记今天」这类高频场景敲 8 个数字更慢。
两条路都留着最省事。

「今天」为何放表单行而非弹窗内:原生 showDatePicker 无法注入自定义动作
(builder 只能包裹整个 Dialog,拿不到内部选中态;塞进 Column 还会因 Dialog
在无界高度下贪心布局而溢出)。放表单行反而更快——一键落值连弹窗都不用开,
把容易走错的月份导航整段绕开,且一次实现 7 处形态完全一致。

各调用点原有的 firstDate/lastDate 业务约束原样传入、一字未改(健康事件
不许未来、到期日不许补记过去、疫苗 allowFuture 双态、生日不许未来),
并由 widget 测试直接断言 DatePickerDialog.firstDate/lastDate 防后续悄悄放宽。

顺带修配色:此前从未定制 datePickerTheme,选中日直接吃 ColorScheme.fromSeed
由珊瑚橙派生的暗红棕,与品牌脱节。新增 _datePickerTheme 只复用 05 号规范
(iteration-2/05、iteration-3/05)已审计的色对,不新造色值:头部
surfaceTint + primaryDark 7.98:1(选中 chip 同款)、选中日/年 primaryStrong
实底白字 4.49:1、今日 primaryStrong 1.5px 描边、星期表头 inkSoft 6.59:1、
越界日 muted(DEBT-2 允许的禁用态用途)。headerHeadlineStyle 取 22px
(默认 32):中文「9月10日周四」在横屏侧栏头部 26px 起就折行。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
卡片标签硬编码「本月花费」,而服务端 summary.monthlyExpense 本就返回 month
(ISO year-month,按 tz 归月)。用户看不到实际月份,所以无法自证「我这条记录
到底落在哪个月」,把正确的 ¥0 当成统计故障。

已核实不改后端聚合:用户记录落在 2026-04-09、当天是 2026-09-10,
「本月花费 ¥0」是正确行为。本单只让客户端把口径亮出来。

- health_record_display 新增纯函数 monthlyExpenseCardLabel(month, {now}):
  同年「9 月花费」(窄卡一行放得下,故不取「2026-09 花费」);跨年(服务端
  归月年份 ≠ 设备当前年份)「2026/12 花费」补年份消歧;串非法退回
  「本月花费」不崩不显示脏值。
- 可点提示:_SummaryCard 在 onTap 非空时右上角补 chevron_right。四张卡本都
  可点进明细页却无任何视觉提示(用户反馈不知道能点),提示形态沿用项目既有
  可点行/卡(宠物列表卡、健康提醒卡、资料页设置行)的 chevron_right,不自创。
- 整卡包 MergeSemantics:读屏一次读全「¥128.50,9 月花费,按钮」而非两段
  孤立文字。没有用 excludeSemantics——那会连带丢掉 InkWell 的可激活性。

既有测试口径调整:pet_detail_page_test 两处断言改为实际月份,且样本
monthlyExpense.month 改用当月串使断言不随年份漂移(跨年格式由纯函数单测覆盖)。

新增 integration_test/client_ux_live_test.dart(环境变量门控,默认 skip,
沿用 M3 既有 live 测试形态):compose 六容器 + 真实 App 走完三项修复——
日期选择器全中文/品牌配色/手输录入/一键今天,以及花费卡实际月份 + chevron。
本机是 Wayland 会话、X11 import -window root 取不到根窗口,改为把整棵 App 包
一层 RepaintBoundary 后 toImage() 直出真实渲染像素落 build/ux-live/。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
资料页头部三项(展示名 / 头像 / 数字)自此全部来自服务端,176 行硬编码
demo(「萌宠新手(豆豆家长)」/ 24 / 1.8k / 2)退役。

- `PatchField<T>` 承载契约 v1.4.0 的 PATCH 三态(absent 不落键 / clear 落
  显式 null / value 落值)。三态必须由类型承载而非 `T?` 加约定:把「不改」
  也编码成 null,用户只改昵称就会连头像一起被服务端清掉。
- `UserProfile` 补 nickname / avatarUrl,展示回退 `nickname ?? username`
  **做在客户端展示层**——服务端 /me 刻意返回 DB 原值,编辑页因此只用原值
  预填,避免把展示约定固化成真实昵称。
- `ProfileController`:`/me` 主链路四态 + 两块统计独立三态(统计失败只降级
  这一块,不为两个数字丢掉整页);登出 reset 防跨账号泄漏。
- 编辑页维护三态意图而非「当前值整体提交」:昵称 / 头像各有显式清除入口,
  未改字段的键根本不进 JSON;空 patch 直接短路(服务端对空 patch 答 400)。
- 昵称校验按**码点**计 1~32 且先 btrim,与 `ck_users_nickname` 对齐;
  纯空白是校验失败而非隐式清空。
- `MediaUploader` 的 purpose 参数化 + 复用其六态编排的 `AvatarUploadSheet`
  (单图、预览确认后才交付 ready assetId,孤儿防护不变)。
- 修复 `/api/v1/me` 端口线路:该端点由 user 服务(:8082)提供,`ApiAuthRepository`
  此前只挂 auth(:8081)会得到 404。此前无人消费 me(),故这条错线一直没被
  触发;本单是第一个真实消费者,桌面实测即暴露。

测试 526 → 588(+62)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- `Pet.avatarUrl` 入模型;列表卡与详情头像改用真实预签名 URL(无图仍回退
  爪印占位——服务端对非 ready 的 asset 直接给 null,客户端只有一种占位)。
- 详情页铅笔角标自此有功能:接 `MediaUploader`(purpose=pet_avatar);已有
  头像时先给「更换 / 移除」二选一,移除即三态显式 null。
- 权限按 **WRITE 档**呈现(ADR-022 D3.5-3:owner + caregiver 可改头像,
  viewer 无入口),与资料编辑的 MANAGE 档刻意不同。因此纯头像 PATCH 只发
  `version` + `avatarAssetId`,**不夹带任何资料字段**——夹带会让服务端按更严
  的一半定档,caregiver 立刻 403。
- `UpdatePetRequest.avatarAssetId` 是该 schema 唯一的三态字段,其余字段保持
  M2 两态语义(它们的 CHECK 约束本就不允许空值,「清空」无意义)。
- 40902 冲突后重取档案让用户自行决定重来,不静默重放(头像是用户可见的
  覆盖操作,不该自动生效两次)。

测试 588 → 597(+9)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
新增:首页问候语真实化 + 刻意保留的 demo 占位显式登记(T3.5-10,ADR-022)
CI / flutter-gates (push) Successful in 3m48s
CI / flutter-gates (pull_request) Has been cancelled
694543647f
- 「下午好,豆豆」改真实展示名(`nickname ?? username`,与资料页共用同一个
  ProfileController:一次 /me 供两个消费点,改昵称后两处一起变)。资料未到手
  时退化为不称名的「下午好 👋」,不编造假名。
- 按 ADR-022 决策 D3.5-1 钉死范围,**其余首页 demo 一律不动**,并在代码里逐项
  标注为「刻意保留的 demo 占位」:天气/位置(需接外部服务)、圈子=话题
  (ADR-018 剪出)、促销卡(M5 服务域)、搜索与本地服务段。
  widget 测试同时钉住「保留项仍在」,避免后续以「顺手清理」越出拍板范围。
- 新增 compose 真链路桌面实测脚本 `integration_test/profile_avatar_live_test.dart`
  (默认跳过,`PATBOND_PROFILE_LIVE=1 ... -d linux` 开启):UI 注册 → 登录 →
  资料真实化 → 设昵称 → 传用户头像 → Feed 作者名同步 → 宠物头像,逐步截图。
  只有选图与压缩两层是桌面替身,createUpload / 预签名 PUT / confirm / PATCH
  全为生产实现。两处实测坑写进注释:Feed 作者名有服务端 60s 缓存滞后;
  1×1 的极小图能上传能下载但 Flutter 解码器拒绝,会被误判成「没传上」。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lixi added 1 commit 2026-09-14 09:53:48 +08:00
新增:M3.5 E2E 烟囱脚本——资料读写/双头像/获赞聚合十场景(v0.4.0 发布门禁)
CI / flutter-gates (push) Successful in 3m46s
CI / flutter-gates (pull_request) Successful in 3m44s
fbcd734688
v0.4.0 发布 checklist 第 2 步:既有三份回归脚本(M1 7/7、M2 11/11、M3 14/14)
对 compose 六容器全绿后,补上 M3.5 新增对外面的验证,与前三份并列于仓库根目录。

覆盖 10 场景 / 95 断言(冻结契约 openapi v1.4.0,32 路径 45 操作 75 schema):
- GET /me 初始形态:nickname/avatarUrl 键恒在且双 null,不外露 avatarAssetId
- PATCH /me 三态:键缺省不改 / 显式 null 清空 / 给值设置;空 patch、纯空白、
  空串、只带未声明字段、非法 UUID、坏 JSON 六格 400/40000;同 body 重放幂等
- 昵称边界按码点计:1 与 32 CJK 可、32 emoji 可(UTF-16 长度 64 却过关)、
  33 两种字符集均拒、btrim 对齐
- 用户头像端到端:两步上传 user_avatar → 挂载 → 预签名 GET 逐字节一致 →
  去签名直访 403(桶私有)
- 宠物头像:PATCH /pets 挂 pet_avatar → 详情与列表两处 avatarUrl 均可下载 →
  显式 null 清除;version 仍必填
- 两域 asset 校验矩阵:幽灵/他人/错用途(含 user_avatar↔pet_avatar 跨域)
  → 404/40405 且防枚举响应体一致;本人用途相符但 uploading → 422/42203;
  失败路径零副作用
- GET /me/community-stats:无 token 401、空数据 0/0 永不 404、自赞计入、
  草稿不计、他人帖不串号、软删后获赞归零、与帖详情 likeCount 对账
- media purpose 白名单三值放行,白名单外 id_card → 400/40000

结果:10/10 全绿,契约偏差 0,无需放宽任何断言(错 purpose 实测 404/40405,
与契约一致)。三份既有脚本逐字未改。

脱敏:token 截断、预签名 URL 签名 query 整体抹为 <SIGNATURE_REDACTED>、
Idempotency-Key 占位。

门禁:flutter analyze 0 issue;dart format --set-exit-if-changed lib test 0 changed
(四份根目录脚本另跑显式检查亦 0 changed);flutter test 597 passed 与基线一致。
patbond-api 与 patbond-doc 代码/契约零改动。

报告:patbond-doc iteration-3.5/08-release-e2e-regression.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lixi merged commit fbcd734688 into main 2026-09-14 11:15:05 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: zhaoyuxi/patbond-flutter#1