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,69 +1,70 @@
import { Injectable } from '@nestjs/common';
import { getDb } from '../config/database.js';
import { neo4jDriver } from '../config/neo4j.js';
import { textbooks, chapters, knowledgePoints } from './textbooks.schema.js';
import { v4 as uuidv4 } from 'uuid';
import { eq } from 'drizzle-orm';
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { Injectable } from "@nestjs/common";
import { db } from "../config/database.js";
import { textbooks, type Textbook } from "./textbooks.schema.js";
import {
NotFoundError,
ValidationError,
} from "../shared/errors/application-error.js";
export interface CreateTextbookInput {
title: string;
subjectId: string;
gradeId: string;
version: string;
}
export interface UpdateTextbookInput {
title?: string;
subjectId?: string;
gradeId?: string;
version?: string;
}
@Injectable()
export class TextbooksService {
async create(data: { title: string; subjectId: string; gradeId: string; version: string }) {
const id = uuidv4();
const db = getDb();
await db.insert(textbooks).values({ id, ...data });
return { id, ...data };
async create(input: CreateTextbookInput): Promise<{ id: string }> {
if (!input.title || !input.subjectId || !input.gradeId || !input.version) {
throw new ValidationError(
"title, subjectId, gradeId, version are required",
);
}
const id = randomUUID();
await db.insert(textbooks).values({
id,
title: input.title,
subjectId: input.subjectId,
gradeId: input.gradeId,
version: input.version,
});
return { id };
}
async list() {
const db = getDb();
async list(): Promise<Textbook[]> {
return db.select().from(textbooks);
}
async getById(id: string) {
const db = getDb();
const [result] = await db.select().from(textbooks).where(eq(textbooks.id, id));
async getById(id: string): Promise<Textbook> {
const [result] = await db
.select()
.from(textbooks)
.where(eq(textbooks.id, id))
.limit(1);
if (!result) {
throw new NotFoundError("Textbook", id);
}
return result;
}
// 知识图谱:在 Neo4j 中创建知识点节点和关系
async createKnowledgeGraph(knowledgePointId: string, title: string, prerequisiteIds: string[]): Promise<void> {
const session = neo4jDriver.session();
try {
await session.executeWrite((tx) =>
tx.run(
'MERGE (kp:KnowledgePoint {id: $id, title: $title})',
{ id: knowledgePointId, title }
)
);
for (const prereqId of prerequisiteIds) {
await session.executeWrite((tx) =>
tx.run(
`MATCH (prereq:KnowledgePoint {id: $prereqId}), (kp:KnowledgePoint {id: $kpId})
MERGE (prereq)-[:PREREQUISITE_OF]->(kp)`,
{ prereqId, kpId: knowledgePointId }
)
);
}
} finally {
await session.close();
}
async update(id: string, data: UpdateTextbookInput): Promise<void> {
await this.getById(id);
await db.update(textbooks).set(data).where(eq(textbooks.id, id));
}
// 查询知识图谱(前置知识点)
async getPrerequisites(knowledgePointId: string): Promise<unknown[]> {
const session = neo4jDriver.session();
try {
const result = await session.executeRead((tx) =>
tx.run(
`MATCH (kp:KnowledgePoint {id: $id})<-[:PREREQUISITE_OF*1..5]-(prereq)
RETURN prereq.id as id, prereq.title as title`,
{ id: knowledgePointId }
)
);
return result.records.map((r) => ({ id: r.get('id'), title: r.get('title') }));
} finally {
await session.close();
}
async delete(id: string): Promise<void> {
await this.getById(id);
await db.delete(textbooks).where(eq(textbooks.id, id));
}
}