Files
Edu/services/content/src/knowledge-points/knowledge-points.service.ts
SpecialX 921fe82771 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通过
2026-07-09 08:52:15 +08:00

165 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomUUID } from "node:crypto";
import { Injectable, Logger } from "@nestjs/common";
import { knowledgePointsRepository } from "./knowledge-points.repository.js";
import type { KnowledgePoint } from "./knowledge-points.schema.js";
import { getNeo4jSession } from "../config/neo4j.js";
import {
InternalError,
NotFoundError,
ValidationError,
} from "../shared/errors/application-error.js";
export interface CreateKnowledgePointInput {
chapterId: string;
title: string;
description?: string;
}
export interface UpdateKnowledgePointInput {
title?: string;
description?: string;
}
export interface PrerequisiteNode {
id: string;
title: string;
}
@Injectable()
export class KnowledgePointsService {
private readonly logger = new Logger(KnowledgePointsService.name);
async createKnowledgePoint(
input: CreateKnowledgePointInput,
): Promise<{ id: string }> {
if (!input.chapterId || !input.title) {
throw new ValidationError("chapterId, title are required");
}
const id = randomUUID();
await knowledgePointsRepository.create({
id,
chapterId: input.chapterId,
title: input.title,
description: input.description,
});
// Neo4j创建知识点节点。非阻塞——失败仅记录日志不影响 MySQL 写入。
await this.safeCreateNode(id, input.title);
return { id };
}
async getKnowledgePoint(id: string): Promise<KnowledgePoint> {
const kp = await knowledgePointsRepository.findById(id);
if (!kp) {
throw new NotFoundError("KnowledgePoint", id);
}
return kp;
}
async listByChapter(chapterId: string): Promise<KnowledgePoint[]> {
return knowledgePointsRepository.findByChapterId(chapterId);
}
async updateKnowledgePoint(
id: string,
data: UpdateKnowledgePointInput,
): Promise<void> {
await this.getKnowledgePoint(id);
await knowledgePointsRepository.update(id, data);
}
async deleteKnowledgePoint(id: string): Promise<void> {
await this.getKnowledgePoint(id);
await knowledgePointsRepository.delete(id);
}
async getPrerequisites(
knowledgePointId: string,
): Promise<PrerequisiteNode[]> {
const session = getNeo4jSession();
if (!session) {
return [];
}
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): PrerequisiteNode => {
const rawId: unknown = r.get("id");
const rawTitle: unknown = r.get("title");
return {
id: typeof rawId === "string" ? rawId : String(rawId),
title: typeof rawTitle === "string" ? rawTitle : String(rawTitle),
};
});
} catch (err) {
this.logger.warn(
`Neo4j getPrerequisites failed: ${err instanceof Error ? err.message : String(err)}`,
);
return [];
} finally {
await session.close();
}
}
async addPrerequisite(id: string, prerequisiteId: string): Promise<void> {
if (id === prerequisiteId) {
throw new ValidationError(
"A knowledge point cannot be a prerequisite of itself",
);
}
// 先校验两个知识点在 MySQL 中都存在
await this.getKnowledgePoint(id);
await this.getKnowledgePoint(prerequisiteId);
const session = getNeo4jSession();
if (!session) {
throw new InternalError(
"Neo4j is not available, cannot add prerequisite",
);
}
try {
await session.executeWrite((tx) =>
tx.run(
`MATCH (kp:KnowledgePoint {id: $kpId}), (prereq:KnowledgePoint {id: $prereqId})
MERGE (prereq)-[:PREREQUISITE_OF]->(kp)`,
{ kpId: id, prereqId: prerequisiteId },
),
);
} finally {
await session.close();
}
}
private async safeCreateNode(id: string, title: string): Promise<void> {
const session = getNeo4jSession();
if (!session) {
return;
}
try {
await session.executeWrite((tx) =>
tx.run("MERGE (kp:KnowledgePoint {id: $id, title: $title})", {
id,
title,
}),
);
} catch (err) {
this.logger.warn(
`Neo4j createNode failed (non-blocking): ${err instanceof Error ? err.message : String(err)}`,
);
} finally {
await session.close();
}
}
}