feat(teacher-bff): admin 命名空间 + 5 个 gRPC client + health probes + merge-resolvers + nextstep 文档
This commit is contained in:
26
services/teacher-bff/src/shared/health/ai-grpc.probe.ts
Normal file
26
services/teacher-bff/src/shared/health/ai-grpc.probe.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// ai gRPC 健康探针(/readyz P5 必需依赖)
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { AI_CLIENT } from "../../clients/ai/ai-client.interface.js";
|
||||
import type { AiClient } from "../../clients/ai/ai-client.interface.js";
|
||||
import type { HealthProbe } from "./readiness.probe.js";
|
||||
|
||||
@Injectable()
|
||||
export class AiGrpcHealthProbe implements HealthProbe {
|
||||
readonly name = "ai-grpc";
|
||||
readonly required = true;
|
||||
|
||||
constructor(@Inject(AI_CLIENT) private readonly ai: AiClient) {}
|
||||
|
||||
async check(): Promise<{
|
||||
status: "up" | "down";
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await this.ai.checkHealth();
|
||||
return {
|
||||
status: result.serving ? "up" : "down",
|
||||
latencyMs: result.latencyMs,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
28
services/teacher-bff/src/shared/health/content-grpc.probe.ts
Normal file
28
services/teacher-bff/src/shared/health/content-grpc.probe.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// content gRPC 健康探针(/readyz P4 必需依赖)
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { CONTENT_CLIENT } from "../../clients/content/content-client.interface.js";
|
||||
import type { ContentClient } from "../../clients/content/content-client.interface.js";
|
||||
import type { HealthProbe } from "./readiness.probe.js";
|
||||
|
||||
@Injectable()
|
||||
export class ContentGrpcHealthProbe implements HealthProbe {
|
||||
readonly name = "content-grpc";
|
||||
readonly required = true;
|
||||
|
||||
constructor(
|
||||
@Inject(CONTENT_CLIENT) private readonly content: ContentClient,
|
||||
) {}
|
||||
|
||||
async check(): Promise<{
|
||||
status: "up" | "down";
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await this.content.checkHealth();
|
||||
return {
|
||||
status: result.serving ? "up" : "down",
|
||||
latencyMs: result.latencyMs,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// core-edu gRPC 健康探针(/readyz P3 必需依赖)
|
||||
// 调用 grpc.health.v1.Health/Check 检查 core-edu:50053 是否 SERVING
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { CORE_EDU_CLIENT } from "../../clients/core-edu/core-edu-client.interface.js";
|
||||
import type { CoreEduClient } from "../../clients/core-edu/core-edu-client.interface.js";
|
||||
import type { HealthProbe } from "./readiness.probe.js";
|
||||
|
||||
@Injectable()
|
||||
export class CoreEduGrpcHealthProbe implements HealthProbe {
|
||||
readonly name = "core-edu-grpc";
|
||||
readonly required = true;
|
||||
|
||||
constructor(
|
||||
@Inject(CORE_EDU_CLIENT) private readonly coreEdu: CoreEduClient,
|
||||
) {}
|
||||
|
||||
async check(): Promise<{
|
||||
status: "up" | "down";
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await this.coreEdu.checkHealth();
|
||||
return {
|
||||
status: result.serving ? "up" : "down",
|
||||
latencyMs: result.latencyMs,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// data-ana gRPC 健康探针(/readyz P4 必需依赖)
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { DATA_ANA_CLIENT } from "../../clients/data-ana/data-ana-client.interface.js";
|
||||
import type { DataAnaClient } from "../../clients/data-ana/data-ana-client.interface.js";
|
||||
import type { HealthProbe } from "./readiness.probe.js";
|
||||
|
||||
@Injectable()
|
||||
export class DataAnaGrpcHealthProbe implements HealthProbe {
|
||||
readonly name = "data-ana-grpc";
|
||||
readonly required = true;
|
||||
|
||||
constructor(
|
||||
@Inject(DATA_ANA_CLIENT) private readonly dataAna: DataAnaClient,
|
||||
) {}
|
||||
|
||||
async check(): Promise<{
|
||||
status: "up" | "down";
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await this.dataAna.checkHealth();
|
||||
return {
|
||||
status: result.serving ? "up" : "down",
|
||||
latencyMs: result.latencyMs,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
26
services/teacher-bff/src/shared/health/msg-grpc.probe.ts
Normal file
26
services/teacher-bff/src/shared/health/msg-grpc.probe.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// msg gRPC 健康探针(/readyz P5 必需依赖)
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { MSG_CLIENT } from "../../clients/msg/msg-client.interface.js";
|
||||
import type { MsgClient } from "../../clients/msg/msg-client.interface.js";
|
||||
import type { HealthProbe } from "./readiness.probe.js";
|
||||
|
||||
@Injectable()
|
||||
export class MsgGrpcHealthProbe implements HealthProbe {
|
||||
readonly name = "msg-grpc";
|
||||
readonly required = true;
|
||||
|
||||
constructor(@Inject(MSG_CLIENT) private readonly msg: MsgClient) {}
|
||||
|
||||
async check(): Promise<{
|
||||
status: "up" | "down";
|
||||
latencyMs: number;
|
||||
error?: string;
|
||||
}> {
|
||||
const result = await this.msg.checkHealth();
|
||||
return {
|
||||
status: result.serving ? "up" : "down",
|
||||
latencyMs: result.latencyMs,
|
||||
error: result.error,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import pino from "pino";
|
||||
import { pino } from "pino";
|
||||
import { env } from "../../config/env.js";
|
||||
|
||||
export const logger = pino({
|
||||
@@ -16,4 +16,4 @@ export const logger = pino({
|
||||
: undefined,
|
||||
});
|
||||
|
||||
export type Logger = typeof logger;
|
||||
export type Logger = ReturnType<typeof pino>;
|
||||
|
||||
122
services/teacher-bff/src/shared/validation/schemas.ts
Normal file
122
services/teacher-bff/src/shared/validation/schemas.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// Zod 验证 schemas(P6 Nice to Have 补全:全 Controller 覆盖)
|
||||
// 对齐 teacher-bff.schema.graphql Input 类型
|
||||
import { z } from "zod";
|
||||
|
||||
/** 创建考试 Input 验证 */
|
||||
export const createExamInputSchema = z.object({
|
||||
classId: z.string().min(1, "classId is required"),
|
||||
title: z.string().min(1, "title is required").max(200, "title too long"),
|
||||
description: z.string().max(1000, "description too long").default(""),
|
||||
examDate: z.string().min(1, "examDate is required"),
|
||||
duration: z.string().min(1, "duration is required"),
|
||||
totalScore: z.string().min(1, "totalScore is required"),
|
||||
});
|
||||
|
||||
/** 布置作业 Input 验证 */
|
||||
export const assignHomeworkInputSchema = z.object({
|
||||
classId: z.string().min(1, "classId is required"),
|
||||
title: z.string().min(1, "title is required").max(200, "title too long"),
|
||||
description: z.string().max(1000, "description too long").default(""),
|
||||
dueDate: z.string().min(1, "dueDate is required"),
|
||||
});
|
||||
|
||||
/** 录入成绩 Input 验证 */
|
||||
export const recordGradeInputSchema = z.object({
|
||||
studentId: z.string().min(1, "studentId is required"),
|
||||
examId: z.string().optional(),
|
||||
homeworkId: z.string().optional(),
|
||||
score: z.string().min(1, "score is required"),
|
||||
feedback: z.string().max(500, "feedback too long").optional(),
|
||||
});
|
||||
|
||||
/** AI 出题 Input 验证 */
|
||||
export const generateQuestionInputSchema = z.object({
|
||||
prompt: z.string().min(1, "prompt is required").max(2000, "prompt too long"),
|
||||
subject: z.string().min(1, "subject is required"),
|
||||
difficulty: z.enum(["easy", "medium", "hard"]),
|
||||
knowledgePointIds: z.array(z.string()).default([]),
|
||||
questionType: z
|
||||
.enum([
|
||||
"single_choice",
|
||||
"multi_choice",
|
||||
"fill_blank",
|
||||
"short_answer",
|
||||
"essay",
|
||||
])
|
||||
.default("short_answer"),
|
||||
count: z.number().int().min(1).max(10).default(1),
|
||||
});
|
||||
|
||||
/** admin 创建用户 Input 验证 */
|
||||
export const adminCreateUserInputSchema = z.object({
|
||||
email: z.string().email("invalid email"),
|
||||
name: z.string().min(1, "name is required").max(50, "name too long"),
|
||||
roles: z.array(z.string()).min(1, "at least one role required"),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, "password must be at least 8 characters")
|
||||
.max(128, "password too long"),
|
||||
});
|
||||
|
||||
/** admin 更新用户 Input 验证 */
|
||||
export const adminUpdateUserInputSchema = z.object({
|
||||
name: z.string().min(1).max(50).optional(),
|
||||
roles: z.array(z.string()).optional(),
|
||||
status: z.enum(["ACTIVE", "INACTIVE", "SUSPENDED"]).optional(),
|
||||
});
|
||||
|
||||
/** admin 创建角色 Input 验证 */
|
||||
export const adminCreateRoleInputSchema = z.object({
|
||||
name: z.string().min(1, "name is required").max(50, "name too long"),
|
||||
description: z
|
||||
.string()
|
||||
.max(200, "description too long")
|
||||
.optional()
|
||||
.default(""),
|
||||
permissions: z.array(z.string()).min(1, "at least one permission required"),
|
||||
});
|
||||
|
||||
/** admin 更新学校 Input 验证 */
|
||||
export const adminUpdateSchoolInputSchema = z.object({
|
||||
name: z.string().min(1, "name is required").max(100, "name too long"),
|
||||
address: z.string().max(200, "address too long").optional().default(""),
|
||||
phone: z.string().max(20, "phone too long").optional().default(""),
|
||||
email: z.string().email("invalid email").optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
/** admin 创建组织 Input 验证 */
|
||||
export const adminCreateOrganizationInputSchema = z.object({
|
||||
name: z.string().min(1, "name is required").max(100, "name too long"),
|
||||
type: z.enum(["DEPARTMENT", "TEACHING_GROUP", "GRADE", "CLASS"]),
|
||||
parentId: z.string().optional().default(""),
|
||||
});
|
||||
|
||||
/** 日期范围 Input 验证 */
|
||||
export const dateRangeInputSchema = z.object({
|
||||
start: z.string().min(1, "start date is required"),
|
||||
end: z.string().min(1, "end date is required"),
|
||||
});
|
||||
|
||||
/** 验证失败错误类型 */
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** 执行验证并返回结构化错误(data 类型为 schema 的 output 类型) */
|
||||
export function validateInput<S extends z.ZodSchema>(
|
||||
schema: S,
|
||||
input: unknown,
|
||||
):
|
||||
| { success: true; data: z.output<S> }
|
||||
| { success: false; errors: ValidationError[] } {
|
||||
const result = schema.safeParse(input);
|
||||
if (result.success) {
|
||||
return { success: true, data: result.data };
|
||||
}
|
||||
const errors: ValidationError[] = result.error.issues.map((issue) => ({
|
||||
field: issue.path.join("."),
|
||||
message: issue.message,
|
||||
}));
|
||||
return { success: false, errors };
|
||||
}
|
||||
Reference in New Issue
Block a user