feat(content): v2 扩展 Elective/LessonPlan/CoursePlan 三业务域
新增 3 个业务域(10 RPC): - ElectiveService: 选修课列表/学生选课记录/选课/退课(含容量与重复校验) - LessonPlanService: 教师备课列表/学生备课列表(仅 published)/详情 - CoursePlanService: 学生课程计划列表/详情 - KnowledgeGraphService.GetKnowledgePath: 与 GetLearningPath 同实现 新增 4 张 MySQL 表(elective_courses/selections/lesson_plans/course_plans),含完整索引。 新增 11 个权限点,覆盖 admin/teacher/student/parent 四角色。 proto 由 4 Service/22 RPC 扩展至 7 Service/32 RPC,v1 全部 RPC 保持向后兼容。 修复 logger.ts pino 导入: default import 在 NodeNext ESM 下不可调用, 改用 named import(与 iam/msg/core-edu 对齐)。 Docker 本地测试全部通过(HTTP + gRPC 双协议),健康检查、 Elective/LessonPlan/CoursePlan CRUD、4 个新 gRPC Service 全部验证通过。 nextstep-v2.md 已创建,记录上下游依赖与 6 项联调待办。
This commit is contained in:
85
services/content/src/lesson-plans/lesson-plans.controller.ts
Normal file
85
services/content/src/lesson-plans/lesson-plans.controller.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { LessonPlansService } from "./lesson-plans.service.js";
|
||||
import type { LessonPlan } from "./lesson-plans.schema.js";
|
||||
import {
|
||||
Permissions,
|
||||
RequirePermission,
|
||||
} from "../middleware/permission.guard.js";
|
||||
import {
|
||||
createLessonPlanSchema,
|
||||
updateLessonPlanSchema,
|
||||
listLessonPlansByTeacherSchema,
|
||||
listLessonPlansByStudentSchema,
|
||||
} from "./lesson-plans.dto.js";
|
||||
|
||||
@Controller("lesson-plans")
|
||||
export class LessonPlansController {
|
||||
constructor(private readonly service: LessonPlansService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission(Permissions.CONTENT_LESSON_PLAN_CREATE)
|
||||
async create(
|
||||
@Body() body: unknown,
|
||||
): Promise<{ success: true; data: { id: string } }> {
|
||||
const input = createLessonPlanSchema.parse(body);
|
||||
const result = await this.service.create(input);
|
||||
return { success: true, data: result };
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission(Permissions.CONTENT_LESSON_PLAN_READ)
|
||||
async list(
|
||||
@Query() query: unknown,
|
||||
): Promise<{ success: true; data: LessonPlan[] }> {
|
||||
const q = query as Record<string, string>;
|
||||
if (q.teacherId) {
|
||||
const input = listLessonPlansByTeacherSchema.parse(query);
|
||||
const data = await this.service.listByTeacher(input);
|
||||
return { success: true, data };
|
||||
}
|
||||
if (q.studentId) {
|
||||
const input = listLessonPlansByStudentSchema.parse(query);
|
||||
const data = await this.service.listByStudent(input);
|
||||
return { success: true, data };
|
||||
}
|
||||
return { success: true, data: [] };
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@RequirePermission(Permissions.CONTENT_LESSON_PLAN_READ)
|
||||
async getById(
|
||||
@Param("id") id: string,
|
||||
): Promise<{ success: true; data: LessonPlan }> {
|
||||
const data = await this.service.getById(id);
|
||||
return { success: true, data };
|
||||
}
|
||||
|
||||
@Put(":id")
|
||||
@RequirePermission(Permissions.CONTENT_LESSON_PLAN_UPDATE)
|
||||
async update(
|
||||
@Param("id") id: string,
|
||||
@Body() body: unknown,
|
||||
): Promise<{ success: true; data: { success: true } }> {
|
||||
const input = updateLessonPlanSchema.parse(body);
|
||||
await this.service.update(id, input);
|
||||
return { success: true, data: { success: true } };
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@RequirePermission(Permissions.CONTENT_LESSON_PLAN_DELETE)
|
||||
async remove(
|
||||
@Param("id") id: string,
|
||||
): Promise<{ success: true; data: { success: true } }> {
|
||||
await this.service.delete(id);
|
||||
return { success: true, data: { success: true } };
|
||||
}
|
||||
}
|
||||
37
services/content/src/lesson-plans/lesson-plans.dto.ts
Normal file
37
services/content/src/lesson-plans/lesson-plans.dto.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createLessonPlanSchema = z.object({
|
||||
teacherId: z.string().min(1).max(32),
|
||||
classId: z.string().min(1).max(32),
|
||||
subjectId: z.string().min(1).max(32),
|
||||
title: z.string().min(1).max(255),
|
||||
content: z.string().min(1),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
export const updateLessonPlanSchema = z.object({
|
||||
title: z.string().min(1).max(255).optional(),
|
||||
content: z.string().min(1).optional(),
|
||||
status: z.enum(["draft", "published", "archived"]).optional(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
export const listLessonPlansByTeacherSchema = z.object({
|
||||
teacherId: z.string().min(1).max(32),
|
||||
classId: z.string().optional(),
|
||||
subjectId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const listLessonPlansByStudentSchema = z.object({
|
||||
studentId: z.string().min(1).max(32),
|
||||
classId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CreateLessonPlanDto = z.infer<typeof createLessonPlanSchema>;
|
||||
export type UpdateLessonPlanDto = z.infer<typeof updateLessonPlanSchema>;
|
||||
export type ListLessonPlansByTeacherDto = z.infer<
|
||||
typeof listLessonPlansByTeacherSchema
|
||||
>;
|
||||
export type ListLessonPlansByStudentDto = z.infer<
|
||||
typeof listLessonPlansByStudentSchema
|
||||
>;
|
||||
10
services/content/src/lesson-plans/lesson-plans.module.ts
Normal file
10
services/content/src/lesson-plans/lesson-plans.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { LessonPlansController } from "./lesson-plans.controller.js";
|
||||
import { LessonPlansService } from "./lesson-plans.service.js";
|
||||
|
||||
@Module({
|
||||
controllers: [LessonPlansController],
|
||||
providers: [LessonPlansService],
|
||||
exports: [LessonPlansService],
|
||||
})
|
||||
export class LessonPlansModule {}
|
||||
69
services/content/src/lesson-plans/lesson-plans.repository.ts
Normal file
69
services/content/src/lesson-plans/lesson-plans.repository.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { getDb } from "../config/database.js";
|
||||
import {
|
||||
lessonPlans,
|
||||
type LessonPlan,
|
||||
type NewLessonPlan,
|
||||
} from "./lesson-plans.schema.js";
|
||||
|
||||
export class LessonPlansRepository {
|
||||
async findById(id: string): Promise<LessonPlan | undefined> {
|
||||
const [result] = await getDb()
|
||||
.select()
|
||||
.from(lessonPlans)
|
||||
.where(eq(lessonPlans.id, id))
|
||||
.limit(1);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findByTeacher(query: {
|
||||
teacherId: string;
|
||||
classId?: string;
|
||||
subjectId?: string;
|
||||
}): Promise<LessonPlan[]> {
|
||||
const db = getDb();
|
||||
const conditions = [eq(lessonPlans.teacherId, query.teacherId)];
|
||||
if (query.classId) {
|
||||
conditions.push(eq(lessonPlans.classId, query.classId));
|
||||
}
|
||||
if (query.subjectId) {
|
||||
conditions.push(eq(lessonPlans.subjectId, query.subjectId));
|
||||
}
|
||||
return db
|
||||
.select()
|
||||
.from(lessonPlans)
|
||||
.where(and(...conditions));
|
||||
}
|
||||
|
||||
async findByStudent(query: {
|
||||
studentId: string;
|
||||
classId?: string;
|
||||
}): Promise<LessonPlan[]> {
|
||||
// 学生视角:按班级查询教师发布的备课计划
|
||||
// studentId 不直接关联备课计划(备课计划是教师创建的),
|
||||
// 通过 classId 过滤学生所在班级的备课计划
|
||||
const db = getDb();
|
||||
const conditions = [eq(lessonPlans.status, "published")];
|
||||
if (query.classId) {
|
||||
conditions.push(eq(lessonPlans.classId, query.classId));
|
||||
}
|
||||
return db
|
||||
.select()
|
||||
.from(lessonPlans)
|
||||
.where(and(...conditions));
|
||||
}
|
||||
|
||||
async create(data: NewLessonPlan): Promise<void> {
|
||||
await getDb().insert(lessonPlans).values(data);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<NewLessonPlan>): Promise<void> {
|
||||
await getDb().update(lessonPlans).set(data).where(eq(lessonPlans.id, id));
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await getDb().delete(lessonPlans).where(eq(lessonPlans.id, id));
|
||||
}
|
||||
}
|
||||
|
||||
export const lessonPlansRepository = new LessonPlansRepository();
|
||||
34
services/content/src/lesson-plans/lesson-plans.schema.ts
Normal file
34
services/content/src/lesson-plans/lesson-plans.schema.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
mysqlTable,
|
||||
varchar,
|
||||
timestamp,
|
||||
text,
|
||||
json,
|
||||
index,
|
||||
} from "drizzle-orm/mysql-core";
|
||||
|
||||
// 备课计划:教师为班级/学科创建的教学计划
|
||||
export const lessonPlans = mysqlTable(
|
||||
"content_lesson_plans",
|
||||
{
|
||||
id: varchar("id", { length: 32 }).notNull().primaryKey(),
|
||||
teacherId: varchar("teacher_id", { length: 32 }).notNull(),
|
||||
classId: varchar("class_id", { length: 32 }).notNull(),
|
||||
subjectId: varchar("subject_id", { length: 32 }).notNull(),
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
content: text("content").notNull(),
|
||||
status: varchar("status", { length: 32 }).notNull().default("draft"),
|
||||
metadata: json("metadata").$type<Record<string, unknown> | null>(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow().onUpdateNow(),
|
||||
},
|
||||
(table) => ({
|
||||
teacherIdx: index("idx_lesson_plan_teacher").on(table.teacherId),
|
||||
classIdx: index("idx_lesson_plan_class").on(table.classId),
|
||||
subjectIdx: index("idx_lesson_plan_subject").on(table.subjectId),
|
||||
statusIdx: index("idx_lesson_plan_status").on(table.status),
|
||||
}),
|
||||
);
|
||||
|
||||
export type LessonPlan = typeof lessonPlans.$inferSelect;
|
||||
export type NewLessonPlan = typeof lessonPlans.$inferInsert;
|
||||
73
services/content/src/lesson-plans/lesson-plans.service.ts
Normal file
73
services/content/src/lesson-plans/lesson-plans.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { lessonPlansRepository } from "./lesson-plans.repository.js";
|
||||
import type { LessonPlan, NewLessonPlan } from "./lesson-plans.schema.js";
|
||||
import { NotFoundError } from "../shared/errors/application-error.js";
|
||||
|
||||
export interface CreateLessonPlanInput {
|
||||
teacherId: string;
|
||||
classId: string;
|
||||
subjectId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface UpdateLessonPlanInput {
|
||||
title?: string;
|
||||
content?: string;
|
||||
status?: string;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LessonPlansService {
|
||||
async create(input: CreateLessonPlanInput): Promise<{ id: string }> {
|
||||
const id = createId();
|
||||
const record: NewLessonPlan = {
|
||||
id,
|
||||
teacherId: input.teacherId,
|
||||
classId: input.classId,
|
||||
subjectId: input.subjectId,
|
||||
title: input.title,
|
||||
content: input.content,
|
||||
status: "draft",
|
||||
metadata: input.metadata ?? null,
|
||||
};
|
||||
await lessonPlansRepository.create(record);
|
||||
return { id };
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<LessonPlan> {
|
||||
const plan = await lessonPlansRepository.findById(id);
|
||||
if (!plan) {
|
||||
throw new NotFoundError("LessonPlan", id);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
async listByTeacher(query: {
|
||||
teacherId: string;
|
||||
classId?: string;
|
||||
subjectId?: string;
|
||||
}): Promise<LessonPlan[]> {
|
||||
return lessonPlansRepository.findByTeacher(query);
|
||||
}
|
||||
|
||||
async listByStudent(query: {
|
||||
studentId: string;
|
||||
classId?: string;
|
||||
}): Promise<LessonPlan[]> {
|
||||
return lessonPlansRepository.findByStudent(query);
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateLessonPlanInput): Promise<void> {
|
||||
await this.getById(id);
|
||||
await lessonPlansRepository.update(id, data);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.getById(id);
|
||||
await lessonPlansRepository.delete(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user