forked from wangziqi/gongxue-base
fix permissions and teacher attendance workflows
This commit is contained in:
@@ -2,10 +2,21 @@ 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';
|
||||
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:create', name: '新增学生', group: 'student' },
|
||||
{ code: 'student:edit', name: '编辑学生', group: 'student' },
|
||||
@@ -87,7 +98,7 @@ const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> =
|
||||
{ code: 'department:delete', name: '删除部门', group: 'department' },
|
||||
];
|
||||
|
||||
const PRESET_ROLES: Array<{
|
||||
export const PRESET_ROLES: Array<{
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
@@ -119,6 +130,8 @@ const PRESET_ROLES: Array<{
|
||||
'class',
|
||||
'schedule',
|
||||
'attendance',
|
||||
'notification',
|
||||
'profile',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -126,36 +139,61 @@ const PRESET_ROLES: Array<{
|
||||
code: 'teacher',
|
||||
description: '查看和管理本班学生',
|
||||
isSystem: true,
|
||||
permissionGroups: ['student'],
|
||||
extraPermissions: ['student:view'],
|
||||
permissionGroups: ['notification', 'profile'],
|
||||
extraPermissions: [
|
||||
'student:view',
|
||||
'class:view',
|
||||
'schedule:view',
|
||||
'attendance:view',
|
||||
'attendance:create',
|
||||
'attendance:export',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '机构负责人',
|
||||
code: 'institution_head',
|
||||
description: '管理机构教室和课程',
|
||||
isSystem: true,
|
||||
permissionGroups: ['classroom', 'rental', 'tenant'],
|
||||
permissionGroups: ['classroom', 'rental', 'tenant', 'notification', 'profile'],
|
||||
},
|
||||
{
|
||||
name: '财务',
|
||||
code: 'finance',
|
||||
description: '管理费用、账单与押金',
|
||||
isSystem: true,
|
||||
permissionGroups: ['expense', 'bill', 'deposit', 'dashboard'],
|
||||
permissionGroups: ['expense', 'bill', 'deposit', 'dashboard', 'notification', 'profile'],
|
||||
},
|
||||
{
|
||||
name: '宿管',
|
||||
code: 'dorm_manager',
|
||||
description: '管理宿舍入住与宿舍信息',
|
||||
isSystem: true,
|
||||
permissionGroups: ['student', 'room', 'occupancy', 'deposit', 'dashboard'],
|
||||
permissionGroups: [
|
||||
'student',
|
||||
'room',
|
||||
'occupancy',
|
||||
'deposit',
|
||||
'dashboard',
|
||||
'notification',
|
||||
'profile',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '教务',
|
||||
code: 'academic',
|
||||
description: '管理班级、排课、考勤、学习与考试',
|
||||
isSystem: true,
|
||||
permissionGroups: ['class', 'schedule', 'attendance', 'classroom', 'learning', 'exam', 'dashboard'],
|
||||
permissionGroups: [
|
||||
'class',
|
||||
'schedule',
|
||||
'attendance',
|
||||
'classroom',
|
||||
'learning',
|
||||
'exam',
|
||||
'dashboard',
|
||||
'notification',
|
||||
'profile',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -189,7 +227,12 @@ export class RbacService {
|
||||
const exists = await this.roleRepo.findOne({ where: { name: r.name } });
|
||||
if (!exists) {
|
||||
await this.roleRepo.save(
|
||||
this.roleRepo.create({ name: r.name, description: r.description, isSystem: r.isSystem }),
|
||||
this.roleRepo.create({
|
||||
name: r.name,
|
||||
code: r.code,
|
||||
description: r.description,
|
||||
isSystem: r.isSystem,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -197,8 +240,12 @@ export class RbacService {
|
||||
|
||||
// Step 3: 构建角色-权限关联
|
||||
for (const preset of PRESET_ROLES) {
|
||||
const role = allRoles.find((r) => r.name === preset.name);
|
||||
const role = allRoles.find((r) => r.name === preset.name || r.code === preset.code);
|
||||
if (!role) continue;
|
||||
if (role.code !== preset.code) {
|
||||
role.code = preset.code;
|
||||
await this.roleRepo.save(role);
|
||||
}
|
||||
|
||||
let perms: Permission[];
|
||||
if (preset.permissionGroups.length === 0) {
|
||||
@@ -215,11 +262,12 @@ export class RbacService {
|
||||
);
|
||||
}
|
||||
|
||||
// 幂等:只插入尚未关联的
|
||||
const existingIds = new Set(role.permissions.map((p) => p.id));
|
||||
const toAdd = perms.filter((p) => !existingIds.has(p.id));
|
||||
if (toAdd.length > 0) {
|
||||
role.permissions = [...role.permissions, ...toAdd];
|
||||
// 系统角色只补齐预置权限,不移除管理员手动授予的额外权限。
|
||||
// 这样新增权限(例如 profile:view)会自动补上,同时避免重启后覆盖人工配置。
|
||||
const currentIds = new Set(role.permissions.map((permission) => permission.id));
|
||||
const missingPerms = perms.filter((permission) => !currentIds.has(permission.id));
|
||||
if (missingPerms.length > 0) {
|
||||
role.permissions = [...role.permissions, ...missingPerms];
|
||||
await this.roleRepo.save(role);
|
||||
}
|
||||
}
|
||||
@@ -247,7 +295,6 @@ export class RbacService {
|
||||
this.logger.log(`种子数据初始化完成: ${allPerms.length} 权限点, ${allRoles.length} 角色`);
|
||||
}
|
||||
|
||||
|
||||
async findAllRoles(): Promise<Role[]> {
|
||||
return this.roleRepo.find({
|
||||
relations: ['permissions'],
|
||||
@@ -450,14 +497,18 @@ export class RbacService {
|
||||
};
|
||||
}
|
||||
|
||||
async updateUserProfile(id: number, dto: { subjects?: string[]; joinedAt?: string; qualifications?: string }) {
|
||||
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,
|
||||
qualifications:
|
||||
dto.qualifications !== undefined ? dto.qualifications : current.qualifications,
|
||||
};
|
||||
await this.userRepo.save(user);
|
||||
return { message: '资料已更新', profile: user.profile };
|
||||
@@ -501,6 +552,7 @@ export class RbacService {
|
||||
.andWhere('cs.startDate <= :today', { today: todayStr })
|
||||
.andWhere('cs.endDate >= :today', { today: todayStr })
|
||||
.andWhere('cs.status = :status', { status: 'active' })
|
||||
.andWhere('cs.teacherId = :userId', { userId })
|
||||
.orderBy('cs.startTime', 'ASC')
|
||||
.getMany();
|
||||
|
||||
@@ -524,6 +576,8 @@ export class RbacService {
|
||||
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,
|
||||
@@ -535,18 +589,18 @@ export class RbacService {
|
||||
}
|
||||
|
||||
async getTeachers(query?: { search?: string; page?: number; pageSize?: number }) {
|
||||
const page = query?.page || 1;
|
||||
const pageSize = query?.pageSize || 20;
|
||||
const teacherRoleCodes = ['teacher', 'class_teacher', 'dormitory_supervisor', 'super_admin'];
|
||||
const teacherRoleNames = ['老师', '班主任', '宿管老师', '超管'];
|
||||
|
||||
const qb = this.userRepo
|
||||
.createQueryBuilder('u')
|
||||
.leftJoin('u.roles', 'role')
|
||||
.leftJoin(ClassTeacher, 'ct', 'ct.userId = u.id')
|
||||
.leftJoin('ct.class', 'c')
|
||||
.select([
|
||||
'u.id', 'u.username', 'u.name', 'u.isActive', 'u.profile', 'u.lastLoginAt',
|
||||
'role.code', 'role.name',
|
||||
'ct.roleType', 'ct.subject', 'ct.id',
|
||||
'c.id', 'c.name',
|
||||
])
|
||||
.where('role.code IN (:...roles)', { roles: ['teacher', 'class_teacher', 'dormitory_supervisor', 'super_admin'] });
|
||||
.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}%` });
|
||||
@@ -555,37 +609,38 @@ export class RbacService {
|
||||
const total = await qb.getCount();
|
||||
const users = await qb
|
||||
.orderBy('u.name', 'ASC')
|
||||
.skip(((query?.page || 1) - 1) * (query?.pageSize || 20))
|
||||
.take(query?.pageSize || 20)
|
||||
.skip((page - 1) * pageSize)
|
||||
.take(pageSize)
|
||||
.getMany();
|
||||
|
||||
const list = users.map((u) => {
|
||||
// TypeORM injects __ct__ and __class__ via leftJoin on non-entity relations
|
||||
const raw = u as unknown as Record<string, unknown>;
|
||||
const classAssignments: Array<{ roleType?: string; subject?: string; className: string | null }> = [];
|
||||
const rawCt = raw['__ct__'];
|
||||
if (Array.isArray(rawCt)) {
|
||||
for (const ct of rawCt) {
|
||||
const ctRaw = ct as Record<string, unknown>;
|
||||
const cls = ctRaw['__class__'] as Record<string, unknown> | undefined;
|
||||
classAssignments.push({
|
||||
roleType: typeof ctRaw['roleType'] === 'string' ? ctRaw['roleType'] : undefined,
|
||||
subject: typeof ctRaw['subject'] === 'string' ? ctRaw['subject'] : undefined,
|
||||
className: cls && typeof cls['name'] === 'string' ? cls['name'] : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
name: u.name,
|
||||
isActive: u.isActive,
|
||||
profile: u.profile,
|
||||
lastLoginAt: u.lastLoginAt,
|
||||
roles: u.roles || [],
|
||||
classAssignments,
|
||||
};
|
||||
});
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user