tyt-api-nest/src/patients/b-patients/b-patients.service.ts
2026-03-13 13:23:59 +08:00

394 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Prisma } from '../../generated/prisma/client.js';
import { Role } from '../../generated/prisma/enums.js';
import { PrismaService } from '../../prisma.service.js';
import type { ActorContext } from '../../common/actor-context.js';
import { MESSAGES } from '../../common/messages.js';
import { CreatePatientDto } from '../dto/create-patient.dto.js';
import { UpdatePatientDto } from '../dto/update-patient.dto.js';
const PATIENT_OWNER_ROLES: Role[] = [Role.DOCTOR, Role.DIRECTOR, Role.LEADER];
/**
* B 端患者服务:承载院内可见性隔离与患者 CRUD。
*/
@Injectable()
export class BPatientsService {
constructor(private readonly prisma: PrismaService) {}
/**
* 查询当前角色可见患者列表。
*/
async findVisiblePatients(actor: ActorContext, requestedHospitalId?: number) {
const hospitalId = this.resolveHospitalId(actor, requestedHospitalId);
const where = this.buildVisiblePatientWhere(actor, hospitalId);
return this.prisma.patient.findMany({
where,
include: {
hospital: { select: { id: true, name: true } },
doctor: { select: { id: true, name: true, role: true } },
devices: true,
},
orderBy: { id: 'desc' },
});
}
/**
* 查询当前角色可见归属人员列表(医生/主任/组长),用于患者表单选择。
*/
async findVisibleDoctors(actor: ActorContext, requestedHospitalId?: number) {
const hospitalId = this.resolveHospitalId(actor, requestedHospitalId);
const where: Prisma.UserWhereInput = {
role: { in: PATIENT_OWNER_ROLES },
hospitalId,
};
switch (actor.role) {
case Role.DOCTOR:
where.id = actor.id;
break;
case Role.LEADER:
if (!actor.groupId) {
throw new BadRequestException(MESSAGES.PATIENT.GROUP_REQUIRED);
}
where.groupId = actor.groupId;
break;
case Role.DIRECTOR:
if (!actor.departmentId) {
throw new BadRequestException(MESSAGES.PATIENT.DEPARTMENT_REQUIRED);
}
where.departmentId = actor.departmentId;
break;
case Role.HOSPITAL_ADMIN:
case Role.SYSTEM_ADMIN:
break;
default:
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
return this.prisma.user.findMany({
where,
select: {
id: true,
name: true,
phone: true,
hospitalId: true,
departmentId: true,
groupId: true,
role: true,
},
orderBy: { id: 'desc' },
});
}
/**
* 创建患者。
*/
async createPatient(actor: ActorContext, dto: CreatePatientDto) {
const doctor = await this.resolveWritableDoctor(actor, dto.doctorId);
return this.prisma.patient.create({
data: {
name: this.normalizeRequiredString(dto.name, 'name'),
phone: this.normalizePhone(dto.phone),
idCardHash: this.normalizeRequiredString(dto.idCardHash, 'idCardHash'),
hospitalId: doctor.hospitalId!,
doctorId: doctor.id,
},
include: {
hospital: { select: { id: true, name: true } },
doctor: { select: { id: true, name: true, role: true } },
devices: true,
},
});
}
/**
* 查询患者详情。
*/
async findPatientById(actor: ActorContext, id: number) {
const patient = await this.findPatientWithScope(id);
this.assertPatientScope(actor, patient);
return patient;
}
/**
* 更新患者信息。
*/
async updatePatient(actor: ActorContext, id: number, dto: UpdatePatientDto) {
const patient = await this.findPatientWithScope(id);
this.assertPatientScope(actor, patient);
const data: Prisma.PatientUpdateInput = {};
if (dto.name !== undefined) {
data.name = this.normalizeRequiredString(dto.name, 'name');
}
if (dto.phone !== undefined) {
data.phone = this.normalizePhone(dto.phone);
}
if (dto.idCardHash !== undefined) {
data.idCardHash = this.normalizeRequiredString(dto.idCardHash, 'idCardHash');
}
if (dto.doctorId !== undefined) {
const doctor = await this.resolveWritableDoctor(actor, dto.doctorId);
data.doctor = { connect: { id: doctor.id } };
data.hospital = { connect: { id: doctor.hospitalId! } };
}
return this.prisma.patient.update({
where: { id: patient.id },
data,
include: {
hospital: { select: { id: true, name: true } },
doctor: { select: { id: true, name: true, role: true } },
devices: true,
},
});
}
/**
* 删除患者。
*/
async removePatient(actor: ActorContext, id: number) {
const patient = await this.findPatientWithScope(id);
this.assertPatientScope(actor, patient);
try {
return await this.prisma.patient.delete({
where: { id: patient.id },
include: {
hospital: { select: { id: true, name: true } },
doctor: { select: { id: true, name: true, role: true } },
devices: true,
},
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2003'
) {
throw new ConflictException(MESSAGES.PATIENT.DELETE_CONFLICT);
}
throw error;
}
}
/**
* 查询患者并附带医生组织信息,用于权限判定。
*/
private async findPatientWithScope(id: number) {
const patientId = Number(id);
if (!Number.isInteger(patientId)) {
throw new BadRequestException('id 必须为整数');
}
const patient = await this.prisma.patient.findUnique({
where: { id: patientId },
include: {
hospital: { select: { id: true, name: true } },
doctor: {
select: {
id: true,
name: true,
role: true,
hospitalId: true,
departmentId: true,
groupId: true,
},
},
devices: true,
},
});
if (!patient) {
throw new NotFoundException(MESSAGES.PATIENT.NOT_FOUND);
}
return patient;
}
/**
* 校验当前角色是否可操作该患者。
*/
private assertPatientScope(
actor: ActorContext,
patient: {
hospitalId: number;
doctorId: number;
doctor: { departmentId: number | null; groupId: number | null };
},
) {
switch (actor.role) {
case Role.SYSTEM_ADMIN:
return;
case Role.HOSPITAL_ADMIN:
if (!actor.hospitalId || actor.hospitalId !== patient.hospitalId) {
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
return;
case Role.DIRECTOR:
if (!actor.departmentId || patient.doctor.departmentId !== actor.departmentId) {
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
return;
case Role.LEADER:
if (!actor.groupId || patient.doctor.groupId !== actor.groupId) {
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
return;
case Role.DOCTOR:
if (patient.doctorId !== actor.id) {
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
return;
default:
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
}
/**
* 校验并返回当前角色可写的医生。
*/
private async resolveWritableDoctor(actor: ActorContext, doctorId: number) {
const normalizedDoctorId = Number(doctorId);
if (!Number.isInteger(normalizedDoctorId)) {
throw new BadRequestException('doctorId 必须为整数');
}
const doctor = await this.prisma.user.findUnique({
where: { id: normalizedDoctorId },
select: {
id: true,
role: true,
hospitalId: true,
departmentId: true,
groupId: true,
},
});
if (!doctor) {
throw new NotFoundException(MESSAGES.PATIENT.DOCTOR_NOT_FOUND);
}
if (!PATIENT_OWNER_ROLES.includes(doctor.role)) {
throw new BadRequestException(MESSAGES.PATIENT.DOCTOR_ROLE_REQUIRED);
}
if (!doctor.hospitalId) {
throw new BadRequestException(MESSAGES.PATIENT.DOCTOR_NOT_FOUND);
}
switch (actor.role) {
case Role.SYSTEM_ADMIN:
return doctor;
case Role.HOSPITAL_ADMIN:
if (!actor.hospitalId || doctor.hospitalId !== actor.hospitalId) {
throw new ForbiddenException(MESSAGES.PATIENT.DOCTOR_SCOPE_FORBIDDEN);
}
return doctor;
case Role.DIRECTOR:
if (!actor.departmentId || doctor.departmentId !== actor.departmentId) {
throw new ForbiddenException(MESSAGES.PATIENT.DOCTOR_SCOPE_FORBIDDEN);
}
return doctor;
case Role.LEADER:
if (!actor.groupId || doctor.groupId !== actor.groupId) {
throw new ForbiddenException(MESSAGES.PATIENT.DOCTOR_SCOPE_FORBIDDEN);
}
return doctor;
case Role.DOCTOR:
if (doctor.id !== actor.id) {
throw new ForbiddenException(MESSAGES.PATIENT.DOCTOR_SCOPE_FORBIDDEN);
}
return doctor;
default:
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
}
/**
* 按角色构造患者可见性查询条件。
*/
private buildVisiblePatientWhere(actor: ActorContext, hospitalId: number) {
const where: Record<string, unknown> = { hospitalId };
switch (actor.role) {
case Role.DOCTOR:
where.doctorId = actor.id;
break;
case Role.LEADER:
if (!actor.groupId) {
throw new BadRequestException(MESSAGES.PATIENT.GROUP_REQUIRED);
}
where.doctor = {
groupId: actor.groupId,
role: { in: PATIENT_OWNER_ROLES },
};
break;
case Role.DIRECTOR:
if (!actor.departmentId) {
throw new BadRequestException(MESSAGES.PATIENT.DEPARTMENT_REQUIRED);
}
where.doctor = {
departmentId: actor.departmentId,
role: { in: PATIENT_OWNER_ROLES },
};
break;
case Role.HOSPITAL_ADMIN:
case Role.SYSTEM_ADMIN:
break;
default:
throw new ForbiddenException(MESSAGES.PATIENT.ROLE_FORBIDDEN);
}
return where;
}
/**
* 解析 B 端查询 hospitalId系统管理员必须显式指定医院。
*/
private resolveHospitalId(
actor: ActorContext,
requestedHospitalId?: number,
): number {
if (actor.role === Role.SYSTEM_ADMIN) {
const normalizedHospitalId = requestedHospitalId;
if (
normalizedHospitalId == null ||
!Number.isInteger(normalizedHospitalId)
) {
throw new BadRequestException(MESSAGES.PATIENT.SYSTEM_ADMIN_HOSPITAL_REQUIRED);
}
return normalizedHospitalId;
}
if (!actor.hospitalId) {
throw new BadRequestException(MESSAGES.PATIENT.ACTOR_HOSPITAL_REQUIRED);
}
return actor.hospitalId;
}
private normalizeRequiredString(value: unknown, fieldName: string) {
if (typeof value !== 'string') {
throw new BadRequestException(`${fieldName} 必须是字符串`);
}
const trimmed = value.trim();
if (!trimmed) {
throw new BadRequestException(`${fieldName} 不能为空`);
}
return trimmed;
}
private normalizePhone(phone: unknown) {
const normalized = this.normalizeRequiredString(phone, 'phone');
if (!/^1\d{10}$/.test(normalized)) {
throw new BadRequestException('phone 必须是合法手机号');
}
return normalized;
}
}