feat(mobile): api client, session storage and contract-aligned models

P3 计划 Task 2(apps/mobile):

- core/config.dart:API_BASE/VIEWER_BASE 经 --dart-define 覆盖,缺省
  10.0.2.2 模拟器宿主回环;viewer 相册链接构造 {VIEWER_BASE}/a/{token}。
- core/error_codes.dart:errors.ts 17 码 1:1 镜像(含注释),另设 4 个
  客户端本地伪码(NETWORK_ERROR/NETWORK_TIMEOUT/CANCELLED/HTTP_ERROR)。
- core/api_client.dart:dio 封装——Bearer 注入(登录端点豁免)、401
  驱逐(清会话 + onUnauthorized 回调去重,App 壳注册跳 /login)、
  DioException/错误体 {code,message,requestId} 归一 AppException、
  通用游标分页助手;auth/albums/members/categories/admin/uploads/photos
  全端点方法;riverpod provider 出口。
- core/session_store.dart:SessionStore 抽象 + flutter_secure_storage
  实现(token/expiresAt/account 三键,encode/decode 纯函数可测)。
- features/{auth,albums,uploads,photos}/models.dart:手写 fromJson/
  toJson,逐字段对齐 openapi.yaml(类注释标注契约 schema/operationId);
  locale 按字符串透传(zh-CN 标识符不兼容),业务枚举 Dart 化并在
  未知值 fail-fast。
- 测试 66 项:17 码表驱动 + 网络/超时/取消 + 兜底映射、游标边界
  (含 CursorPageMeta 不变式违约容错)、模型往返(fixture 转写自
  openapi.yaml schema 与 api e2e 断言,来源在测试头注释)、拦截器
  行为(手写 FakeAdapter:Bearer 注入/401 驱逐/登录豁免/并发去重/
  尽力而为 logout/refresh 滑动续期)。flutter analyze 零告警。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-19 00:40:02 +08:00
parent 8bc50e07ef
commit f975321b67
18 changed files with 3430 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
/// 认证域模型(P3 计划 Task 2)。
///
/// 契约来源:`packages/contracts/openapi.yaml` tag `auth`。
library;
/// 密码登录响应。
///
/// 契约 `components.schemas.LoginResponse`(operationId `loginWithPassword`
/// ,POST /auth/password → 200)。
class LoginResponse {
const LoginResponse({required this.sessionToken, required this.expiresAt});
factory LoginResponse.fromJson(Map<String, dynamic> json) => LoginResponse(
sessionToken: json['sessionToken'] as String,
expiresAt: DateTime.parse(json['expiresAt'] as String),
);
/// 会话令牌(Bearer)。
final String sessionToken;
/// 会话过期时间(date-time)。
final DateTime expiresAt;
Map<String, dynamic> toJson() => {
'sessionToken': sessionToken,
'expiresAt': expiresAt.toUtc().toIso8601String(),
};
}
/// 会话滑动续期响应。
///
/// 契约 `components.schemas.RefreshResponse`(operationId `refreshSession`
/// ,POST /auth/refresh → 200):令牌不变,仅返回新的过期时间。
class RefreshResponse {
const RefreshResponse({required this.expiresAt});
factory RefreshResponse.fromJson(Map<String, dynamic> json) =>
RefreshResponse(expiresAt: DateTime.parse(json['expiresAt'] as String));
/// 续期后的过期时间(date-time)。
final DateTime expiresAt;
Map<String, dynamic> toJson() =>
{'expiresAt': expiresAt.toUtc().toIso8601String()};
}