新增 B 端上传接口与列表接口,统一文件上传和分页查询能力 上传能力支持医院级数据隔离:系统管理员需显式指定医院,院内角色按登录医院自动隔离 图片上传自动压缩并转为 webp,视频上传自动转码并压缩为 mp4,普通文件按原始类型存储 增加上传目录与公开访问能力,统一输出可直接预览的访问地址 前端新增影像库页面,支持按类型筛选、关键字检索、分页浏览、在线预览与原文件访问 前端新增通用上传组件,支持在页面内复用并返回上传结果 管理后台新增影像库菜单与路由,并补充页面级角色权限控制 患者手术相关表单接入上传复用能力,支持术前资料与设备标签上传回填 新增上传模块 e2e 用例,覆盖成功路径、权限矩阵与关键失败场景 补充上传模块文档与安装依赖说明,完善工程内使用说明
178 lines
5.2 KiB
TypeScript
178 lines
5.2 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
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 { OrganizationAccessService } from '../organization-common/organization-access.service.js';
|
|
import { CreateDepartmentDto } from './dto/create-department.dto.js';
|
|
import { UpdateDepartmentDto } from './dto/update-department.dto.js';
|
|
import { OrganizationQueryDto } from '../organization-common/dto/organization-query.dto.js';
|
|
|
|
/**
|
|
* 科室资源服务:聚焦科室实体 CRUD 与医院作用域限制。
|
|
*/
|
|
@Injectable()
|
|
export class DepartmentsService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly access: OrganizationAccessService,
|
|
) {}
|
|
|
|
/**
|
|
* 创建科室:系统管理员可跨院创建;院管仅可创建本院科室。
|
|
*/
|
|
async create(actor: ActorContext, dto: CreateDepartmentDto) {
|
|
this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
|
|
const hospitalId = this.access.toInt(
|
|
dto.hospitalId,
|
|
MESSAGES.ORG.HOSPITAL_ID_REQUIRED,
|
|
);
|
|
await this.access.ensureHospitalExists(hospitalId);
|
|
this.access.assertHospitalScope(actor, hospitalId);
|
|
|
|
return this.prisma.department.create({
|
|
data: {
|
|
name: this.access.normalizeName(
|
|
dto.name,
|
|
MESSAGES.ORG.DEPARTMENT_NAME_REQUIRED,
|
|
),
|
|
hospitalId,
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 查询科室列表:院管限定本院。
|
|
*/
|
|
async findAll(actor: ActorContext, query: OrganizationQueryDto) {
|
|
this.access.assertRole(actor, [
|
|
Role.SYSTEM_ADMIN,
|
|
Role.HOSPITAL_ADMIN,
|
|
Role.DIRECTOR,
|
|
Role.LEADER,
|
|
Role.DOCTOR,
|
|
]);
|
|
const paging = this.access.resolvePaging(query);
|
|
const where: Prisma.DepartmentWhereInput = {};
|
|
|
|
if (query.keyword) {
|
|
where.name = { contains: query.keyword.trim(), mode: 'insensitive' };
|
|
}
|
|
|
|
if (actor.role === Role.HOSPITAL_ADMIN) {
|
|
where.hospitalId = this.access.requireActorHospitalId(actor);
|
|
} else if (
|
|
actor.role === Role.DIRECTOR ||
|
|
actor.role === Role.LEADER ||
|
|
actor.role === Role.DOCTOR
|
|
) {
|
|
where.id = this.access.requireActorDepartmentId(actor);
|
|
} else if (query.hospitalId != null) {
|
|
where.hospitalId = this.access.toInt(
|
|
query.hospitalId,
|
|
MESSAGES.ORG.HOSPITAL_ID_REQUIRED,
|
|
);
|
|
}
|
|
|
|
const [total, list] = await this.prisma.$transaction([
|
|
this.prisma.department.count({ where }),
|
|
this.prisma.department.findMany({
|
|
where,
|
|
include: {
|
|
hospital: true,
|
|
_count: { select: { users: true, groups: true } },
|
|
},
|
|
skip: paging.skip,
|
|
take: paging.take,
|
|
orderBy: { id: 'desc' },
|
|
}),
|
|
]);
|
|
|
|
return { total, ...paging, list };
|
|
}
|
|
|
|
/**
|
|
* 查询科室详情:院管仅可查看本院。
|
|
*/
|
|
async findOne(actor: ActorContext, id: number) {
|
|
this.access.assertRole(actor, [
|
|
Role.SYSTEM_ADMIN,
|
|
Role.HOSPITAL_ADMIN,
|
|
Role.DIRECTOR,
|
|
Role.LEADER,
|
|
Role.DOCTOR,
|
|
]);
|
|
const departmentId = this.access.toInt(
|
|
id,
|
|
MESSAGES.ORG.DEPARTMENT_ID_REQUIRED,
|
|
);
|
|
const department = await this.prisma.department.findUnique({
|
|
where: { id: departmentId },
|
|
include: {
|
|
hospital: true,
|
|
_count: { select: { users: true, groups: true } },
|
|
},
|
|
});
|
|
if (!department) {
|
|
throw new NotFoundException(MESSAGES.ORG.DEPARTMENT_NOT_FOUND);
|
|
}
|
|
if (actor.role === Role.HOSPITAL_ADMIN) {
|
|
this.access.assertHospitalScope(actor, department.hospitalId);
|
|
} else if (
|
|
actor.role === Role.DIRECTOR ||
|
|
actor.role === Role.LEADER ||
|
|
actor.role === Role.DOCTOR
|
|
) {
|
|
const actorDepartmentId = this.access.requireActorDepartmentId(actor);
|
|
if (department.id !== actorDepartmentId) {
|
|
throw new ForbiddenException(MESSAGES.ORG.HOSPITAL_ADMIN_SCOPE_INVALID);
|
|
}
|
|
}
|
|
return department;
|
|
}
|
|
|
|
/**
|
|
* 更新科室:院管仅可修改本院。
|
|
*/
|
|
async update(actor: ActorContext, id: number, dto: UpdateDepartmentDto) {
|
|
const current = await this.findOne(actor, id);
|
|
const data: Prisma.DepartmentUpdateInput = {};
|
|
|
|
if (dto.hospitalId !== undefined) {
|
|
throw new BadRequestException(MESSAGES.ORG.DEPARTMENT_REPARENT_FORBIDDEN);
|
|
}
|
|
|
|
if (dto.name !== undefined) {
|
|
data.name = this.access.normalizeName(
|
|
dto.name,
|
|
MESSAGES.ORG.DEPARTMENT_NAME_REQUIRED,
|
|
);
|
|
}
|
|
|
|
return this.prisma.department.update({
|
|
where: { id: current.id },
|
|
data,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 删除科室:院管仅可删本院科室。
|
|
*/
|
|
async remove(actor: ActorContext, id: number) {
|
|
this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]);
|
|
const current = await this.findOne(actor, id);
|
|
try {
|
|
return await this.prisma.department.delete({ where: { id: current.id } });
|
|
} catch (error) {
|
|
this.access.handleDeleteConflict(error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|