feat: 添加分页功能和缓存管理,优化问答记录展示

This commit is contained in:
2026-05-23 00:44:47 +08:00
parent c48d57df14
commit cc7125e681
16 changed files with 172 additions and 243 deletions
+11 -16
View File
@@ -1,27 +1,22 @@
// server/api/cache/clear.post.ts - 清空答题缓存接口
import { answerCache } from "~~/server/utils/cache";
import { serverEnv } from "~~/server/utils/env";
// server/api/cache/clear.post.ts - 清空当前用户 DB 缓存接口
import { prisma } from "~~/server/utils/db";
import { createApiLogger } from "~~/server/utils/logging";
/**
* 清空内存缓存
* 清缓存:将当前用户的 cacheClearedAt 更新为当前时间
*
* 这是一个管理类接口,必须携带有效 session cookie(由 api-auth middleware 统一鉴权)
* 清理的是当前 Nuxt 进程内缓存;多实例部署时,每个实例都有自己的内存缓存
* 之后查 DB 缓存时只命中该时间之后写入的记录,历史记录全部保留。
* 必须携带有效 session cookie(由 api-auth middleware 统一鉴权)
*/
export default defineEventHandler((event) => {
export default defineEventHandler(async (event) => {
const logger = createApiLogger(event, "api.cache.clear");
const userId = event.context.auth!.user.id;
// 缓存被关闭时不报错,只明确告诉调用方当前没有缓存可清
if (!serverEnv.enableCache || !answerCache) {
return {
success: false,
message: "缓存未启用"
};
}
await prisma.user.update({
where: { id: userId },
data: { cacheClearedAt: new Date() }
});
// 清空当前进程内缓存,DB 记录不受影响
answerCache.clear();
logger.info("finish_success");
return {
+1 -2
View File
@@ -6,14 +6,13 @@ import { SERVICE_VERSION } from "~~/server/utils/runtimeState";
* 健康检查不需要鉴权
*
* 这个接口用于部署平台、Docker healthcheck 或人工确认服务是否启动;
* 返回模型名和缓存开关,但不返回 API Key、baseURL 或其他敏感配置
* 返回模型名,但不返回 API Key、baseURL 或其他敏感配置
*/
export default defineEventHandler(() => {
return {
status: "ok",
message: "AI题库服务运行正常",
version: SERVICE_VERSION,
cache_enabled: serverEnv.enableCache,
model: serverEnv.openAiModel
};
});
+3 -3
View File
@@ -18,9 +18,9 @@ export default defineEventHandler(async (event) => {
const query = getQuery(event);
const rawPage = Number.parseInt(query.page?.toString() || "1", 10);
const rawSize = Number.parseInt(query.size?.toString() || "10", 10);
const size = Number.isFinite(rawSize)
? Math.min(Math.max(rawSize, 1), 100)
: 10;
// size 只允许 10 / 50 / 100;传入其他值(含超出上限)一律退回 10
const ALLOWED_SIZES = new Set([10, 50, 100]);
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, {
+21 -17
View File
@@ -1,4 +1,6 @@
// server/api/search.ts - OCS AnswererWrapper 兼容搜索接口
import { createHash } from "node:crypto";
import {
getQuery,
type H3Event,
@@ -16,11 +18,10 @@ import {
type SearchParams
} from "~~/server/utils/answer";
import { getAuthSession } from "~~/server/utils/auth";
import { answerCache } from "~~/server/utils/cache";
import { prisma } from "~~/server/utils/db";
import { createApiLogger, toSafeLogError } from "~~/server/utils/logging";
import { askAnswerStream } from "~~/server/utils/openai";
import { addQaRecord } from "~~/server/utils/runtimeState";
import { addQaRecord, lookupCachedAnswer } from "~~/server/utils/runtimeState";
/**
* 把 query/body/form 中的值统一转成字符串
@@ -138,17 +139,25 @@ export default defineEventHandler(async (event) => {
// 鉴权:优先 session(浏览器登录),无 session 则用 body.token 查 apiToken
// OCS 油猴脚本跨域无法携带 cookie,需在 body 中传入用户自己的 apiToken
// 同时取出 cacheClearedAt,用于 DB 缓存过滤
let userId: string | null = null;
let cacheClearedAt: Date | null = null;
const session = await getAuthSession(event);
if (session) {
userId = session.user.id;
const userData = await prisma.user.findUnique({
where: { id: userId },
select: { cacheClearedAt: true }
});
cacheClearedAt = userData?.cacheClearedAt ?? null;
} else if (params.token) {
const user = await prisma.user.findUnique({
where: { apiToken: params.token },
select: { id: true }
select: { id: true, cacheClearedAt: true }
});
userId = user?.id ?? null;
cacheClearedAt = user?.cacheClearedAt ?? null;
}
if (!userId) {
@@ -167,12 +176,12 @@ export default defineEventHandler(async (event) => {
return createOcsErrorResponse("未提供问题内容");
}
// 缓存 key 包含题目题型选项;同题不同选项不能共用答案
const cachedAnswer = answerCache?.get(
params.title,
params.type,
params.options
);
// 计算缓存 key题目+题型+选项的 MD5;同题不同选项不能共用答案
const hash = createHash("md5")
.update(`${params.title}|${params.type}|${params.options}`, "utf8")
.digest("hex");
const cachedAnswer = await lookupCachedAnswer(userId, hash, cacheClearedAt);
if (cachedAnswer) {
logger.info("cache_hit");
@@ -191,19 +200,14 @@ export default defineEventHandler(async (event) => {
});
const processedAnswer = extractAnswer(streamResult.answer, params.type);
// 先缓存再记录;这两步失败风险很低,且都是内存操作,不会阻塞主链路
answerCache?.set(
params.title,
processedAnswer,
params.type,
params.options
);
// 写入 DB 记录(带 hash);记录本身即为后续缓存查询的来源
await addQaRecord({
userId,
question: params.title,
type: params.type,
options: params.options,
answer: processedAnswer
answer: processedAnswer,
hash
});
logger.info("finish_success", {
+11 -4
View File
@@ -1,16 +1,23 @@
// server/api/stats.get.ts - 服务运行统计接口
import { prisma } from "~~/server/utils/db";
import { createApiLogger } from "~~/server/utils/logging";
import { getRuntimeStats } from "~~/server/utils/runtimeState";
/**
* 运行统计接口
*
* 统计信息包含 uptime、模型名、缓存数量、最近问答记录数
* 返回进程 uptime、模型名以及当前用户的问答记录
* 必须携带有效 session cookie(由 api-auth middleware 统一鉴权)
*/
export default defineEventHandler((event) => {
export default defineEventHandler(async (event) => {
const logger = createApiLogger(event, "api.stats");
// getRuntimeStats 会实时清理过期缓存,再返回有效缓存数量
const userId = event.context.auth!.user.id;
const [runtimeStats, qa_records_count] = await Promise.all([
Promise.resolve(getRuntimeStats()),
prisma.qaRecord.count({ where: { userId } })
]);
logger.info("finish_success");
return getRuntimeStats();
return { ...runtimeStats, qa_records_count };
});