- 新增 API 契约:docs/api/openapi.yaml(T6a 正式化)与契约说明页(契约先行原则) - 入档报告 16(后端 JWT 会话,37→73 测试)与 17(Flutter 登录纵切,7→30 测试) - 进展看板更新至第三波完成,第四波为联调 E2E → CI → 编排 → 埋点 - 门禁:mkdocs build --strict 通过
7.4 KiB
17 · Flutter 登录纵切实现报告
作者:Frontend Developer 日期:2026-09-04 依据:12-ui-design-qa-and-assembly.md(组装稿)、ADR-003/ADR-004、开发计划 §4.2、接口契约冻结稿 提交:
patbond-flutterdev 分支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 / me;logout 服务端失败也保证本地清除 |
| 页面 | 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);错误三层映射——字段级 errorText(onChanged 即清)、40100 → 横幅「用户名或密码错误」+ SemanticsService.sendAnnouncement 播报、HTTP 429 → 横幅「尝试次数过多,请稍后再试」、网络 → SnackBar「网络异常,请检查网络后重试」+ 重试 action;成功后 finishAutofillContext(),状态机 300ms fade 进主壳;协议行与预留区一律不渲染(ADR-004)。
注册页(组装稿 §6):透明返回栏顶部左对齐;四字段(用户名/手机号/密码/确认密码)失焦校验 + 提交总校验,文案照 04 规范 §3.2;密码 helperText 走主题 muted(FIX-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. 遗留问题与备忘
SemanticsService.announce已废弃:Flutter 3.44 标记 deprecated,横幅播报改用替代 APIsendAnnouncement(View.of(context), ...),行为等价,组装稿 §4 后续修订时可同步文案。- 会话过期的 Splash 最短停留:refresh 被服务端判 40102 时清会话即切登录页,该罕见分支可能早于 500ms 最短停留(正常成功/失败/无 token 三路均严格遵守);fade 过渡下无闪烁,判定可接受。
- access token 过期时间未做本地预判:当前依赖 401/40101 被动刷新(契约行为完备);
accessTokenExpiresAt已持久化,后续可加过期前主动刷新优化首个请求延迟。 - 未与真实后端联调:后端按同一冻结契约并行实现中,本报告所有验证基于 mock dio;联调烟囱测试建议列入下一波工单。
- DEBT-1(TagPill 对比度)按 12 号报告裁决仍另开工单,本次未动。
Frontend Developer · 2026-09-04 · 门禁:format 0 changed / analyze 0 issues / test 30 passed