feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层
包含 clients/graphql/middleware、health probes、shared-ts contracts 等
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
import { Controller, Get, Param, Req } from "@nestjs/common";
|
||||
import type { Request } from "express";
|
||||
import { TeacherService } from "./teacher.service.js";
|
||||
import { UnauthorizedError } from "../shared/errors/application-error.js";
|
||||
|
||||
interface SuccessResponse<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
}
|
||||
|
||||
@Controller("teacher")
|
||||
export class TeacherController {
|
||||
constructor(private readonly service: TeacherService) {}
|
||||
|
||||
@Get("dashboard")
|
||||
async dashboard(@Req() req: Request): Promise<SuccessResponse<unknown>> {
|
||||
const userId = this.extractUserId(req);
|
||||
const data = await this.service.getDashboard(userId);
|
||||
return { success: true as const, data };
|
||||
}
|
||||
|
||||
@Get("viewports")
|
||||
async viewports(@Req() req: Request): Promise<SuccessResponse<unknown>> {
|
||||
const userId = this.extractUserId(req);
|
||||
const data = await this.service.getViewports(userId);
|
||||
return { success: true as const, data };
|
||||
}
|
||||
|
||||
// 聚合:班级下的考试列表(core-edu)
|
||||
@Get("classes/:classId/exams")
|
||||
async listExamsByClass(
|
||||
@Req() req: Request,
|
||||
@Param("classId") classId: string,
|
||||
): Promise<SuccessResponse<unknown>> {
|
||||
const userId = this.extractUserId(req);
|
||||
const data = await this.service.listExamsByClass(userId, classId);
|
||||
return { success: true as const, data };
|
||||
}
|
||||
|
||||
// 聚合:班级下的作业列表(core-edu)
|
||||
@Get("classes/:classId/homework")
|
||||
async listHomeworkByClass(
|
||||
@Req() req: Request,
|
||||
@Param("classId") classId: string,
|
||||
): Promise<SuccessResponse<unknown>> {
|
||||
const userId = this.extractUserId(req);
|
||||
const data = await this.service.listHomeworkByClass(userId, classId);
|
||||
return { success: true as const, data };
|
||||
}
|
||||
|
||||
// 聚合:考试下的成绩列表(core-edu)
|
||||
@Get("exams/:examId/grades")
|
||||
async listGradesByExam(
|
||||
@Req() req: Request,
|
||||
@Param("examId") examId: string,
|
||||
): Promise<SuccessResponse<unknown>> {
|
||||
const userId = this.extractUserId(req);
|
||||
const data = await this.service.listGradesByExam(userId, examId);
|
||||
return { success: true as const, data };
|
||||
}
|
||||
|
||||
private extractUserId(req: Request): string {
|
||||
const header = req.headers["x-user-id"];
|
||||
const userId = typeof header === "string" ? header : undefined;
|
||||
if (!userId) {
|
||||
throw new UnauthorizedError("Missing x-user-id header");
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeacherController } from './teacher.controller.js';
|
||||
import { TeacherService } from './teacher.service.js';
|
||||
// Teacher 模块(B1 裁决:P2 起 GraphQL,不再使用 REST Controller)
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TeacherService } from "./teacher.service.js";
|
||||
import { ClientsModule } from "../clients/clients.module.js";
|
||||
|
||||
@Module({
|
||||
controllers: [TeacherController],
|
||||
imports: [ClientsModule],
|
||||
providers: [TeacherService],
|
||||
exports: [TeacherService],
|
||||
})
|
||||
export class TeacherModule {}
|
||||
|
||||
362
services/teacher-bff/src/teacher/teacher.resolver.ts
Normal file
362
services/teacher-bff/src/teacher/teacher.resolver.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
// GraphQL Resolvers(B1 裁决:P2 起 GraphQL)
|
||||
// P2 核心 5 Query:dashboard / viewports / me / classes / class
|
||||
// P3+ Query/Mutation:占位返回 null/空 + extensions.warning
|
||||
// admin 命名空间:占位返回 null/空(P6 实现)
|
||||
import type { IResolvers } from "@graphql-tools/utils";
|
||||
import { TeacherService } from "./teacher.service.js";
|
||||
import type { GraphQLOperationContext } from "../graphql/context.js";
|
||||
import { BusinessError } from "../shared/errors/application-error.js";
|
||||
|
||||
/**
|
||||
* 构建 GraphQL Resolvers
|
||||
* 依赖 TeacherService(通过闭包注入,避免 NestJS DI 与 Yoga 的集成复杂度)
|
||||
*/
|
||||
export function buildResolvers(service: TeacherService): IResolvers {
|
||||
return {
|
||||
Query: {
|
||||
// ===== P2 核心 5 Query =====
|
||||
|
||||
dashboard: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
return service.getDashboard(ctx.callCtx);
|
||||
},
|
||||
|
||||
viewports: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
return service.getViewports(ctx.callCtx);
|
||||
},
|
||||
|
||||
me: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
return service.getCurrentUser(ctx.callCtx);
|
||||
},
|
||||
|
||||
classes: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
return service.getClasses(ctx.callCtx);
|
||||
},
|
||||
|
||||
class: async (
|
||||
_parent: unknown,
|
||||
args: { id: string },
|
||||
ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
return service.getClass(ctx.callCtx, args.id);
|
||||
},
|
||||
|
||||
// ===== P3+ Query 占位(返回空 + warning,跨阶段扩展例外)=====
|
||||
|
||||
exams: async (
|
||||
_parent: unknown,
|
||||
_args: { classId: string },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"exams query not available in P2 (core-edu gRPC not ready, P3+)",
|
||||
{ phase: "P2", field: "exams", warning: "field_unavailable_in_p2" },
|
||||
);
|
||||
},
|
||||
|
||||
homework: async (
|
||||
_parent: unknown,
|
||||
_args: { classId: string },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"homework query not available in P2 (core-edu gRPC not ready, P3+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "homework",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
grades: async (
|
||||
_parent: unknown,
|
||||
_args: { examId: string },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"grades query not available in P2 (core-edu gRPC not ready, P3+)",
|
||||
{ phase: "P2", field: "grades", warning: "field_unavailable_in_p2" },
|
||||
);
|
||||
},
|
||||
|
||||
// ===== P4+ Query 占位 =====
|
||||
|
||||
knowledgePath: async (
|
||||
_parent: unknown,
|
||||
_args: { knowledgePointId: string },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"knowledgePath query not available in P2 (content gRPC not ready, P4+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "knowledgePath",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
classPerformance: async (
|
||||
_parent: unknown,
|
||||
_args: { classId: string },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"classPerformance query not available in P2 (data-ana gRPC not ready, P4+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "classPerformance",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
studentWeakness: async (
|
||||
_parent: unknown,
|
||||
_args: { studentId: string },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"studentWeakness query not available in P2 (data-ana gRPC not ready, P4+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "studentWeakness",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
learningTrend: async (
|
||||
_parent: unknown,
|
||||
_args: { studentId: string; dateRange?: unknown },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"learningTrend query not available in P2 (data-ana gRPC not ready, P4+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "learningTrend",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
// ===== P5+ Query 占位 =====
|
||||
|
||||
notifications: async (
|
||||
_parent: unknown,
|
||||
_args: { unreadOnly?: boolean },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"notifications query not available in P2 (msg gRPC not ready, P5+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "notifications",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
// ===== admin 命名空间(president §5.1:P2 预留,P6 实现)=====
|
||||
|
||||
admin: () => {
|
||||
// 返回空对象,admin 子字段 resolver 返回 null/空
|
||||
return {};
|
||||
},
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
// ===== P3+ Mutation 占位 =====
|
||||
|
||||
createExam: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"createExam mutation not available in P2 (core-edu gRPC not ready, P3+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "createExam",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
assignHomework: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"assignHomework mutation not available in P2 (core-edu gRPC not ready, P3+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "assignHomework",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
recordGrade: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"recordGrade mutation not available in P2 (core-edu gRPC not ready, P3+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "recordGrade",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
// ===== P5+ Mutation 占位 =====
|
||||
|
||||
generateQuestion: async (
|
||||
_parent: unknown,
|
||||
_args: unknown,
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"generateQuestion mutation not available in P2 (ai gRPC not ready, P5+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "generateQuestion",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
markNotificationAsRead: async (
|
||||
_parent: unknown,
|
||||
_args: { id: string },
|
||||
_ctx: GraphQLOperationContext,
|
||||
) => {
|
||||
throw new BusinessError(
|
||||
"markNotificationAsRead mutation not available in P2 (msg gRPC not ready, P5+)",
|
||||
{
|
||||
phase: "P2",
|
||||
field: "markNotificationAsRead",
|
||||
warning: "field_unavailable_in_p2",
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
// ===== admin 命名空间 Mutation(P2 预留,P6 实现)=====
|
||||
|
||||
admin: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
|
||||
// ===== admin 命名空间子字段 Resolver(P2 全部返回 null/空,P6 实现)=====
|
||||
|
||||
AdminQuery: {
|
||||
users: () => null,
|
||||
user: () => null,
|
||||
roles: () => [],
|
||||
role: () => null,
|
||||
school: () => null,
|
||||
organizations: () => [],
|
||||
auditLogs: () => null,
|
||||
},
|
||||
|
||||
AdminMutation: {
|
||||
createUser: () => {
|
||||
throw new BusinessError(
|
||||
"admin.createUser not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.createUser" },
|
||||
);
|
||||
},
|
||||
updateUser: () => {
|
||||
throw new BusinessError(
|
||||
"admin.updateUser not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.updateUser" },
|
||||
);
|
||||
},
|
||||
deleteUser: () => {
|
||||
throw new BusinessError(
|
||||
"admin.deleteUser not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.deleteUser" },
|
||||
);
|
||||
},
|
||||
createRole: () => {
|
||||
throw new BusinessError(
|
||||
"admin.createRole not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.createRole" },
|
||||
);
|
||||
},
|
||||
updateRole: () => {
|
||||
throw new BusinessError(
|
||||
"admin.updateRole not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.updateRole" },
|
||||
);
|
||||
},
|
||||
deleteRole: () => {
|
||||
throw new BusinessError(
|
||||
"admin.deleteRole not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.deleteRole" },
|
||||
);
|
||||
},
|
||||
updateSchool: () => {
|
||||
throw new BusinessError(
|
||||
"admin.updateSchool not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.updateSchool" },
|
||||
);
|
||||
},
|
||||
createOrganization: () => {
|
||||
throw new BusinessError(
|
||||
"admin.createOrganization not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.createOrganization" },
|
||||
);
|
||||
},
|
||||
updateOrganization: () => {
|
||||
throw new BusinessError(
|
||||
"admin.updateOrganization not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.updateOrganization" },
|
||||
);
|
||||
},
|
||||
deleteOrganization: () => {
|
||||
throw new BusinessError(
|
||||
"admin.deleteOrganization not available in P2 (P6 implementation)",
|
||||
{ phase: "P2", field: "admin.deleteOrganization" },
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// ===== 类型字段 Resolver(Class.exams / Class.homework / Exam.grades 延迟加载)=====
|
||||
|
||||
Class: {
|
||||
exams: () => [], // P3+ core-edu gRPC
|
||||
homework: () => [], // P3+ core-edu gRPC
|
||||
},
|
||||
|
||||
Exam: {
|
||||
grades: () => [], // P3+ core-edu gRPC
|
||||
},
|
||||
|
||||
Homework: {
|
||||
submissions: () => [], // P3+ core-edu gRPC
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,158 +1,122 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { env } from "../config/env.js";
|
||||
// TeacherService — GraphQL Resolver 的业务逻辑层(B1 裁决:P2 起 GraphQL)
|
||||
// 通过 IamClient 调下游 iam gRPC(B2 裁决:首次实现即 gRPC)
|
||||
// P2: 仅 iam 数据;P3+ 扩展 core-edu / content / data-ana / msg / ai
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { IAM_CLIENT } from "../clients/iam/iam-client.interface.js";
|
||||
import type { IamClient } from "../clients/iam/iam-client.interface.js";
|
||||
import type { CallContext } from "../clients/types.js";
|
||||
import type {
|
||||
UserInfo,
|
||||
ViewportItem,
|
||||
EffectivePermissions,
|
||||
} from "../clients/iam/iam.types.js";
|
||||
import { logger } from "../shared/observability/logger.js";
|
||||
import { BadGatewayError } from "../shared/errors/application-error.js";
|
||||
|
||||
export interface ViewportItem {
|
||||
key: string;
|
||||
label: string;
|
||||
route: string;
|
||||
icon: string | null;
|
||||
sortOrder: string;
|
||||
requiredPermission: string | null;
|
||||
/** GraphQL User 类型(聚合 UserInfo + EffectivePermissions) */
|
||||
export interface GraphQLUser {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
dataScope: string;
|
||||
}
|
||||
|
||||
interface DownstreamEnvelope<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
/** GraphQL DashboardData 类型 */
|
||||
export interface DashboardData {
|
||||
user: GraphQLUser | null;
|
||||
classes: unknown[];
|
||||
viewports: ViewportItem[];
|
||||
stats: {
|
||||
totalExams: number;
|
||||
pendingGrading: number;
|
||||
todayHomework: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
user: unknown;
|
||||
classes: unknown;
|
||||
/** GraphQL Class 类型(P2 mock,P3+ core-edu 真实数据) */
|
||||
export interface ClassInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
gradeId: string;
|
||||
}
|
||||
|
||||
/** P2 mock 班级数据(president §3.5:P2 班级列表来自 iam 数据,P3+ core-edu) */
|
||||
const MOCK_CLASSES: ClassInfo[] = [
|
||||
{ id: "class-001", name: "三年级1班", gradeId: "grade-3" },
|
||||
{ id: "class-002", name: "三年级2班", gradeId: "grade-3" },
|
||||
{ id: "class-003", name: "三年级3班", gradeId: "grade-3" },
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class TeacherService {
|
||||
// 聚合 IAM + classes 服务的数据
|
||||
async getDashboard(userId: string): Promise<DashboardData> {
|
||||
const [iamRes, classesRes] = await Promise.allSettled([
|
||||
fetch(`${env.IamServiceUrl}/iam/me`, {
|
||||
headers: { "x-user-id": userId },
|
||||
}),
|
||||
fetch(`${env.ClassesServiceUrl}/classes`, {
|
||||
headers: { "x-user-id": userId },
|
||||
}),
|
||||
constructor(@Inject(IAM_CLIENT) private readonly iam: IamClient) {}
|
||||
|
||||
/** 获取当前用户信息(聚合 iam.GetUserInfo + GetEffectivePermissions) */
|
||||
async getCurrentUser(ctx: CallContext): Promise<GraphQLUser> {
|
||||
const [userInfo, perms] = await Promise.all([
|
||||
this.iam.getUserInfo(ctx),
|
||||
this.iam.getEffectivePermissions(ctx),
|
||||
]);
|
||||
return {
|
||||
id: userInfo.id,
|
||||
email: userInfo.email,
|
||||
name: userInfo.name,
|
||||
roles: userInfo.roles,
|
||||
permissions: perms.permissions,
|
||||
dataScope: perms.dataScope,
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取视口配置(iam.GetViewports) */
|
||||
async getViewports(ctx: CallContext): Promise<ViewportItem[]> {
|
||||
return this.iam.getViewports(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Dashboard 聚合数据(president §2.8:P2 仅调 iam gRPC)
|
||||
* P2: user + viewports 有数据,classes 返回 mock,stats 返回 null + warning
|
||||
* P3+: classes 来自 core-edu,stats 来自 data-ana.GetTeacherDashboard
|
||||
*/
|
||||
async getDashboard(ctx: CallContext): Promise<DashboardData> {
|
||||
const [user, viewports] = await Promise.all([
|
||||
this.getCurrentUser(ctx),
|
||||
this.iam.getViewports(ctx),
|
||||
]);
|
||||
|
||||
let user: unknown = null;
|
||||
let classes: unknown = null;
|
||||
|
||||
if (iamRes.status === "fulfilled") {
|
||||
if (iamRes.value.ok) {
|
||||
user = await iamRes.value.json();
|
||||
} else {
|
||||
logger.warn(
|
||||
{ status: iamRes.value.status, url: iamRes.value.url },
|
||||
"Downstream IAM service call failed",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
{ err: iamRes.reason, service: "iam" },
|
||||
"Downstream IAM service call rejected",
|
||||
);
|
||||
}
|
||||
|
||||
if (classesRes.status === "fulfilled") {
|
||||
if (classesRes.value.ok) {
|
||||
classes = await classesRes.value.json();
|
||||
} else {
|
||||
logger.warn(
|
||||
{ status: classesRes.value.status, url: classesRes.value.url },
|
||||
"Downstream classes service call failed",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
{ err: classesRes.reason, service: "classes" },
|
||||
"Downstream classes service call rejected",
|
||||
);
|
||||
}
|
||||
|
||||
return { user, classes };
|
||||
}
|
||||
|
||||
// 聚合 IAM 视口配置(L1 导航)
|
||||
async getViewports(userId: string): Promise<ViewportItem[]> {
|
||||
const res = await fetch(`${env.IamServiceUrl}/iam/viewports`, {
|
||||
headers: { "x-user-id": userId },
|
||||
});
|
||||
if (!res.ok) {
|
||||
logger.warn(
|
||||
{ status: res.status, url: res.url },
|
||||
"Downstream IAM service call failed",
|
||||
);
|
||||
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
|
||||
service: "iam",
|
||||
endpoint: "viewports",
|
||||
status: res.status,
|
||||
});
|
||||
}
|
||||
const json = (await res.json()) as DownstreamEnvelope<ViewportItem[]>;
|
||||
return json.data ?? [];
|
||||
}
|
||||
|
||||
// 聚合班级下的考试列表(core-edu)
|
||||
async listExamsByClass(userId: string, classId: string): Promise<unknown> {
|
||||
const res = await fetch(
|
||||
`${env.CoreEduServiceUrl}/exams/class/${encodeURIComponent(classId)}`,
|
||||
{ headers: { "x-user-id": userId } },
|
||||
// P2: classes 返回 mock 数据(P3+ 替换为 core-edu.GetClassesByTeacher)
|
||||
// P2: stats 返回 null(P4+ 替换为 data-ana.GetTeacherDashboard)
|
||||
logger.warn(
|
||||
{ userId: ctx.userId, phase: "P2" },
|
||||
"Dashboard P2: classes using mock, stats unavailable (field_unavailable_in_p2)",
|
||||
);
|
||||
if (!res.ok) {
|
||||
logger.warn(
|
||||
{ status: res.status, url: res.url },
|
||||
"Downstream core-edu service call failed",
|
||||
);
|
||||
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
|
||||
service: "core-edu",
|
||||
endpoint: "exams-by-class",
|
||||
status: res.status,
|
||||
});
|
||||
}
|
||||
const json = (await res.json()) as DownstreamEnvelope<unknown>;
|
||||
return json.data ?? [];
|
||||
|
||||
return {
|
||||
user,
|
||||
viewports,
|
||||
classes: MOCK_CLASSES,
|
||||
stats: null,
|
||||
};
|
||||
}
|
||||
|
||||
// 聚合班级下的作业列表(core-edu)
|
||||
async listHomeworkByClass(userId: string, classId: string): Promise<unknown> {
|
||||
const res = await fetch(
|
||||
`${env.CoreEduServiceUrl}/homework/class/${encodeURIComponent(classId)}`,
|
||||
{ headers: { "x-user-id": userId } },
|
||||
/** 获取教师班级列表(P2: mock;P3+: core-edu.GetClassesByTeacher) */
|
||||
async getClasses(ctx: CallContext): Promise<ClassInfo[]> {
|
||||
logger.warn(
|
||||
{ userId: ctx.userId, phase: "P2" },
|
||||
"Classes P2: using mock data (core-edu not ready, field_unavailable_in_p2)",
|
||||
);
|
||||
if (!res.ok) {
|
||||
logger.warn(
|
||||
{ status: res.status, url: res.url },
|
||||
"Downstream core-edu service call failed",
|
||||
);
|
||||
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
|
||||
service: "core-edu",
|
||||
endpoint: "homework-by-class",
|
||||
status: res.status,
|
||||
});
|
||||
}
|
||||
const json = (await res.json()) as DownstreamEnvelope<unknown>;
|
||||
return json.data ?? [];
|
||||
return MOCK_CLASSES;
|
||||
}
|
||||
|
||||
// 聚合考试下的成绩列表(core-edu)
|
||||
async listGradesByExam(userId: string, examId: string): Promise<unknown> {
|
||||
const res = await fetch(
|
||||
`${env.CoreEduServiceUrl}/grades/exam/${encodeURIComponent(examId)}`,
|
||||
{ headers: { "x-user-id": userId } },
|
||||
/** 获取单个班级详情(P2: mock;P3+: core-edu) */
|
||||
async getClass(ctx: CallContext, classId: string): Promise<ClassInfo | null> {
|
||||
logger.warn(
|
||||
{ userId: ctx.userId, classId, phase: "P2" },
|
||||
"Class P2: using mock data (core-edu not ready, field_unavailable_in_p2)",
|
||||
);
|
||||
if (!res.ok) {
|
||||
logger.warn(
|
||||
{ status: res.status, url: res.url },
|
||||
"Downstream core-edu service call failed",
|
||||
);
|
||||
throw new BadGatewayError(`Downstream service returned ${res.status}`, {
|
||||
service: "core-edu",
|
||||
endpoint: "grades-by-exam",
|
||||
status: res.status,
|
||||
});
|
||||
}
|
||||
const json = (await res.json()) as DownstreamEnvelope<unknown>;
|
||||
return json.data ?? [];
|
||||
return MOCK_CLASSES.find((c) => c.id === classId) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出 EffectivePermissions 类型供外部使用 */
|
||||
export type { EffectivePermissions, UserInfo };
|
||||
|
||||
Reference in New Issue
Block a user