feat: 接口统一优化
This commit is contained in:
Vendored
+15
-10
@@ -1,6 +1,9 @@
|
||||
// server/api/cache/clear.post.ts - 清空当前用户 DB 缓存接口
|
||||
import { setResponseStatus } from "h3";
|
||||
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
import { createApiLogger } from "~~/server/utils/logging";
|
||||
import { apiErr, apiOk } from "~~/server/utils/response";
|
||||
|
||||
/**
|
||||
* 清缓存:将当前用户的 cacheClearedAt 更新为当前时间
|
||||
@@ -12,15 +15,17 @@ export default defineEventHandler(async (event) => {
|
||||
const logger = createApiLogger(event, "api.cache.clear");
|
||||
const userId = event.context.auth!.user.id;
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { cacheClearedAt: new Date() }
|
||||
});
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { cacheClearedAt: new Date() }
|
||||
});
|
||||
|
||||
logger.info("finish_success");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "缓存已清除"
|
||||
};
|
||||
logger.info("finish_success");
|
||||
return apiOk(null);
|
||||
} catch (err) {
|
||||
logger.error("db_error", { userId, err: String(err) });
|
||||
setResponseStatus(event, 500);
|
||||
return apiErr(500, "服务器内部错误");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// server/api/health.get.ts - 服务健康检查接口
|
||||
import { serverEnv } from "~~/server/utils/env";
|
||||
import { apiOk } from "~~/server/utils/response";
|
||||
import { SERVICE_VERSION } from "~~/server/utils/runtimeState";
|
||||
|
||||
/**
|
||||
@@ -9,10 +10,10 @@ import { SERVICE_VERSION } from "~~/server/utils/runtimeState";
|
||||
* 返回模型名,但不返回 API Key、baseURL 或其他敏感配置
|
||||
*/
|
||||
export default defineEventHandler(() => {
|
||||
return {
|
||||
status: "ok",
|
||||
return apiOk({
|
||||
status: "ok" as const,
|
||||
message: "AI题库服务运行正常",
|
||||
version: SERVICE_VERSION,
|
||||
model: serverEnv.openAiModel
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
+19
-18
@@ -1,7 +1,8 @@
|
||||
// server/api/records.get.ts - 用户问答记录接口,供 Nuxt Dashboard 表格展示
|
||||
import { getQuery } from "h3";
|
||||
import { getQuery, setResponseStatus } from "h3";
|
||||
|
||||
import { createApiLogger } from "~~/server/utils/logging";
|
||||
import { apiErr, apiOk } from "~~/server/utils/response";
|
||||
import { getQaRecords } from "~~/server/utils/runtimeState";
|
||||
|
||||
/**
|
||||
@@ -23,23 +24,23 @@ export default defineEventHandler(async (event) => {
|
||||
const size = ALLOWED_SIZES.has(rawSize) ? rawSize : 10;
|
||||
const page = Number.isFinite(rawPage) ? Math.max(rawPage, 1) : 1;
|
||||
|
||||
const { records, total } = await getQaRecords(event.context.auth!.user.id, {
|
||||
page,
|
||||
size
|
||||
});
|
||||
try {
|
||||
const { records, total } = await getQaRecords(event.context.auth!.user.id, {
|
||||
page,
|
||||
size
|
||||
});
|
||||
|
||||
logger.info("finish_success", {
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
count: records.length
|
||||
});
|
||||
logger.info("finish_success", {
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
count: records.length
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
records,
|
||||
page,
|
||||
size,
|
||||
total
|
||||
};
|
||||
return apiOk({ records, page, size, total });
|
||||
} catch (err) {
|
||||
logger.error("db_error", { err: String(err) });
|
||||
setResponseStatus(event, 500);
|
||||
return apiErr(500, "服务器内部错误");
|
||||
}
|
||||
});
|
||||
|
||||
+15
-6
@@ -1,6 +1,9 @@
|
||||
// server/api/stats.get.ts - 服务运行统计接口
|
||||
import { setResponseStatus } from "h3";
|
||||
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
import { createApiLogger } from "~~/server/utils/logging";
|
||||
import { apiErr, apiOk } from "~~/server/utils/response";
|
||||
import { getRuntimeStats } from "~~/server/utils/runtimeState";
|
||||
|
||||
/**
|
||||
@@ -13,11 +16,17 @@ export default defineEventHandler(async (event) => {
|
||||
const logger = createApiLogger(event, "api.stats");
|
||||
const userId = event.context.auth!.user.id;
|
||||
|
||||
const [runtimeStats, qa_records_count] = await Promise.all([
|
||||
Promise.resolve(getRuntimeStats()),
|
||||
prisma.qaRecord.count({ where: { userId } })
|
||||
]);
|
||||
try {
|
||||
const [runtimeStats, qa_records_count] = await Promise.all([
|
||||
Promise.resolve(getRuntimeStats()),
|
||||
prisma.qaRecord.count({ where: { userId } })
|
||||
]);
|
||||
|
||||
logger.info("finish_success");
|
||||
return { ...runtimeStats, qa_records_count };
|
||||
logger.info("finish_success");
|
||||
return apiOk({ ...runtimeStats, qa_records_count });
|
||||
} catch (err) {
|
||||
logger.error("db_error", { userId, err: String(err) });
|
||||
setResponseStatus(event, 500);
|
||||
return apiErr(500, "服务器内部错误");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { setResponseStatus } from "h3";
|
||||
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
import { createApiLogger } from "~~/server/utils/logging";
|
||||
import { apiErr, apiOk } from "~~/server/utils/response";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const logger = createApiLogger(event, "api.user.refresh-token");
|
||||
@@ -21,9 +22,9 @@ export default defineEventHandler(async (event) => {
|
||||
} catch (err) {
|
||||
logger.error("db_error", { userId, err: String(err) });
|
||||
setResponseStatus(event, 500);
|
||||
return { success: false, msg: "服务器内部错误" };
|
||||
return apiErr(500, "服务器内部错误");
|
||||
}
|
||||
|
||||
logger.info("finish_success", { userId });
|
||||
return { success: true, msg: "刷新成功", apiToken: newToken };
|
||||
return apiOk({ apiToken: newToken });
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// server/middleware/api-auth.ts - 统一 API 鉴权层,所有非公开 /api/* 请求都必须通过此处
|
||||
import { createError, getRequestURL } from "h3";
|
||||
import { getRequestURL, setResponseStatus } from "h3";
|
||||
|
||||
import { isPublicApiRoute } from "~~/server/utils/api-auth-rules";
|
||||
import { getAuthSession } from "~~/server/utils/auth";
|
||||
import { apiErr } from "~~/server/utils/response";
|
||||
|
||||
/**
|
||||
* 统一 API 鉴权 middleware
|
||||
@@ -27,7 +28,8 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const session = await getAuthSession(event);
|
||||
if (!session) {
|
||||
throw createError({ statusCode: 401, statusMessage: "未登录" });
|
||||
setResponseStatus(event, 401);
|
||||
return apiErr(401, "unauthorized");
|
||||
}
|
||||
|
||||
// 挂到 event.context,后续 handler 直接读取,不再重复查询 session
|
||||
|
||||
+7
-15
@@ -1,4 +1,8 @@
|
||||
// server/utils/answer.ts - OCS 答题提示词构建、答案清洗和响应格式化
|
||||
import type {
|
||||
ISearchErrorResponse,
|
||||
ISearchSuccessResponse
|
||||
} from "~~/shared/types/answer";
|
||||
|
||||
/** `/api/search` 从 OCS 请求中最终抽取出的标准参数 */
|
||||
export interface SearchParams {
|
||||
@@ -10,21 +14,9 @@ export interface SearchParams {
|
||||
options: string;
|
||||
}
|
||||
|
||||
/** OCS AnswererWrapper handler 期望的成功响应结构 */
|
||||
export interface OcsSuccessResponse {
|
||||
code: 1;
|
||||
/** 返回原题,handler 会将它作为匹配题目展示 */
|
||||
question: string;
|
||||
/** 返回最终答案;多选题必须是 `#` 分隔字符串,而不是数组 */
|
||||
answer: string;
|
||||
}
|
||||
|
||||
/** OCS AnswererWrapper handler 期望的失败响应结构 */
|
||||
export interface OcsErrorResponse {
|
||||
code: 0;
|
||||
/** 只放本地业务文案,不暴露上游或内部异常详情 */
|
||||
msg: string;
|
||||
}
|
||||
/** OCS 兼容响应类型别名,与 shared/types/answer.ts 保持一致 */
|
||||
export type OcsSuccessResponse = ISearchSuccessResponse;
|
||||
export type OcsErrorResponse = ISearchErrorResponse;
|
||||
|
||||
/**
|
||||
* 不同题型对应的补充说明
|
||||
|
||||
@@ -23,18 +23,18 @@ export const publicApiRoutes: ApiRouteRule[] = [
|
||||
];
|
||||
|
||||
/** 路径匹配:支持精确匹配、`/**` 前缀通配和 RegExp */
|
||||
function matchPath(rulePath: string | RegExp, pathname: string): boolean {
|
||||
const matchPath = (rulePath: string | RegExp, pathname: string): boolean => {
|
||||
if (rulePath instanceof RegExp) return rulePath.test(pathname);
|
||||
if (rulePath.endsWith("/**")) {
|
||||
const prefix = rulePath.slice(0, -3);
|
||||
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
||||
}
|
||||
return pathname === rulePath;
|
||||
}
|
||||
};
|
||||
|
||||
/** 判断当前请求是否命中公开路由规则 */
|
||||
export function isPublicApiRoute(pathname: string, method: string): boolean {
|
||||
return publicApiRoutes.some((rule) => {
|
||||
export const isPublicApiRoute = (pathname: string, method: string): boolean =>
|
||||
publicApiRoutes.some((rule) => {
|
||||
const methods = Array.isArray(rule.method)
|
||||
? rule.method
|
||||
: rule.method
|
||||
@@ -43,4 +43,3 @@ export function isPublicApiRoute(pathname: string, method: string): boolean {
|
||||
const methodMatched = !methods || methods.includes(method as HttpMethod);
|
||||
return methodMatched && matchPath(rule.path, pathname);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// server/utils/response.ts - 统一 API 响应工具函数(非 OCS 接口专用)
|
||||
// OCS 搜题接口 /api/search 有固定的兼容格式,不使用此工具
|
||||
|
||||
/** 成功响应:code 0,data 为业务数据,msg 固定为"请求成功" */
|
||||
export const apiOk = <T>(data: T) => ({
|
||||
code: 0 as const,
|
||||
data,
|
||||
msg: "请求成功"
|
||||
});
|
||||
|
||||
/** 错误响应:data 固定为 null,msg 为本地安全文案,不暴露上游细节 */
|
||||
export const apiErr = (code: number, msg: string) => ({
|
||||
code,
|
||||
data: null,
|
||||
msg
|
||||
});
|
||||
Reference in New Issue
Block a user