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:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user