123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
// 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";
|
||
|
||
/** GraphQL User 类型(聚合 UserInfo + EffectivePermissions) */
|
||
export interface GraphQLUser {
|
||
id: string;
|
||
email: string;
|
||
name: string;
|
||
roles: string[];
|
||
permissions: string[];
|
||
dataScope: string;
|
||
}
|
||
|
||
/** GraphQL DashboardData 类型 */
|
||
export interface DashboardData {
|
||
user: GraphQLUser | null;
|
||
classes: unknown[];
|
||
viewports: ViewportItem[];
|
||
stats: {
|
||
totalExams: number;
|
||
pendingGrading: number;
|
||
todayHomework: number;
|
||
} | null;
|
||
}
|
||
|
||
/** 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 {
|
||
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),
|
||
]);
|
||
|
||
// 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)",
|
||
);
|
||
|
||
return {
|
||
user,
|
||
viewports,
|
||
classes: MOCK_CLASSES,
|
||
stats: null,
|
||
};
|
||
}
|
||
|
||
/** 获取教师班级列表(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)",
|
||
);
|
||
return MOCK_CLASSES;
|
||
}
|
||
|
||
/** 获取单个班级详情(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)",
|
||
);
|
||
return MOCK_CLASSES.find((c) => c.id === classId) ?? null;
|
||
}
|
||
}
|
||
|
||
/** 导出 EffectivePermissions 类型供外部使用 */
|
||
export type { EffectivePermissions, UserInfo };
|