feat(p4): content analysis service with Neo4j knowledge graph and ClickHouse analytics

P4 阶段交付物:
- services/content: 内容资源服务(NestJS)
  - textbooks: 教材 CRUD + 知识图谱绑定
  - config/neo4j.ts: Neo4j driver 单例
  - textbooks.service.ts: MySQL CRUD + Neo4j 知识图谱(createKnowledgeGraph/getPrerequisites)
  - package.json: 补充 @opentelemetry/sdk-node + exporter-trace-otlp-http
- services/data-ana: 数据分析服务(Python FastAPI)
  - main.py: FastAPI + /healthz + class_performance + student_weakness 骨架
  - clickhouse_client.py: ClickHouse 客户端封装
  - config.py: 环境变量配置
- packages/shared-proto/proto/content.proto: TextbookService + KnowledgeGraphService 契约
- packages/shared-proto/proto/analytics.proto: AnalyticsService 契约(class_performance/student_weakness)
This commit is contained in:
SpecialX
2026-07-08 01:38:35 +08:00
parent 23246ade6d
commit 9850bfcfd1
28 changed files with 936 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
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';
@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 list() {
const db = getDb();
return db.select().from(textbooks);
}
async getById(id: string) {
const db = getDb();
const [result] = await db.select().from(textbooks).where(eq(textbooks.id, 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 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();
}
}
}