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:
@@ -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(),
|
||||
);
|
||||
Reference in New Issue
Block a user