import { ConflictException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import type { AuthUser } from '../auth/types/auth-user.type.js'; import { PasswordService } from '../auth/password.service.js'; import { UserRole } from '../generated/prisma/enums.js'; import { CreateUserDto } from './dto/create-user.dto.js'; import { UpdateUserDto } from './dto/update-user.dto.js'; import { PrismaService } from '../prisma.service.js'; @Injectable() export class UsersService { // 统一定义用户输出字段,避免泄露密码哈希与 openId 明文。 private readonly userSelect = { id: true, phone: true, name: true, role: true, hospitalId: true, departmentId: true, medicalGroupId: true, managerId: true, wechatMiniOpenId: true, wechatOfficialOpenId: true, isActive: true, lastLoginAt: true, createdAt: true, updatedAt: true, } as const; constructor( private readonly prisma: PrismaService, private readonly passwordService: PasswordService, ) {} async create(currentUser: AuthUser, createUserDto: CreateUserDto) { // 只有系统管理员和医院管理员可以创建用户。 if (!this.isAdmin(currentUser.role)) { throw new ForbiddenException('无创建用户权限'); } // 医院管理员创建用户时强制绑定本院,防止跨院越权。 const role = createUserDto.role ?? UserRole.DOCTOR; let hospitalId = createUserDto.hospitalId; if (currentUser.role === UserRole.HOSPITAL_ADMIN) { if (!currentUser.hospitalId) { throw new ForbiddenException('当前管理员未绑定医院'); } if (hospitalId !== undefined && hospitalId !== currentUser.hospitalId) { throw new ForbiddenException('医院管理员不能跨院创建用户'); } if (role === UserRole.SYSTEM_ADMIN) { throw new ForbiddenException('医院管理员不能创建系统管理员'); } hospitalId = currentUser.hospitalId; } // 手机号唯一约束用显式检查提升错误可读性。 const existingUser = await this.prisma.user.findUnique({ where: { phone: createUserDto.phone }, select: { id: true }, }); if (existingUser) { throw new ConflictException('手机号已存在'); } // 新建用户时密码必须做哈希存储,禁止明文落库。 const passwordHash = await this.passwordService.hashPassword( createUserDto.password, ); await this.assertManagerValid(createUserDto.managerId, hospitalId ?? null); const createdUser = await this.prisma.user.create({ data: { phone: createUserDto.phone, passwordHash, name: createUserDto.name, role, hospitalId, departmentId: createUserDto.departmentId, medicalGroupId: createUserDto.medicalGroupId, managerId: createUserDto.managerId, isActive: createUserDto.isActive ?? true, }, select: this.userSelect, }); return this.toUserView(createdUser); } async findAll(currentUser: AuthUser) { // 按角色动态构造可见范围,避免在 controller 中散落权限判断。 const where = await this.buildVisibilityWhere(currentUser); const users = await this.prisma.user.findMany({ where, select: this.userSelect, orderBy: { id: 'asc' }, }); return users.map((user) => this.toUserView(user)); } async findOne(currentUser: AuthUser, id: number) { // 先查再判,便于区分不存在和无权限两类错误。 const targetUser = await this.prisma.user.findUnique({ where: { id }, select: this.userSelect, }); if (!targetUser) { throw new NotFoundException('用户不存在'); } const canAccess = await this.canAccessUser(currentUser, targetUser); if (!canAccess) { throw new ForbiddenException('无访问权限'); } return this.toUserView(targetUser); } async update(currentUser: AuthUser, id: number, updateUserDto: UpdateUserDto) { // 读取目标用户,用于后续做跨院和角色越权判断。 const targetUser = await this.prisma.user.findUnique({ where: { id }, select: this.userSelect, }); if (!targetUser) { throw new NotFoundException('用户不存在'); } const canAccess = await this.canAccessUser(currentUser, targetUser); if (!canAccess) { throw new ForbiddenException('无更新权限'); } // 非管理员只允许修改自己,且只能改基础资料。 if (!this.isAdmin(currentUser.role)) { if (currentUser.id !== id) { throw new ForbiddenException('只能修改自己的资料'); } if ( updateUserDto.role !== undefined || updateUserDto.hospitalId !== undefined || updateUserDto.departmentId !== undefined || updateUserDto.medicalGroupId !== undefined || updateUserDto.managerId !== undefined || updateUserDto.isActive !== undefined ) { throw new ForbiddenException('无权修改组织和角色信息'); } } // 医院管理员不允许操作系统管理员,且不能把用户迁移到其他医院。 if (currentUser.role === UserRole.HOSPITAL_ADMIN) { if (targetUser.role === UserRole.SYSTEM_ADMIN) { throw new ForbiddenException('医院管理员不能操作系统管理员'); } if ( updateUserDto.hospitalId !== undefined && updateUserDto.hospitalId !== currentUser.hospitalId ) { throw new ForbiddenException('医院管理员不能跨院修改'); } if (updateUserDto.role === UserRole.SYSTEM_ADMIN) { throw new ForbiddenException('不能提升为系统管理员'); } } // 仅写入传入字段,避免误覆盖数据库已有值。 const data: { phone?: string; name?: string | null; role?: UserRole; hospitalId?: number | null; departmentId?: number | null; medicalGroupId?: number | null; managerId?: number | null; isActive?: boolean; } = {}; if (updateUserDto.phone !== undefined) { data.phone = updateUserDto.phone; } if (updateUserDto.name !== undefined) { data.name = updateUserDto.name; } if (updateUserDto.role !== undefined) { data.role = updateUserDto.role; } if (updateUserDto.hospitalId !== undefined) { data.hospitalId = updateUserDto.hospitalId; } if (updateUserDto.departmentId !== undefined) { data.departmentId = updateUserDto.departmentId; } if (updateUserDto.medicalGroupId !== undefined) { data.medicalGroupId = updateUserDto.medicalGroupId; } if (updateUserDto.managerId !== undefined) { data.managerId = updateUserDto.managerId; } if (updateUserDto.isActive !== undefined) { data.isActive = updateUserDto.isActive; } if (data.managerId !== undefined && data.managerId === id) { throw new ForbiddenException('不能把自己设置为上级'); } // 如果变更了上级,需要校验上级存在且组织关系合法。 const nextHospitalId = data.hospitalId ?? targetUser.hospitalId; await this.assertManagerValid(data.managerId, nextHospitalId); const updatedUser = await this.prisma.user.update({ where: { id }, data, select: this.userSelect, }); return this.toUserView(updatedUser); } async remove(currentUser: AuthUser, id: number) { // 删除接口采用“停用账号”实现,保留审计和业务关联。 if (!this.isAdmin(currentUser.role)) { throw new ForbiddenException('无删除权限'); } if (currentUser.id === id) { throw new ForbiddenException('不能停用自己'); } const targetUser = await this.prisma.user.findUnique({ where: { id }, select: this.userSelect, }); if (!targetUser) { throw new NotFoundException('用户不存在'); } const canAccess = await this.canAccessUser(currentUser, targetUser); if (!canAccess) { throw new ForbiddenException('无删除权限'); } if ( currentUser.role === UserRole.HOSPITAL_ADMIN && targetUser.role === UserRole.SYSTEM_ADMIN ) { throw new ForbiddenException('医院管理员不能停用系统管理员'); } const disabledUser = await this.prisma.user.update({ where: { id }, data: { isActive: false }, select: this.userSelect, }); return this.toUserView(disabledUser); } private async buildVisibilityWhere(currentUser: AuthUser) { // 系统管理员可见全量用户。 if (currentUser.role === UserRole.SYSTEM_ADMIN) { return {}; } // 医院管理员仅可见本院用户。 if (currentUser.role === UserRole.HOSPITAL_ADMIN) { if (!currentUser.hospitalId) { return { id: -1 }; } return { hospitalId: currentUser.hospitalId }; } // 主任/组长可见自己和所有下级。 if ( currentUser.role === UserRole.DIRECTOR || currentUser.role === UserRole.TEAM_LEAD ) { const subordinateIds = await this.getSubordinateIds(currentUser.id); return { id: { in: [currentUser.id, ...subordinateIds] } }; } // 医生、工程师默认只可见自己。 return { id: currentUser.id }; } private async canAccessUser( currentUser: AuthUser, targetUser: { id: number; role: UserRole; hospitalId: number | null }, ): Promise { // 系统管理员总是有权限。 if (currentUser.role === UserRole.SYSTEM_ADMIN) { return true; } // 医院管理员限制在本院范围。 if (currentUser.role === UserRole.HOSPITAL_ADMIN) { return ( currentUser.hospitalId !== null && targetUser.hospitalId === currentUser.hospitalId ); } // 主任/组长需要命中自己或下级链路。 if ( currentUser.role === UserRole.DIRECTOR || currentUser.role === UserRole.TEAM_LEAD ) { if (targetUser.id === currentUser.id) { return true; } const subordinateIds = await this.getSubordinateIds(currentUser.id); return subordinateIds.includes(targetUser.id); } // 普通角色仅能访问自己。 return targetUser.id === currentUser.id; } private async getSubordinateIds(rootUserId: number) { // 使用 BFS 逐层展开下级,支持任意深度上下级关系。 const visited = new Set(); let frontier = [rootUserId]; while (frontier.length > 0) { const rows = await this.prisma.user.findMany({ where: { managerId: { in: frontier } }, select: { id: true }, }); const nextFrontier: number[] = []; for (const row of rows) { if (visited.has(row.id)) { continue; } visited.add(row.id); nextFrontier.push(row.id); } frontier = nextFrontier; } return Array.from(visited); } private isAdmin(role: UserRole) { // 系统管理员和医院管理员都属于平台管理角色。 return role === UserRole.SYSTEM_ADMIN || role === UserRole.HOSPITAL_ADMIN; } private async assertManagerValid( managerId: number | null | undefined, hospitalId: number | null, ) { // 未设置上级时无需校验。 if (managerId === undefined || managerId === null) { return; } // 上级用户必须存在且处于启用状态。 const manager = await this.prisma.user.findUnique({ where: { id: managerId }, select: { id: true, hospitalId: true, isActive: true }, }); if (!manager || !manager.isActive) { throw new NotFoundException('上级用户不存在或已停用'); } // 若当前用户绑定了医院,上级也必须在同一医院。 if (hospitalId !== null && manager.hospitalId !== hospitalId) { throw new ForbiddenException('上级必须与用户在同一医院'); } } private toUserView(user: { id: number; phone: string; name: string | null; role: UserRole; hospitalId: number | null; departmentId: number | null; medicalGroupId: number | null; managerId: number | null; wechatMiniOpenId: string | null; wechatOfficialOpenId: string | null; isActive: boolean; lastLoginAt: Date | null; createdAt: Date; updatedAt: Date; }) { // 返回 API 时只透出是否绑定,不透出 openId 实值。 return { id: user.id, phone: user.phone, name: user.name, role: user.role, hospitalId: user.hospitalId, departmentId: user.departmentId, medicalGroupId: user.medicalGroupId, managerId: user.managerId, isActive: user.isActive, lastLoginAt: user.lastLoginAt, wechatMiniLinked: Boolean(user.wechatMiniOpenId), wechatOfficialLinked: Boolean(user.wechatOfficialOpenId), createdAt: user.createdAt, updatedAt: user.updatedAt, }; } }