feat(data): add repository layer with Auth/Chat/User/Metrics + shared Result<T>

引入仓库层(`src/data/repositories/`),对齐原 Dart `lib/data/repositories/`
的四个仓库:Auth(hybrid: API+AuthStorage+UserStorage)、Chat(hybrid:
API+LocalChatStorage via Dexie)、User(纯远程)、Metrics(纯远程)。

同步抽取 4 个 `*_api.ts` 中重复的 envelope-unwrap 代码到
`src/data/api/response_helper.ts`,失败时抛 `ApiError`(之前抛裸 Error)。

新建 `src/data/result.ts` 提供仓库层统一 `Result<T>`(`{success,data}` 形态),
与 `src/data/storage/result.ts`(`{kind,value}` 形态)并存。

更新 `barrelsby.json` 让 `src/data/repositories/` 加入 barrel 生成。
This commit is contained in:
2026-06-08 19:16:28 +08:00
parent 75e685d418
commit a240f5965d
13 changed files with 695 additions and 71 deletions
+289
View File
@@ -0,0 +1,289 @@
"use client";
/**
* AuthRepository
*
* 认证仓库:编排远程 API + 本地 AuthStorage + UserStorage。
* 行为对齐原 Dart `lib/data/repositories/auth_repository_impl.dart`
* - 登录成功后调 `_saveLoginData` 持久化 token + User
* - 登出先调 API,再**强制**清本地态(无论 API 成败)
* - `getCurrentUser` 成功后**尽力**写本地 User 缓存(不因缓存失败影响主流程)
* - 写 storage 失败用 `console.warn` 记录,不让本地缓存失败影响主流程
*
* 原始 Dart: lib/data/repositories/auth_repository_impl.dart
*/
import { AuthApi, authApi } from "@/data/api";
import { AppleLoginRequest } from "@/data/dto/auth/apple_login_request";
import { FacebookLoginRequest } from "@/data/dto/auth/facebook_login_request";
import { FbIdLoginRequest } from "@/data/dto/auth/fb_id_login_request";
import { GoogleLoginRequest } from "@/data/dto/auth/google_login_request";
import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request";
import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
import { LoginRequest } from "@/data/dto/auth/login_request";
import { LoginResponse } from "@/data/dto/auth/login_response";
import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request";
import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
import { RegisterRequest } from "@/data/dto/auth/register_request";
import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
import { User } from "@/data/dto/user/user";
import { Result } from "@/data/result";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import type { IAuthStorage } from "@/data/storage/auth/iauth_storage";
import { UserStorage } from "@/data/storage/user/user_storage";
import type { IUserStorage } from "@/data/storage/user/iuser_storage";
/** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`Web 平台)。 */
const WEB_PLATFORM = "web";
export class AuthRepository {
constructor(
private readonly api: AuthApi,
private readonly storage: IAuthStorage,
private readonly userStorage: IUserStorage,
) {}
// ============ 公共方法 ============
/**
* 用户注册。
* 注册成功不会自动登录(与 Dart 行为一致),调用方需自行调 `emailLogin`。
*/
async register(input: {
username: string;
email: string;
password: string;
guestId?: string;
}): Promise<Result<void>> {
return Result.wrap(async () => {
await this.api.register(
RegisterRequest.from({
username: input.username,
email: input.email,
password: input.password,
platform: WEB_PLATFORM,
guestId: input.guestId ?? "",
}),
);
});
}
/** 邮箱/用户名 + 密码登录。 */
async emailLogin(input: {
email?: string;
username?: string;
password: string;
guestId?: string;
}): Promise<Result<LoginResponse>> {
return Result.wrap(async () => {
const response = await this.api.emailLogin(
LoginRequest.from({
email: input.email ?? "",
username: input.username ?? "",
password: input.password,
platform: WEB_PLATFORM,
guestId: input.guestId ?? "",
}),
);
await this._saveLoginData(response);
return response;
});
}
/** 发送邮箱验证码。 */
async sendCode(email: string): Promise<Result<void>> {
return Result.wrap(async () => {
await this.api.sendCode(SendCodeRequest.from({ email }));
});
}
/**
* 退出登录:先调 API 注销服务端会话,**无论成败**都清本地登录态。
* 强制清本地的逻辑:用户即使在 API 失败时也想从设备上登出。
*/
async logout(): Promise<Result<void>> {
try {
await this.api.logout();
} catch (e) {
// API 失败也要继续清本地态
console.warn("[AuthRepository] logout API failed, clearing local anyway", e);
}
try {
await this.storage.clearAuthData();
await this.userStorage.clearUserData();
return Result.ok(undefined);
} catch (e) {
// 清本地是 best-effort;理论上 LS.remove 不应失败
return Result.err(e);
}
}
/**
* 游客登录:使用 deviceId 换取 guest token。
* 成功后写 guest token + deviceId + userId。
*/
async guestLogin(deviceId: string): Promise<Result<GuestLoginResponse>> {
return Result.wrap(async () => {
const response = await this.api.guestLogin(
GuestLoginRequest.from({ deviceId }),
);
await this.storage.setGuestToken(response.token);
await this.storage.setDeviceId(deviceId);
if (response.userId) {
await this.userStorage.setUserId(response.userId);
}
return response;
});
}
/** Google 登录。 */
async googleLogin(input: {
idToken: string;
guestId?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
this.api.googleLogin(
GoogleLoginRequest.from({
idToken: input.idToken,
platform: WEB_PLATFORM,
guestId: input.guestId ?? "",
}),
),
);
}
/** Facebook 登录(accessToken 流程)。 */
async facebookLogin(input: {
accessToken: string;
guestId?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
this.api.facebookLogin(
FacebookLoginRequest.from({
accessToken: input.accessToken,
platform: WEB_PLATFORM,
guestId: input.guestId ?? "",
}),
),
);
}
/** Facebook ID 登录(v7.0 新增,fbId 流程)。 */
async facebookIdLogin(input: {
fbId: string;
avatarUrl?: string;
}): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
this.api.facebookIdLogin(
FbIdLoginRequest.from({
fbId: input.fbId,
avatarUrl: input.avatarUrl ?? "",
}),
),
);
}
/**
* Apple 登录。
* 注:Dart 端另有一个 `uploadFacebookAvatar` 方法,但当前 TS 端 `AuthApi`
* 尚未实现该端点,故仓库暂不暴露。等 `AuthApi` 补齐后再加。
*/
async appleLogin(identityToken: string): Promise<Result<LoginResponse>> {
return this._socialLogin(() =>
this.api.appleLogin(
AppleLoginRequest.from({
identityToken,
platform: WEB_PLATFORM,
}),
),
);
}
/**
* 刷新 token:先读本地的 refresh token,空则直接返回错误;
* 调用 API 成功后写回新的 login token + refresh token。
*/
async refreshToken(): Promise<Result<RefreshTokenResponse>> {
const existing = await this.storage.getRefreshToken();
if (existing.kind !== "success" || !existing.value) {
return Result.err(new Error("No refresh token available"));
}
const refreshToken = existing.value;
return Result.wrap(async () => {
const response = await this.api.refreshToken(
RefreshTokenRequest.from({ refreshToken }),
);
await this.storage.setLoginToken(response.token);
if (response.refreshToken) {
await this.storage.setRefreshToken(response.refreshToken);
}
return response;
});
}
/**
* 获取当前登录用户。成功后**尽力**写本地 User 缓存(offline hydrate),
* 缓存失败用 `console.warn` 记录,不影响主流程。
*/
async getCurrentUser(): Promise<Result<User>> {
return Result.wrap(async () => {
const user = await this.api.getCurrentUser();
const writeResult = await this.userStorage.setUser(user.toJson());
if (writeResult.kind === "failure") {
console.warn(
"[AuthRepository] failed to cache current user",
writeResult.error,
);
}
return user;
});
}
// ============ 私有助手 ============
/**
* 社交登录的公共包装:调 API → 调 `_saveLoginData` 持久化。
* `_saveLoginData` 内部全部 best-effort,存储写失败不抛错。
*/
private async _socialLogin(
call: () => Promise<LoginResponse>,
): Promise<Result<LoginResponse>> {
return Result.wrap(async () => {
const response = await call();
await this._saveLoginData(response);
return response;
});
}
/**
* 持久化登录态:login token → refresh token → User 对象 → userId。
* 全部 best-effort,错误用 `console.warn` 记录但不让登录「失败」——
* 调用方已经从 API 拿到了 LoginResponse,缓存只是离线加速。
*/
private async _saveLoginData(data: LoginResponse): Promise<void> {
const r1 = await this.storage.setLoginToken(data.token);
if (r1.kind === "failure") {
console.warn("[AuthRepository] setLoginToken failed", r1.error);
}
if (data.refreshToken) {
const r2 = await this.storage.setRefreshToken(data.refreshToken);
if (r2.kind === "failure") {
console.warn("[AuthRepository] setRefreshToken failed", r2.error);
}
}
const r3 = await this.userStorage.setUser(data.user.toJson());
if (r3.kind === "failure") {
console.warn("[AuthRepository] setUser failed", r3.error);
}
const r4 = await this.userStorage.setUserId(data.user.id);
if (r4.kind === "failure") {
console.warn("[AuthRepository] setUserId failed", r4.error);
}
}
}
/** 全局单例。 */
export const authRepository = new AuthRepository(
authApi,
AuthStorage.getInstance(),
UserStorage.getInstance(),
);