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
+2 -1
View File
@@ -10,7 +10,8 @@
"./src/data/storage/auth",
"./src/data/storage/user",
"./src/data/storage/chat",
"./src/data/storage/app",
"./src/data/storage/app",
"./src/data/repositories",
"./src/design"
]
}
+1 -17
View File
@@ -6,6 +6,7 @@
*/
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
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";
@@ -22,23 +23,6 @@ import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
import { User } from "@/data/dto/user/user";
import type { UserData } from "@/data/schemas/user/user";
/**
* 后端统一响应包装
*/
interface ApiEnvelope<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
function unwrap<T>(envelope: ApiEnvelope<T>): T {
if (!envelope.success || envelope.data === undefined) {
throw new Error(envelope.message ?? envelope.error ?? "API call failed");
}
return envelope.data;
}
export class AuthApi {
/**
* 发送验证码
+1 -17
View File
@@ -6,6 +6,7 @@
*/
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
import { ChatSyncData } from "@/data/dto/chat/chat_sync_data";
@@ -15,23 +16,6 @@ import { SendMessageRequest } from "@/data/dto/chat/send_message_request";
import { SttData } from "@/data/dto/chat/stt_data";
import { SyncMessage } from "@/data/dto/chat/sync_message";
/**
* 后端统一响应包装
*/
interface ApiEnvelope<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
function unwrap<T>(envelope: ApiEnvelope<T>): T {
if (!envelope.success || envelope.data === undefined) {
throw new Error(envelope.message ?? envelope.error ?? "API call failed");
}
return envelope.data;
}
export class ChatApi {
/**
* 发送消息
+1
View File
@@ -8,6 +8,7 @@ export * from "./auth_api";
export * from "./chat_api";
export * from "./http_client";
export * from "./metrics_api";
export * from "./response_helper";
export * from "./user_api";
export * from "./config/api_config";
export * from "./interceptor/auth_refresh_interceptor";
+3 -19
View File
@@ -6,26 +6,10 @@
*/
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrapOptional } from "./response_helper";
import { AppEvent } from "@/data/dto/metrics/app_event";
import { PwaEvent } from "@/data/dto/metrics/pwa_event";
/**
* 后端统一响应包装
*/
interface ApiEnvelope<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
function unwrap<T>(envelope: ApiEnvelope<T>): T | undefined {
if (!envelope.success) {
throw new Error(envelope.message ?? envelope.error ?? "API call failed");
}
return envelope.data;
}
export class MetricsApi {
/**
* 上报 PWA 事件
@@ -35,7 +19,7 @@ export class MetricsApi {
ApiPath.metricsPwaEvent,
{ method: "POST", body: body.toJson() }
);
unwrap(env);
unwrapOptional(env);
}
/**
@@ -46,7 +30,7 @@ export class MetricsApi {
ApiPath.reportUserInfo,
{ method: "POST", body: body.toJson() }
);
unwrap(env);
unwrapOptional(env);
}
}
+49
View File
@@ -0,0 +1,49 @@
/**
* 共享的后端 envelope 解析工具
*
* 4 个 `*_api.ts` 文件中重复的 `interface ApiEnvelope<T>` 与 `function unwrap<T>`
* 集中到这里。HTTP 拦截器(`http_client.ts`)已经对非 2xx 响应抛 `ApiError`
* 本工具处理第二种失败:2xx 但 `success: false` 的 envelope。
*
* 行为升级:失败时抛 `ApiError``ApiError extends Error`),而不是裸 `Error`。
* 这样调用方在 `Result.err` 内能 `instanceof ApiError` 检查 `.code` / `.status`。
*
* 原始 Dart: lib/data/services/api/api_client.dart 的 `NetResult<T>` 解析。
*/
import { ApiError, ErrorCode } from "./api_result";
/** 后端统一响应包装。 */
export interface ApiEnvelope<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
/**
* 解包 envelope,失败时抛 `ApiError`。
* 用于:data 字段必有(API 一定返回业务数据)的端点。
*/
export function unwrap<T>(envelope: ApiEnvelope<T>): T {
if (!envelope.success || envelope.data === undefined) {
throw new ApiError(
ErrorCode.unknownError,
envelope.message ?? envelope.error ?? "API call failed",
);
}
return envelope.data;
}
/**
* 解包 envelope,但允许 `data` 字段为 `undefined`(用于 fire-and-forget 类端点)。
* 失败时仍抛 `ApiError`。
*/
export function unwrapOptional<T>(envelope: ApiEnvelope<T>): T | undefined {
if (!envelope.success) {
throw new ApiError(
ErrorCode.unknownError,
envelope.message ?? envelope.error ?? "API call failed",
);
}
return envelope.data;
}
+1 -17
View File
@@ -6,6 +6,7 @@
*/
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
import { CreditsData } from "@/data/dto/user/credits_data";
import { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
@@ -13,23 +14,6 @@ import { User } from "@/data/dto/user/user";
import { UserStatsResponse } from "@/data/dto/user/user_stats_response";
import type { UserData } from "@/data/schemas/user/user";
/**
* 后端统一响应包装
*/
interface ApiEnvelope<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
function unwrap<T>(envelope: ApiEnvelope<T>): T {
if (!envelope.success || envelope.data === undefined) {
throw new Error(envelope.message ?? envelope.error ?? "API call failed");
}
return envelope.data;
}
export class UserApi {
/**
* 获取用户统计信息
+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(),
);
+150
View File
@@ -0,0 +1,150 @@
"use client";
/**
* ChatRepository
*
* 聊天仓库:编排远程 ChatApi + 本地 LocalChatStorageDexie/IndexedDB)。
* 行为对齐原 Dart `lib/data/repositories/chat_repository_impl.dart`
* - `saveMessagesToLocal` 保留「先清后写」语义(Dart `box.clear()` + bulk add
* - 替代 Dart 同步 `int get localMessageCount`,改为异步 `getLocalMessageCount()`
* Dexie 异步 API 决定)
*
* ChatMessage ↔ LocalMessage 的字段映射在仓库内 inline 完成(用户决策):
* - 写:ChatMessage 的 id/role/content/createdAt → LocalMessagesessionId=""
* - 读:LocalMessage 的 id/role/content/createdAt → ChatMessage(丢弃 sessionId
*
* 原始 Dart: lib/data/repositories/chat_repository_impl.dart
*/
import { ChatApi, chatApi } from "@/data/api";
import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
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 "@/data/result";
import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage";
import { LocalMessage } from "@/data/storage/chat/local_message";
export class ChatRepository {
constructor(
private readonly api: ChatApi,
private readonly localStorage: LocalChatStorage,
) {}
// ============ 远程操作 ============
/** 发送一条消息。 */
async sendMessage(
message: string,
options?: { image?: string; useWebSocket?: boolean },
): Promise<Result<ChatSendResponse>> {
return Result.wrap(async () => {
const request = SendMessageRequest.from({
message,
image: options?.image ?? "",
useWebSocket: options?.useWebSocket ?? false,
});
return await this.api.sendMessage(request);
});
}
/** 获取聊天历史,分页参数默认 limit=50, offset=0。 */
async getHistory(
limit = 50,
offset = 0,
): Promise<Result<ChatHistoryResponse>> {
return Result.wrap(() => this.api.getHistory(limit, offset));
}
// ============ 本地(Dexie)操作 ============
/** 把一条消息写入本地存储。 */
async saveMessageToLocal(message: ChatMessage): Promise<Result<void>> {
return this._mapLocalStorageResult(
this.localStorage.saveMessage(this._chatToLocal(message)),
);
}
/**
* 批量覆盖写入:先清空本地存储,再写入新列表。
* 保留 Dart 端的「先清后写」语义(用于同步场景的整批刷新)。
*/
async saveMessagesToLocal(
messages: readonly ChatMessage[],
): Promise<Result<void>> {
const cleared = await this.localStorage.clearAll();
if (cleared.kind === "failure") {
return Result.err(cleared.error);
}
return this._mapLocalStorageResult(
this.localStorage.saveMessages(messages.map((m) => this._chatToLocal(m))),
);
}
/** 读取所有本地消息,按 dbId 升序。 */
async getLocalMessages(): Promise<Result<ChatMessage[]>> {
const result = await this.localStorage.getAllMessages();
if (result.kind === "failure") {
return Result.err(result.error);
}
return Result.ok(result.value.map((lm) => this._localToChat(lm)));
}
/** 清空本地消息。 */
async clearLocalMessages(): Promise<Result<void>> {
return this._mapLocalStorageResult(this.localStorage.clearAll());
}
/**
* 获取本地消息数量。
* 替代 Dart 的同步 `int get localMessageCount`——Dexie 异步 API 决定必须 async。
*/
async getLocalMessageCount(): Promise<Result<number>> {
return this._mapLocalStorageResult(this.localStorage.getMessageCount());
}
// ============ 私有助手 ============
/** ChatMessage → LocalMessage:丢弃 wire 字段,注入 storage 层的 sessionId=""。 */
private _chatToLocal(message: ChatMessage): LocalMessage {
return LocalMessage.from({
id: message.id,
role: message.role,
content: message.content,
createdAt: message.createdAt,
sessionId: "",
});
}
/** LocalMessage → ChatMessage:丢弃 storage 层的 sessionId。 */
private _localToChat(local: LocalMessage): ChatMessage {
return ChatMessage.from({
id: local.id,
role: local.role,
content: local.content,
createdAt: local.createdAt,
});
}
/**
* 把 storage 层 `{kind, value/error}` 形态的 `Result<T>` 桥接到仓库层
* `{success, data/error}` 形态。错误统一用 `Error` 包装。
*/
private async _mapLocalStorageResult<T>(
promise: Promise<
| { readonly kind: "success"; readonly value: T }
| { readonly kind: "failure"; readonly error: unknown }
>,
): Promise<Result<T>> {
const r = await promise;
if (r.kind === "success") {
return Result.ok(r.value);
}
return Result.err(r.error);
}
}
/** 全局单例。 */
export const chatRepository = new ChatRepository(
chatApi,
LocalChatStorage.getInstance(),
);
+8
View File
@@ -0,0 +1,8 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./auth_repository";
export * from "./chat_repository";
export * from "./metrics_repository";
export * from "./user_repository";
@@ -0,0 +1,62 @@
"use client";
/**
* MetricsRepository
*
* 指标/埋点仓库:纯远程 fire-and-forget 上报,无本地状态。
*
* 原始 Dart: lib/data/repositories/metrics_repository_impl.dart
*/
import { MetricsApi, metricsApi } from "@/data/api";
import { AppEvent } from "@/data/dto/metrics/app_event";
import { PwaEvent } from "@/data/dto/metrics/pwa_event";
import { Result } from "@/data/result";
export class MetricsRepository {
constructor(private readonly api: MetricsApi) {}
/**
* 上报 PWA 事件。
* 自动注入当前秒级时间戳。
*/
async reportPwaEvent(input: {
deviceId: string;
deviceType: string;
pwaInstalled: boolean;
pwaSupported: boolean;
}): Promise<Result<void>> {
return Result.wrap(async () => {
await this.api.reportPwaEvent(
PwaEvent.from({
deviceId: input.deviceId,
deviceType: input.deviceType,
timestamp: Math.floor(Date.now() / 1000),
pwaInstalled: input.pwaInstalled,
pwaSupported: input.pwaSupported,
}),
);
});
}
/**
* 上报用户环境信息(浏览器、UA 等)。
*/
async reportUserInfo(input: {
userId: string;
browser: string;
userAgent: string;
}): Promise<Result<void>> {
return Result.wrap(async () => {
await this.api.reportUserInfo(
AppEvent.from({
userId: input.userId,
browser: input.browser,
userAgent: input.userAgent,
}),
);
});
}
}
/** 全局单例。 */
export const metricsRepository = new MetricsRepository(metricsApi);
+52
View File
@@ -0,0 +1,52 @@
"use client";
/**
* UserRepository
*
* 用户信息仓库:纯远程调用,不直接写入本地 storage
* (用户数据持久化由 AuthRepository 在登录流程中统一处理)。
*
* 原始 Dart: lib/data/repositories/user_repository_impl.dart
*/
import { UserApi, userApi } from "@/data/api";
import { CreditsData } from "@/data/dto/user/credits_data";
import { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
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 "@/data/result";
export class UserRepository {
constructor(private readonly api: UserApi) {}
/** 获取用户统计信息。 */
async getUserStats(): Promise<Result<UserStatsResponse>> {
return Result.wrap(() => this.api.getUserStats());
}
/** 获取当前登录用户信息。 */
async getCurrentUser(): Promise<Result<User>> {
return Result.wrap(() => this.api.getCurrentUser());
}
/** 更新个人资料。 */
async updateProfile(request: UpdateProfileRequest): Promise<Result<User>> {
return Result.wrap(() => this.api.updateProfile(request));
}
/** 查询积分余额。 */
async getCredits(): Promise<Result<CreditsData>> {
return Result.wrap(() => this.api.getCredits());
}
/** 查询积分操作历史,分页参数默认 limit=50, offset=0。 */
async getCreditsHistory(
limit = 50,
offset = 0,
): Promise<Result<CreditsHistoryData>> {
return Result.wrap(() => this.api.getCreditsHistory(limit, offset));
}
}
/** 全局单例。 */
export const userRepository = new UserRepository(userApi);
+76
View File
@@ -0,0 +1,76 @@
/**
* 仓库层统一 Result<T>
*
* 与 `src/data/storage/result.ts` 的 `Result<T>``{kind, value}` 形态)并存:
* - 本类型供仓库层(`src/data/repositories/*`)使用,`{success, data}` 形态更贴近
* 原 Dart `Result.success` / `Result.failure` 命名与 API 端 envelope 风格。
* - 存储层继续使用 `{kind, value}`,因为它的消费者(localStorage、Dexie)已经在
* 大量代码中 pattern-match `r.kind`。
* - 两个类型路径不同(`@/data/result` vs `@/data/storage/result`),不冲突;
* 如需在同模块中混用,可用 `import { Result as StorageResult } from
* "@/data/storage/result"` 别名。
*
* `error` 固定为 `Error`(非 `unknown`):HTTP 拦截器与共享 unwrap 都抛 `ApiError`
* `ApiError extends Error`),仓库 `Result.wrap` 捕获后用 `toError` 规范化,
* 调用方能可靠地 `instanceof ApiError` / `error.message` 访问错误信息。
*/
export type Result<T> =
| { readonly success: true; readonly data: T }
| { readonly success: false; readonly error: Error };
export const Result = {
/** 构造成功结果。 */
ok<T>(data: T): Result<T> {
return { success: true, data };
},
/**
* 构造失败结果。`error` 接受任意 thrown 值,内部用 `toError` 归一化为 `Error`。
*/
err<T = never>(error: unknown): Result<T> {
return { success: false, error: toError(error) };
},
/** 类型守卫:true 分支。 */
isOk<T>(r: Result<T>): r is { readonly success: true; readonly data: T } {
return r.success;
},
/** 类型守卫:false 分支。 */
isErr<T>(r: Result<T>): r is { readonly success: false; readonly error: Error } {
return !r.success;
},
/** 不可变的链式 map:失败时原样传递。 */
map<T, U>(r: Result<T>, fn: (v: T) => U): Result<U> {
return r.success ? Result.ok(fn(r.data)) : r;
},
/**
* 捕获异步 thunk 的异常并包装成 `Result<T>`。仓库内主要用这个把
* `*Api` 调用从「throw 风格」桥接到「Result 风格」。
*/
async wrap<T>(thunk: () => Promise<T>): Promise<Result<T>> {
try {
return Result.ok(await thunk());
} catch (e) {
return Result.err(e);
}
},
} as const;
/**
* 把任意 thrown 值规范化为 `Error`
* - 已是 `Error` 子类 → 原样返回(保留 `.code` / `.status` 等自定义字段)
* - string → `new Error(str)`
* - 其他 → `new Error(JSON.stringify(value))`,失败则 `new Error(String(value))`
*/
export function toError(value: unknown): Error {
if (value instanceof Error) return value;
if (typeof value === "string") return new Error(value);
try {
return new Error(JSON.stringify(value));
} catch {
return new Error(String(value));
}
}