refactor(data): add I*Repository interfaces and depend on them in stores

为 /src/data/repositories/ 下每个仓库类增加对应的接口契约,
接口文件存放在独立的 interfaces/ 子目录中。

改动:
- 新建 4 个接口文件 (iauth_repository / ichat_repository /
  imetrics_repository / iuser_repository),纯 type-only,方法签名
  与实现类完全一致
- 4 个实现类加 implements I{Name}Repository 子句,编译器自动校验契约
- 3 个 stores 文件 (auth / chat / user state machines) 改为依赖接口
  类型:通过 `const xxxRepo: I{Name}Repository = xxxRepository` 局部别名,
  actor 内部调用全部走别名
- barrelsby.json 加入 interfaces/ 目录,让自动 barrel 能覆盖
- Auto-regenerated repositories/index.ts 与 interfaces/index.ts

参照 storage 层已有的 IAuthStorage / IChatStorage / IUserStorage 模式。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:00:47 +08:00
parent 1151a9a61b
commit 982d9ba790
14 changed files with 232 additions and 15 deletions
+1
View File
@@ -1,6 +1,7 @@
{ {
"directory": [ "directory": [
"./src/data/repositories", "./src/data/repositories",
"./src/data/repositories/interfaces",
"./src/data/services/api", "./src/data/services/api",
"./src/data/dto/auth", "./src/data/dto/auth",
"./src/data/dto/chat", "./src/data/dto/chat",
+2 -1
View File
@@ -27,6 +27,7 @@ import { RegisterRequest } from "@/data/dto/auth/register_request";
import { SendCodeRequest } from "@/data/dto/auth/send_code_request"; import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
import { User } from "@/data/dto/user/user"; import { User } from "@/data/dto/user/user";
import { Result } from "@/utils/result"; import { Result } from "@/utils/result";
import type { IAuthRepository } from "@/data/repositories/interfaces/iauth_repository";
import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage";
import type { IAuthStorage } from "@/data/storage/auth/iauth_storage"; import type { IAuthStorage } from "@/data/storage/auth/iauth_storage";
import { UserStorage } from "@/data/storage/user/user_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 平台)。 */ /** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`Web 平台)。 */
const WEB_PLATFORM = "web"; const WEB_PLATFORM = "web";
export class AuthRepository { export class AuthRepository implements IAuthRepository {
constructor( constructor(
private readonly api: AuthApi, private readonly api: AuthApi,
private readonly storage: IAuthStorage, private readonly storage: IAuthStorage,
+2 -1
View File
@@ -21,10 +21,11 @@ import { ChatMessage } from "@/data/dto/chat/chat_message";
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
import { SendMessageRequest } from "@/data/dto/chat/send_message_request"; import { SendMessageRequest } from "@/data/dto/chat/send_message_request";
import { Result } from "@/utils/result"; import { Result } from "@/utils/result";
import type { IChatRepository } from "@/data/repositories/interfaces/ichat_repository";
import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage"; import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage";
import { LocalMessage } from "@/data/storage/chat/local_message"; import { LocalMessage } from "@/data/storage/chat/local_message";
export class ChatRepository { export class ChatRepository implements IChatRepository {
constructor( constructor(
private readonly api: ChatApi, private readonly api: ChatApi,
private readonly localStorage: LocalChatStorage, private readonly localStorage: LocalChatStorage,
+4
View File
@@ -6,3 +6,7 @@ export * from "./auth_repository";
export * from "./chat_repository"; export * from "./chat_repository";
export * from "./metrics_repository"; export * from "./metrics_repository";
export * from "./user_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";
@@ -0,0 +1,76 @@
"use client";
/**
* IAuthRepository 接口
*
* 对齐 Dart 端 `AuthRepository` 抽象(lib/data/repositories/auth_repository.dart):
* - 登录 / 登出 / 刷新 token / 第三方 OAuth 登录的公共契约
* - 登出强制清本地态(无论 API 成败)
* - `getCurrentUser` 成功后尽力写本地 User 缓存(best-effort
* - 所有方法返回 `Promise<Result<T>>`,与 Dart `Future<Result<T>>` 对齐
*
* 注:仓库内私有方法(`_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<Result<void>>;
/** 邮箱/用户名 + 密码登录。 */
emailLogin(input: {
email?: string;
username?: string;
password: string;
guestId?: string;
}): Promise<Result<LoginResponse>>;
/** 发送邮箱验证码。 */
sendCode(email: string): Promise<Result<void>>;
/**
* 退出登录:先调 API 注销服务端会话,**无论成败**都清本地登录态。
*/
logout(): Promise<Result<void>>;
/** 游客登录:使用 deviceId 换取 guest token。 */
guestLogin(deviceId: string): Promise<Result<GuestLoginResponse>>;
/** Google 登录。 */
googleLogin(input: {
idToken: string;
guestId?: string;
}): Promise<Result<LoginResponse>>;
/** Facebook 登录(accessToken 流程)。 */
facebookLogin(input: {
accessToken: string;
guestId?: string;
}): Promise<Result<LoginResponse>>;
/** Facebook ID 登录(v7.0 新增,fbId 流程)。 */
facebookIdLogin(input: {
fbId: string;
avatarUrl?: string;
}): Promise<Result<LoginResponse>>;
/** Apple 登录。 */
appleLogin(identityToken: string): Promise<Result<LoginResponse>>;
/** 刷新 token:先读本地的 refresh token,空则返回错误。 */
refreshToken(): Promise<Result<RefreshTokenResponse>>;
/** 获取当前登录用户。成功后尽力写本地 User 缓存。 */
getCurrentUser(): Promise<Result<User>>;
}
@@ -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<Result<ChatSendResponse>>;
/** 获取聊天历史,分页参数默认 limit=50, offset=0。 */
getHistory(limit?: number, offset?: number): Promise<Result<ChatHistoryResponse>>;
/** 把一条消息写入本地存储。 */
saveMessageToLocal(message: ChatMessage): Promise<Result<void>>;
/** 批量覆盖写入:先清空本地存储,再写入新列表。 */
saveMessagesToLocal(messages: readonly ChatMessage[]): Promise<Result<void>>;
/** 读取所有本地消息,按 dbId 升序。 */
getLocalMessages(): Promise<Result<ChatMessage[]>>;
/** 清空本地消息。 */
clearLocalMessages(): Promise<Result<void>>;
/** 获取本地消息数量。 */
getLocalMessageCount(): Promise<Result<number>>;
}
@@ -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<Result<void>>;
/** 上报用户环境信息(浏览器、UA 等)。 */
reportUserInfo(input: {
userId: string;
browser: string;
userAgent: string;
}): Promise<Result<void>>;
}
@@ -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";
@@ -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<Result<UserStatsResponse>>;
/** 获取当前登录用户信息。 */
getCurrentUser(): Promise<Result<User>>;
/** 更新个人资料。 */
updateProfile(request: UpdateProfileRequest): Promise<Result<User>>;
/** 查询积分余额。 */
getCredits(): Promise<Result<CreditsData>>;
/** 查询积分操作历史,分页参数默认 limit=50, offset=0。 */
getCreditsHistory(
limit?: number,
offset?: number,
): Promise<Result<CreditsHistoryData>>;
}
+2 -1
View File
@@ -11,8 +11,9 @@ import { MetricsApi, metricsApi } from "@/data/services/api";
import { AppEvent } from "@/data/dto/metrics/app_event"; import { AppEvent } from "@/data/dto/metrics/app_event";
import { PwaEvent } from "@/data/dto/metrics/pwa_event"; import { PwaEvent } from "@/data/dto/metrics/pwa_event";
import { Result } from "@/utils/result"; 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) {} constructor(private readonly api: MetricsApi) {}
/** /**
+2 -1
View File
@@ -15,8 +15,9 @@ import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
import { User } from "@/data/dto/user/user"; import { User } from "@/data/dto/user/user";
import { UserStatsResponse } from "@/data/dto/user/user_stats_response"; import { UserStatsResponse } from "@/data/dto/user/user_stats_response";
import { Result } from "@/utils/result"; 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) {} constructor(private readonly api: UserApi) {}
/** 获取用户统计信息。 */ /** 获取用户统计信息。 */
+8 -5
View File
@@ -14,6 +14,7 @@ import { signIn } from "next-auth/react";
import type { AuthProvider } from "@/lib/auth/auth_platform"; import type { AuthProvider } from "@/lib/auth/auth_platform";
import type { LoginStatus } from "@/models/auth/login-status"; import type { LoginStatus } from "@/models/auth/login-status";
import { authRepository } from "@/data/repositories/auth_repository"; import { authRepository } from "@/data/repositories/auth_repository";
import type { IAuthRepository } from "@/data/repositories/interfaces";
import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { deviceIdentifier } from "@/utils/device_identifier"; import { deviceIdentifier } from "@/utils/device_identifier";
import { Result } from "@/utils/result"; import { Result } from "@/utils/result";
@@ -40,11 +41,13 @@ async function readGuestId(): Promise<string | undefined> {
// ============================================================ // ============================================================
// Actors(异步服务) // Actors(异步服务)
// ============================================================ // ============================================================
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
const authRepo: IAuthRepository = authRepository;
const emailLoginActor = fromPromise<LoginStatus, { email: string; password: string }>( const emailLoginActor = fromPromise<LoginStatus, { email: string; password: string }>(
async ({ input }) => { async ({ input }) => {
const guestId = await readGuestId(); 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; if (Result.isErr(result)) throw result.error;
return "email" as LoginStatus; return "email" as LoginStatus;
}, },
@@ -56,11 +59,11 @@ const emailRegisterThenLoginActor = fromPromise<
>(async ({ input }) => { >(async ({ input }) => {
const guestId = await readGuestId(); 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; if (Result.isErr(registerResult)) throw registerResult.error;
// 注册后自动登录(对齐 Dart 行为) // 注册后自动登录(对齐 Dart 行为)
const loginResult = await authRepository.emailLogin({ const loginResult = await authRepo.emailLogin({
email: input.email, email: input.email,
password: input.password, password: input.password,
guestId, guestId,
@@ -84,7 +87,7 @@ const oauthSignInActor = fromPromise<void, AuthProvider>(async ({ input }) => {
const syncGoogleBackendActor = fromPromise<LoginStatus, { idToken: string }>( const syncGoogleBackendActor = fromPromise<LoginStatus, { idToken: string }>(
async ({ input }) => { async ({ input }) => {
const guestId = await readGuestId(); const guestId = await readGuestId();
const result = await authRepository.googleLogin({ const result = await authRepo.googleLogin({
idToken: input.idToken, idToken: input.idToken,
guestId, guestId,
}); });
@@ -98,7 +101,7 @@ const syncFacebookBackendActor = fromPromise<
{ accessToken: string } { accessToken: string }
>(async ({ input }) => { >(async ({ input }) => {
const guestId = await readGuestId(); const guestId = await readGuestId();
const result = await authRepository.facebookLogin({ const result = await authRepo.facebookLogin({
accessToken: input.accessToken, accessToken: input.accessToken,
guestId, guestId,
}); });
+8 -4
View File
@@ -18,6 +18,7 @@ import { setup, fromPromise, assign } from "xstate";
import type { UiMessage } from "@/models/chat/ui-message"; import type { UiMessage } from "@/models/chat/ui-message";
import { chatRepository } from "@/data/repositories/chat_repository"; import { chatRepository } from "@/data/repositories/chat_repository";
import type { IChatRepository } from "@/data/repositories/interfaces";
import { ChatStorage } from "@/data/storage/chat/chat_storage"; import { ChatStorage } from "@/data/storage/chat/chat_storage";
import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { formatDate } from "@/utils/date"; import { formatDate } from "@/utils/date";
@@ -72,6 +73,9 @@ function mapTotalQuotaResult(
return fallback; return fallback;
} }
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
const chatRepo: IChatRepository = chatRepository;
interface InitResult { interface InitResult {
localMessages: UiMessage[]; localMessages: UiMessage[];
isGuest: boolean; isGuest: boolean;
@@ -86,7 +90,7 @@ async function readInitData(): Promise<InitResult> {
const [dailyResult, totalResult, localResult] = await Promise.all([ const [dailyResult, totalResult, localResult] = await Promise.all([
chatStorage.getGuestDailyChatQuota(), chatStorage.getGuestDailyChatQuota(),
chatStorage.getGuestTotalQuota(), chatStorage.getGuestTotalQuota(),
isGuest ? chatRepository.getLocalMessages() : Promise.resolve(null), isGuest ? chatRepo.getLocalMessages() : Promise.resolve(null),
]); ]);
return { return {
@@ -115,11 +119,11 @@ const sendMessageHttpActor = fromPromise<
{ messages: UiMessage[] }, { messages: UiMessage[] },
{ content: string } { content: string }
>(async ({ input }) => { >(async ({ input }) => {
const result = await chatRepository.sendMessage(input.content); const result = await chatRepo.sendMessage(input.content);
if (Result.isErr(result)) throw result.error; if (Result.isErr(result)) throw result.error;
// 拉取最新本地历史 // 拉取最新本地历史
const local = await chatRepository.getLocalMessages(); const local = await chatRepo.getLocalMessages();
if (Result.isOk(local) && local.data) { if (Result.isOk(local) && local.data) {
return { messages: localMessagesToUi(local.data) }; return { messages: localMessagesToUi(local.data) };
} }
@@ -130,7 +134,7 @@ const loadMoreHistoryActor = fromPromise<
{ messages: UiMessage[]; hasMore: boolean; newOffset: number }, { messages: UiMessage[]; hasMore: boolean; newOffset: number },
{ offset: number } { offset: number }
>(async ({ input }) => { >(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; if (Result.isErr(result)) throw result.error;
const page = localMessagesToUi(result.data.messages); const page = localMessagesToUi(result.data.messages);
+10 -2
View File
@@ -14,6 +14,10 @@ import { setup, fromPromise, assign } from "xstate";
import type { UserView } from "@/models/user/user"; import type { UserView } from "@/models/user/user";
import { userRepository } from "@/data/repositories/user_repository"; import { userRepository } from "@/data/repositories/user_repository";
import { authRepository } from "@/data/repositories/auth_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 { UserStorage } from "@/data/storage/user/user_storage";
import { Result } from "@/utils/result"; import { Result } from "@/utils/result";
@@ -81,10 +85,14 @@ async function readInitData(): Promise<InitData> {
// ============================================================ // ============================================================
// Actors // Actors
// ============================================================ // ============================================================
// 仓库以接口类型注入:调用面只看接口,运行时仍是同一单例
const userRepo: IUserRepository = userRepository;
const authRepo: IAuthRepository = authRepository;
const userInitActor = fromPromise<InitData>(async () => readInitData()); const userInitActor = fromPromise<InitData>(async () => readInitData());
const userFetchActor = fromPromise<UserView | null>(async () => { const userFetchActor = fromPromise<UserView | null>(async () => {
const result = await userRepository.getCurrentUser(); const result = await userRepo.getCurrentUser();
if (Result.isErr(result)) return null; if (Result.isErr(result)) return null;
const view = toView(result.data.toJson()); const view = toView(result.data.toJson());
// 持久化到本地 // 持久化到本地
@@ -93,7 +101,7 @@ const userFetchActor = fromPromise<UserView | null>(async () => {
}); });
const userLogoutActor = fromPromise<void>(async () => { const userLogoutActor = fromPromise<void>(async () => {
await authRepository.logout(); await authRepo.logout();
await userStorage.clearUserData(); await userStorage.clearUserData();
}); });