feat: 重构配置管理,使用数据库替代环境变量,优化系统配置读取
This commit is contained in:
+1
-18
@@ -1,21 +1,4 @@
|
||||
# OpenAI API 配置
|
||||
OPENAI_API_KEY=your-api-key-here
|
||||
OPENAI_API_BASE=https://api.openai.com/v1
|
||||
|
||||
# 模型设置
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
MAX_TOKENS=500
|
||||
TEMPERATURE=0.7
|
||||
OPENAI_STREAM_MAX_SSE_LINE_BYTES=1048576
|
||||
|
||||
# 缓存配置
|
||||
ENABLE_CACHE=true
|
||||
CACHE_EXPIRATION=86400
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Prisma 配置,当前答题服务不依赖数据库,但项目保留 Prisma
|
||||
# Prisma 配置
|
||||
DATABASE_URL="mysql://user:password@localhost:3306/database"
|
||||
|
||||
# better-auth 配置
|
||||
|
||||
@@ -32,8 +32,6 @@ OPENAI_API_BASE=https://api.openai.com/v1
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
MAX_TOKENS=500
|
||||
TEMPERATURE=0.7
|
||||
ENABLE_CACHE=true
|
||||
CACHE_EXPIRATION=86400
|
||||
```
|
||||
|
||||
如需限制访问:
|
||||
@@ -82,11 +80,11 @@ pnpm dev
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `title` | 是 | 题目内容 |
|
||||
| `type` | 否 | `single`、`multiple`、`judgement`、`completion` |
|
||||
| `options` | 否 | 选项文本 |
|
||||
| 参数 | 必填 | 说明 |
|
||||
| ----------- | ---- | ------------------------------------------------------- |
|
||||
| `title` | 是 | 题目内容 |
|
||||
| `type` | 否 | `single`、`multiple`、`judgement`、`completion` |
|
||||
| `options` | 否 | 选项文本 |
|
||||
|
||||
成功:
|
||||
|
||||
|
||||
+57
-3
@@ -13,19 +13,22 @@ datasource db {
|
||||
// Better Auth 标准用户表
|
||||
// apiToken 由服务端注册 hook 生成,供 OCS 油猴脚本跨域身份验证
|
||||
model User {
|
||||
id String @id
|
||||
id String @id
|
||||
name String
|
||||
email String @unique
|
||||
email String @unique
|
||||
emailVerified Boolean
|
||||
image String?
|
||||
createdAt DateTime
|
||||
updatedAt DateTime
|
||||
apiToken String? @unique
|
||||
apiToken String? @unique
|
||||
/// 用户最近一次清缓存的时间;只有在此时间之后写入的记录才算缓存命中
|
||||
cacheClearedAt DateTime?
|
||||
/// 用户角色:user(普通用户)、admin(管理员)、superadmin(超级管理员)
|
||||
role String @default("user")
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
qaRecords QaRecord[]
|
||||
userConfigs UserConfig[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
@@ -77,6 +80,57 @@ model Verification {
|
||||
@@map("verification")
|
||||
}
|
||||
|
||||
// 系统级全局配置表(KV 结构),存放 OpenAI 等运行时配置
|
||||
// 管理员通过后台修改,服务端读取时使用 60 秒 TTL 缓存,不再依赖环境变量
|
||||
model SystemConfig {
|
||||
key String @id
|
||||
value String @db.Text
|
||||
/// 配置项中文描述
|
||||
description String?
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("system_config")
|
||||
}
|
||||
|
||||
// 用户自定义配置(与系统配置独立),后续支持用户覆盖 API Key 等
|
||||
model UserConfig {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
/// 配置键,与 system_config 的 key 语义相同
|
||||
key String
|
||||
value String @db.Text
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, key])
|
||||
@@map("user_config")
|
||||
}
|
||||
|
||||
// 可分配的权限定义,预留结构,暂无实际权限可配置
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
/// 权限标识符,如 "manage_users"、"view_all_records"
|
||||
name String @unique
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
rolePermissions RolePermission[]
|
||||
|
||||
@@map("permission")
|
||||
}
|
||||
|
||||
// 角色-权限映射表,预留;role 字段存角色字符串,方便后续新增角色
|
||||
model RolePermission {
|
||||
id String @id @default(cuid())
|
||||
/// 角色名称,如 "user"、"admin"、"superadmin"
|
||||
role String
|
||||
permissionId String
|
||||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([role, permissionId])
|
||||
@@index([role])
|
||||
@@map("role_permission")
|
||||
}
|
||||
|
||||
// 用户问答记录,按用户隔离存储
|
||||
// search 接口在拿到有效 session 或 apiToken 后写入,与当前登录用户绑定
|
||||
model QaRecord {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// server/api/health.get.ts - 服务健康检查接口
|
||||
import { serverEnv } from "~~/server/utils/env";
|
||||
import { apiOk } from "~~/server/utils/response";
|
||||
import { SERVICE_VERSION } from "~~/server/utils/runtimeState";
|
||||
import { getSysConfig } from "~~/server/utils/sysConfig";
|
||||
|
||||
/**
|
||||
* 健康检查不需要鉴权
|
||||
@@ -9,11 +9,12 @@ import { SERVICE_VERSION } from "~~/server/utils/runtimeState";
|
||||
* 这个接口用于部署平台、Docker healthcheck 或人工确认服务是否启动;
|
||||
* 返回模型名,但不返回 API Key、baseURL 或其他敏感配置
|
||||
*/
|
||||
export default defineEventHandler(() => {
|
||||
export default defineEventHandler(async () => {
|
||||
const cfg = await getSysConfig();
|
||||
return apiOk({
|
||||
status: "ok" as const,
|
||||
message: "AI题库服务运行正常",
|
||||
version: SERVICE_VERSION,
|
||||
model: serverEnv.openAiModel
|
||||
model: cfg.openAiModel
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
try {
|
||||
const [runtimeStats, qa_records_count] = await Promise.all([
|
||||
Promise.resolve(getRuntimeStats()),
|
||||
getRuntimeStats(),
|
||||
prisma.qaRecord.count({ where: { userId } })
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// server/plugins/initConfig.ts - 服务启动时检查 system_config 表,补全缺失的默认配置
|
||||
// 只在首次部署或新增配置项时写入,不覆盖已有值;不读取环境变量
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
|
||||
// 所有系统配置的默认值,key 对应 system_config.key
|
||||
// 管理员后续通过后台修改 value;openai_api_key 默认留空,需手动填入
|
||||
const DEFAULT_CONFIGS = [
|
||||
{
|
||||
key: "openai_api_key",
|
||||
value: "",
|
||||
description: "OpenAI 或兼容 API Key,仅服务端使用,不能返回给前端"
|
||||
},
|
||||
{
|
||||
key: "openai_api_base",
|
||||
value: "https://api.openai.com/v1",
|
||||
description: "OpenAI 兼容 API base URL,例如 https://api.openai.com/v1"
|
||||
},
|
||||
{
|
||||
key: "openai_model",
|
||||
value: "gpt-3.5-turbo",
|
||||
description: "Chat Completions 使用的模型名"
|
||||
},
|
||||
{
|
||||
key: "max_tokens",
|
||||
value: "500",
|
||||
description: "单次回答最大 token 数"
|
||||
},
|
||||
{
|
||||
key: "temperature",
|
||||
value: "0.7",
|
||||
description: "模型采样温度,越低越稳定,范围 0~2"
|
||||
},
|
||||
{
|
||||
key: "openai_stream_max_sse_line_bytes",
|
||||
value: "1048576",
|
||||
description: "单行 SSE data 最大字节数,防止异常流无限堆内存"
|
||||
}
|
||||
];
|
||||
|
||||
export default defineNitroPlugin(async () => {
|
||||
try {
|
||||
const existingRows = await prisma.systemConfig.findMany({
|
||||
select: { key: true }
|
||||
});
|
||||
const existingKeys = new Set(existingRows.map((r) => r.key));
|
||||
|
||||
const missing = DEFAULT_CONFIGS.filter((c) => !existingKeys.has(c.key));
|
||||
if (missing.length === 0) return;
|
||||
|
||||
await prisma.systemConfig.createMany({
|
||||
data: missing,
|
||||
skipDuplicates: true
|
||||
});
|
||||
console.log(
|
||||
`[initConfig] 已写入 ${missing.length} 项默认系统配置:${missing.map((c) => c.key).join(", ")}`
|
||||
);
|
||||
} catch (err) {
|
||||
// 初始化失败不阻断进程启动,记录警告供排查
|
||||
console.error("[initConfig] 系统配置初始化失败:", String(err));
|
||||
}
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
// server/utils/env.ts - 服务端环境变量读取与规范化
|
||||
|
||||
/**
|
||||
* 允许 `.env` 里保留类似 Python 旧项目的写法:
|
||||
* `OPENAI_API_BASE=https://example.com/v1 # 注释`
|
||||
* Nuxt/Node 读到的是整行值,所以这里主动裁掉行尾注释
|
||||
*/
|
||||
const stripInlineComment = (value: string) => {
|
||||
return value.replace(/\s+#.*$/u, "").trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取字符串环境变量
|
||||
*
|
||||
* - 空字符串视为未配置,回退到 fallback
|
||||
* - 返回值统一去掉行尾注释,避免 baseURL、model 之类配置带上注释文本
|
||||
*/
|
||||
const readString = (key: string, fallback = "") => {
|
||||
const value = process.env[key];
|
||||
if (value === undefined || value === null || value.trim() === "") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return stripInlineComment(value);
|
||||
};
|
||||
|
||||
/** 读取可选字符串;空值统一转成 undefined,方便后续判断是否启用某能力 */
|
||||
const readOptionalString = (key: string) => {
|
||||
const value = readString(key);
|
||||
return value || undefined;
|
||||
};
|
||||
|
||||
/** 兼容常见布尔写法,便于 Docker、面板和手写 `.env` 配置 */
|
||||
const readBoolean = (key: string, fallback: boolean) => {
|
||||
const value = readString(key);
|
||||
if (!value) return fallback;
|
||||
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
||||
};
|
||||
|
||||
/** 读取整数配置;非法值不抛错,使用默认值保证服务能启动 */
|
||||
const readInteger = (key: string, fallback: number) => {
|
||||
const value = Number.parseInt(readString(key), 10);
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
};
|
||||
|
||||
/** 读取小数配置,主要用于 OpenAI temperature */
|
||||
const readNumber = (key: string, fallback: number) => {
|
||||
const value = Number.parseFloat(readString(key));
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
};
|
||||
|
||||
/** OpenAI 兼容接口 baseURL 统一不带结尾斜杠,后续再拼 `/chat/completions` */
|
||||
const normalizeBaseUrl = (url: string) => {
|
||||
return url.replace(/\/+$/u, "");
|
||||
};
|
||||
|
||||
/**
|
||||
* 服务端环境配置的唯一出口
|
||||
*
|
||||
* 业务代码不要直接读 `process.env`,这样可以把默认值、格式修正和安全边界
|
||||
* 都收敛在这里,后续迁移部署平台时也只需要检查这个文件
|
||||
*/
|
||||
export const serverEnv = {
|
||||
/** OpenAI 或兼容中转平台 API Key,仅服务端使用,不能返回给前端 */
|
||||
openAiApiKey: readString("OPENAI_API_KEY"),
|
||||
/** OpenAI 兼容 API base,例如 `https://api.openai.com/v1` */
|
||||
openAiApiBase: normalizeBaseUrl(
|
||||
readString("OPENAI_API_BASE", "https://api.openai.com/v1")
|
||||
),
|
||||
/** Chat Completions 使用的模型名 */
|
||||
openAiModel: readString("OPENAI_MODEL", "gpt-3.5-turbo"),
|
||||
/** 单次回答最大 token 数,沿用旧 Python 服务的配置语义 */
|
||||
maxTokens: readInteger("MAX_TOKENS", 500),
|
||||
/** 模型采样温度,越低越稳定 */
|
||||
temperature: readNumber("TEMPERATURE", 0.7),
|
||||
/** 预留日志级别配置,目前日志工具只负责安全输出 */
|
||||
logLevel: readString("LOG_LEVEL", "INFO"),
|
||||
/** 单行 SSE data 的最大字节数,用于防止异常流无限堆内存 */
|
||||
maxSseLineBytes: readInteger("OPENAI_STREAM_MAX_SSE_LINE_BYTES", 1_048_576)
|
||||
};
|
||||
+20
-14
@@ -1,8 +1,6 @@
|
||||
// server/utils/openai.ts - OpenAI Chat Completions 流式调用工具
|
||||
import { serverEnv } from "~~/server/utils/env";
|
||||
import { getSysConfig } from "~~/server/utils/sysConfig";
|
||||
|
||||
/** 旧项目的 `OPENAI_API_BASE` 语义是完整 base,例如 `https://api.openai.com/v1` */
|
||||
const CHAT_COMPLETIONS_URL = `${serverEnv.openAiApiBase}/chat/completions`;
|
||||
/** 复用编码器计算 SSE data 字节长度,避免循环里频繁创建对象 */
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
@@ -56,21 +54,25 @@ export const askAnswerStream = async ({
|
||||
prompt: string;
|
||||
systemPrompt: string;
|
||||
}): Promise<AskAnswerStreamResult> => {
|
||||
if (!serverEnv.openAiApiKey) {
|
||||
const cfg = await getSysConfig();
|
||||
|
||||
if (!cfg.openAiApiKey) {
|
||||
throw new Error("OPENAI_API_KEY is not set.");
|
||||
}
|
||||
|
||||
const chatCompletionsUrl = `${cfg.openAiApiBase}/chat/completions`;
|
||||
|
||||
// 这里不要记录 request body:其中包含完整题目和可能的选项文本
|
||||
const response = await fetch(CHAT_COMPLETIONS_URL, {
|
||||
const response = await fetch(chatCompletionsUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${serverEnv.openAiApiKey}`,
|
||||
Authorization: `Bearer ${cfg.openAiApiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: serverEnv.openAiModel,
|
||||
temperature: serverEnv.temperature,
|
||||
max_tokens: serverEnv.maxTokens,
|
||||
model: cfg.openAiModel,
|
||||
temperature: cfg.temperature,
|
||||
max_tokens: cfg.maxTokens,
|
||||
stream: true,
|
||||
messages: [
|
||||
{
|
||||
@@ -94,7 +96,10 @@ export const askAnswerStream = async ({
|
||||
}
|
||||
|
||||
// 将 SSE delta 读完后再返回给 API handler,由 handler 统一做答案清洗和缓存
|
||||
const upstreamResponse = await readChatCompletionStream(response.body);
|
||||
const upstreamResponse = await readChatCompletionStream(
|
||||
response.body,
|
||||
cfg.maxSseLineBytes
|
||||
);
|
||||
|
||||
return {
|
||||
answer: upstreamResponse.content.trim(),
|
||||
@@ -112,7 +117,8 @@ export const askAnswerStream = async ({
|
||||
* 网络 chunk 不一定按行对齐,所以用 buffer 保存尚未拼完整的一行
|
||||
*/
|
||||
export const readChatCompletionStream = async (
|
||||
stream: ReadableStream<Uint8Array>
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
maxSseLineBytes: number
|
||||
): Promise<ChatCompletionStreamResult> => {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
@@ -146,14 +152,14 @@ export const readChatCompletionStream = async (
|
||||
|
||||
// stream: true 可以正确处理跨 chunk 的多字节字符,例如中文
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
assertSseBufferSize(buffer, serverEnv.maxSseLineBytes);
|
||||
assertSseBufferSize(buffer, maxSseLineBytes);
|
||||
|
||||
// 只处理已经遇到换行的完整 SSE 行,最后一段留到下次 chunk 再拼
|
||||
const lines = buffer.split(/\r?\n/u);
|
||||
buffer = lines.pop() ?? "";
|
||||
|
||||
for (const line of lines) {
|
||||
const chunk = parseSseDataLine(line, serverEnv.maxSseLineBytes);
|
||||
const chunk = parseSseDataLine(line, maxSseLineBytes);
|
||||
if (chunk) collectChunk(chunk);
|
||||
}
|
||||
}
|
||||
@@ -161,7 +167,7 @@ export const readChatCompletionStream = async (
|
||||
// 处理流结束时仍留在 buffer 中的最后一行
|
||||
const finalText = buffer + decoder.decode();
|
||||
for (const line of finalText.split(/\r?\n/u)) {
|
||||
const chunk = parseSseDataLine(line, serverEnv.maxSseLineBytes);
|
||||
const chunk = parseSseDataLine(line, maxSseLineBytes);
|
||||
if (chunk) collectChunk(chunk);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// server/utils/runtimeState.ts - 服务启动时间内存状态 + 问答记录 DB 读写
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
import { serverEnv } from "~~/server/utils/env";
|
||||
import { getSysConfig } from "~~/server/utils/sysConfig";
|
||||
|
||||
/** 进程启动时间,用于 `/api/stats` 返回 uptime */
|
||||
const startTime = Date.now();
|
||||
@@ -100,10 +100,11 @@ export const getQaRecords = async (
|
||||
return { records, total };
|
||||
};
|
||||
/** 生成 `/api/stats` 的基础运行时信息,不包含用户相关数据 */
|
||||
export const getRuntimeStats = () => {
|
||||
export const getRuntimeStats = async () => {
|
||||
const cfg = await getSysConfig();
|
||||
return {
|
||||
version: SERVICE_VERSION,
|
||||
uptime: (Date.now() - startTime) / 1000,
|
||||
model: serverEnv.openAiModel
|
||||
model: cfg.openAiModel
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// server/utils/sysConfig.ts - 从数据库读取系统配置,带 60 秒 TTL 内存缓存
|
||||
// 替代原 env.ts 中的 OpenAI 配置项,配置以 DB 为准,管理员可通过后台修改
|
||||
import { prisma } from "~~/server/utils/db";
|
||||
|
||||
/** 系统配置字段,对应 system_config 表中的各 key */
|
||||
export interface SysConfig {
|
||||
/** OpenAI 或兼容 API Key,仅服务端使用,不能返回给前端 */
|
||||
openAiApiKey: string;
|
||||
/** OpenAI 兼容 API base URL,不含结尾斜杠 */
|
||||
openAiApiBase: string;
|
||||
/** Chat Completions 使用的模型名 */
|
||||
openAiModel: string;
|
||||
/** 单次回答最大 token 数 */
|
||||
maxTokens: number;
|
||||
/** 模型采样温度,越低越稳定 */
|
||||
temperature: number;
|
||||
/** 单行 SSE data 最大字节数,防止异常流无限堆内存 */
|
||||
maxSseLineBytes: number;
|
||||
}
|
||||
|
||||
// system_config 表 key 字段的常量映射
|
||||
const CONFIG_KEYS = {
|
||||
openAiApiKey: "openai_api_key",
|
||||
openAiApiBase: "openai_api_base",
|
||||
openAiModel: "openai_model",
|
||||
maxTokens: "max_tokens",
|
||||
temperature: "temperature",
|
||||
maxSseLineBytes: "openai_stream_max_sse_line_bytes"
|
||||
} as const;
|
||||
|
||||
// 硬编码默认值,与原 env.ts 的 fallback 保持一致
|
||||
const DEFAULTS: SysConfig = {
|
||||
openAiApiKey: "",
|
||||
openAiApiBase: "https://api.openai.com/v1",
|
||||
openAiModel: "gpt-3.5-turbo",
|
||||
maxTokens: 500,
|
||||
temperature: 0.7,
|
||||
maxSseLineBytes: 1_048_576
|
||||
};
|
||||
|
||||
// 内存缓存:减少每请求查 DB 的开销
|
||||
let _cachedConfig: SysConfig | null = null;
|
||||
let _cacheExpiresAt = 0;
|
||||
const CACHE_TTL_MS = 60_000;
|
||||
|
||||
/** baseURL 不能带结尾斜杠,后续拼 `/chat/completions` 时格式才正确 */
|
||||
const normalizeBaseUrl = (url: string) => url.replace(/\/+$/u, "");
|
||||
|
||||
/**
|
||||
* 读取系统配置
|
||||
*
|
||||
* 60 秒内命中内存缓存直接返回,过期后重新查 DB;
|
||||
* 管理员更新配置后可调用 invalidateSysConfigCache() 主动失效
|
||||
*/
|
||||
export const getSysConfig = async (): Promise<SysConfig> => {
|
||||
const now = Date.now();
|
||||
if (_cachedConfig && now < _cacheExpiresAt) {
|
||||
return _cachedConfig;
|
||||
}
|
||||
|
||||
const rows = await prisma.systemConfig.findMany({
|
||||
select: { key: true, value: true }
|
||||
});
|
||||
const map = new Map(rows.map((r) => [r.key, r.value]));
|
||||
|
||||
const str = (key: string, fallback: string) => map.get(key) ?? fallback;
|
||||
const int = (key: string, fallback: number) => {
|
||||
const v = Number.parseInt(map.get(key) ?? "", 10);
|
||||
return Number.isNaN(v) ? fallback : v;
|
||||
};
|
||||
const float = (key: string, fallback: number) => {
|
||||
const v = Number.parseFloat(map.get(key) ?? "");
|
||||
return Number.isNaN(v) ? fallback : v;
|
||||
};
|
||||
|
||||
const config: SysConfig = {
|
||||
openAiApiKey: str(CONFIG_KEYS.openAiApiKey, DEFAULTS.openAiApiKey),
|
||||
openAiApiBase: normalizeBaseUrl(
|
||||
str(CONFIG_KEYS.openAiApiBase, DEFAULTS.openAiApiBase)
|
||||
),
|
||||
openAiModel: str(CONFIG_KEYS.openAiModel, DEFAULTS.openAiModel),
|
||||
maxTokens: int(CONFIG_KEYS.maxTokens, DEFAULTS.maxTokens),
|
||||
temperature: float(CONFIG_KEYS.temperature, DEFAULTS.temperature),
|
||||
maxSseLineBytes: int(CONFIG_KEYS.maxSseLineBytes, DEFAULTS.maxSseLineBytes)
|
||||
};
|
||||
|
||||
_cachedConfig = config;
|
||||
_cacheExpiresAt = now + CACHE_TTL_MS;
|
||||
return config;
|
||||
};
|
||||
|
||||
/** 主动使缓存失效,管理员更新系统配置后调用,下次请求将重新查 DB */
|
||||
export const invalidateSysConfigCache = () => {
|
||||
_cachedConfig = null;
|
||||
_cacheExpiresAt = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user