Files
patbond-doc/docs/development/iterations/iteration-1/17-flutter-login-report.md
T
lixi 273064c10d docs: 第三波交付收口——认证契约与两端实现报告入档
- 新增 API 契约:docs/api/openapi.yaml(T6a 正式化)与契约说明页(契约先行原则)
- 入档报告 16(后端 JWT 会话,37→73 测试)与 17(Flutter 登录纵切,7→30 测试)
- 进展看板更新至第三波完成,第四波为联调 E2E → CI → 编排 → 埋点
- 门禁:mkdocs build --strict 通过
2026-09-04 12:18:29 +08:00

68 lines
7.4 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.
# 17 · Flutter 登录纵切实现报告
> 作者:Frontend Developer
> 日期:2026-09-04
> 依据:12-ui-design-qa-and-assembly.md(组装稿)、ADR-003/ADR-004、开发计划 §4.2、接口契约冻结稿
> 提交:`patbond-flutter` dev 分支 `8d890c0`(门禁全绿后提交,未 push)
---
## 1. 交付总览
登录纵切完整落地:网络层(dio)+ 认证会话(安全存储)+ Splash / 登录 / 注册三页 + 主壳真实退出登录,另完成 FIX-1 / FIX-2 / m2 三项顺带修复。门禁三连全绿:`dart format --output=none --set-exit-if-changed lib test`0 changed)、`flutter analyze`No issues)、`flutter test`**30 passed**,其中新增 23 个)。
**契约偏差:零**。所有路径、请求/响应字段名、错误码与冻结稿逐字一致。额外附带两个契约外请求头(服务端可忽略):注册请求带 `Idempotency-Key`(每次提交生成 UUID,token 刷新后的自动重放沿用同一个键),所有请求带 `X-Device-Id`(首启生成、安全存储持久化的设备 UUID)。
## 2. 分层与文件
按开发计划 §4.2 的 Page → Repository → API Client 分层(登录表单状态照组装稿放页面 state,不引入独立 Controller 层):
| 层 | 文件 | 职责 |
| --- | --- | --- |
| 网络 | `lib/core/network/api_client.dart` | dio 封装;base URL 经 `--dart-define=PATBOND_API_BASE_URL` 注入(默认 `http://127.0.0.1:8081`);`validateStatus` 全放行,错误信封统一解析;`AuthInterceptor` 附加 Bearer;鉴权请求遇 HTTP 401 / code 40101 → 单飞刷新后重放一次,重放仍失败清会话抛 `SessionExpiredException` |
| 网络 | `lib/core/network/token_refresher.dart` | 单飞(single-flight)刷新:并发 401 只发一次 `POST /auth/refresh`**仅 40102 / HTTP 401 清会话**,网络失败与 5xx 一律保留 token |
| 网络 | `lib/core/network/api_exception.dart``api_envelope.dart` | 类型化异常(`ApiNetworkException` / `ApiBusinessException` / `ApiRateLimitException` / `SessionExpiredException`+ 错误码常量 + 信封解析 |
| 认证 | `lib/features/auth/session_manager.dart` | token 内存副本 + `flutter_secure_storage` 持久化(`TokenStore` 抽象,测试注入内存实现);认证状态机 unknown/authenticated/unauthenticated**token 不进 SharedPreferences** |
| 认证 | `lib/features/auth/auth_repository.dart` | `AuthRepository` 抽象 + `ApiAuthRepository`login / register / logout / restoreSession / melogout 服务端失败也保证本地清除 |
| 页面 | `lib/features/auth/splash_page.dart``login_page.dart``register_page.dart` | 照组装稿逐项实现(见 §3) |
| 根 | `lib/app/app.dart` | 认证状态机驱动 Splash ↔ 登录 ↔ 主壳,AnimatedSwitcher 300ms fade;测试注入口(sessionManager / authRepository 可注入) |
| 导航 | `lib/core/navigation/fade_route.dart` | `PageRouteBuilder` + `FadeTransition` 300ms(登录 → 注册 push 用) |
## 3. 页面与状态覆盖
**Splash**(组装稿 §7):checking / failed 双态;BrandMark 与登录页同构保证过渡对位;spinner 等待 >300ms 才出现(占位保高度不跳动);最短停留 500ms;refresh 超时 5s;错误态「重试」+「改用账号登录」逃生口(清凭证进登录页);**网络失败不清 refresh token,仅服务端 401/40102 才清**。
**登录页**(组装稿 §5):垂直居中、无 Spacer;两字段仅非空校验(去首尾空格),Focus 包裹失焦校验 + 提交总校验;提交中整表单锁定(字段禁用、注册链接置 null、按钮 loading);错误三层映射——字段级 errorTextonChanged 即清)、40100 → 横幅「用户名或密码错误」+ `SemanticsService.sendAnnouncement` 播报、HTTP 429 → 横幅「尝试次数过多,请稍后再试」、网络 → SnackBar「网络异常,请检查网络后重试」+ 重试 action;成功后 `finishAutofillContext()`,状态机 300ms fade 进主壳;协议行与预留区一律不渲染(ADR-004)。
**注册页**(组装稿 §6):透明返回栏顶部左对齐;四字段(用户名/手机号/密码/确认密码)失焦校验 + 提交总校验,文案照 04 规范 §3.2;密码 helperText 走主题 mutedFIX-2);密码变更时确认密码已有值则重校验一致性;40900 → 用户名字段「该用户名已被使用」、40901 → 手机号字段「该手机号已注册,可直接登录」;注册成功即建立会话直接进首页(popUntil 首路由,不回登录页)。
**主壳/个人中心**`ProfilePage` 的「切换账号或退出登录」接入真实 logout(`POST /auth/logout` Bearer + refreshToken,随后清会话,状态机自动回登录页)。
## 4. 顺带修复
- **FIX-1**:首页促销卡渐变改 `[primaryStrong, primary]`(深端在左承载白字,AA 达标;`brandGradient` 本身未动)。
- **FIX-2**`inputDecorationTheme``helperStyle: TextStyle(color: muted, fontSize: 12)`
- **m2**README 验证命令补 `--output=none`
## 5. 测试(30 通过 = 既有 7 + 新增 23
| 文件 | 数量 | 覆盖 |
| --- | --- | --- |
| `test/core/network/token_refresher_test.dart` | 5 | 并发单飞(仅 1 次请求 + token 轮换)、40102 清会话抛 SessionExpired、网络失败保留 token、单飞复位可重刷、无本地 refresh 直接判失效 |
| `test/features/auth/auth_repository_test.dart` | 10 | 登录成功存会话(含请求体逐字段断言)、40100 业务异常、注册 Idempotency-Key + 40900、40101 刷新后重放一次携带新 token、重放仍 401 清会话、登出网络失败也清本地、会话恢复两分支、5xx → 系统错误、429 → 限流异常(全部 mock dio 假 adapter |
| `test/features/auth/login_page_test.dart` | 4 | 初始 / loading(字段禁用+链接置灰)/ 字段错误(输入即清)/ 横幅错误(40100 文案 + 输入即清)四态 |
| `test/features/auth/register_page_test.dart` | 4 | 渲染(含预留区不渲染断言)、空表单拦截、四字段格式文案逐项、合法提交调接口 |
既有 `widget_test.dart` 改为注入已认证会话 + 假仓库后 pump `App`,断言不变仍通过。任务描述中的「现有 13 个测试」与实际不符——工单开工时仓库为 **7 个**测试(上次提交信息「6 个 widget 测试」+ 1 个导航冒烟),7 个全部保持通过。
## 6. 遗留问题与备忘
1. **`SemanticsService.announce` 已废弃**Flutter 3.44 标记 deprecated,横幅播报改用替代 API `sendAnnouncement(View.of(context), ...)`,行为等价,组装稿 §4 后续修订时可同步文案。
2. **会话过期的 Splash 最短停留**refresh 被服务端判 40102 时清会话即切登录页,该罕见分支可能早于 500ms 最短停留(正常成功/失败/无 token 三路均严格遵守);fade 过渡下无闪烁,判定可接受。
3. **access token 过期时间未做本地预判**:当前依赖 401/40101 被动刷新(契约行为完备);`accessTokenExpiresAt` 已持久化,后续可加过期前主动刷新优化首个请求延迟。
4. **未与真实后端联调**:后端按同一冻结契约并行实现中,本报告所有验证基于 mock dio;联调烟囱测试建议列入下一波工单。
5. DEBT-1TagPill 对比度)按 12 号报告裁决仍另开工单,本次未动。
---
**Frontend Developer** · 2026-09-04 · 门禁:format 0 changed / analyze 0 issues / test 30 passed