diff --git a/barrelsby.json b/barrelsby.json index 3a93788f..24e4334a 100644 --- a/barrelsby.json +++ b/barrelsby.json @@ -1,6 +1,7 @@ { "directory": [ "./src/data/repositories", + "./src/data/repositories/interfaces", "./src/data/services/api", "./src/data/dto/auth", "./src/data/dto/chat", diff --git a/src/data/repositories/auth_repository.ts b/src/data/repositories/auth_repository.ts index 4e47847a..d63c0c76 100644 --- a/src/data/repositories/auth_repository.ts +++ b/src/data/repositories/auth_repository.ts @@ -27,6 +27,7 @@ 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 "@/utils/result"; +import type { IAuthRepository } from "@/data/repositories/interfaces/iauth_repository"; 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"; @@ -35,7 +36,7 @@ import type { IUserStorage } from "@/data/storage/user/iuser_storage"; /** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`(Web 平台)。 */ const WEB_PLATFORM = "web"; -export class AuthRepository { +export class AuthRepository implements IAuthRepository { constructor( private readonly api: AuthApi, private readonly storage: IAuthStorage, diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index 64ee94c4..1c2fb6e7 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -21,10 +21,11 @@ import { ChatMessage } from "@/data/dto/chat/chat_message"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; import { SendMessageRequest } from "@/data/dto/chat/send_message_request"; import { Result } from "@/utils/result"; +import type { IChatRepository } from "@/data/repositories/interfaces/ichat_repository"; import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage"; import { LocalMessage } from "@/data/storage/chat/local_message"; -export class ChatRepository { +export class ChatRepository implements IChatRepository { constructor( private readonly api: ChatApi, private readonly localStorage: LocalChatStorage, diff --git a/src/data/repositories/index.ts b/src/data/repositories/index.ts index 6922ab27..a45833f1 100644 --- a/src/data/repositories/index.ts +++ b/src/data/repositories/index.ts @@ -6,3 +6,7 @@ export * from "./auth_repository"; export * from "./chat_repository"; export * from "./metrics_repository"; export * from "./user_repository"; +export * from "./interfaces/iauth_repository"; +export * from "./interfaces/ichat_repository"; +export * from "./interfaces/imetrics_repository"; +export * from "./interfaces/iuser_repository"; diff --git a/src/data/repositories/interfaces/iauth_repository.ts b/src/data/repositories/interfaces/iauth_repository.ts new file mode 100644 index 00000000..ecf2cf9a --- /dev/null +++ b/src/data/repositories/interfaces/iauth_repository.ts @@ -0,0 +1,76 @@ +"use client"; + +/** + * IAuthRepository 接口 + * + * 对齐 Dart 端 `AuthRepository` 抽象(lib/data/repositories/auth_repository.dart): + * - 登录 / 登出 / 刷新 token / 第三方 OAuth 登录的公共契约 + * - 登出强制清本地态(无论 API 成败) + * - `getCurrentUser` 成功后尽力写本地 User 缓存(best-effort) + * - 所有方法返回 `Promise>`,与 Dart `Future>` 对齐 + * + * 注:仓库内私有方法(`_socialLogin` / `_saveLoginData`)不暴露在接口上。 + * 原始 Dart: lib/data/repositories/auth_repository_impl.dart + */ + +import type { Result } from "@/utils/result"; +import type { GuestLoginResponse } from "@/data/dto/auth/guest_login_response"; +import type { LoginResponse } from "@/data/dto/auth/login_response"; +import type { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response"; +import type { User } from "@/data/dto/user/user"; + +export interface IAuthRepository { + /** 用户注册。注册成功不会自动登录(与 Dart 行为一致)。 */ + register(input: { + username: string; + email: string; + password: string; + guestId?: string; + }): Promise>; + + /** 邮箱/用户名 + 密码登录。 */ + emailLogin(input: { + email?: string; + username?: string; + password: string; + guestId?: string; + }): Promise>; + + /** 发送邮箱验证码。 */ + sendCode(email: string): Promise>; + + /** + * 退出登录:先调 API 注销服务端会话,**无论成败**都清本地登录态。 + */ + logout(): Promise>; + + /** 游客登录:使用 deviceId 换取 guest token。 */ + guestLogin(deviceId: string): Promise>; + + /** Google 登录。 */ + googleLogin(input: { + idToken: string; + guestId?: string; + }): Promise>; + + /** Facebook 登录(accessToken 流程)。 */ + facebookLogin(input: { + accessToken: string; + guestId?: string; + }): Promise>; + + /** Facebook ID 登录(v7.0 新增,fbId 流程)。 */ + facebookIdLogin(input: { + fbId: string; + avatarUrl?: string; + }): Promise>; + + /** Apple 登录。 */ + appleLogin(identityToken: string): Promise>; + + /** 刷新 token:先读本地的 refresh token,空则返回错误。 */ + refreshToken(): Promise>; + + /** 获取当前登录用户。成功后尽力写本地 User 缓存。 */ + getCurrentUser(): Promise>; +} diff --git a/src/data/repositories/interfaces/ichat_repository.ts b/src/data/repositories/interfaces/ichat_repository.ts new file mode 100644 index 00000000..a0f52360 --- /dev/null +++ b/src/data/repositories/interfaces/ichat_repository.ts @@ -0,0 +1,43 @@ +"use client"; + +/** + * IChatRepository 接口 + * + * 对齐 Dart 端 `ChatRepository` 抽象(lib/data/repositories/chat_repository.dart): + * - 远程:发送消息、获取历史 + * - 本地:写入 / 批量覆盖 / 读取 / 清空 / 计数 + * + * 私有助手(`_chatToLocal` / `_localToChat` / `_mapLocalStorageResult`)不暴露。 + * 原始 Dart: lib/data/repositories/chat_repository_impl.dart + */ + +import type { Result } from "@/utils/result"; +import type { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response"; +import type { ChatMessage } from "@/data/dto/chat/chat_message"; +import type { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; + +export interface IChatRepository { + /** 发送一条消息。 */ + sendMessage( + message: string, + options?: { image?: string; useWebSocket?: boolean }, + ): Promise>; + + /** 获取聊天历史,分页参数默认 limit=50, offset=0。 */ + getHistory(limit?: number, offset?: number): Promise>; + + /** 把一条消息写入本地存储。 */ + saveMessageToLocal(message: ChatMessage): Promise>; + + /** 批量覆盖写入:先清空本地存储,再写入新列表。 */ + saveMessagesToLocal(messages: readonly ChatMessage[]): Promise>; + + /** 读取所有本地消息,按 dbId 升序。 */ + getLocalMessages(): Promise>; + + /** 清空本地消息。 */ + clearLocalMessages(): Promise>; + + /** 获取本地消息数量。 */ + getLocalMessageCount(): Promise>; +} diff --git a/src/data/repositories/interfaces/imetrics_repository.ts b/src/data/repositories/interfaces/imetrics_repository.ts new file mode 100644 index 00000000..070289c8 --- /dev/null +++ b/src/data/repositories/interfaces/imetrics_repository.ts @@ -0,0 +1,29 @@ +"use client"; + +/** + * IMetricsRepository 接口 + * + * 对齐 Dart 端 `MetricsRepository` 抽象(lib/data/repositories/metrics_repository.dart): + * - 纯远程 fire-and-forget 上报,无本地状态 + * + * 原始 Dart: lib/data/repositories/metrics_repository_impl.dart + */ + +import type { Result } from "@/utils/result"; + +export interface IMetricsRepository { + /** 上报 PWA 事件。自动注入当前秒级时间戳。 */ + reportPwaEvent(input: { + deviceId: string; + deviceType: string; + pwaInstalled: boolean; + pwaSupported: boolean; + }): Promise>; + + /** 上报用户环境信息(浏览器、UA 等)。 */ + reportUserInfo(input: { + userId: string; + browser: string; + userAgent: string; + }): Promise>; +} diff --git a/src/data/repositories/interfaces/index.ts b/src/data/repositories/interfaces/index.ts new file mode 100644 index 00000000..3d0eb5b3 --- /dev/null +++ b/src/data/repositories/interfaces/index.ts @@ -0,0 +1,8 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from "./iauth_repository"; +export * from "./ichat_repository"; +export * from "./imetrics_repository"; +export * from "./iuser_repository"; diff --git a/src/data/repositories/interfaces/iuser_repository.ts b/src/data/repositories/interfaces/iuser_repository.ts new file mode 100644 index 00000000..ce1aefe7 --- /dev/null +++ b/src/data/repositories/interfaces/iuser_repository.ts @@ -0,0 +1,37 @@ +"use client"; + +/** + * IUserRepository 接口 + * + * 对齐 Dart 端 `UserRepository` 抽象(lib/data/repositories/user_repository.dart): + * - 纯远程调用,不直接写本地 storage(持久化由 AuthRepository 在登录流程中处理) + * + * 原始 Dart: lib/data/repositories/user_repository_impl.dart + */ + +import type { Result } from "@/utils/result"; +import type { CreditsData } from "@/data/dto/user/credits_data"; +import type { CreditsHistoryData } from "@/data/dto/user/credits_history_data"; +import type { UpdateProfileRequest } from "@/data/dto/user/update_profile_request"; +import type { User } from "@/data/dto/user/user"; +import type { UserStatsResponse } from "@/data/dto/user/user_stats_response"; + +export interface IUserRepository { + /** 获取用户统计信息。 */ + getUserStats(): Promise>; + + /** 获取当前登录用户信息。 */ + getCurrentUser(): Promise>; + + /** 更新个人资料。 */ + updateProfile(request: UpdateProfileRequest): Promise>; + + /** 查询积分余额。 */ + getCredits(): Promise>; + + /** 查询积分操作历史,分页参数默认 limit=50, offset=0。 */ + getCreditsHistory( + limit?: number, + offset?: number, + ): Promise>; +} diff --git a/src/data/repositories/metrics_repository.ts b/src/data/repositories/metrics_repository.ts index 42e96848..508a4c34 100644 --- a/src/data/repositories/metrics_repository.ts +++ b/src/data/repositories/metrics_repository.ts @@ -11,8 +11,9 @@ import { MetricsApi, metricsApi } from "@/data/services/api"; import { AppEvent } from "@/data/dto/metrics/app_event"; import { PwaEvent } from "@/data/dto/metrics/pwa_event"; import { Result } from "@/utils/result"; +import type { IMetricsRepository } from "@/data/repositories/interfaces/imetrics_repository"; -export class MetricsRepository { +export class MetricsRepository implements IMetricsRepository { constructor(private readonly api: MetricsApi) {} /** diff --git a/src/data/repositories/user_repository.ts b/src/data/repositories/user_repository.ts index c68d5e98..b0bab76c 100644 --- a/src/data/repositories/user_repository.ts +++ b/src/data/repositories/user_repository.ts @@ -15,8 +15,9 @@ import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request"; import { User } from "@/data/dto/user/user"; import { UserStatsResponse } from "@/data/dto/user/user_stats_response"; import { Result } from "@/utils/result"; +import type { IUserRepository } from "@/data/repositories/interfaces/iuser_repository"; -export class UserRepository { +export class UserRepository implements IUserRepository { constructor(private readonly api: UserApi) {} /** 获取用户统计信息。 */ diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 16ed3c65..1656e364 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -14,6 +14,7 @@ import { signIn } from "next-auth/react"; import type { AuthProvider } from "@/lib/auth/auth_platform"; import type { LoginStatus } from "@/models/auth/login-status"; import { authRepository } from "@/data/repositories/auth_repository"; +import type { IAuthRepository } from "@/data/repositories/interfaces"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { deviceIdentifier } from "@/utils/device_identifier"; import { Result } from "@/utils/result"; @@ -40,11 +41,13 @@ async function readGuestId(): Promise { // ============================================================ // Actors(异步服务) // ============================================================ +// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例 +const authRepo: IAuthRepository = authRepository; const emailLoginActor = fromPromise( async ({ input }) => { const guestId = await readGuestId(); - const result = await authRepository.emailLogin({ ...input, guestId }); + const result = await authRepo.emailLogin({ ...input, guestId }); if (Result.isErr(result)) throw result.error; return "email" as LoginStatus; }, @@ -56,11 +59,11 @@ const emailRegisterThenLoginActor = fromPromise< >(async ({ input }) => { const guestId = await readGuestId(); - const registerResult = await authRepository.register({ ...input, guestId }); + const registerResult = await authRepo.register({ ...input, guestId }); if (Result.isErr(registerResult)) throw registerResult.error; // 注册后自动登录(对齐 Dart 行为) - const loginResult = await authRepository.emailLogin({ + const loginResult = await authRepo.emailLogin({ email: input.email, password: input.password, guestId, @@ -84,7 +87,7 @@ const oauthSignInActor = fromPromise(async ({ input }) => { const syncGoogleBackendActor = fromPromise( async ({ input }) => { const guestId = await readGuestId(); - const result = await authRepository.googleLogin({ + const result = await authRepo.googleLogin({ idToken: input.idToken, guestId, }); @@ -98,7 +101,7 @@ const syncFacebookBackendActor = fromPromise< { accessToken: string } >(async ({ input }) => { const guestId = await readGuestId(); - const result = await authRepository.facebookLogin({ + const result = await authRepo.facebookLogin({ accessToken: input.accessToken, guestId, }); diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index 6ed38a8a..20940fa0 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -18,6 +18,7 @@ import { setup, fromPromise, assign } from "xstate"; import type { UiMessage } from "@/models/chat/ui-message"; import { chatRepository } from "@/data/repositories/chat_repository"; +import type { IChatRepository } from "@/data/repositories/interfaces"; import { ChatStorage } from "@/data/storage/chat/chat_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { formatDate } from "@/utils/date"; @@ -72,6 +73,9 @@ function mapTotalQuotaResult( return fallback; } +// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例 +const chatRepo: IChatRepository = chatRepository; + interface InitResult { localMessages: UiMessage[]; isGuest: boolean; @@ -86,7 +90,7 @@ async function readInitData(): Promise { const [dailyResult, totalResult, localResult] = await Promise.all([ chatStorage.getGuestDailyChatQuota(), chatStorage.getGuestTotalQuota(), - isGuest ? chatRepository.getLocalMessages() : Promise.resolve(null), + isGuest ? chatRepo.getLocalMessages() : Promise.resolve(null), ]); return { @@ -115,11 +119,11 @@ const sendMessageHttpActor = fromPromise< { messages: UiMessage[] }, { content: string } >(async ({ input }) => { - const result = await chatRepository.sendMessage(input.content); + const result = await chatRepo.sendMessage(input.content); if (Result.isErr(result)) throw result.error; // 拉取最新本地历史 - const local = await chatRepository.getLocalMessages(); + const local = await chatRepo.getLocalMessages(); if (Result.isOk(local) && local.data) { return { messages: localMessagesToUi(local.data) }; } @@ -130,7 +134,7 @@ const loadMoreHistoryActor = fromPromise< { messages: UiMessage[]; hasMore: boolean; newOffset: number }, { offset: number } >(async ({ input }) => { - const result = await chatRepository.getHistory(PAGE_SIZE, input.offset); + const result = await chatRepo.getHistory(PAGE_SIZE, input.offset); if (Result.isErr(result)) throw result.error; const page = localMessagesToUi(result.data.messages); diff --git a/src/stores/user/user-machine.ts b/src/stores/user/user-machine.ts index b410da47..e403eb78 100644 --- a/src/stores/user/user-machine.ts +++ b/src/stores/user/user-machine.ts @@ -14,6 +14,10 @@ import { setup, fromPromise, assign } from "xstate"; import type { UserView } from "@/models/user/user"; import { userRepository } from "@/data/repositories/user_repository"; import { authRepository } from "@/data/repositories/auth_repository"; +import type { + IAuthRepository, + IUserRepository, +} from "@/data/repositories/interfaces"; import { UserStorage } from "@/data/storage/user/user_storage"; import { Result } from "@/utils/result"; @@ -81,10 +85,14 @@ async function readInitData(): Promise { // ============================================================ // Actors // ============================================================ +// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例 +const userRepo: IUserRepository = userRepository; +const authRepo: IAuthRepository = authRepository; + const userInitActor = fromPromise(async () => readInitData()); const userFetchActor = fromPromise(async () => { - const result = await userRepository.getCurrentUser(); + const result = await userRepo.getCurrentUser(); if (Result.isErr(result)) return null; const view = toView(result.data.toJson()); // 持久化到本地 @@ -93,7 +101,7 @@ const userFetchActor = fromPromise(async () => { }); const userLogoutActor = fromPromise(async () => { - await authRepository.logout(); + await authRepo.logout(); await userStorage.clearUserData(); });