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, @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, ) {} 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 { // 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 { return this.roleRepo.find({ relations: ['permissions'], order: { id: 'ASC' }, }); } async findRoleById(id: number): Promise { return this.roleRepo.findOneOrFail({ where: { id }, relations: ['permissions'] }); } async createRole(dto: { name: string; description?: string; permissionIds?: number[]; }): Promise { 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 { 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 { 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(); 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 { const user = await this.userRepo.findOne({ where: { id: userId }, relations: ['roles', 'roles.permissions'], }); if (!user || !user.roles) return []; const codes = new Set(); 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(); 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); } }