658 lines
24 KiB
TypeScript
658 lines
24 KiB
TypeScript
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: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:approve', 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: 'tenant:view', name: '查看租赁方', group: 'tenant' },
|
||
{ code: 'tenant:create', name: '新增租赁方', group: 'tenant' },
|
||
{ code: 'tenant:edit', name: '编辑租赁方', group: 'tenant' },
|
||
{ code: 'tenant:delete', name: '删除租赁方', group: 'tenant' },
|
||
{ 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: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' },
|
||
];
|
||
|
||
export const PRESET_ROLES: Array<{
|
||
name: string;
|
||
code: string;
|
||
description: string;
|
||
isSystem: boolean;
|
||
permissionGroups: string[];
|
||
extraPermissions?: string[];
|
||
}> = [
|
||
{
|
||
name: '超管',
|
||
code: 'super_admin',
|
||
description: '系统超级管理员,拥有全部权限',
|
||
isSystem: true,
|
||
permissionGroups: [],
|
||
},
|
||
{
|
||
name: '宿管老师',
|
||
code: 'dormitory_supervisor',
|
||
description: '管理宿舍相关业务',
|
||
isSystem: true,
|
||
permissionGroups: [
|
||
'student',
|
||
'room',
|
||
'occupancy',
|
||
'expense',
|
||
'bill',
|
||
'deposit',
|
||
'log',
|
||
'dashboard',
|
||
'class',
|
||
'schedule',
|
||
'attendance',
|
||
'notification',
|
||
'profile',
|
||
],
|
||
},
|
||
{
|
||
name: '老师',
|
||
code: 'teacher',
|
||
description: '查看和管理本班学生',
|
||
isSystem: true,
|
||
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', 'notification', 'profile'],
|
||
},
|
||
{
|
||
name: '财务',
|
||
code: 'finance',
|
||
description: '管理费用、账单与押金',
|
||
isSystem: true,
|
||
permissionGroups: ['expense', 'bill', 'deposit', 'dashboard', 'notification', 'profile'],
|
||
},
|
||
{
|
||
name: '宿管',
|
||
code: 'dorm_manager',
|
||
description: '管理宿舍入住与宿舍信息',
|
||
isSystem: true,
|
||
permissionGroups: [
|
||
'student',
|
||
'room',
|
||
'occupancy',
|
||
'deposit',
|
||
'dashboard',
|
||
'notification',
|
||
'profile',
|
||
],
|
||
},
|
||
{
|
||
name: '教务',
|
||
code: 'academic',
|
||
description: '管理班级、排课、考勤、学习与考试',
|
||
isSystem: true,
|
||
permissionGroups: [
|
||
'class',
|
||
'schedule',
|
||
'attendance',
|
||
'classroom',
|
||
'learning',
|
||
'exam',
|
||
'dashboard',
|
||
'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>,
|
||
) {}
|
||
|
||
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: { name: r.name } });
|
||
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'] });
|
||
|
||
// Step 3: 构建角色-权限关联
|
||
for (const preset of PRESET_ROLES) {
|
||
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) {
|
||
// 超管:全部权限
|
||
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,
|
||
);
|
||
}
|
||
|
||
// 系统角色只补齐预置权限,不移除管理员手动授予的额外权限。
|
||
// 这样新增权限(例如 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);
|
||
}
|
||
}
|
||
|
||
// 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.name === '超管');
|
||
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' })
|
||
.andWhere('cs.teacherId = :userId', { userId })
|
||
.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', 'class_teacher', 'dormitory_supervisor', '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);
|
||
}
|
||
}
|