Files
gongxue-base/apps/server/src/rbac/rbac.service.ts
wangziqi 46a817503e feat(task1): restructure directories for turborepo monorepo
- Move backend/ to apps/server/ via git mv
- Move frontend/ to apps/admin/ via git mv
- Create packages/typescript-config/ with base, nestjs, and react-vite presets
2026-07-02 15:05:12 +08:00

329 lines
13 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 } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import { Permission, Role, User } from '../entities';
const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [
{ code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' },
{ 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: '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: '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' },
];
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'],
},
{
name: '老师',
code: 'teacher',
description: '查看和管理本班学生',
isSystem: true,
permissionGroups: ['student'],
extraPermissions: ['student:view'],
},
{
name: '机构负责人',
code: 'institution_head',
description: '管理机构教室和课程',
isSystem: true,
permissionGroups: ['classroom', 'rental', 'tenant'],
},
];
@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>,
) {}
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, 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);
if (!role) continue;
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 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];
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() {
const users = await this.userRepo.find({
relations: ['roles'],
order: { createdAt: 'DESC' },
});
return users.map(u => ({
id: u.id,
username: u.username,
name: u.name,
isActive: u.isActive,
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 deleteUser(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.remove(user);
return { message: '用户已删除' };
}
}