docs: 第三波交付收口——认证契约与两端实现报告入档
- 新增 API 契约:docs/api/openapi.yaml(T6a 正式化)与契约说明页(契约先行原则) - 入档报告 16(后端 JWT 会话,37→73 测试)与 17(Flutter 登录纵切,7→30 测试) - 进展看板更新至第三波完成,第四波为联调 E2E → CI → 编排 → 埋点 - 门禁:mkdocs build --strict 通过
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# 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 / 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. 遗留问题与备忘
|
||||
|
||||
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-1(TagPill 对比度)按 12 号报告裁决仍另开工单,本次未动。
|
||||
|
||||
---
|
||||
**Frontend Developer** · 2026-09-04 · 门禁:format 0 changed / analyze 0 issues / test 30 passed
|
||||
Reference in New Issue
Block a user