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:
61
services/content/src/questions/questions.controller.ts
Normal file
61
services/content/src/questions/questions.controller.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
QuestionsService,
|
||||
type CreateQuestionInput,
|
||||
type UpdateQuestionInput,
|
||||
} from "./questions.service.js";
|
||||
import type { Question } from "./questions.schema.js";
|
||||
|
||||
@Controller("questions")
|
||||
export class QuestionsController {
|
||||
constructor(private readonly service: QuestionsService) {}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Body() body: CreateQuestionInput,
|
||||
): Promise<{ success: true; data: { id: string } }> {
|
||||
const result = await this.service.createQuestion(body);
|
||||
return { success: true, data: result };
|
||||
}
|
||||
|
||||
@Get("knowledge-point/:knowledgePointId")
|
||||
async listByKnowledgePoint(
|
||||
@Param("knowledgePointId") knowledgePointId: string,
|
||||
): Promise<{ success: true; data: Question[] }> {
|
||||
const data = await this.service.listByKnowledgePoint(knowledgePointId);
|
||||
return { success: true, data };
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
async getById(
|
||||
@Param("id") id: string,
|
||||
): Promise<{ success: true; data: Question }> {
|
||||
const data = await this.service.getQuestion(id);
|
||||
return { success: true, data };
|
||||
}
|
||||
|
||||
@Put(":id")
|
||||
async update(
|
||||
@Param("id") id: string,
|
||||
@Body() body: UpdateQuestionInput,
|
||||
): Promise<{ success: true; data: { success: true } }> {
|
||||
await this.service.updateQuestion(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.deleteQuestion(id);
|
||||
return { success: true, data: { success: true } };
|
||||
}
|
||||
}
|
||||
10
services/content/src/questions/questions.module.ts
Normal file
10
services/content/src/questions/questions.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { QuestionsController } from "./questions.controller.js";
|
||||
import { QuestionsService } from "./questions.service.js";
|
||||
|
||||
@Module({
|
||||
controllers: [QuestionsController],
|
||||
providers: [QuestionsService],
|
||||
exports: [QuestionsService],
|
||||
})
|
||||
export class QuestionsModule {}
|
||||
39
services/content/src/questions/questions.repository.ts
Normal file
39
services/content/src/questions/questions.repository.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../config/database.js";
|
||||
import {
|
||||
questions,
|
||||
type Question,
|
||||
type NewQuestion,
|
||||
} from "./questions.schema.js";
|
||||
|
||||
export class QuestionsRepository {
|
||||
async findById(id: string): Promise<Question | undefined> {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(questions)
|
||||
.where(eq(questions.id, id))
|
||||
.limit(1);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findByKnowledgePointId(knowledgePointId: string): Promise<Question[]> {
|
||||
return db
|
||||
.select()
|
||||
.from(questions)
|
||||
.where(eq(questions.knowledgePointId, knowledgePointId));
|
||||
}
|
||||
|
||||
async create(data: NewQuestion): Promise<void> {
|
||||
await db.insert(questions).values(data);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<NewQuestion>): Promise<void> {
|
||||
await db.update(questions).set(data).where(eq(questions.id, id));
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await db.delete(questions).where(eq(questions.id, id));
|
||||
}
|
||||
}
|
||||
|
||||
export const questionsRepository = new QuestionsRepository();
|
||||
23
services/content/src/questions/questions.schema.ts
Normal file
23
services/content/src/questions/questions.schema.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
mysqlTable,
|
||||
char,
|
||||
varchar,
|
||||
text,
|
||||
int,
|
||||
timestamp,
|
||||
} from "drizzle-orm/mysql-core";
|
||||
|
||||
export const questions = mysqlTable("content_questions", {
|
||||
id: char("id", { length: 36 }).notNull().primaryKey(),
|
||||
knowledgePointId: char("knowledge_point_id", { length: 36 }).notNull(),
|
||||
type: varchar("type", { length: 50 }).notNull(),
|
||||
content: text("content").notNull(),
|
||||
answer: text("answer"),
|
||||
explanation: text("explanation"),
|
||||
difficulty: int("difficulty").default(3),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
|
||||
});
|
||||
|
||||
export type Question = typeof questions.$inferSelect;
|
||||
export type NewQuestion = typeof questions.$inferInsert;
|
||||
83
services/content/src/questions/questions.service.ts
Normal file
83
services/content/src/questions/questions.service.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { questionsRepository } from "./questions.repository.js";
|
||||
import type { Question } from "./questions.schema.js";
|
||||
import {
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
} from "../shared/errors/application-error.js";
|
||||
|
||||
export interface CreateQuestionInput {
|
||||
knowledgePointId: string;
|
||||
type: string;
|
||||
content: string;
|
||||
answer?: string;
|
||||
explanation?: string;
|
||||
difficulty?: number;
|
||||
}
|
||||
|
||||
export interface UpdateQuestionInput {
|
||||
type?: string;
|
||||
content?: string;
|
||||
answer?: string;
|
||||
explanation?: string;
|
||||
difficulty?: number;
|
||||
}
|
||||
|
||||
const VALID_TYPES = new Set([
|
||||
"single_choice",
|
||||
"multiple_choice",
|
||||
"short_answer",
|
||||
"essay",
|
||||
]);
|
||||
|
||||
@Injectable()
|
||||
export class QuestionsService {
|
||||
async createQuestion(input: CreateQuestionInput): Promise<{ id: string }> {
|
||||
if (!input.knowledgePointId || !input.type || !input.content) {
|
||||
throw new ValidationError("knowledgePointId, type, content are required");
|
||||
}
|
||||
if (!VALID_TYPES.has(input.type)) {
|
||||
throw new ValidationError(
|
||||
`Invalid question type: ${input.type}. Must be one of: ${[...VALID_TYPES].join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const id = randomUUID();
|
||||
await questionsRepository.create({
|
||||
id,
|
||||
knowledgePointId: input.knowledgePointId,
|
||||
type: input.type,
|
||||
content: input.content,
|
||||
answer: input.answer,
|
||||
explanation: input.explanation,
|
||||
difficulty: input.difficulty,
|
||||
});
|
||||
return { id };
|
||||
}
|
||||
|
||||
async getQuestion(id: string): Promise<Question> {
|
||||
const question = await questionsRepository.findById(id);
|
||||
if (!question) {
|
||||
throw new NotFoundError("Question", id);
|
||||
}
|
||||
return question;
|
||||
}
|
||||
|
||||
async listByKnowledgePoint(knowledgePointId: string): Promise<Question[]> {
|
||||
return questionsRepository.findByKnowledgePointId(knowledgePointId);
|
||||
}
|
||||
|
||||
async updateQuestion(id: string, data: UpdateQuestionInput): Promise<void> {
|
||||
await this.getQuestion(id);
|
||||
if (data.type && !VALID_TYPES.has(data.type)) {
|
||||
throw new ValidationError(`Invalid question type: ${data.type}`);
|
||||
}
|
||||
await questionsRepository.update(id, data);
|
||||
}
|
||||
|
||||
async deleteQuestion(id: string): Promise<void> {
|
||||
await this.getQuestion(id);
|
||||
await questionsRepository.delete(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user