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

@@ -0,0 +1,61 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from "@nestjs/common";
import {
ChaptersService,
type CreateChapterInput,
type UpdateChapterInput,
} from "./chapters.service.js";
import type { Chapter } from "./chapters.schema.js";
@Controller("chapters")
export class ChaptersController {
constructor(private readonly service: ChaptersService) {}
@Post()
async create(
@Body() body: CreateChapterInput,
): Promise<{ success: true; data: { id: string } }> {
const result = await this.service.createChapter(body);
return { success: true, data: result };
}
@Get("textbook/:textbookId")
async listByTextbook(
@Param("textbookId") textbookId: string,
): Promise<{ success: true; data: Chapter[] }> {
const data = await this.service.listChaptersByTextbook(textbookId);
return { success: true, data };
}
@Get(":id")
async getById(
@Param("id") id: string,
): Promise<{ success: true; data: Chapter }> {
const data = await this.service.getChapter(id);
return { success: true, data };
}
@Put(":id")
async update(
@Param("id") id: string,
@Body() body: UpdateChapterInput,
): Promise<{ success: true; data: { success: true } }> {
await this.service.updateChapter(id, body);
return { success: true, data: { success: true } };
}
@Delete(":id")
async remove(
@Param("id") id: string,
): Promise<{ success: true; data: { success: true } }> {
await this.service.deleteChapter(id);
return { success: true, data: { success: true } };
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { ChaptersController } from "./chapters.controller.js";
import { ChaptersService } from "./chapters.service.js";
@Module({
controllers: [ChaptersController],
providers: [ChaptersService],
exports: [ChaptersService],
})
export class ChaptersModule {}

View File

@@ -0,0 +1,35 @@
import { eq } from "drizzle-orm";
import { db } from "../config/database.js";
import { chapters, type Chapter, type NewChapter } from "./chapters.schema.js";
export class ChaptersRepository {
async findById(id: string): Promise<Chapter | undefined> {
const [result] = await db
.select()
.from(chapters)
.where(eq(chapters.id, id))
.limit(1);
return result;
}
async findByTextbookId(textbookId: string): Promise<Chapter[]> {
return db
.select()
.from(chapters)
.where(eq(chapters.textbookId, textbookId));
}
async create(data: NewChapter): Promise<void> {
await db.insert(chapters).values(data);
}
async update(id: string, data: Partial<NewChapter>): Promise<void> {
await db.update(chapters).set(data).where(eq(chapters.id, id));
}
async delete(id: string): Promise<void> {
await db.delete(chapters).where(eq(chapters.id, id));
}
}
export const chaptersRepository = new ChaptersRepository();

View File

@@ -0,0 +1,5 @@
export {
chapters,
type Chapter,
type NewChapter,
} from "../textbooks/textbooks.schema.js";

View File

@@ -0,0 +1,62 @@
import { randomUUID } from "node:crypto";
import { Injectable } from "@nestjs/common";
import { chaptersRepository } from "./chapters.repository.js";
import type { Chapter } from "./chapters.schema.js";
import {
NotFoundError,
ValidationError,
} from "../shared/errors/application-error.js";
export interface CreateChapterInput {
textbookId: string;
title: string;
order: number;
parentId?: string;
}
export interface UpdateChapterInput {
title?: string;
order?: number;
parentId?: string;
}
@Injectable()
export class ChaptersService {
async createChapter(input: CreateChapterInput): Promise<{ id: string }> {
if (!input.textbookId || !input.title || input.order === undefined) {
throw new ValidationError("textbookId, title, order are required");
}
const id = randomUUID();
await chaptersRepository.create({
id,
textbookId: input.textbookId,
title: input.title,
order: input.order,
parentId: input.parentId,
});
return { id };
}
async getChapter(id: string): Promise<Chapter> {
const chapter = await chaptersRepository.findById(id);
if (!chapter) {
throw new NotFoundError("Chapter", id);
}
return chapter;
}
async listChaptersByTextbook(textbookId: string): Promise<Chapter[]> {
return chaptersRepository.findByTextbookId(textbookId);
}
async updateChapter(id: string, data: UpdateChapterInput): Promise<void> {
await this.getChapter(id);
await chaptersRepository.update(id, data);
}
async deleteChapter(id: string): Promise<void> {
await this.getChapter(id);
await chaptersRepository.delete(id);
}
}