import { Injectable, Logger } 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, AttendanceSession } from '../entities'; import { PRESET_ROLES, PRESET_PERMISSIONS, DEPRECATED_PERMISSION_CODES, DEPRECATED_PERMISSION_CODE_SET, getChinaDateParts, } from './rbac-presets'; @Injectable() export class RbacService { private readonly logger = new Logger(RbacService.name); constructor( @InjectRepository(Permission) private permRepo: Repository, @InjectRepository(Role) private roleRepo: Repository, @InjectRepository(User) private userRepo: Repository, @InjectRepository(Class) private classRepo: Repository, @InjectRepository(ClassStudent) private classStudentRepo: Repository, @InjectRepository(ClassTeacher) private classTeacherRepo: Repository, @InjectRepository(ClassSchedule) private classScheduleRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, @InjectRepository(AttendanceSession) private attendanceSessionRepo: Repository, ) {} async findAllRoles(): Promise { return this.roleRepo.find({ relations: ['permissions'], order: { id: 'ASC' }, }); } async findRoleById(id: number): Promise { return this.roleRepo.findOneOrFail({ where: { id }, relations: ['permissions'] }); } 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: [] }; } 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 China business date and day of week (1=Monday, 7=Sunday) const { date: todayStr, weekDay: adjustedWeekDay } = getChinaDateParts(); 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(); 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, }; } } @Injectable() export class RbacSeedService { private readonly logger = new Logger(RbacSeedService.name); constructor( @InjectRepository(Permission) private permRepo: Repository, @InjectRepository(Role) private roleRepo: Repository, @InjectRepository(User) private userRepo: Repository, ) {} private async findLegacyPresetRole(preset: (typeof PRESET_ROLES)[number]): Promise { 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 { // 仅当角色表为空(首次初始化)或显式设置 SEED_ROLES=true 时才播种; // 否则跳过,避免每次启动都无条件重激活角色/权限,覆盖管理员的自定义调整。 const forceSeed = process.env.SEED_ROLES === 'true'; const roleCount = await this.roleRepo.count(); if (roleCount > 0 && !forceSeed) { this.logger.log( `角色表已有 ${roleCount} 条记录,跳过种子数据重激活(如需强制播种请设置 SEED_ROLES=true)`, ); await this.ensureDefaultAdmin(); return; } 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 deprecatedUserDeletePermission = await this.permRepo.findOne({ where: { code: 'user:delete' }, }); const allPerms = (await this.permRepo.find()).filter( (permission) => permission.code !== 'user:delete' && !DEPRECATED_PERMISSION_CODE_SET.has(permission.code), ); 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'] }); if (deprecatedUserDeletePermission) { for (const role of allRoles) { const permissions = role.permissions ?? []; if (permissions.some((permission) => permission.id === deprecatedUserDeletePermission.id)) { role.permissions = permissions.filter( (permission) => permission.id !== deprecatedUserDeletePermission.id, ); await this.roleRepo.save(role); } } await this.permRepo.remove(deprecatedUserDeletePermission); } const deprecatedPermissions = await this.permRepo.find({ where: { code: In([...DEPRECATED_PERMISSION_CODES]) }, }); if (deprecatedPermissions.length > 0) { const deprecatedIds = new Set(deprecatedPermissions.map((permission) => permission.id)); for (const role of allRoles) { const permissions = role.permissions ?? []; if (permissions.some((permission) => deprecatedIds.has(permission.id))) { role.permissions = permissions.filter((permission) => !deprecatedIds.has(permission.id)); await this.roleRepo.save(role); } } await this.permRepo.remove(deprecatedPermissions); this.logger.log(`已清理废弃权限点: ${deprecatedPermissions.map((p) => p.code).join(', ')}`); } 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); } } await this.ensureDefaultAdmin(); this.logger.log(`种子数据初始化完成: ${allPerms.length} 权限点, ${allRoles.length} 角色`); } /** * 首次初始化时创建默认管理员。 * 即使跳过角色/权限播种(角色表已有数据)也会执行,避免出现「有角色但无管理员」的状态。 */ private async ensureDefaultAdmin(): Promise { const count = await this.userRepo.count(); if (count > 0) return; const adminPassword = process.env.ADMIN_PASSWORD; // 生产环境禁止使用默认弱口令 if (!adminPassword) { if (process.env.NODE_ENV === 'production') { throw new Error('生产环境必须设置 ADMIN_PASSWORD 后再初始化默认管理员'); } this.logger.warn('ADMIN_PASSWORD 未设置,开发环境使用默认密码 admin123'); } const password = adminPassword || 'admin123'; const hash = await bcrypt.hash(password, 10); const adminUser = this.userRepo.create({ username: 'admin', passwordHash: hash, name: '管理员', }); const superAdminRole = await this.roleRepo.findOne({ where: { code: 'super_admin' } }); if (superAdminRole) { adminUser.roles = [superAdminRole]; } await this.userRepo.save(adminUser); // 不打印密码,避免凭据落入日志 this.logger.log('已创建默认管理员: admin(请妥善保管密码)'); } }