NestJS (6 services): implement @RequirePermission decorator with SetMetadata+Reflector, register APP_GUARD globally, fix as assertions to type guards, add explicit return types, fix import type for express, fix /metrics implicit any, replace native Error with ApplicationError, remove typeorm remnants, register LifecycleService. teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward real userId to downstream, log downstream failures, migrate health controller to shared/health. Go (2 services): interface to any, doc comments, CORS dev whitelist, JWT secret fail-fast, push-gateway internal API auth, metrics and readyz endpoints, remove dead code. Python (2 services): lifespan return type, dev_mode to bool, data-ana APIRouter, ai POST body model, ClickHouse async wrapping.
159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
import { Injectable } from "@nestjs/common";
|
||
import { env } from "../config/env.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;
|
||
}
|
||
|
||
interface DownstreamEnvelope<T> {
|
||
success: boolean;
|
||
data?: T;
|
||
}
|
||
|
||
interface DashboardData {
|
||
user: unknown;
|
||
classes: unknown;
|
||
}
|
||
|
||
@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 },
|
||
}),
|
||
]);
|
||
|
||
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 } },
|
||
);
|
||
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 ?? [];
|
||
}
|
||
|
||
// 聚合班级下的作业列表(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 } },
|
||
);
|
||
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 ?? [];
|
||
}
|
||
|
||
// 聚合考试下的成绩列表(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 } },
|
||
);
|
||
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 ?? [];
|
||
}
|
||
}
|