2. 扩展患者手术与材料模型,更新种子数据 3. 新增字典模块,增强设备植入目录管理能力 4. 重构患者后台服务与表单链路,统一权限与参数校验 5. 管理台新增字典页面并改造患者/设备页面与路由权限 6. 补充字典及相关领域 e2e 测试并更新文档"
185 lines
4.6 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|