feat(client): C 端 1-2 账号链路(邮箱登录/注册 + 令牌与 401 + 匿名/登录分叉)

为什么:客户端此前完全没有账号概念(mine 页的"登录"只是本地 demo 开关),
而受限动作(收藏/评论)要对到具体账号。本批把账号链路接上,为后续互动、进度同步、下载打底。

- AuthApi + AuthController + AuthSessionStore:注册/登录/登出/刷新,令牌与设备号存 shared_preferences
  (用 SharedPreferencesAsync,与 engagement 仓库同一套 API)
- ApiClient:统一带 Authorization 与 X-Device-Id;401 自动刷新一次并重试,刷新失败清登录态走 onUnauthorized
- 登录/注册页(pages/auth,沿用 binding/logic/view 与现有视觉):邮箱格式、密码 8-64 位含字母数字本地先校验,
  错误提示走 l10n;登录/注册一键切换
- 匿名/登录分叉:首页、发现、详情保持匿名可看;收藏/点赞等受限动作走 ensureSignedIn() 弹底部提示引导登录
- mine 页展示真实账号(昵称 + uid),拿不到才回落到演示文案
- 测试:新增 auth 控制器(注册落盘/401 刷新重试/刷新失败清会话/登出)与登录页(校验/切换)用例;
  widget_test 改为预置本地会话(账号不再是本地开关);修掉 playback 用例对 host 大小写的环境依赖断言

门禁:flutter analyze 0 问题;flutter test 148 全过。
This commit is contained in:
Vicky committed 2026-10-15 17:53:35 +08:00
1 parent 77d5748dbb
commit e1b98df509
24 files changed
+2871 -1016

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
/// 登录态:服务端下发的令牌与用户摘要。
///
/// 只保留客户端真正要用的字段(uid / 昵称 / 头像),不做整份 AppUserRespVO 的映射 ——
/// 首页卡片、个人页需要的都在这里,避免为用不到的字段维护一份平行模型。
class AuthUser {
const AuthUser({required this.uid, required this.nickname, this.avatarUrl = ''});
final String uid;
final String nickname;
final String avatarUrl;
factory AuthUser.fromJson(Map<String, dynamic> json) => AuthUser(
uid: json['uid'] as String? ?? '',
nickname: json['nickname'] as String? ?? '',
avatarUrl: json['avatarUrl'] as String? ?? '',
);
Map<String, dynamic> toJson() => {
'uid': uid,
'nickname': nickname,
'avatarUrl': avatarUrl,
};
}
class AuthSession {
const AuthSession({
required this.accessToken,
required this.refreshToken,
this.expiresIn = 0,
required this.user,
});
final String accessToken;
final String refreshToken;
final int expiresIn;
final AuthUser user;
factory AuthSession.fromJson(Map<String, dynamic> json) => AuthSession(
accessToken: json['accessToken'] as String? ?? '',
refreshToken: json['refreshToken'] as String? ?? '',
expiresIn: (json['expiresIn'] as num?)?.toInt() ?? 0,
user: AuthUser.fromJson((json['user'] as Map?)?.cast<String, dynamic>() ?? const {}),
);
Map<String, dynamic> toJson() => {
'accessToken': accessToken,
'refreshToken': refreshToken,
'expiresIn': expiresIn,
'user': user.toJson(),
};
}