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 = { 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; } }