feat(content): 修复服务并添加chapters/knowledge-points/questions模块

- database.ts 导出db常量替代getDb()函数

- env.ts JWT_SECRET/ES_URL/NEO4J_URL改optional加DEV_MODE

- neo4j.ts driver惰性创建+try/catch+connectionTimeout:3000

- health/lifecycle改用Drizzle原生查询

- textbooks.schema修复integer到int+导出NewTextbook类型

- 新建chapters/knowledge-points/questions三模块CRUD

- knowledge-points含Neo4j前置依赖图非阻塞查询

- content-init.sql创建4张表

端到端验证: textbooks/chapters/knowledge-points/questions全CRUD通过
This commit is contained in:
SpecialX
2026-07-09 08:52:15 +08:00
parent 033c083619
commit 921fe82771
29 changed files with 1031 additions and 246 deletions

View File

@@ -1,24 +1,16 @@
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import { env } from './env.js';
import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";
import { env } from "./env.js";
let pool: mysql.Pool | null = null;
const pool = mysql.createPool({
uri: env.DATABASE_URL,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
export function getDb() {
if (!pool) {
pool = mysql.createPool({
uri: env.DATABASE_URL,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
}
return drizzle(pool);
}
export const db = drizzle(pool);
export async function closeDb(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
}
await pool.end();
}

View File

@@ -1,17 +1,22 @@
import { z } from 'zod';
import { z } from "zod";
const envSchema = z.object({
PORT: z.string().default('3005'),
PORT: z.string().default("3005"),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
NEO4J_URL: z.string().url(),
NEO4J_PASSWORD: z.string(),
ES_URL: z.string().url(),
JWT_SECRET: z.string(),
JWT_ISSUER: z.string().default('next-edu-cloud'),
NEO4J_URL: z.string().url().optional(),
NEO4J_PASSWORD: z.string().optional(),
ES_URL: z.string().url().optional(),
JWT_SECRET: z.string().optional(),
JWT_ISSUER: z.string().default("next-edu-cloud"),
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
LOG_LEVEL: z
.enum(["fatal", "error", "warn", "info", "debug", "trace"])
.default("info"),
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
DEV_MODE: z.string().optional().default("false"),
});
export type Env = z.infer<typeof envSchema>;
@@ -19,8 +24,11 @@ export type Env = z.infer<typeof envSchema>;
export function loadEnv(): Env {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('❌ Invalid environment variables:', result.error.flatten().fieldErrors);
throw new Error('Invalid environment configuration');
console.error(
"❌ Invalid environment variables:",
result.error.flatten().fieldErrors,
);
throw new Error("Invalid environment configuration");
}
return result.data;
}

View File

@@ -1,11 +1,38 @@
import neo4j from 'neo4j-driver';
import { env } from './env.js';
import neo4j from "neo4j-driver";
import type { Driver, Session } from "neo4j-driver";
import { env } from "./env.js";
export const neo4jDriver = neo4j.driver(
env.NEO4J_URL,
neo4j.auth.basic('neo4j', env.NEO4J_PASSWORD)
);
// Neo4j driver 创建为惰性初始化:未配置 NEO4J_URL / NEO4J_PASSWORD 时
// driver 保持 null服务仍可正常启动。所有依赖 Neo4j 的查询在
// driver 为 null 时返回空结果或抛出可控错误,不会阻塞主流程。
let driver: Driver | null = null;
try {
if (env.NEO4J_URL && env.NEO4J_PASSWORD) {
driver = neo4j.driver(
env.NEO4J_URL,
neo4j.auth.basic("neo4j", env.NEO4J_PASSWORD),
// 连接超时 3sNeo4j 不可用时快速失败,避免拖慢 HTTP 响应
{ connectionTimeout: 3000, maxConnectionLifetime: 60_000 },
);
}
} catch (err) {
console.warn(
"Neo4j driver init failed, running without Neo4j:",
err instanceof Error ? err.message : String(err),
);
driver = null;
}
export function getNeo4jSession(): Session | null {
if (!driver) {
return null;
}
return driver.session();
}
export async function closeNeo4j(): Promise<void> {
await neo4jDriver.close();
if (driver) {
await driver.close();
}
}