feat: 新增api接口

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-24 15:36:44 +08:00
parent 278f1aab85
commit 5ab74ad9dd
13 changed files with 537 additions and 59 deletions
+55
View File
@@ -0,0 +1,55 @@
// server/api/auth/login.post.ts
import type { UserLoginRequest, UserLoginResult } from "#shared/types";
import { createUpstreamErrorResponse } from "~~/server/utils";
/**
* 允许从请求体透传给上游的字段列表
* 只有在这里声明过的字段才会被转发,其余字段一律丢弃,防止参数污染
* 与 NewAPI 文档字段保持一致
*/
const LOGIN_FIELDS = ["username", "password"] as const satisfies ReadonlyArray<
keyof UserLoginRequest
>;
/**
* POST /api/auth/login
*
* 登录流程:
* 1. 解析并校验前端传来的 JSON 请求体
* 2. 从请求体里只挑出文档允许的字段组装 payload
* 3. 通过 newApiFetch 转发到上游 NewAPI 登录接口
* 4. 上游响应体为空,成功时返回 { code: 0, data: null, msg: "登录成功" }
*/
export default defineEventHandler(async (event) => {
// 读取并反序列化请求体;如果前端没有传 body,readBody 会返回 null
const requestBody = await readBody<Partial<UserLoginRequest> | null>(event);
// 防御性校验:请求体只能是 JSON 对象或空,不接受字符串/数字等原始值
if (requestBody !== null && typeof requestBody !== "object") {
return createErrorResponse(400, "请求体必须是 JSON 对象");
}
// 白名单过滤:只保留文档里声明的字段,且必须是字符串类型
const payload: UserLoginRequest = {};
for (const field of LOGIN_FIELDS) {
const value = requestBody?.[field];
if (typeof value === "string") {
payload[field] = value;
}
}
try {
// 调用上游 NewAPI 登录接口,baseURL 已在 newApiFetch 内部写死,无需再传
await newApiFetch<UserLoginResult>("/api/user/login", {
method: "POST",
body: payload,
headers: {
"Content-Type": "application/json"
}
});
return createSuccessResponse(null, "登录成功");
} catch (error) {
return createUpstreamErrorResponse(error, "登录失败");
}
});
+21
View File
@@ -0,0 +1,21 @@
// server/api/auth/logout.get.ts
import type { UserLogoutResult } from "#shared/types";
/**
* GET /api/auth/logout
*
* 登出流程:
* 1. 服务端直接转发 GET 请求到上游 NewAPI
* 2. 上游成功后统一返回 { code: 0, data: null, msg: "登出成功" }
*/
export default defineEventHandler(async () => {
try {
await newApiFetch<UserLogoutResult>("/api/user/logout", {
method: "GET"
});
return createSuccessResponse(null, "登出成功");
} catch (error) {
return createUpstreamErrorResponse(error, "登出失败");
}
});
+66
View File
@@ -0,0 +1,66 @@
// server/api/auth/register.post.ts
import type { UserRegisterRequest, UserRegisterResult } from "#shared/types";
import {
createUpstreamErrorResponse,
createErrorResponse,
createSuccessResponse,
newApiFetch
} from "../../utils";
/**
* 允许从请求体透传给上游的字段列表
* 只有在这里声明过的字段才会被转发,其余字段一律丢弃,防止参数污染
* 与 NewAPI 文档字段保持一致
*/
const REGISTER_FIELDS = [
"username",
"password",
"email",
"verification_code",
"aff_code"
] as const satisfies ReadonlyArray<keyof UserRegisterRequest>;
/**
* POST /api/auth/register
*
* 注册流程:
* 1. 解析并校验前端传来的 JSON 请求体
* 2. 从请求体里只挑出文档允许的字段组装 payload
* 3. 通过 newApiFetch 转发到上游 NewAPI 注册接口
* 4. 将上游结果统一包装为 ApiResponse 格式返回给前端
*/
export default defineEventHandler(async (event) => {
// 读取并反序列化请求体;如果前端没有传 body,readBody 会返回 null
const requestBody = await readBody<Partial<UserRegisterRequest> | null>(
event
);
// 防御性校验:请求体只能是 JSON 对象或空,不接受字符串/数字等原始值
if (requestBody !== null && typeof requestBody !== "object") {
return createErrorResponse(400, "请求体必须是 JSON 对象");
}
// 白名单过滤:只保留文档里声明的字段,且必须是字符串类型
const payload: UserRegisterRequest = {};
for (const field of REGISTER_FIELDS) {
const value = requestBody?.[field];
if (typeof value === "string") {
payload[field] = value;
}
}
try {
const result = await newApiFetch<UserRegisterResult>("/api/user/register", {
method: "POST",
body: payload,
headers: {
"Content-Type": "application/json"
}
});
// 上游成功
return createSuccessResponse(result, "注册成功");
} catch (error) {
return createUpstreamErrorResponse(error, "注册失败");
}
});
+73
View File
@@ -0,0 +1,73 @@
import type { ApiResponse } from "#shared/types";
/**
* 构造统一成功响应
*
* @param data 要返回给前端的数据,没有数据时传 null(默认)
* @param msg 提示信息,默认 "请求成功"
* @returns 符合 ApiResponse 格式的对象,code 固定为 0
*/
export const createSuccessResponse = <T>(
data: T | null = null,
msg: string = "请求成功"
): ApiResponse<T> => {
return {
code: 0,
data,
msg
};
};
/**
* 构造统一错误响应
*
* @param code 错误码,默认 400(请求参数错误);上游错误时传上游状态码
* @param msg 错误描述,展示给前端的提示文字
* @param data 附带的错误详情,通常是上游返回的原始响应体,默认 null
* @returns 符合 ApiResponse 格式的对象,data 固定为 null 或错误详情
*/
export const createErrorResponse = (
code: number = 400,
msg: string = "请求失败",
data: any = null
): ApiResponse => {
return {
code,
data,
msg
};
};
/**
* 将上游请求错误转换为统一响应
*
* @param error ofetch 抛出的错误对象
* @param fallbackMessage 当上游没有提供可用错误信息时的兜底提示
* @returns 符合 ApiResponse 格式的错误响应
*/
export const createUpstreamErrorResponse = (
error: unknown,
fallbackMessage: string = "请求失败"
): ApiResponse => {
const fetchError = error as {
data?: unknown;
message?: string;
response?: {
_data?: unknown;
status?: number;
};
statusCode?: number;
};
const errorData = fetchError.data ?? fetchError.response?._data ?? null;
const errorMessage =
typeof errorData === "string"
? errorData
: fetchError.message || fallbackMessage;
return createErrorResponse(
fetchError.statusCode ?? fetchError.response?.status ?? 500,
errorMessage,
errorData
);
};
+25
View File
@@ -0,0 +1,25 @@
/** 上游 NewAPI 服务的根地址 */
const BASE_URL = "https://api.qflink.xyz";
/**
* 向上游 NewAPI 发起请求
*
* @param path 相对路径,如 "/api/user/register",会自动拼接到 BASE_URL 后面
* @param options 透传给 $fetch 的所有选项(method、body、headers 等)
* @returns Promise<T>T 是上游返回的数据类型
*
* @example
* const result = await newApiFetch<string>("/api/user/register", {
* method: "POST",
* body: { username: "xxx" }
* })
*/
export const newApiFetch = <T>(
path: string,
options?: Parameters<typeof $fetch>[1]
): Promise<T> => {
return $fetch<T>(path, {
baseURL: BASE_URL,
...options
}) as Promise<T>;
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./createApiResponse";
export * from "./fetch";