Files
gongxue-base/apps/server/src/rbac/rbac.service.ts

716 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import {
Permission,
Role,
User,
Class,
ClassStudent,
ClassTeacher,
ClassSchedule,
Student,
} from '../entities';
const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [
{ code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' },
{ code: 'profile:view', name: '查看个人资料', group: 'profile' },
{ code: 'notification:view', name: '查看通知', group: 'notification' },
{ code: 'student:view', name: '查看学生管理', group: 'student' },
{ code: 'student:basic-view', name: '查看学生基础信息', group: 'student-scope' },
{ code: 'teacher-workspace:view', name: '查看教师工作台', group: 'teacher-workspace' },
{ code: 'teacher:view', name: '查看教师', group: 'teacher' },
{ code: 'teacher:edit', name: '编辑教师', group: 'teacher' },
{ code: 'student:create', name: '新增学生', group: 'student' },
{ code: 'student:edit', name: '编辑学生', group: 'student' },
{ code: 'student:delete', name: '删除学生', group: 'student' },
{ code: 'student:import', name: '导入学生', group: 'student' },
{ code: 'student:export', name: '导出学生', group: 'student' },
{ code: 'room:view', name: '查看宿舍', group: 'room' },
{ code: 'room:create', name: '新增宿舍', group: 'room' },
{ code: 'room:edit', name: '编辑宿舍', group: 'room' },
{ code: 'room:delete', name: '删除宿舍', group: 'room' },
{ code: 'occupancy:view', name: '查看入住', group: 'occupancy' },
{ code: 'occupancy:checkin', name: '办理入住', group: 'occupancy' },
{ code: 'occupancy:checkout', name: '办理退宿', group: 'occupancy' },
{ code: 'occupancy:transfer', name: '调换宿舍', group: 'occupancy' },
{ code: 'occupancy:delete', name: '删除入住记录', group: 'occupancy' },
{ code: 'expense:view', name: '查看费用', group: 'expense' },
{ code: 'expense:create', name: '录入费用', group: 'expense' },
{ code: 'expense:edit', name: '编辑费用', group: 'expense' },
{ code: 'expense:delete', name: '删除费用', group: 'expense' },
{ code: 'bill:view', name: '查看账单', group: 'bill' },
{ code: 'bill:generate', name: '生成账单', group: 'bill' },
{ code: 'bill:confirm', name: '确认账单', group: 'bill' },
{ code: 'bill:delete', name: '删除账单', group: 'bill' },
{ code: 'bill:export-excel', name: '导出 Excel', group: 'bill' },
{ code: 'bill:export-pdf', name: '导出 PDF', group: 'bill' },
{ code: 'deposit:view', name: '查看押金', group: 'deposit' },
{ code: 'deposit:create', name: '新增押金', group: 'deposit' },
{ code: 'deposit:edit', name: '编辑押金', group: 'deposit' },
{ code: 'deposit:delete', name: '删除押金', group: 'deposit' },
{ code: 'deposit:refund', name: '直接退还押金', group: 'deposit' },
{ code: 'classroom:view', name: '查看教室', group: 'classroom' },
{ code: 'classroom:create', name: '新增教室', group: 'classroom' },
{ code: 'classroom:edit', name: '编辑教室', group: 'classroom' },
{ code: 'classroom:delete', name: '删除教室', group: 'classroom' },
{ code: 'organization:view', name: '查看机构', group: 'organization' },
{ code: 'organization:create', name: '新增机构', group: 'organization' },
{ code: 'organization:edit', name: '编辑机构', group: 'organization' },
{ code: 'organization:delete', name: '归档机构', group: 'organization' },
{ code: 'rental:view', name: '查看租赁订单', group: 'rental' },
{ code: 'rental:create', name: '新增租赁订单', group: 'rental' },
{ code: 'rental:edit', name: '编辑租赁订单', group: 'rental' },
{ code: 'rental:delete', name: '删除租赁订单', group: 'rental' },
{ code: 'log:view', name: '查看操作日志', group: 'log' },
{ code: 'log:create', name: '写入操作日志', group: 'log' },
{ code: 'user:view', name: '查看用户', group: 'user' },
{ code: 'user:create', name: '创建用户', group: 'user' },
{ code: 'user:edit', name: '编辑用户', group: 'user' },
{ code: 'user:delete', name: '删除用户', group: 'user' },
{ code: 'user:reset-password', name: '重置密码', group: 'user' },
{ code: 'role:view', name: '查看角色', group: 'role' },
{ code: 'role:create', name: '创建角色', group: 'role' },
{ code: 'role:edit', name: '编辑角色', group: 'role' },
{ code: 'role:delete', name: '删除角色', group: 'role' },
{ code: 'class:view', name: '查看班级', group: 'class' },
{ code: 'class:create', name: '创建班级', group: 'class' },
{ code: 'class:edit', name: '编辑班级', group: 'class' },
{ code: 'class:delete', name: '删除班级', group: 'class' },
{ code: 'schedule:view', name: '查看排课', group: 'schedule' },
{ code: 'schedule:create', name: '创建排课', group: 'schedule' },
{ code: 'schedule:edit', name: '编辑排课', group: 'schedule' },
{ code: 'schedule:delete', name: '删除排课', group: 'schedule' },
{ code: 'attendance:view', name: '查看考勤', group: 'attendance' },
{ code: 'attendance:create', name: '新增考勤', group: 'attendance' },
{ code: 'attendance:edit', name: '编辑全部考勤', group: 'attendance' },
{ code: 'attendance:self-edit', name: '编辑任教班级考勤', group: 'attendance-scope' },
{ code: 'attendance:export', name: '导出考勤', group: 'attendance' },
{ code: 'attendance:generate', name: '按课表生成考勤', group: 'attendance' },
{ code: 'learning:create', name: '创建学习任务', group: 'learning' },
{ code: 'learning:edit', name: '编辑学习任务', group: 'learning' },
{ code: 'learning:delete', name: '删除学习任务', group: 'learning' },
{ code: 'exam:create', name: '创建考试', group: 'exam' },
{ code: 'exam:edit', name: '编辑考试', group: 'exam' },
{ code: 'exam:delete', name: '删除考试', group: 'exam' },
{ code: 'sync:trigger', name: '触发数据同步', group: 'sync' },
{ code: 'sync:read', name: '查看同步状态', group: 'sync' },
{ code: 'integration:trigger', name: '触发集成', group: 'integration' },
{ code: 'integration:read', name: '查看集成状态', group: 'integration' },
{ code: 'department:view', name: '查看部门', group: 'department' },
{ code: 'department:edit', name: '编辑部门', group: 'department' },
{ code: 'department:delete', name: '删除部门', group: 'department' },
{ code: 'ai:config:read', name: '查看 AI 配置', group: 'ai' },
{ code: 'ai:config:write', name: '修改 AI 配置', group: 'ai' },
{ code: 'ai:config:test', name: '测试 AI 连接', group: 'ai' },
];
export const PRESET_ROLES: Array<{
name: string;
code: string;
description: string;
isSystem: boolean;
permissionGroups: string[];
extraPermissions?: string[];
legacyNames?: string[];
legacyCodes?: string[];
}> = [
{
name: '超级管理员',
code: 'super_admin',
description: '系统初始化、应急维护和全局权限处理',
isSystem: true,
permissionGroups: [],
legacyNames: ['超管', 'super_admin'],
},
{
name: '任课老师',
code: 'teacher',
description: '查看自己的排课、今日课程和任教班级考勤',
isSystem: true,
permissionGroups: ['notification', 'profile'],
extraPermissions: [
'teacher-workspace:view',
'schedule:view',
'attendance:view',
'attendance:create',
'attendance:self-edit',
],
legacyNames: ['老师'],
},
{
name: '教务管理员',
code: 'academic',
description: '管理学生、班级、教师、全局排课和历史考勤',
isSystem: true,
permissionGroups: [
'student',
'class',
'schedule',
'attendance',
'classroom',
'learning',
'exam',
'dashboard',
'notification',
'profile',
],
extraPermissions: [
'teacher-workspace:view',
'teacher:view',
'teacher:edit',
'sync:read',
'sync:trigger',
],
legacyNames: ['教务'],
},
{
name: '住宿运营管理员',
code: 'accommodation_operations',
description: '管理宿舍、入住、住宿费用、账单、押金和退宿结算',
isSystem: true,
permissionGroups: [
'room',
'occupancy',
'expense',
'bill',
'deposit',
'dashboard',
'notification',
'profile',
],
extraPermissions: ['student:basic-view', 'organization:view'],
legacyNames: ['宿管老师', '宿管', '财务'],
legacyCodes: ['dormitory_supervisor', 'dorm_manager', 'finance'],
},
{
name: '教室运营管理员',
code: 'classroom_operations',
description: '管理教室、教室排期、外部机构和租赁订单',
isSystem: true,
permissionGroups: ['classroom', 'rental', 'organization', 'notification', 'profile'],
legacyNames: ['机构负责人'],
legacyCodes: ['institution_head'],
},
{
name: '系统管理员',
code: 'system_admin',
description: '管理账号、角色、日志、同步和系统配置',
isSystem: true,
permissionGroups: [
'user',
'role',
'log',
'integration',
'sync',
'ai',
'department',
'notification',
'profile',
],
},
];
@Injectable()
export class RbacService {
private readonly logger = new Logger(RbacService.name);
constructor(
@InjectRepository(Permission) private permRepo: Repository<Permission>,
@InjectRepository(Role) private roleRepo: Repository<Role>,
@InjectRepository(User) private userRepo: Repository<User>,
@InjectRepository(Class) private classRepo: Repository<Class>,
@InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
@InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
@InjectRepository(ClassSchedule) private classScheduleRepo: Repository<ClassSchedule>,
@InjectRepository(Student) private studentRepo: Repository<Student>,
) {}
private async findLegacyPresetRole(preset: (typeof PRESET_ROLES)[number]): Promise<Role | null> {
for (const code of preset.legacyCodes ?? []) {
const role = await this.roleRepo.findOne({ where: { code } });
if (role) return role;
}
for (const name of preset.legacyNames ?? []) {
const role = await this.roleRepo.findOne({ where: { name } });
if (role) return role;
}
return null;
}
async seedData(): Promise<void> {
// Step 1: 幂等插入所有权限点(先查后插,兼容 SQLite/MySQL
for (const p of PRESET_PERMISSIONS) {
const exists = await this.permRepo.findOne({ where: { code: p.code } });
if (!exists) {
await this.permRepo.save(this.permRepo.create(p));
}
}
const allPerms = await this.permRepo.find();
// Step 2: 幂等插入预置角色
for (const r of PRESET_ROLES) {
const exists =
(await this.roleRepo.findOne({ where: { code: r.code } })) ||
(await this.roleRepo.findOne({ where: { name: r.name } })) ||
(await this.findLegacyPresetRole(r));
if (!exists) {
await this.roleRepo.save(
this.roleRepo.create({
name: r.name,
code: r.code,
description: r.description,
isSystem: r.isSystem,
}),
);
}
}
const allRoles = await this.roleRepo.find({ relations: ['permissions', 'users'] });
// Step 3: 合并旧角色并构建新的职责权限矩阵
for (const preset of PRESET_ROLES) {
const matchesPreset = (role: Role) =>
role.name === preset.name ||
role.code === preset.code ||
preset.legacyNames?.includes(role.name) ||
preset.legacyCodes?.includes(role.code);
const candidates = allRoles.filter(matchesPreset);
const role = candidates.find((candidate) => candidate.code === preset.code) ?? candidates[0];
if (!role) continue;
const duplicateRoles = candidates.filter((candidate) => candidate.id !== role.id);
if (duplicateRoles.length > 0) {
for (const duplicate of duplicateRoles) {
for (const relatedUser of duplicate.users ?? []) {
const user = await this.userRepo.findOne({
where: { id: relatedUser.id },
relations: ['roles'],
});
if (!user) continue;
const remainingRoles = (user.roles ?? []).filter(
(assignedRole) => assignedRole.id !== duplicate.id && assignedRole.id !== role.id,
);
user.roles = [...remainingRoles, role];
await this.userRepo.save(user);
}
await this.roleRepo.remove(duplicate);
}
}
if (role.code !== preset.code || role.name !== preset.name || role.description !== preset.description) {
role.code = preset.code;
role.name = preset.name;
role.description = preset.description;
role.isSystem = preset.isSystem;
role.status = 1;
await this.roleRepo.save(role);
}
let perms: Permission[];
if (preset.permissionGroups.length === 0) {
// 超管:全部权限
perms = allPerms;
} else {
// 按 group 匹配 + 额外权限(如老师的 student:view
const byGroup = allPerms.filter((p) => preset.permissionGroups.includes(p.group));
const byExtra = preset.extraPermissions
? allPerms.filter((p) => preset.extraPermissions!.includes(p.code))
: [];
perms = [...byGroup, ...byExtra].filter(
(p, i, arr) => arr.findIndex((x) => x.id === p.id) === i,
);
}
// 系统预置角色必须严格遵循职责矩阵;额外授权请创建自定义角色叠加。
const currentIds = role.permissions.map((permission) => permission.id).sort((a, b) => a - b);
const targetIds = perms.map((permission) => permission.id).sort((a, b) => a - b);
if (currentIds.join(',') !== targetIds.join(',')) {
role.permissions = perms;
await this.roleRepo.save(role);
}
}
// Step 4: 初始化 admin 用户
const count = await this.userRepo.count();
if (count === 0) {
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
const hash = await bcrypt.hash(adminPassword, 10);
const adminUser = this.userRepo.create({
username: 'admin',
passwordHash: hash,
name: '管理员',
});
const superAdminRole = allRoles.find((r) => r.code === 'super_admin');
if (superAdminRole) {
adminUser.roles = [superAdminRole];
}
await this.userRepo.save(adminUser);
this.logger.log(
`已创建默认管理员: admin / ${adminPassword === 'admin123' ? 'admin123 (请尽快修改!)' : '******'}`,
);
}
this.logger.log(`种子数据初始化完成: ${allPerms.length} 权限点, ${allRoles.length} 角色`);
}
async findAllRoles(): Promise<Role[]> {
return this.roleRepo.find({
relations: ['permissions'],
order: { id: 'ASC' },
});
}
async findRoleById(id: number): Promise<Role> {
return this.roleRepo.findOneOrFail({ where: { id }, relations: ['permissions'] });
}
async createRole(dto: {
name: string;
description?: string;
permissionIds?: number[];
}): Promise<Role> {
const role = this.roleRepo.create({ name: dto.name, description: dto.description });
if (dto.permissionIds && dto.permissionIds.length > 0) {
role.permissions = await this.permRepo.findByIds(dto.permissionIds);
}
return this.roleRepo.save(role);
}
async updateRole(
id: number,
dto: { name?: string; description?: string; permissionIds?: number[] },
): Promise<Role> {
const role = await this.roleRepo.findOneOrFail({
where: { id },
relations: ['permissions'],
});
if (dto.name !== undefined && !role.isSystem) role.name = dto.name;
if (dto.description !== undefined) role.description = dto.description;
if (dto.permissionIds !== undefined) {
role.permissions =
dto.permissionIds.length > 0 ? await this.permRepo.findByIds(dto.permissionIds) : [];
}
return this.roleRepo.save(role);
}
async deleteRole(id: number): Promise<{ message: string }> {
const role = await this.roleRepo.findOneOrFail({ where: { id } });
if (role.isSystem) throw new Error('系统角色不可删除');
await this.roleRepo.remove(role);
return { message: '角色已删除' };
}
async findAllPermissions(): Promise<Permission[]> {
return this.permRepo.find({ order: { group: 'ASC', code: 'ASC' } });
}
async getPermissionTree(): Promise<{ group: string; permissions: Permission[] }[]> {
const all = await this.findAllPermissions();
const map = new Map<string, Permission[]>();
for (const p of all) {
if (!map.has(p.group)) map.set(p.group, []);
map.get(p.group)!.push(p);
}
return Array.from(map.entries()).map(([group, permissions]) => ({ group, permissions }));
}
async getUserPermissions(userId: number): Promise<string[]> {
const user = await this.userRepo.findOne({
where: { id: userId },
relations: ['roles', 'roles.permissions'],
});
if (!user || !user.roles) return [];
const codes = new Set<string>();
for (const role of user.roles) {
if (role.status !== 1) continue;
for (const perm of role.permissions) {
codes.add(perm.code);
}
}
return Array.from(codes);
}
// ---- 用户管理 ----
async findAllUsers(isArchived = false) {
const users = await this.userRepo.find({
where: { isArchived },
relations: ['roles'],
order: { createdAt: 'DESC' },
});
const userIds = users.map((u) => u.id);
const students = await this.studentRepo.find({
where: { userId: In(userIds) },
select: ['userId', 'status'],
});
const statusMap = new Map(students.map((s) => [s.userId, s.status]));
return users.map((u) => ({
id: u.id,
username: u.username,
name: u.name,
isActive: u.isActive,
isArchived: u.isArchived,
studentStatus: statusMap.get(u.id) || null,
lastLoginAt: u.lastLoginAt,
createdAt: u.createdAt,
updatedAt: u.updatedAt,
roles: u.roles?.map((r) => ({ id: r.id, name: r.name })) || [],
}));
}
async createUser(dto: { username: string; password: string; name: string; roleIds?: number[] }) {
const exists = await this.userRepo.findOne({ where: { username: dto.username } });
if (exists) throw new Error('用户名已存在');
const hash = await bcrypt.hash(dto.password, 10);
const user = this.userRepo.create({
username: dto.username,
passwordHash: hash,
name: dto.name,
});
if (dto.roleIds && dto.roleIds.length > 0) {
user.roles = await this.roleRepo.findByIds(dto.roleIds);
}
await this.userRepo.save(user);
return { message: '用户创建成功' };
}
async updateUser(
id: number,
dto: { username?: string; name?: string; isActive?: boolean; roleIds?: number[] },
) {
const user = await this.userRepo.findOne({ where: { id }, relations: ['roles'] });
if (!user) throw new Error('用户不存在');
if (dto.username !== undefined && dto.username !== user.username) {
const exists = await this.userRepo.findOne({ where: { username: dto.username } });
if (exists) throw new Error('用户名已存在');
user.username = dto.username;
}
if (dto.name !== undefined) user.name = dto.name;
if (dto.isActive !== undefined) user.isActive = dto.isActive;
if (dto.roleIds !== undefined) {
user.roles = dto.roleIds.length > 0 ? await this.roleRepo.findByIds(dto.roleIds) : [];
}
await this.userRepo.save(user);
return { message: '更新成功' };
}
async resetPassword(id: number, newPassword: string) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new Error('用户不存在');
user.passwordHash = await bcrypt.hash(newPassword, 10);
await this.userRepo.save(user);
return { message: '密码已重置' };
}
async archiveUser(id: number) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new Error('用户不存在');
if (user.username === 'admin') throw new Error('不能归档默认管理员');
await this.userRepo.update(id, { isArchived: true });
return { message: '用户已归档' };
}
async restoreUser(id: number) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new Error('用户不存在');
await this.userRepo.update(id, { isArchived: false });
return { message: '用户已恢复' };
}
async markAsStaff(userId: number) {
const student = await this.studentRepo.findOne({ where: { userId } });
if (!student) throw new Error('该用户没有学员记录');
await this.studentRepo.update(student.id, { status: 'staff' });
this.logger.log(`User ${userId} Student ${student.id} marked as staff`);
return { message: '已标记为教职工' };
}
async markAsStudent(userId: number) {
const student = await this.studentRepo.findOne({ where: { userId } });
if (!student) throw new Error('该用户没有学员记录');
await this.studentRepo.update(student.id, { status: 'active' });
this.logger.log(`User ${userId} Student ${student.id} restored to student`);
return { message: '已恢复为学员' };
}
async deleteUser(id: number) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new Error('用户不存在');
if (user.username === 'admin') throw new Error('不能删除默认管理员');
if (!user.isArchived) throw new Error('请先归档再删除');
await this.userRepo.remove(user);
return { message: '用户已删除' };
}
// ---- 用户资料 ----
async getUserProfile(id: number) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new Error('用户不存在');
return {
id: user.id,
username: user.username,
name: user.name,
profile: user.profile || {},
};
}
async updateUserProfile(
id: number,
dto: { subjects?: string[]; joinedAt?: string; qualifications?: string },
) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new Error('用户不存在');
const current = user.profile || {};
user.profile = {
subjects: dto.subjects !== undefined ? dto.subjects : current.subjects,
joinedAt: dto.joinedAt !== undefined ? dto.joinedAt : current.joinedAt,
qualifications:
dto.qualifications !== undefined ? dto.qualifications : current.qualifications,
};
await this.userRepo.save(user);
return { message: '资料已更新', profile: user.profile };
}
// ---- 教师工作台 ----
async getTeacherWorkspace(userId: number) {
// Find all classes where this user is a teacher
const teacherAssignments = await this.classTeacherRepo.find({
where: { userId },
relations: ['class'],
});
const classIds = [...new Set(teacherAssignments.map((t) => t.classId))];
if (classIds.length === 0) {
return { assignedClasses: [], todaySchedules: [], myStudents: [] };
}
// Get assigned classes
const assignedClasses = teacherAssignments.map((t) => ({
classId: t.classId,
className: t.class?.name || '',
classCode: t.class?.code || '',
roleType: t.roleType,
subject: t.subject,
}));
// Get today's day of week (1=Monday, 7=Sunday)
const today = new Date();
const weekDay = today.getDay(); // 0=Sun → convert to 1-7
const adjustedWeekDay = weekDay === 0 ? 7 : weekDay;
const todayStr = today.toISOString().slice(0, 10);
// Get today's schedules for assigned classes
const todaySchedules = await this.classScheduleRepo
.createQueryBuilder('cs')
.where('cs.classId IN (:...classIds)', { classIds })
.andWhere('cs.weekDay = :weekDay', { weekDay: adjustedWeekDay })
.andWhere('cs.startDate <= :today', { today: todayStr })
.andWhere('cs.endDate >= :today', { today: todayStr })
.andWhere('cs.status = :status', { status: 'active' })
.orderBy('cs.startTime', 'ASC')
.getMany();
// Get students in assigned classes
const classStudents = await this.classStudentRepo.find({
where: { classId: In(classIds), status: 'active' },
relations: ['student', 'class'],
});
const myStudents = classStudents.map((cs) => ({
studentId: cs.studentId,
studentName: cs.student?.name || '',
studentNo: cs.student?.studentNo || '',
className: cs.class?.name || '',
classId: cs.classId,
joinDate: cs.joinDate,
}));
return {
assignedClasses,
todaySchedules: todaySchedules.map((s) => ({
id: s.id,
classId: s.classId,
classroomId: s.classroomId,
teacherId: s.teacherId,
weekDay: s.weekDay,
startTime: s.startTime,
endTime: s.endTime,
subject: s.subject,
scheduleType: s.scheduleType,
})),
myStudents,
};
}
async getTeachers(query?: { search?: string; page?: number; pageSize?: number }) {
const page = query?.page || 1;
const pageSize = query?.pageSize || 20;
const teacherRoleCodes = ['teacher', 'super_admin'];
const teacherRoleNames = ['任课老师', '老师', '超级管理员', '超管'];
const qb = this.userRepo
.createQueryBuilder('u')
.leftJoinAndSelect('u.roles', 'role')
.where('(role.code IN (:...roleCodes) OR role.name IN (:...roleNames))', {
roleCodes: teacherRoleCodes,
roleNames: teacherRoleNames,
});
if (query?.search) {
qb.andWhere('(u.name LIKE :s OR u.username LIKE :s)', { s: `%${query.search}%` });
}
const total = await qb.getCount();
const users = await qb
.orderBy('u.name', 'ASC')
.skip((page - 1) * pageSize)
.take(pageSize)
.getMany();
const userIds = users.map((user) => user.id);
const assignments =
userIds.length > 0
? await this.classTeacherRepo.find({ where: { userId: In(userIds) }, relations: ['class'] })
: [];
const assignmentsByUser = new Map<number, ClassTeacher[]>();
for (const assignment of assignments) {
const list = assignmentsByUser.get(assignment.userId) || [];
list.push(assignment);
assignmentsByUser.set(assignment.userId, list);
}
const list = users.map((u) => ({
id: u.id,
username: u.username,
name: u.name,
isActive: u.isActive,
profile: u.profile,
lastLoginAt: u.lastLoginAt,
roles: u.roles || [],
classAssignments: (assignmentsByUser.get(u.id) || []).map((assignment) => ({
id: assignment.id,
classId: assignment.classId,
roleType: assignment.roleType,
subject: assignment.subject,
className: assignment.class?.name || null,
})),
}));
return { list, total };
}
async updateTeacherProfile(
id: number,
profile: { subjects?: string[]; joinedAt?: string; qualifications?: string },
) {
const user = await this.userRepo.findOne({ where: { id } });
if (!user) throw new NotFoundException('用户不存在');
user.profile = { ...user.profile, ...profile };
return this.userRepo.save(user);
}
}