import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { Prisma } from '../generated/prisma/client.js'; import { Role } from '../generated/prisma/enums.js'; import type { ActorContext } from '../common/actor-context.js'; import { MESSAGES } from '../common/messages.js'; import { PrismaService } from '../prisma.service.js'; import type { OrganizationQueryDto } from './dto/organization-query.dto.js'; /** * 组织域通用能力服务: * 负责角色校验、作用域校验、分页标准化和基础存在性检查。 */ @Injectable() export class OrganizationAccessService { constructor(private readonly prisma: PrismaService) {} /** * 校验角色是否在允许范围内。 */ assertRole(actor: ActorContext, roles: Role[]) { if (!roles.includes(actor.role)) { throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); } } /** * 校验系统管理员权限。 */ assertSystemAdmin(actor: ActorContext, message: string) { if (actor.role !== Role.SYSTEM_ADMIN) { throw new ForbiddenException(message); } } /** * 校验组织域医院级作用域限制。 */ assertHospitalScope(actor: ActorContext, targetHospitalId: number) { if (actor.role === Role.SYSTEM_ADMIN) { return; } const hospitalScopedRoles: Role[] = [ Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER, ]; if (!hospitalScopedRoles.includes(actor.role)) { return; } const actorHospitalId = this.requireActorHospitalId(actor); if (actorHospitalId !== targetHospitalId) { throw new ForbiddenException(MESSAGES.ORG.HOSPITAL_ADMIN_SCOPE_INVALID); } } /** * 读取并校验当前登录上下文的医院 ID。 */ requireActorHospitalId(actor: ActorContext): number { if ( typeof actor.hospitalId !== 'number' || !Number.isInteger(actor.hospitalId) || actor.hospitalId <= 0 ) { throw new BadRequestException(MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED); } return actor.hospitalId; } /** * 读取并校验当前登录上下文的科室 ID。 */ requireActorDepartmentId(actor: ActorContext): number { if ( typeof actor.departmentId !== 'number' || !Number.isInteger(actor.departmentId) || actor.departmentId <= 0 ) { throw new BadRequestException(MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED); } return actor.departmentId; } /** * 读取并校验当前登录上下文的小组 ID。 */ requireActorGroupId(actor: ActorContext): number { if ( typeof actor.groupId !== 'number' || !Number.isInteger(actor.groupId) || actor.groupId <= 0 ) { throw new BadRequestException(MESSAGES.ORG.ACTOR_GROUP_REQUIRED); } return actor.groupId; } /** * 分页参数标准化。 */ resolvePaging(query: OrganizationQueryDto) { const page = query.page && query.page > 0 ? query.page : 1; const pageSize = query.pageSize && query.pageSize > 0 && query.pageSize <= 100 ? query.pageSize : 20; return { page, pageSize, skip: (page - 1) * pageSize, take: pageSize, }; } /** * 名称字段标准化并确保非空。 */ normalizeName(value: string, message: string) { const trimmed = value?.trim(); if (!trimmed) { throw new BadRequestException(message); } return trimmed; } /** * 数字参数标准化。 */ toInt(value: unknown, message: string) { const parsed = Number(value); if (!Number.isInteger(parsed)) { throw new BadRequestException(message); } return parsed; } /** * 确认医院存在。 */ async ensureHospitalExists(id: number) { const hospital = await this.prisma.hospital.findUnique({ where: { id }, select: { id: true }, }); if (!hospital) { throw new NotFoundException(MESSAGES.ORG.HOSPITAL_NOT_FOUND); } return hospital; } /** * 确认科室存在,并返回归属医院信息。 */ async ensureDepartmentExists(id: number) { const department = await this.prisma.department.findUnique({ where: { id }, select: { id: true, hospitalId: true }, }); if (!department) { throw new NotFoundException(MESSAGES.ORG.DEPARTMENT_NOT_FOUND); } return department; } /** * 统一处理删除冲突(存在外键引用)。 */ handleDeleteConflict(error: unknown) { if ( error instanceof Prisma.PrismaClientKnownRequestError && (error.code === 'P2003' || error.code === 'P2014') ) { throw new ConflictException(MESSAGES.ORG.DELETE_CONFLICT); } } }