tyt-api-nest/src/organization-common/organization-access.service.ts
EL 73082225f6 "1. 新增系统字典与全局植入目录相关表结构及迁移
2. 扩展患者手术与材料模型,更新种子数据
3. 新增字典模块,增强设备植入目录管理能力
4. 重构患者后台服务与表单链路,统一权限与参数校验
5. 管理台新增字典页面并改造患者/设备页面与路由权限
6. 补充字典及相关领域 e2e 测试并更新文档"
2026-03-19 20:42:17 +08:00

185 lines
4.6 KiB
TypeScript

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);
}
}
}