refactor(data): replace schema classes with readonly models

This commit is contained in:
2026-07-17 13:21:40 +08:00
parent 3437312167
commit ae97366a4a
103 changed files with 1220 additions and 2117 deletions
@@ -34,7 +34,7 @@ describe("FeedbackApi", () => {
images: [image],
});
expect(response.toJson()).toEqual({ feedbackId: "feedback-123" });
expect(response).toEqual({ feedbackId: "feedback-123" });
expect(httpClientMock).toHaveBeenCalledWith(
"/api/feedback",
expect.objectContaining({ method: "POST", body: expect.any(FormData) }),
@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
SendMessageRequest,
UnlockHistoryRequest,
UnlockPrivateRequest,
SendMessageRequestSchema,
UnlockHistoryRequestSchema,
UnlockPrivateRequestSchema,
} from "@/data/schemas/chat";
const httpClientMock = vi.hoisted(() => vi.fn());
@@ -34,7 +34,7 @@ describe("multi-character API contract", () => {
});
await new ChatApi().sendMessage(
SendMessageRequest.from({
SendMessageRequestSchema.parse({
characterId: CHARACTER_ID,
message: "Hello",
}),
@@ -76,13 +76,13 @@ describe("multi-character API contract", () => {
});
await api.unlockPrivateMessage(
UnlockPrivateRequest.from({
UnlockPrivateRequestSchema.parse({
characterId: CHARACTER_ID,
messageId: "message-1",
}),
);
await api.unlockHistory(
UnlockHistoryRequest.from({ characterId: CHARACTER_ID }),
UnlockHistoryRequestSchema.parse({ characterId: CHARACTER_ID }),
);
expect(httpClientMock).toHaveBeenNthCalledWith(
@@ -37,7 +37,7 @@ describe("PaymentApi", () => {
const response = await new PaymentApi().getTipPlans();
expect(httpClientMock).toHaveBeenCalledWith("/api/payment/tip-plans");
expect(response.toJson()).toEqual({
expect(response).toEqual({
plans: [
{
planId: "tip_coffee_usd_19_99",
+40 -38
View File
@@ -2,32 +2,37 @@
* Auth API
*
* 认证相关接口
*
*
*/
import { z } from "zod";
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
import {
FacebookIdentityRequest,
FacebookIdentityResponse,
FacebookIdentityResponseSchema,
FacebookLoginRequest,
FacebookPsidLoginRequest,
FacebookPsidLoginResponse,
FacebookPsidLoginResponseSchema,
FbIdLoginRequest,
GoogleLoginRequest,
GuestLoginRequest,
GuestLoginResponse,
GuestLoginResponseSchema,
LoginRequest,
LoginResponse,
LoginResponseSchema,
RefreshTokenRequest,
RefreshTokenResponse,
RefreshTokenResponseSchema,
RegisterRequest,
} from "@/data/schemas/auth";
import { User } from "@/data/schemas/user";
import { User, UserSchema } from "@/data/schemas/user";
import type { UserData } from "@/data/schemas/user/user";
import { Logger } from "@/utils/logger";
import { ApiPath } from "./api_path";
import { httpClient } from "./http_client";
import { ApiEnvelope, unwrap } from "./response_helper";
const log = new Logger("DataServicesApiAuthApi");
@@ -36,11 +41,11 @@ export class AuthApi {
* 邮箱密码登录
*/
async emailLogin(body: LoginRequest): Promise<LoginResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.emailLogin,
{ method: "POST", body: body.toJson() }
);
return LoginResponse.fromJson(unwrap(env) as Record<string, unknown>);
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.emailLogin, {
method: "POST",
body,
});
return LoginResponseSchema.parse(unwrap(env) as Record<string, unknown>);
}
/**
@@ -49,7 +54,7 @@ export class AuthApi {
async register(body: RegisterRequest): Promise<void> {
await httpClient<ApiEnvelope<unknown>>(ApiPath.register, {
method: "POST",
body: body.toJson(),
body,
});
}
@@ -59,20 +64,20 @@ export class AuthApi {
async googleLogin(body: GoogleLoginRequest): Promise<LoginResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.googleLogin, {
method: "POST",
body: body.toJson(),
body,
});
return LoginResponse.fromJson(unwrap(env) as Record<string, unknown>);
return LoginResponseSchema.parse(unwrap(env) as Record<string, unknown>);
}
/**
* Facebook 登录
*/
async facebookLogin(body: FacebookLoginRequest): Promise<LoginResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.facebookLogin,
{ method: "POST", body: body.toJson() }
);
return LoginResponse.fromJson(unwrap(env) as Record<string, unknown>);
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.facebookLogin, {
method: "POST",
body,
});
return LoginResponseSchema.parse(unwrap(env) as Record<string, unknown>);
}
/**
@@ -81,9 +86,9 @@ export class AuthApi {
async facebookIdLogin(body: FbIdLoginRequest): Promise<LoginResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.facebookIdLogin,
{ method: "POST", body: body.toJson() }
{ method: "POST", body },
);
return LoginResponse.fromJson(unwrap(env) as Record<string, unknown>);
return LoginResponseSchema.parse(unwrap(env) as Record<string, unknown>);
}
/**
@@ -94,9 +99,9 @@ export class AuthApi {
): Promise<FacebookPsidLoginResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.facebookPsidLogin,
{ method: "POST", body: body.toJson() },
{ method: "POST", body },
);
return FacebookPsidLoginResponse.fromJson(
return FacebookPsidLoginResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
@@ -109,9 +114,9 @@ export class AuthApi {
): Promise<FacebookIdentityResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.userFacebookIdentity,
{ method: "POST", body: body.toJson() },
{ method: "POST", body },
);
return FacebookIdentityResponse.fromJson(
return FacebookIdentityResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
@@ -120,7 +125,7 @@ export class AuthApi {
* 退出登录(fire-and-forget
*
* 后端 logout 端点的 envelope `data` 字段常为 `null`(登出端点没有业务数据),
* 不要再用 `unwrap` + `LogoutResponse.fromJson` 解析 —— Zod schema 是 z.object
* 不要再用 `unwrap` + `LogoutResponseSchema.parse` 解析 —— Zod schema 是 z.object
* 对 null 会抛 ZodError "Invalid input: expected object, received null"。
*
* 对齐 `register` 的写法:调通就完事,不解析响应体。
@@ -137,19 +142,20 @@ export class AuthApi {
async guestLogin(body: GuestLoginRequest): Promise<GuestLoginResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.guestLogin, {
method: "POST",
body: body.toJson(),
body,
});
log.debug("[AuthApi.guestLogin] raw envelope from backend", env);
const unwrapped = unwrap(env) as Record<string, unknown>;
const userObj = unwrapped.user as { lastMessageAt?: unknown } | undefined;
log.debug("[AuthApi.guestLogin] unwrapped data (pre-parse)", {
hasUser: "user" in unwrapped,
userKeys: userObj && typeof userObj === "object" ? Object.keys(userObj) : null,
userKeys:
userObj && typeof userObj === "object" ? Object.keys(userObj) : null,
lastMessageAt: userObj?.lastMessageAt,
lastMessageAtType: typeof userObj?.lastMessageAt,
});
try {
return GuestLoginResponse.fromJson(unwrapped);
return GuestLoginResponseSchema.parse(unwrapped);
} catch (e) {
if (e instanceof z.ZodError) {
log.error("[AuthApi.guestLogin] ZodError parsing response", {
@@ -167,15 +173,13 @@ export class AuthApi {
/**
* 刷新 Token
*/
async refreshToken(
body: RefreshTokenRequest
): Promise<RefreshTokenResponse> {
async refreshToken(body: RefreshTokenRequest): Promise<RefreshTokenResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.refresh, {
method: "POST",
body: body.toJson(),
body,
});
return RefreshTokenResponse.fromJson(
unwrap(env) as Record<string, unknown>
return RefreshTokenResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
@@ -183,10 +187,8 @@ export class AuthApi {
* 获取当前用户信息
*/
async getCurrentUser(): Promise<User> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.getCurrentUser
);
return User.fromJson(unwrap(env) as UserData);
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.getCurrentUser);
return UserSchema.parse(unwrap(env) as UserData);
}
}
+5 -2
View File
@@ -1,4 +1,7 @@
import { CharactersResponse } from "@/data/schemas/character";
import {
CharactersResponse,
CharactersResponseSchema,
} from "@/data/schemas/character";
import { ApiPath } from "./api_path";
import { httpClient } from "./http_client";
@@ -7,7 +10,7 @@ import { type ApiEnvelope, unwrap } from "./response_helper";
export class CharacterApi {
async getCharacters(): Promise<CharactersResponse> {
const envelope = await httpClient<ApiEnvelope<unknown>>(ApiPath.characters);
return CharactersResponse.fromJson(unwrap(envelope));
return CharactersResponseSchema.parse(unwrap(envelope));
}
}
+16 -12
View File
@@ -2,20 +2,24 @@
* Chat API
*
* 聊天相关接口
*
*
*/
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
import {
ChatHistoryResponse,
ChatHistoryResponseSchema,
ChatSendResponse,
ChatSendResponseSchema,
SendMessageRequest,
UnlockHistoryRequest,
UnlockHistoryResponse,
UnlockHistoryResponseSchema,
UnlockPrivateRequest,
UnlockPrivateResponse,
UnlockPrivateResponseSchema,
} from "@/data/schemas/chat";
import { ApiPath } from "./api_path";
import { httpClient } from "./http_client";
import { ApiEnvelope, unwrap } from "./response_helper";
export class ChatApi {
/**
@@ -24,9 +28,9 @@ export class ChatApi {
async sendMessage(body: SendMessageRequest): Promise<ChatSendResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.chatSend, {
method: "POST",
body: body.toJson(),
body,
});
return ChatSendResponse.fromJson(unwrap(env) as Record<string, unknown>);
return ChatSendResponseSchema.parse(unwrap(env) as Record<string, unknown>);
}
/**
@@ -40,8 +44,8 @@ export class ChatApi {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.chatHistory, {
query: { characterId, limit, offset },
});
return ChatHistoryResponse.fromJson(
unwrap(env) as Record<string, unknown>
return ChatHistoryResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
@@ -55,10 +59,10 @@ export class ChatApi {
ApiPath.chatUnlockPrivate,
{
method: "POST",
body: body.toJson(),
body,
},
);
return UnlockPrivateResponse.fromJson(
return UnlockPrivateResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
@@ -73,10 +77,10 @@ export class ChatApi {
ApiPath.chatUnlockHistory,
{
method: "POST",
body: body.toJson(),
body,
},
);
return UnlockHistoryResponse.fromJson(
return UnlockHistoryResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
+2 -1
View File
@@ -1,5 +1,6 @@
import {
FeedbackSubmitResponse,
FeedbackSubmitResponseSchema,
type SubmitFeedbackInput,
} from "@/data/schemas/feedback";
@@ -19,7 +20,7 @@ export class FeedbackApi {
method: "POST",
body,
});
return FeedbackSubmitResponse.fromJson(unwrap(envelope));
return FeedbackSubmitResponseSchema.parse(unwrap(envelope));
}
}
+9 -9
View File
@@ -2,12 +2,12 @@
* Metrics API
*
* 数据看板/上报相关接口
*
*
*/
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrapOptional } from "./response_helper";
import { AppEvent, PwaEvent } from "@/data/schemas/metrics";
import { ApiPath } from "./api_path";
import { httpClient } from "./http_client";
import { ApiEnvelope, unwrapOptional } from "./response_helper";
export class MetricsApi {
/**
@@ -16,7 +16,7 @@ export class MetricsApi {
async reportPwaEvent(body: PwaEvent): Promise<void> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.metricsPwaEvent,
{ method: "POST", body: body.toJson() }
{ method: "POST", body },
);
unwrapOptional(env);
}
@@ -25,10 +25,10 @@ export class MetricsApi {
* 上报用户信息
*/
async reportUserInfo(body: AppEvent): Promise<void> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.reportUserInfo,
{ method: "POST", body: body.toJson() }
);
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.reportUserInfo, {
method: "POST",
body,
});
unwrapOptional(env);
}
}
+11 -12
View File
@@ -6,9 +6,13 @@
import {
CreatePaymentOrderRequest,
CreatePaymentOrderResponse,
CreatePaymentOrderResponseSchema,
PaymentOrderStatusResponse,
PaymentOrderStatusResponseSchema,
PaymentPlansResponse,
PaymentPlansResponseSchema,
TipPaymentPlansResponse,
TipPaymentPlansResponseSchema,
} from "@/data/schemas/payment";
import { ApiPath } from "./api_path";
@@ -21,17 +25,15 @@ export class PaymentApi {
*/
async getPlans(): Promise<PaymentPlansResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.paymentPlans);
return PaymentPlansResponse.fromJson(
return PaymentPlansResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
/** 获取咖啡打赏套餐列表。 */
async getTipPlans(): Promise<TipPaymentPlansResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.paymentTipPlans,
);
return TipPaymentPlansResponse.fromJson(
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.paymentTipPlans);
return TipPaymentPlansResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
@@ -46,10 +48,10 @@ export class PaymentApi {
ApiPath.paymentCreateOrder,
{
method: "POST",
body: body.toJson(),
body,
},
);
return CreatePaymentOrderResponse.fromJson(
return CreatePaymentOrderResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
@@ -57,20 +59,17 @@ export class PaymentApi {
/**
* 查询支付订单状态
*/
async getOrderStatus(
orderId: string,
): Promise<PaymentOrderStatusResponse> {
async getOrderStatus(orderId: string): Promise<PaymentOrderStatusResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.paymentOrderStatus,
{
query: { order_id: orderId },
},
);
return PaymentOrderStatusResponse.fromJson(
return PaymentOrderStatusResponseSchema.parse(
unwrap(env) as Record<string, unknown>,
);
}
}
/**
+6 -4
View File
@@ -1,9 +1,11 @@
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
import {
PrivateAlbumsResponse,
PrivateAlbumsResponseSchema,
PrivateAlbumUnlockResponse,
PrivateAlbumUnlockResponseSchema,
UnlockPrivateAlbumRequest,
} from "@/data/schemas/private-room";
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
import { ApiPath } from "./api_path";
import { httpClient } from "./http_client";
@@ -27,7 +29,7 @@ export class PrivateRoomApi {
},
},
);
return PrivateAlbumsResponse.fromJson(unwrap(env));
return PrivateAlbumsResponseSchema.parse(unwrap(env));
}
async unlockAlbum(
@@ -38,10 +40,10 @@ export class PrivateRoomApi {
ApiPath.privateRoomAlbumUnlock(albumId),
{
method: "POST",
body: body.toJson(),
body,
},
);
return PrivateAlbumUnlockResponse.fromJson(unwrap(env));
return PrivateAlbumUnlockResponseSchema.parse(unwrap(env));
}
}
+11 -7
View File
@@ -2,16 +2,18 @@
* User API
*
* 用户相关接口
*
*
*/
import { httpClient } from "./http_client";
import { ApiPath } from "./api_path";
import { ApiEnvelope, unwrap } from "./response_helper";
import {
User,
UserEntitlements,
UserEntitlementsSchema,
UserSchema,
} from "@/data/schemas/user";
import type { UserData } from "@/data/schemas/user/user";
import { ApiPath } from "./api_path";
import { httpClient } from "./http_client";
import { ApiEnvelope, unwrap } from "./response_helper";
export class UserApi {
/**
@@ -19,15 +21,17 @@ export class UserApi {
*/
async getCurrentUser(): Promise<User> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.userProfile);
return User.fromJson(unwrap(env) as UserData);
return UserSchema.parse(unwrap(env) as UserData);
}
/**
* 获取当前用户权益快照
*/
async getEntitlements(): Promise<UserEntitlements> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.userEntitlements);
return UserEntitlements.fromJson(unwrap(env) as Record<string, unknown>);
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.userEntitlements,
);
return UserEntitlementsSchema.parse(unwrap(env) as Record<string, unknown>);
}
}