feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edu/shared-ts": "workspace:*",
|
||||
"@nestjs/common": "^10.4.0",
|
||||
"@nestjs/core": "^10.4.0",
|
||||
"@nestjs/platform-express": "^10.4.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@edu/shared-ts/env-loader";
|
||||
import "reflect-metadata";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module.js";
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import pino from 'pino';
|
||||
import { env } from '../../config/env.js';
|
||||
import { pino } from "pino";
|
||||
import { env } from "../../config/env.js";
|
||||
|
||||
export const logger = pino({
|
||||
level: env.LOG_LEVEL,
|
||||
// 修复:pino 默认字段选项为 `base`,而非 `defaultFields`
|
||||
base: {
|
||||
service: 'classes',
|
||||
version: '0.1.0',
|
||||
service: "classes",
|
||||
version: "0.1.0",
|
||||
},
|
||||
transport:
|
||||
env.NODE_ENV === 'development'
|
||||
env.NODE_ENV === "development"
|
||||
? {
|
||||
target: 'pino-pretty',
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
}
|
||||
: undefined,
|
||||
|
||||
74
services/config-service/src/graphql/generated/schema.graphql
Normal file
74
services/config-service/src/graphql/generated/schema.graphql
Normal file
@@ -0,0 +1,74 @@
|
||||
# ------------------------------------------------------
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
type PluginRegistry {
|
||||
pluginId: ID!
|
||||
category: String!
|
||||
version: String!
|
||||
displayName: String!
|
||||
description: String!
|
||||
isBuiltin: Boolean!
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
type LayoutTemplateGql {
|
||||
layoutId: ID!
|
||||
displayName: String!
|
||||
description: String
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
type UserLayoutOverrideGql {
|
||||
userId: ID!
|
||||
activeLayout: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type PluginConfigLayoutGql {
|
||||
layoutId: ID!
|
||||
displayName: String!
|
||||
description: String!
|
||||
availableSlots: [String!]!
|
||||
layoutSchemaJson: String!
|
||||
}
|
||||
|
||||
type PluginConfigSlotGql {
|
||||
slotName: String!
|
||||
navItems: [String!]!
|
||||
}
|
||||
|
||||
type PluginConfigPlacementGql {
|
||||
pluginId: ID!
|
||||
slot: String!
|
||||
sortOrder: Int!
|
||||
sizeJson: String!
|
||||
propsJson: String!
|
||||
isVisible: Boolean!
|
||||
}
|
||||
|
||||
type PluginConfigRegistryItemGql {
|
||||
pluginId: ID!
|
||||
category: String!
|
||||
version: String!
|
||||
displayName: String!
|
||||
description: String!
|
||||
requiredRoles: [String!]!
|
||||
isBuiltin: Boolean!
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
type PluginConfigResponseGql {
|
||||
activeLayout: PluginConfigLayoutGql
|
||||
slots: [PluginConfigSlotGql!]!
|
||||
plugins: [PluginConfigPlacementGql!]!
|
||||
registry: [PluginConfigRegistryItemGql!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
plugin(pluginId: ID!): PluginRegistry
|
||||
plugins: [PluginRegistry!]!
|
||||
layoutTemplates: [LayoutTemplateGql!]!
|
||||
userLayoutOverride(userId: ID!): UserLayoutOverrideGql
|
||||
pluginConfig(userId: ID!, role: String = "student"): PluginConfigResponseGql!
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
import { Module } from "@nestjs/common";
|
||||
import { GraphQLModule } from "@nestjs/graphql";
|
||||
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { ApolloFederationDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { join } from "node:path";
|
||||
import { GraphqlContext } from "@edu/shared-ts/federation";
|
||||
import { ConfigModule } from "../config-config/config.module.js";
|
||||
@@ -27,7 +27,7 @@ import { DataLoaderService } from "./dataloader.service.js";
|
||||
imports: [
|
||||
ConfigModule,
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
driver: ApolloFederationDriver,
|
||||
// Federation 2 子图
|
||||
autoSchemaFile: {
|
||||
path: join(process.cwd(), "src/graphql/generated/schema.graphql"),
|
||||
|
||||
@@ -20,12 +20,17 @@ export class RouterAuthGuard extends BaseRouterAuthGuard {
|
||||
}
|
||||
|
||||
override canActivate(ctx: ExecutionContext): boolean {
|
||||
// DEV_MODE 优先放行(开发态跳过所有校验)
|
||||
if (process.env.DEV_MODE === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const req = ctx.switchToHttp().getRequest<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
// 仅 GraphQL 端点需要校验,REST 路由放行
|
||||
if (!req.url?.startsWith("/graphql")) {
|
||||
if (!req?.url?.startsWith("/graphql")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@edu/shared-ts/env-loader";
|
||||
import "reflect-metadata";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { Transport, MicroserviceOptions } from "@nestjs/microservices";
|
||||
|
||||
@@ -14,6 +14,14 @@ export class GlobalErrorFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(GlobalErrorFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
// GraphQL 上下文(GqlContextType)无 HTTP req/res,需单独处理。
|
||||
// NestJS ContextType 类型未包含 'graphql'(由 @nestjs/graphql 扩展),
|
||||
// 需断言为 string 比较。
|
||||
if ((host.getType() as string) === "graphql") {
|
||||
this.handleGraphQlError(exception);
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
@@ -83,4 +91,25 @@ export class GlobalErrorFilter implements ExceptionFilter {
|
||||
}
|
||||
return exception.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL 错误处理:NestJS Apollo 会自动把 resolver 抛出的异常包装为
|
||||
* GraphQL error 响应,无需 filter 手动写 response。这里仅记录日志,
|
||||
* 原始异常会继续传播给 Apollo Server 的默认格式化器。
|
||||
*/
|
||||
private handleGraphQlError(exception: unknown): void {
|
||||
if (exception instanceof ApplicationError) {
|
||||
this.logger.warn(`[GraphQL] ${exception.code}: ${exception.message}`);
|
||||
} else if (exception instanceof ZodError) {
|
||||
this.logger.warn(`[GraphQL] Validation error: ${exception.message}`);
|
||||
} else if (exception instanceof Error) {
|
||||
this.logger.error(
|
||||
`[GraphQL] Unhandled: ${exception.message}`,
|
||||
exception.stack,
|
||||
);
|
||||
} else {
|
||||
this.logger.error(`[GraphQL] Unknown exception: ${String(exception)}`);
|
||||
}
|
||||
// 不抛出,让 Apollo Server 的默认错误格式化器处理响应
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,414 +1,63 @@
|
||||
# 自动生成的 GraphQL Federation 子图(v2.1 M0)
|
||||
# 源文件:content.proto
|
||||
# 请勿手动修改;如需调整,改 proto 后重新运行 pnpm run proto:gen-graphql
|
||||
# ------------------------------------------------------
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
extend type Query
|
||||
|
||||
type Textbook @key(fields: "id") {
|
||||
id: String
|
||||
title: String
|
||||
subject_id: String
|
||||
grade_id: String
|
||||
version: String
|
||||
status: String
|
||||
tenant_id: String
|
||||
metadata: Struct
|
||||
created_at: String
|
||||
updated_at: String
|
||||
type Textbook {
|
||||
id: ID!
|
||||
title: String!
|
||||
subjectId: String!
|
||||
gradeId: String!
|
||||
version: String!
|
||||
status: String!
|
||||
tenantId: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Chapter @key(fields: "id") {
|
||||
id: String
|
||||
textbook_id: String
|
||||
title: String
|
||||
order: Int
|
||||
parent_id: String
|
||||
status: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
"""
|
||||
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
||||
"""
|
||||
scalar DateTime
|
||||
|
||||
type Chapter {
|
||||
id: ID!
|
||||
textbookId: String!
|
||||
title: String!
|
||||
order: Float!
|
||||
parentId: String
|
||||
status: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type KnowledgePoint @key(fields: "id") {
|
||||
id: String
|
||||
chapter_id: String
|
||||
title: String
|
||||
type KnowledgePoint {
|
||||
id: ID!
|
||||
chapterId: String!
|
||||
title: String!
|
||||
description: String
|
||||
difficulty: Int
|
||||
metadata: Struct
|
||||
created_at: String
|
||||
updated_at: String
|
||||
difficulty: Float!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Question @key(fields: "id") {
|
||||
id: String
|
||||
knowledge_point_id: String
|
||||
type: String
|
||||
content: String
|
||||
options: Struct
|
||||
answer: String
|
||||
type Question {
|
||||
id: ID!
|
||||
knowledgePointId: String!
|
||||
type: String!
|
||||
content: String!
|
||||
answer: String!
|
||||
explanation: String
|
||||
difficulty: Int
|
||||
status: String
|
||||
source: String
|
||||
created_by: String
|
||||
metadata: Struct
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type GetTextbookRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type UpdateTextbookRequest @key(fields: "id") {
|
||||
id: String
|
||||
title: String
|
||||
status: String
|
||||
metadata: Struct
|
||||
}
|
||||
|
||||
type DeleteTextbookRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type GetChapterRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type UpdateChapterRequest @key(fields: "id") {
|
||||
id: String
|
||||
title: String
|
||||
order: Int
|
||||
status: String
|
||||
}
|
||||
|
||||
type DeleteChapterRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type GetQuestionRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type UpdateQuestionRequest @key(fields: "id") {
|
||||
id: String
|
||||
content: String
|
||||
answer: String
|
||||
status: String
|
||||
options: Struct
|
||||
explanation: String
|
||||
difficulty: Int
|
||||
}
|
||||
|
||||
type DeleteQuestionRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type PublishQuestionRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type ElectiveCourse @key(fields: "id") {
|
||||
id: String
|
||||
title: String
|
||||
subject_id: String
|
||||
teacher_id: String
|
||||
capacity: Int
|
||||
enrolled_count: Int
|
||||
status: String
|
||||
description: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type ElectiveSelection @key(fields: "id") {
|
||||
id: String
|
||||
student_id: String
|
||||
course_id: String
|
||||
status: String
|
||||
selected_at: String
|
||||
dropped_at: String
|
||||
}
|
||||
|
||||
type LessonPlan @key(fields: "id") {
|
||||
id: String
|
||||
teacher_id: String
|
||||
class_id: String
|
||||
subject_id: String
|
||||
title: String
|
||||
content: String
|
||||
status: String
|
||||
metadata: Struct
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type GetLessonPlanRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type CoursePlan @key(fields: "id") {
|
||||
id: String
|
||||
student_id: String
|
||||
class_id: String
|
||||
subject_id: String
|
||||
title: String
|
||||
plan_type: String
|
||||
content: String
|
||||
status: String
|
||||
metadata: Struct
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type GetCoursePlanRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type Empty {
|
||||
}
|
||||
|
||||
input CreateTextbookRequestInput {
|
||||
title: String
|
||||
subject_id: String
|
||||
grade_id: String
|
||||
version: String
|
||||
metadata: Struct
|
||||
}
|
||||
|
||||
input ListTextbooksRequestInput {
|
||||
subject_id: String
|
||||
grade_id: String
|
||||
page_token: String
|
||||
page_size: Int
|
||||
}
|
||||
|
||||
input ListTextbooksResponseInput {
|
||||
textbooks: Textbook
|
||||
next_page_token: String
|
||||
}
|
||||
|
||||
input CreateChapterRequestInput {
|
||||
textbook_id: String
|
||||
title: String
|
||||
order: Int
|
||||
parent_id: String
|
||||
}
|
||||
|
||||
input ListChaptersRequestInput {
|
||||
textbook_id: String
|
||||
parent_id: String
|
||||
}
|
||||
|
||||
input ListChaptersResponseInput {
|
||||
chapters: Chapter
|
||||
}
|
||||
|
||||
input GetPrerequisitesRequestInput {
|
||||
knowledge_point_id: String
|
||||
depth: Int
|
||||
}
|
||||
|
||||
input KnowledgePointsResponseInput {
|
||||
points: KnowledgePoint
|
||||
}
|
||||
|
||||
input GetLearningPathRequestInput {
|
||||
student_id: String
|
||||
subject_id: String
|
||||
}
|
||||
|
||||
type LearningPath {
|
||||
points: KnowledgePoint
|
||||
recommended_order: String
|
||||
}
|
||||
|
||||
input AddPrerequisiteRequestInput {
|
||||
kp_id: String
|
||||
prerequisite_id: String
|
||||
}
|
||||
|
||||
input RemovePrerequisiteRequestInput {
|
||||
kp_id: String
|
||||
prerequisite_id: String
|
||||
}
|
||||
|
||||
input CreateQuestionRequestInput {
|
||||
knowledge_point_id: String
|
||||
type: String
|
||||
content: String
|
||||
options: Struct
|
||||
answer: String
|
||||
explanation: String
|
||||
difficulty: Int
|
||||
source: String
|
||||
created_by: String
|
||||
metadata: Struct
|
||||
}
|
||||
|
||||
input BatchCreateQuestionsRequestInput {
|
||||
questions: CreateQuestionRequest
|
||||
}
|
||||
|
||||
input BatchCreateQuestionsResponseInput {
|
||||
ids: String
|
||||
failed: BatchCreateFailure
|
||||
}
|
||||
|
||||
type BatchCreateFailure {
|
||||
index: Int
|
||||
error: String
|
||||
}
|
||||
|
||||
input ListQuestionsRequestInput {
|
||||
knowledge_point_id: String
|
||||
type: String
|
||||
difficulty: Int
|
||||
status: String
|
||||
page_token: String
|
||||
page_size: Int
|
||||
}
|
||||
|
||||
input ListQuestionsResponseInput {
|
||||
questions: Question
|
||||
next_page_token: String
|
||||
}
|
||||
|
||||
input SearchQuestionsRequestInput {
|
||||
q: String
|
||||
type: String
|
||||
difficulty: Int
|
||||
knowledge_point_id: String
|
||||
page_token: String
|
||||
page_size: Int
|
||||
}
|
||||
|
||||
input SearchQuestionsResponseInput {
|
||||
questions: Question
|
||||
total: Int
|
||||
next_page_token: String
|
||||
}
|
||||
|
||||
input GetKnowledgePathRequestInput {
|
||||
class_id: String
|
||||
subject_id: String
|
||||
}
|
||||
|
||||
input ListAvailableElectiveCoursesRequestInput {
|
||||
subject_id: String
|
||||
page_size: Int
|
||||
page_token: String
|
||||
}
|
||||
|
||||
input ListElectiveCoursesResponseInput {
|
||||
courses: ElectiveCourse
|
||||
next_page_token: String
|
||||
}
|
||||
|
||||
input ListElectiveSelectionsByStudentRequestInput {
|
||||
student_id: String
|
||||
status: String
|
||||
}
|
||||
|
||||
input ListElectiveSelectionsResponseInput {
|
||||
selections: ElectiveSelection
|
||||
}
|
||||
|
||||
input SelectCourseRequestInput {
|
||||
student_id: String
|
||||
course_id: String
|
||||
}
|
||||
|
||||
input DropCourseRequestInput {
|
||||
student_id: String
|
||||
course_id: String
|
||||
}
|
||||
|
||||
input ListLessonPlansByTeacherRequestInput {
|
||||
teacher_id: String
|
||||
class_id: String
|
||||
subject_id: String
|
||||
}
|
||||
|
||||
input ListLessonPlansByStudentRequestInput {
|
||||
student_id: String
|
||||
class_id: String
|
||||
}
|
||||
|
||||
input ListLessonPlansResponseInput {
|
||||
lesson_plans: LessonPlan
|
||||
}
|
||||
|
||||
input ListCoursePlansByStudentRequestInput {
|
||||
student_id: String
|
||||
class_id: String
|
||||
plan_type: String
|
||||
}
|
||||
|
||||
input ListCoursePlansResponseInput {
|
||||
course_plans: CoursePlan
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
textbook: Textbook
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
textbooks: [ListTextbooksResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
chapter: Chapter
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
chapters: [ListChaptersResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
prerequisites: KnowledgePointsResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
learningPath: LearningPath
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
knowledgePath: LearningPath
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
question: Question
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
questions: [ListQuestionsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
availableElectiveCourses: [ListElectiveCoursesResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
electiveSelectionsByStudent: [ListElectiveSelectionsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
lessonPlansByTeacher: [ListLessonPlansResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
lessonPlansByStudent: [ListLessonPlansResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
lessonPlan: LessonPlan
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
coursePlansByStudent: [ListCoursePlansResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
coursePlan: CoursePlan
|
||||
}
|
||||
difficulty: Float!
|
||||
status: String!
|
||||
source: String!
|
||||
createdBy: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Query {
|
||||
textbook(id: ID!): Textbook
|
||||
chapter(id: ID!): Chapter
|
||||
knowledgePoint(id: ID!): KnowledgePoint
|
||||
question(id: ID!): Question
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
import { Module } from "@nestjs/common";
|
||||
import { GraphQLModule } from "@nestjs/graphql";
|
||||
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { ApolloFederationDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { join } from "node:path";
|
||||
import { GraphqlContext } from "@edu/shared-ts/federation";
|
||||
import { TextbooksModule } from "../textbooks/textbooks.module.js";
|
||||
@@ -31,7 +31,7 @@ import { DataLoaderService } from "./dataloader.service.js";
|
||||
KnowledgePointsModule,
|
||||
QuestionsModule,
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
driver: ApolloFederationDriver,
|
||||
// Federation 2 子图
|
||||
autoSchemaFile: {
|
||||
path: join(process.cwd(), "src/graphql/generated/schema.graphql"),
|
||||
|
||||
@@ -20,12 +20,17 @@ export class RouterAuthGuard extends BaseRouterAuthGuard {
|
||||
}
|
||||
|
||||
override canActivate(ctx: ExecutionContext): boolean {
|
||||
// DEV_MODE 优先放行(开发态跳过所有校验)
|
||||
if (process.env.DEV_MODE === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const req = ctx.switchToHttp().getRequest<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
// 仅 GraphQL 端点需要校验,REST 路由放行
|
||||
if (!req.url?.startsWith("/graphql")) {
|
||||
if (!req?.url?.startsWith("/graphql")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@edu/shared-ts/env-loader";
|
||||
import "reflect-metadata";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { Transport, MicroserviceOptions } from "@nestjs/microservices";
|
||||
|
||||
@@ -1,724 +1,69 @@
|
||||
# 自动生成的 GraphQL Federation 子图(v2.1 M0)
|
||||
# 源文件:core_edu.proto
|
||||
# 请勿手动修改;如需调整,改 proto 后重新运行 pnpm run proto:gen-graphql
|
||||
# ------------------------------------------------------
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
extend type Query
|
||||
|
||||
type Exam @key(fields: "id") {
|
||||
id: String
|
||||
class_id: String
|
||||
subject_id: String
|
||||
title: String
|
||||
type Exam {
|
||||
id: ID!
|
||||
classId: String!
|
||||
subjectId: String!
|
||||
title: String!
|
||||
description: String
|
||||
exam_date: String
|
||||
duration: Int
|
||||
total_score: String
|
||||
status: String
|
||||
status_changed_at: String
|
||||
status_changed_by: String
|
||||
school_id: String
|
||||
created_by: String
|
||||
archived_at: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
examDate: String!
|
||||
duration: Float!
|
||||
totalScore: String!
|
||||
status: String!
|
||||
statusChangedAt: String!
|
||||
statusChangedBy: String
|
||||
schoolId: String!
|
||||
createdBy: String!
|
||||
archivedAt: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type ExamQuestion @key(fields: "id") {
|
||||
id: String
|
||||
exam_id: String
|
||||
question_id: String
|
||||
order: Int
|
||||
score: String
|
||||
question_type: String
|
||||
}
|
||||
|
||||
type ExamSubmission @key(fields: "id") {
|
||||
id: String
|
||||
exam_id: String
|
||||
student_id: String
|
||||
status: String
|
||||
submitted_at: String
|
||||
graded_at: String
|
||||
graded_by: String
|
||||
total_score: String
|
||||
}
|
||||
|
||||
type CreateExamResponse @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type GetExamRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type UpdateExamRequest @key(fields: "id") {
|
||||
id: String
|
||||
title: String
|
||||
type Homework {
|
||||
id: ID!
|
||||
classId: String!
|
||||
subjectId: String!
|
||||
title: String!
|
||||
description: String
|
||||
exam_date: String
|
||||
duration: Int
|
||||
total_score: String
|
||||
status: String
|
||||
dueDate: String!
|
||||
gracePeriod: Float!
|
||||
status: String!
|
||||
schoolId: String!
|
||||
createdBy: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type DeleteExamRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type PublishExamRequest @key(fields: "id") {
|
||||
id: String
|
||||
published_by: String
|
||||
}
|
||||
|
||||
type Homework @key(fields: "id") {
|
||||
id: String
|
||||
class_id: String
|
||||
subject_id: String
|
||||
title: String
|
||||
description: String
|
||||
due_date: String
|
||||
grace_period: Int
|
||||
status: String
|
||||
school_id: String
|
||||
created_by: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type HomeworkSubmission @key(fields: "id") {
|
||||
id: String
|
||||
homework_id: String
|
||||
student_id: String
|
||||
status: String
|
||||
submitted_at: String
|
||||
graded_at: String
|
||||
graded_by: String
|
||||
total_score: String
|
||||
type Grade {
|
||||
id: ID!
|
||||
studentId: String!
|
||||
examId: String
|
||||
homeworkId: String
|
||||
score: String!
|
||||
totalScore: String!
|
||||
feedback: String
|
||||
gradedBy: String!
|
||||
schoolId: String!
|
||||
idempotencyKey: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type AssignHomeworkResponse @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type GetHomeworkRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type Grade @key(fields: "id") {
|
||||
id: String
|
||||
student_id: String
|
||||
exam_id: String
|
||||
homework_id: String
|
||||
score: String
|
||||
total_score: String
|
||||
feedback: String
|
||||
graded_by: String
|
||||
school_id: String
|
||||
idempotency_key: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type RecordGradeResponse @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type GetGradeRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type UpdateGradeRequest @key(fields: "id") {
|
||||
id: String
|
||||
score: String
|
||||
feedback: String
|
||||
updated_by: String
|
||||
}
|
||||
|
||||
type ClassInfo @key(fields: "id") {
|
||||
id: String
|
||||
name: String
|
||||
grade_id: String
|
||||
head_teacher_id: String
|
||||
type ClassInfo {
|
||||
id: ID!
|
||||
name: String!
|
||||
gradeId: String!
|
||||
headTeacherId: String
|
||||
description: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type StudentInfo @key(fields: "id") {
|
||||
id: String
|
||||
name: String
|
||||
class_id: String
|
||||
}
|
||||
|
||||
type GetClassRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type Attendance @key(fields: "id") {
|
||||
id: String
|
||||
schedule_id: String
|
||||
student_id: String
|
||||
status: String
|
||||
remark: String
|
||||
recorded_by: String
|
||||
school_id: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type RecordAttendanceResponse @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type GetAttendanceRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type ScheduleSlotInfo @key(fields: "id") {
|
||||
id: String
|
||||
course_id: String
|
||||
course_name: String
|
||||
teacher_id: String
|
||||
class_id: String
|
||||
room_id: String
|
||||
start_time: String
|
||||
end_time: String
|
||||
subject_id: String
|
||||
}
|
||||
|
||||
type LeaveRequest @key(fields: "id") {
|
||||
id: String
|
||||
student_id: String
|
||||
class_id: String
|
||||
leave_type: String
|
||||
start_date: String
|
||||
end_date: String
|
||||
reason: String
|
||||
status: String
|
||||
submitted_by: String
|
||||
reviewed_by: String
|
||||
review_comment: String
|
||||
school_id: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type CreateLeaveRequestResponse @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type CancelLeaveRequestRequest @key(fields: "id") {
|
||||
id: String
|
||||
cancelled_by: String
|
||||
}
|
||||
|
||||
type SchoolInfo @key(fields: "id") {
|
||||
id: String
|
||||
name: String
|
||||
address: String
|
||||
principal_id: String
|
||||
created_at: String
|
||||
}
|
||||
|
||||
type GradeLevelInfo @key(fields: "id") {
|
||||
id: String
|
||||
name: String
|
||||
school_id: String
|
||||
order: Int
|
||||
}
|
||||
|
||||
type DepartmentInfo @key(fields: "id") {
|
||||
id: String
|
||||
name: String
|
||||
school_id: String
|
||||
head_id: String
|
||||
created_at: String
|
||||
}
|
||||
|
||||
type AcademicYearInfo @key(fields: "id") {
|
||||
id: String
|
||||
name: String
|
||||
school_id: String
|
||||
start_date: String
|
||||
end_date: String
|
||||
is_current: Boolean
|
||||
}
|
||||
|
||||
input CreateExamRequestInput {
|
||||
class_id: String
|
||||
subject_id: String
|
||||
title: String
|
||||
description: String
|
||||
exam_date: String
|
||||
duration: Int
|
||||
total_score: String
|
||||
school_id: String
|
||||
created_by: String
|
||||
}
|
||||
|
||||
input ListExamsByClassRequestInput {
|
||||
class_id: String
|
||||
}
|
||||
|
||||
input ListExamsResponseInput {
|
||||
exams: Exam
|
||||
}
|
||||
|
||||
input UpdateExamResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
input DeleteExamResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
input PublishExamResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
input SubmitExamRequestInput {
|
||||
exam_id: String
|
||||
student_id: String
|
||||
answers: AnswerInput
|
||||
}
|
||||
|
||||
type AnswerInput {
|
||||
question_id: String
|
||||
answer: String
|
||||
}
|
||||
|
||||
input SubmitExamResponseInput {
|
||||
submission_id: String
|
||||
}
|
||||
|
||||
input GradeExamRequestInput {
|
||||
exam_id: String
|
||||
submission_id: String
|
||||
scores: ScoreInput
|
||||
graded_by: String
|
||||
}
|
||||
|
||||
type ScoreInput {
|
||||
question_id: String
|
||||
score: String
|
||||
teacher_comment: String
|
||||
}
|
||||
|
||||
input GradeExamResponseInput {
|
||||
success: Boolean
|
||||
total_score: String
|
||||
}
|
||||
|
||||
input AssignHomeworkRequestInput {
|
||||
class_id: String
|
||||
subject_id: String
|
||||
title: String
|
||||
description: String
|
||||
due_date: String
|
||||
grace_period: Int
|
||||
school_id: String
|
||||
created_by: String
|
||||
}
|
||||
|
||||
input ListHomeworkByClassRequestInput {
|
||||
class_id: String
|
||||
}
|
||||
|
||||
input ListHomeworkResponseInput {
|
||||
homework: Homework
|
||||
}
|
||||
|
||||
input SubmitHomeworkRequestInput {
|
||||
homework_id: String
|
||||
student_id: String
|
||||
answers: AnswerInput
|
||||
}
|
||||
|
||||
input SubmitHomeworkResponseInput {
|
||||
submission_id: String
|
||||
}
|
||||
|
||||
input GradeHomeworkRequestInput {
|
||||
homework_id: String
|
||||
submission_id: String
|
||||
scores: ScoreInput
|
||||
feedback: String
|
||||
graded_by: String
|
||||
}
|
||||
|
||||
input GradeHomeworkResponseInput {
|
||||
success: Boolean
|
||||
total_score: String
|
||||
}
|
||||
|
||||
input RecordGradeRequestInput {
|
||||
student_id: String
|
||||
exam_id: String
|
||||
homework_id: String
|
||||
score: String
|
||||
total_score: String
|
||||
feedback: String
|
||||
graded_by: String
|
||||
school_id: String
|
||||
idempotency_key: String
|
||||
}
|
||||
|
||||
input ListGradesByStudentRequestInput {
|
||||
student_id: String
|
||||
}
|
||||
|
||||
input ListGradesByExamRequestInput {
|
||||
exam_id: String
|
||||
}
|
||||
|
||||
input ListGradesByHomeworkRequestInput {
|
||||
homework_id: String
|
||||
}
|
||||
|
||||
input ListGradesResponseInput {
|
||||
grades: Grade
|
||||
}
|
||||
|
||||
input UpdateGradeResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
input GetClassesByTeacherRequestInput {
|
||||
teacher_id: String
|
||||
}
|
||||
|
||||
input GetClassesByTeacherResponseInput {
|
||||
classes: ClassInfo
|
||||
}
|
||||
|
||||
input BatchGetClassesRequestInput {
|
||||
ids: String
|
||||
}
|
||||
|
||||
input BatchGetClassesResponseInput {
|
||||
classes: ClassInfo
|
||||
}
|
||||
|
||||
input ListStudentsByClassRequestInput {
|
||||
class_id: String
|
||||
}
|
||||
|
||||
input ListStudentsByClassResponseInput {
|
||||
students: StudentInfo
|
||||
}
|
||||
|
||||
input RecordAttendanceRequestInput {
|
||||
schedule_id: String
|
||||
student_id: String
|
||||
status: String
|
||||
remark: String
|
||||
recorded_by: String
|
||||
school_id: String
|
||||
}
|
||||
|
||||
input ListAttendanceByStudentRequestInput {
|
||||
student_id: String
|
||||
}
|
||||
|
||||
input ListAttendanceByClassRequestInput {
|
||||
class_id: String
|
||||
}
|
||||
|
||||
input ListAttendanceResponseInput {
|
||||
attendance: Attendance
|
||||
}
|
||||
|
||||
input SaveExamDraftRequestInput {
|
||||
exam_id: String
|
||||
student_id: String
|
||||
answers: AnswerInput
|
||||
}
|
||||
|
||||
input SaveExamDraftResponseInput {
|
||||
draft_id: String
|
||||
}
|
||||
|
||||
input RecordExamViolationRequestInput {
|
||||
exam_id: String
|
||||
student_id: String
|
||||
violation_type: String
|
||||
detail: String
|
||||
severity: Int
|
||||
}
|
||||
|
||||
input RecordExamViolationResponseInput {
|
||||
violation_id: String
|
||||
}
|
||||
|
||||
input ExtendExamRequestInput {
|
||||
exam_id: String
|
||||
extension_seconds: Int
|
||||
extended_by: String
|
||||
}
|
||||
|
||||
input ExtendExamResponseInput {
|
||||
success: Boolean
|
||||
new_duration: Int
|
||||
}
|
||||
|
||||
input ForceSubmitExamRequestInput {
|
||||
exam_id: String
|
||||
student_ids: String
|
||||
forced_by: String
|
||||
}
|
||||
|
||||
input ForceSubmitExamResponseInput {
|
||||
affected_count: Int
|
||||
}
|
||||
|
||||
type QuestionOrderInput {
|
||||
question_id: String
|
||||
order: Int
|
||||
}
|
||||
|
||||
input ReorderExamQuestionsRequestInput {
|
||||
exam_id: String
|
||||
orders: QuestionOrderInput
|
||||
reordered_by: String
|
||||
}
|
||||
|
||||
input ReorderExamQuestionsResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
input GetReportCardRequestInput {
|
||||
student_id: String
|
||||
term_id: String
|
||||
}
|
||||
|
||||
type ReportCard {
|
||||
student_id: String
|
||||
term_id: String
|
||||
entries: ReportCardEntry
|
||||
overall_grade: String
|
||||
class_rank: String
|
||||
created_at: String
|
||||
}
|
||||
|
||||
type ReportCardEntry {
|
||||
subject_id: String
|
||||
subject_name: String
|
||||
exam_score: String
|
||||
exam_total: String
|
||||
homework_score: String
|
||||
homework_total: String
|
||||
final_score: String
|
||||
grade_level: String
|
||||
teacher_comment: String
|
||||
}
|
||||
|
||||
input GetScheduleByStudentRequestInput {
|
||||
student_id: String
|
||||
week_start: String
|
||||
}
|
||||
|
||||
input GetScheduleByStudentResponseInput {
|
||||
slots: ScheduleSlotInfo
|
||||
}
|
||||
|
||||
input ListLeaveRequestsByStudentRequestInput {
|
||||
student_id: String
|
||||
status: String
|
||||
}
|
||||
|
||||
input ListLeaveRequestsResponseInput {
|
||||
leave_requests: LeaveRequest
|
||||
}
|
||||
|
||||
input CreateLeaveRequestRequestInput {
|
||||
student_id: String
|
||||
class_id: String
|
||||
leave_type: String
|
||||
start_date: String
|
||||
end_date: String
|
||||
reason: String
|
||||
submitted_by: String
|
||||
school_id: String
|
||||
}
|
||||
|
||||
input CancelLeaveRequestResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
input GetDashboardRequestInput {
|
||||
teacher_id: String
|
||||
}
|
||||
|
||||
type DashboardData {
|
||||
teacher_id: String
|
||||
total_classes: Int
|
||||
total_students: Int
|
||||
pending_homework: Int
|
||||
upcoming_exams: Int
|
||||
ungraded_submissions: Int
|
||||
classes: DashboardClassCard
|
||||
upcoming_exam_list: DashboardExamCard
|
||||
generated_at: String
|
||||
}
|
||||
|
||||
type DashboardClassCard {
|
||||
class_id: String
|
||||
class_name: String
|
||||
student_count: Int
|
||||
}
|
||||
|
||||
type DashboardExamCard {
|
||||
exam_id: String
|
||||
title: String
|
||||
exam_date: String
|
||||
class_id: String
|
||||
class_name: String
|
||||
}
|
||||
|
||||
input GetClassPerformanceRequestInput {
|
||||
class_id: String
|
||||
subject_id: String
|
||||
}
|
||||
|
||||
type ClassPerformance {
|
||||
class_id: String
|
||||
class_name: String
|
||||
student_count: Int
|
||||
average_score: String
|
||||
highest_score: String
|
||||
lowest_score: String
|
||||
median_score: String
|
||||
subjects: ClassPerformanceSubject
|
||||
generated_at: String
|
||||
}
|
||||
|
||||
type ClassPerformanceSubject {
|
||||
subject_id: String
|
||||
subject_name: String
|
||||
average_score: String
|
||||
student_count: Int
|
||||
}
|
||||
|
||||
input ListSchoolsRequestInput {
|
||||
}
|
||||
|
||||
input ListSchoolsResponseInput {
|
||||
schools: SchoolInfo
|
||||
}
|
||||
|
||||
input ListGradeLevelsRequestInput {
|
||||
school_id: String
|
||||
}
|
||||
|
||||
input ListGradeLevelsResponseInput {
|
||||
grade_levels: GradeLevelInfo
|
||||
}
|
||||
|
||||
input ListDepartmentsRequestInput {
|
||||
school_id: String
|
||||
}
|
||||
|
||||
input ListDepartmentsResponseInput {
|
||||
departments: DepartmentInfo
|
||||
}
|
||||
|
||||
input ListAcademicYearsRequestInput {
|
||||
school_id: String
|
||||
}
|
||||
|
||||
input ListAcademicYearsResponseInput {
|
||||
academic_years: AcademicYearInfo
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
exam: Exam
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
examsByClass: [ListExamsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
homework: Homework
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
homeworkByClass: [ListHomeworkResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
grade: Grade
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
gradesByStudent: [ListGradesResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
gradesByExam: [ListGradesResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
gradesByHomework: [ListGradesResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
reportCard: ReportCard
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
class: ClassInfo
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
classesByTeacher: GetClassesByTeacherResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
studentsByClass: [ListStudentsByClassResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
attendance: Attendance
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
attendanceByStudent: [ListAttendanceResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
attendanceByClass: [ListAttendanceResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
scheduleByStudent: GetScheduleByStudentResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
leaveRequestsByStudent: [ListLeaveRequestsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
dashboard: DashboardData
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
classPerformance: ClassPerformance
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
schools: [ListSchoolsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
gradeLevels: [ListGradeLevelsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
departments: [ListDepartmentsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
academicYears: [ListAcademicYearsResponse!]!
|
||||
}
|
||||
type Query {
|
||||
exam(id: ID!): Exam
|
||||
homework(id: ID!): Homework
|
||||
grade(id: ID!): Grade
|
||||
classInfo(id: ID!): ClassInfo
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
import { Module } from "@nestjs/common";
|
||||
import { GraphQLModule } from "@nestjs/graphql";
|
||||
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { ApolloFederationDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { join } from "node:path";
|
||||
import { GraphqlContext } from "@edu/shared-ts/federation";
|
||||
import { ExamsModule } from "../exams/exams.module.js";
|
||||
@@ -23,11 +23,7 @@ import { ClassesModule } from "../classes/classes.module.js";
|
||||
import { ExamResolver } from "./resolvers/exam.resolver.js";
|
||||
import { HomeworkResolver } from "./resolvers/homework.resolver.js";
|
||||
import { GradeResolver } from "./resolvers/grade.resolver.js";
|
||||
import {
|
||||
ClassResolver,
|
||||
StudentInfoResolver,
|
||||
} from "./resolvers/class.resolver.js";
|
||||
import { DataScopeResolver } from "./resolvers/datascope.resolver.js";
|
||||
import { ClassResolver } from "./resolvers/class.resolver.js";
|
||||
import { DataLoaderService } from "./dataloader.service.js";
|
||||
import { getRedisClient } from "../config/redis.js";
|
||||
|
||||
@@ -38,7 +34,7 @@ import { getRedisClient } from "../config/redis.js";
|
||||
GradesModule,
|
||||
ClassesModule,
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
driver: ApolloFederationDriver,
|
||||
// Federation 2 子图
|
||||
autoSchemaFile: {
|
||||
path: join(process.cwd(), "src/graphql/generated/schema.graphql"),
|
||||
@@ -63,8 +59,6 @@ import { getRedisClient } from "../config/redis.js";
|
||||
HomeworkResolver,
|
||||
GradeResolver,
|
||||
ClassResolver,
|
||||
StudentInfoResolver,
|
||||
DataScopeResolver,
|
||||
DataLoaderService,
|
||||
{
|
||||
// 提供 Redis 实例供后续 ScopeToken / 缓存场景使用
|
||||
|
||||
@@ -20,12 +20,17 @@ export class RouterAuthGuard extends BaseRouterAuthGuard {
|
||||
}
|
||||
|
||||
override canActivate(ctx: ExecutionContext): boolean {
|
||||
// DEV_MODE 优先放行(开发态跳过所有校验)
|
||||
if (process.env.DEV_MODE === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const req = ctx.switchToHttp().getRequest<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
// 仅 GraphQL 端点需要校验,REST 路由放行
|
||||
if (!req.url?.startsWith("/graphql")) {
|
||||
if (!req?.url?.startsWith("/graphql")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@edu/shared-ts/env-loader";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module.js";
|
||||
import { env } from "./config/env.js";
|
||||
|
||||
@@ -13,6 +13,14 @@ export interface AuthenticatedRequest extends Request {
|
||||
@Injectable()
|
||||
export class AuthMiddleware implements NestMiddleware {
|
||||
use(req: AuthenticatedRequest, _res: Response, next: NextFunction): void {
|
||||
// DEV_MODE 跳过鉴权(开发态允许 apollo-router 内省 / GraphQL playground)
|
||||
if (process.env.DEV_MODE === "true") {
|
||||
req.userId = (req.headers["x-user-id"] as string) ?? "dev-user-001";
|
||||
req.userRoles = [];
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const userIdHeader = req.headers["x-user-id"];
|
||||
const userId = typeof userIdHeader === "string" ? userIdHeader : undefined;
|
||||
const rolesHeaderRaw = req.headers["x-user-roles"];
|
||||
|
||||
@@ -1,187 +1,31 @@
|
||||
# 自动生成的 GraphQL Federation 子图(v2.1 M0)
|
||||
# 源文件:iam.proto
|
||||
# 请勿手动修改;如需调整,改 proto 后重新运行 pnpm run proto:gen-graphql
|
||||
# ------------------------------------------------------
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
extend type Query
|
||||
|
||||
type UserInfo @key(fields: "id") {
|
||||
id: String
|
||||
email: String
|
||||
name: String
|
||||
roles: String
|
||||
permissions: String
|
||||
data_scope: String
|
||||
status: String
|
||||
type User {
|
||||
userId: ID!
|
||||
email: String!
|
||||
name: String!
|
||||
status: String!
|
||||
dataScope: String!
|
||||
}
|
||||
|
||||
input RegisterRequestInput {
|
||||
email: String
|
||||
password: String
|
||||
name: String
|
||||
type Role {
|
||||
roleId: ID!
|
||||
name: String!
|
||||
description: String
|
||||
roleType: String!
|
||||
level: Float!
|
||||
}
|
||||
|
||||
input LoginRequestInput {
|
||||
email: String
|
||||
password: String
|
||||
type UserDataScope {
|
||||
userId: ID!
|
||||
classScopeToken: String!
|
||||
studentScopeToken: String!
|
||||
}
|
||||
|
||||
input RefreshTokenRequestInput {
|
||||
refresh_token: String
|
||||
}
|
||||
|
||||
input LogoutRequestInput {
|
||||
refresh_token: String
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input LogoutResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
input AuthResponseInput {
|
||||
user: UserInfo
|
||||
tokens: TokenPair
|
||||
}
|
||||
|
||||
type TokenPair {
|
||||
access_token: String
|
||||
refresh_token: String
|
||||
expires_in: Int
|
||||
}
|
||||
|
||||
input GetUserInfoRequestInput {
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input BatchGetUsersRequestInput {
|
||||
user_ids: String
|
||||
}
|
||||
|
||||
input BatchGetUsersResponseInput {
|
||||
users: UserInfo
|
||||
}
|
||||
|
||||
input GetEffectivePermissionsRequestInput {
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input EffectivePermissionsResponseInput {
|
||||
permissions: String
|
||||
}
|
||||
|
||||
input GetEffectiveAccessRequestInput {
|
||||
user_id: String
|
||||
permission: String
|
||||
}
|
||||
|
||||
input EffectiveAccessResponseInput {
|
||||
allowed: Boolean
|
||||
data_scope: String
|
||||
}
|
||||
|
||||
input GetEffectiveDataScopeRequestInput {
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input DataScopeResponseInput {
|
||||
data_scope: String
|
||||
}
|
||||
|
||||
input GetViewportsRequestInput {
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input ViewportsResponseInput {
|
||||
viewports: ViewportItem
|
||||
}
|
||||
|
||||
type ViewportItem {
|
||||
key: String
|
||||
label: String
|
||||
route: String
|
||||
icon: String
|
||||
sort_order: String
|
||||
required_permission: String
|
||||
}
|
||||
|
||||
input GetPublicKeyRequestInput {
|
||||
}
|
||||
|
||||
input PublicKeyResponseInput {
|
||||
kid: String
|
||||
alg: String
|
||||
public_key_pem: String
|
||||
}
|
||||
|
||||
input GetChildrenByParentRequestInput {
|
||||
parent_id: String
|
||||
}
|
||||
|
||||
input ChildrenResponseInput {
|
||||
children: ChildInfo
|
||||
}
|
||||
|
||||
type ChildInfo {
|
||||
student_id: String
|
||||
name: String
|
||||
relation: String
|
||||
}
|
||||
|
||||
type EffectiveDataScope {
|
||||
user_id: String
|
||||
level: String
|
||||
scope_ids: String
|
||||
school_id: String
|
||||
}
|
||||
|
||||
input CreateUserRequestInput {
|
||||
email: String
|
||||
password: String
|
||||
name: String
|
||||
role_id: String
|
||||
data_scope: String
|
||||
}
|
||||
|
||||
input UpdateUserRequestInput {
|
||||
user_id: String
|
||||
name: String
|
||||
email: String
|
||||
status: String
|
||||
data_scope: String
|
||||
}
|
||||
|
||||
input DeleteUserRequestInput {
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input DeleteUserResponseInput {
|
||||
success: Boolean
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
userInfo: UserInfo
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
effectiveDataScope: EffectiveDataScope
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
effectivePermissions: EffectivePermissionsResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
effectiveAccess: EffectiveAccessResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
viewports: ViewportsResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
publicKey: PublicKeyResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
childrenByParent: ChildrenResponse
|
||||
}
|
||||
type Query {
|
||||
user(userId: ID!): User
|
||||
role(roleId: ID!): Role
|
||||
dataScope(userId: ID!): UserDataScope
|
||||
}
|
||||
@@ -11,9 +11,10 @@
|
||||
*/
|
||||
import { Module } from "@nestjs/common";
|
||||
import { GraphQLModule } from "@nestjs/graphql";
|
||||
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { ApolloFederationDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { join } from "node:path";
|
||||
import { GraphqlContext } from "@edu/shared-ts/federation";
|
||||
import { IamModule } from "../iam/iam.module.js";
|
||||
import { UserResolver } from "./resolvers/user.resolver.js";
|
||||
import { RoleResolver } from "./resolvers/role.resolver.js";
|
||||
import { DataScopeResolver } from "./resolvers/datascope.resolver.js";
|
||||
@@ -22,8 +23,9 @@ import { getRedis } from "../config/redis.js";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
IamModule,
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
driver: ApolloFederationDriver,
|
||||
// Federation 2 子图
|
||||
autoSchemaFile: {
|
||||
path: join(process.cwd(), "src/graphql/generated/schema.graphql"),
|
||||
|
||||
@@ -20,12 +20,17 @@ export class RouterAuthGuard extends BaseRouterAuthGuard {
|
||||
}
|
||||
|
||||
override canActivate(ctx: ExecutionContext): boolean {
|
||||
// DEV_MODE 优先放行(开发态跳过所有校验)
|
||||
if (process.env.DEV_MODE === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const req = ctx.switchToHttp().getRequest<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
// 仅 GraphQL 端点需要校验,REST 路由放行
|
||||
if (!req.url?.startsWith("/graphql")) {
|
||||
if (!req?.url?.startsWith("/graphql")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@edu/shared-ts/env-loader";
|
||||
import "reflect-metadata";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { Transport, MicroserviceOptions } from "@nestjs/microservices";
|
||||
|
||||
@@ -1,222 +1,47 @@
|
||||
# 自动生成的 GraphQL Federation 子图(v2.1 M0)
|
||||
# 源文件:msg.proto
|
||||
# 请勿手动修改;如需调整,改 proto 后重新运行 pnpm run proto:gen-graphql
|
||||
# ------------------------------------------------------
|
||||
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
|
||||
# ------------------------------------------------------
|
||||
|
||||
extend type Query
|
||||
|
||||
type Notification @key(fields: "id") {
|
||||
id: String
|
||||
user_id: String
|
||||
type: String
|
||||
title: String
|
||||
content: String
|
||||
channel: String
|
||||
is_read: Boolean
|
||||
created_at: String
|
||||
status: String
|
||||
related_entity_type: String
|
||||
related_entity_id: String
|
||||
group_id: String
|
||||
sender_id: String
|
||||
template_id: String
|
||||
event_id: String
|
||||
read_at: String
|
||||
updated_at: String
|
||||
type Notification {
|
||||
id: ID!
|
||||
userId: String!
|
||||
type: String!
|
||||
title: String!
|
||||
content: String!
|
||||
channel: String!
|
||||
isRead: Boolean!
|
||||
status: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
relatedEntityType: String
|
||||
relatedEntityId: String
|
||||
groupId: String
|
||||
senderId: String
|
||||
templateId: String
|
||||
eventId: String
|
||||
readAt: DateTime
|
||||
}
|
||||
|
||||
type MarkAsReadRequest @key(fields: "id") {
|
||||
id: String
|
||||
user_id: String
|
||||
"""
|
||||
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
||||
"""
|
||||
scalar DateTime
|
||||
|
||||
type NotificationTemplate {
|
||||
id: ID!
|
||||
code: String!
|
||||
type: String!
|
||||
titleTemplate: String!
|
||||
contentTemplate: String!
|
||||
defaultChannels: [String!]!
|
||||
variables: [String!]!
|
||||
locale: String!
|
||||
status: String!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type NotificationPreference @key(fields: "id") {
|
||||
id: String
|
||||
user_id: String
|
||||
type: String
|
||||
channels: String
|
||||
frequency_limit: Int
|
||||
quiet_hours_start: String
|
||||
quiet_hours_end: String
|
||||
quiet_hours_timezone: String
|
||||
enabled: Boolean
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type NotificationTemplate @key(fields: "id") {
|
||||
id: String
|
||||
code: String
|
||||
type: String
|
||||
title_template: String
|
||||
content_template: String
|
||||
default_channels: String
|
||||
variables: String
|
||||
locale: String
|
||||
status: String
|
||||
created_at: String
|
||||
updated_at: String
|
||||
}
|
||||
|
||||
type GetTemplateRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
type UpdateTemplateRequest @key(fields: "id") {
|
||||
id: String
|
||||
title_template: String
|
||||
content_template: String
|
||||
default_channels: String
|
||||
variables: String
|
||||
status: String
|
||||
}
|
||||
|
||||
type DeleteTemplateRequest @key(fields: "id") {
|
||||
id: String
|
||||
}
|
||||
|
||||
input SendNotificationRequestInput {
|
||||
user_id: String
|
||||
type: String
|
||||
title: String
|
||||
content: String
|
||||
channel: String
|
||||
related_entity_type: String
|
||||
related_entity_id: String
|
||||
group_id: String
|
||||
sender_id: String
|
||||
template_id: String
|
||||
event_id: String
|
||||
}
|
||||
|
||||
input BatchSendNotificationRequestInput {
|
||||
items: SendNotificationRequest
|
||||
group_id: String
|
||||
}
|
||||
|
||||
input BatchSendNotificationResponseInput {
|
||||
ids: String
|
||||
failed: BatchSendFailure
|
||||
}
|
||||
|
||||
type BatchSendFailure {
|
||||
user_id: String
|
||||
error: String
|
||||
}
|
||||
|
||||
input ListNotificationsRequestInput {
|
||||
user_id: String
|
||||
only_unread: Boolean
|
||||
type: String
|
||||
page: Int
|
||||
page_size: Int
|
||||
}
|
||||
|
||||
input ListNotificationsResponseInput {
|
||||
notifications: Notification
|
||||
total: Int
|
||||
}
|
||||
|
||||
input GetUnreadCountRequestInput {
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input GetUnreadCountResponseInput {
|
||||
count: Int
|
||||
}
|
||||
|
||||
input BatchMarkAsReadRequestInput {
|
||||
ids: String
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input MarkAllAsReadRequestInput {
|
||||
user_id: String
|
||||
before: String
|
||||
}
|
||||
|
||||
input SearchNotificationsRequestInput {
|
||||
user_id: String
|
||||
query: String
|
||||
type: String
|
||||
page: Int
|
||||
page_size: Int
|
||||
}
|
||||
|
||||
input SearchNotificationsResponseInput {
|
||||
notifications: Notification
|
||||
total: Int
|
||||
}
|
||||
|
||||
input RecallNotificationRequestInput {
|
||||
group_id: String
|
||||
reason: String
|
||||
}
|
||||
|
||||
input RecallNotificationResponseInput {
|
||||
recalled_count: Int
|
||||
}
|
||||
|
||||
input GetPreferencesRequestInput {
|
||||
user_id: String
|
||||
}
|
||||
|
||||
input GetPreferencesResponseInput {
|
||||
preferences: NotificationPreference
|
||||
}
|
||||
|
||||
input UpdatePreferencesRequestInput {
|
||||
user_id: String
|
||||
preferences: NotificationPreference
|
||||
}
|
||||
|
||||
input CreateTemplateRequestInput {
|
||||
code: String
|
||||
type: String
|
||||
title_template: String
|
||||
content_template: String
|
||||
default_channels: String
|
||||
variables: String
|
||||
locale: String
|
||||
}
|
||||
|
||||
input ListTemplatesRequestInput {
|
||||
type: String
|
||||
status: String
|
||||
}
|
||||
|
||||
input ListTemplatesResponseInput {
|
||||
templates: NotificationTemplate
|
||||
}
|
||||
|
||||
input RenderTemplateRequestInput {
|
||||
code: String
|
||||
locale: String
|
||||
}
|
||||
|
||||
type RenderedNotification {
|
||||
title: String
|
||||
content: String
|
||||
}
|
||||
|
||||
type Empty {
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
notifications: [ListNotificationsResponse!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
unreadCount: GetUnreadCountResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
preferences: GetPreferencesResponse
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
template: NotificationTemplate
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
templates: [ListTemplatesResponse!]!
|
||||
}
|
||||
type Query {
|
||||
notifications(userId: ID!): [Notification!]!
|
||||
template(id: ID!): NotificationTemplate
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
import { Module } from "@nestjs/common";
|
||||
import { GraphQLModule } from "@nestjs/graphql";
|
||||
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { ApolloFederationDriver, ApolloDriverConfig } from "@nestjs/apollo";
|
||||
import { join } from "node:path";
|
||||
import { GraphqlContext } from "@edu/shared-ts/federation";
|
||||
import { NotificationResolver } from "./resolvers/notification.resolver.js";
|
||||
@@ -24,7 +24,7 @@ import { getRedis } from "../shared/redis/redis.client.js";
|
||||
@Module({
|
||||
imports: [
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
driver: ApolloFederationDriver,
|
||||
// Federation 2 子图
|
||||
autoSchemaFile: {
|
||||
path: join(process.cwd(), "src/graphql/generated/schema.graphql"),
|
||||
|
||||
@@ -20,12 +20,17 @@ export class RouterAuthGuard extends BaseRouterAuthGuard {
|
||||
}
|
||||
|
||||
override canActivate(ctx: ExecutionContext): boolean {
|
||||
// DEV_MODE 优先放行(开发态跳过所有校验)
|
||||
if (process.env.DEV_MODE === "true") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const req = ctx.switchToHttp().getRequest<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
// 仅 GraphQL 端点需要校验,REST 路由放行
|
||||
if (!req.url?.startsWith("/graphql")) {
|
||||
if (!req?.url?.startsWith("/graphql")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@edu/shared-ts/env-loader";
|
||||
import "reflect-metadata";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module.js";
|
||||
|
||||
Reference in New Issue
Block a user