import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { DeviceStatus, Role, TaskStatus } from '../generated/prisma/enums.js'; import { PrismaService } from '../prisma.service.js'; import type { ActorContext } from '../common/actor-context.js'; import { PublishTaskDto } from './dto/publish-task.dto.js'; import { AcceptTaskDto } from './dto/accept-task.dto.js'; import { CompleteTaskDto } from './dto/complete-task.dto.js'; import { CancelTaskDto } from './dto/cancel-task.dto.js'; import { MESSAGES } from '../common/messages.js'; /** * 任务服务:封装调压任务状态机、院内隔离与事件发布逻辑。 */ @Injectable() export class TaskService { constructor( private readonly prisma: PrismaService, private readonly eventEmitter: EventEmitter2, ) {} /** * 发布任务:医生/主任/组长创建主任务与明细,状态初始化为 PENDING。 */ async publishTask(actor: ActorContext, dto: PublishTaskDto) { this.assertRole(actor, [Role.DOCTOR, Role.DIRECTOR, Role.LEADER]); const hospitalId = this.requireHospitalId(actor); if (!Array.isArray(dto.items) || dto.items.length === 0) { throw new BadRequestException(MESSAGES.TASK.ITEMS_REQUIRED); } const deviceIds = Array.from( new Set( dto.items.map((item) => { if (!Number.isInteger(item.deviceId)) { throw new BadRequestException(`deviceId 非法: ${item.deviceId}`); } if (!Number.isInteger(item.targetPressure)) { throw new BadRequestException(`targetPressure 非法: ${item.targetPressure}`); } return item.deviceId; }), ), ); const devices = await this.prisma.device.findMany({ where: { id: { in: deviceIds }, status: DeviceStatus.ACTIVE, patient: { hospitalId }, }, select: { id: true, currentPressure: true }, }); if (devices.length !== deviceIds.length) { throw new NotFoundException(MESSAGES.TASK.DEVICE_NOT_FOUND); } if (dto.engineerId != null) { const engineer = await this.prisma.user.findFirst({ where: { id: dto.engineerId, role: Role.ENGINEER, hospitalId, }, select: { id: true }, }); if (!engineer) { throw new BadRequestException(MESSAGES.TASK.ENGINEER_INVALID); } } const pressureByDeviceId = new Map( devices.map((device) => [device.id, device.currentPressure] as const), ); const task = await this.prisma.task.create({ data: { status: TaskStatus.PENDING, creatorId: actor.id, engineerId: dto.engineerId ?? null, hospitalId, items: { create: dto.items.map((item) => ({ deviceId: item.deviceId, oldPressure: pressureByDeviceId.get(item.deviceId) ?? 0, targetPressure: item.targetPressure, })), }, }, include: { items: true }, }); await this.eventEmitter.emitAsync('task.published', { taskId: task.id, hospitalId: task.hospitalId, actorId: actor.id, status: task.status, }); return task; } /** * 接收任务:工程师将任务从 PENDING 流转到 ACCEPTED。 */ async acceptTask(actor: ActorContext, dto: AcceptTaskDto) { this.assertRole(actor, [Role.ENGINEER]); const hospitalId = this.requireHospitalId(actor); const task = await this.prisma.task.findFirst({ where: { id: dto.taskId, hospitalId, }, select: { id: true, status: true, hospitalId: true, engineerId: true, }, }); if (!task) { throw new NotFoundException(MESSAGES.TASK.TASK_NOT_FOUND); } if (task.status !== TaskStatus.PENDING) { throw new ConflictException(MESSAGES.TASK.ACCEPT_ONLY_PENDING); } if (task.engineerId != null && task.engineerId !== actor.id) { throw new ForbiddenException(MESSAGES.TASK.ENGINEER_ALREADY_ASSIGNED); } const updatedTask = await this.prisma.task.update({ where: { id: task.id }, data: { status: TaskStatus.ACCEPTED, engineerId: actor.id, }, include: { items: true }, }); await this.eventEmitter.emitAsync('task.accepted', { taskId: updatedTask.id, hospitalId: updatedTask.hospitalId, actorId: actor.id, status: updatedTask.status, }); return updatedTask; } /** * 完成任务:工程师将任务置为 COMPLETED,并同步设备当前压力。 */ async completeTask(actor: ActorContext, dto: CompleteTaskDto) { this.assertRole(actor, [Role.ENGINEER]); const hospitalId = this.requireHospitalId(actor); const task = await this.prisma.task.findFirst({ where: { id: dto.taskId, hospitalId, }, include: { items: true, }, }); if (!task) { throw new NotFoundException(MESSAGES.TASK.TASK_NOT_FOUND); } if (task.status !== TaskStatus.ACCEPTED) { throw new ConflictException(MESSAGES.TASK.COMPLETE_ONLY_ACCEPTED); } if (task.engineerId !== actor.id) { throw new ForbiddenException(MESSAGES.TASK.ENGINEER_ONLY_ASSIGNEE); } const completedTask = await this.prisma.$transaction(async (tx) => { const nextTask = await tx.task.update({ where: { id: task.id }, data: { status: TaskStatus.COMPLETED }, include: { items: true }, }); await Promise.all( task.items.map((item) => tx.device.update({ where: { id: item.deviceId }, data: { currentPressure: item.targetPressure }, }), ), ); return nextTask; }); await this.eventEmitter.emitAsync('task.completed', { taskId: completedTask.id, hospitalId: completedTask.hospitalId, actorId: actor.id, status: completedTask.status, }); return completedTask; } /** * 取消任务:任务创建者可将 PENDING/ACCEPTED 任务取消。 */ async cancelTask(actor: ActorContext, dto: CancelTaskDto) { this.assertRole(actor, [Role.DOCTOR, Role.DIRECTOR, Role.LEADER]); const hospitalId = this.requireHospitalId(actor); const task = await this.prisma.task.findFirst({ where: { id: dto.taskId, hospitalId, }, select: { id: true, status: true, creatorId: true, hospitalId: true, }, }); if (!task) { throw new NotFoundException(MESSAGES.TASK.TASK_NOT_FOUND); } if (task.creatorId !== actor.id) { throw new ForbiddenException(MESSAGES.TASK.CANCEL_ONLY_CREATOR); } if ( task.status !== TaskStatus.PENDING && task.status !== TaskStatus.ACCEPTED ) { throw new ConflictException(MESSAGES.TASK.CANCEL_ONLY_PENDING_ACCEPTED); } const cancelledTask = await this.prisma.task.update({ where: { id: task.id }, data: { status: TaskStatus.CANCELLED }, include: { items: true }, }); await this.eventEmitter.emitAsync('task.cancelled', { taskId: cancelledTask.id, hospitalId: cancelledTask.hospitalId, actorId: actor.id, status: cancelledTask.status, }); return cancelledTask; } /** * 校验角色权限。 */ private assertRole(actor: ActorContext, allowedRoles: Role[]) { if (!allowedRoles.includes(actor.role)) { throw new ForbiddenException(MESSAGES.TASK.ACTOR_ROLE_FORBIDDEN); } } /** * 校验并返回 hospitalId(B 端强依赖租户隔离)。 */ private requireHospitalId(actor: ActorContext): number { if (!actor.hospitalId) { throw new BadRequestException(MESSAGES.TASK.ACTOR_HOSPITAL_REQUIRED); } return actor.hospitalId; } }