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

284 lines
7.8 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 { 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);
}
}
/**
* 校验并返回 hospitalIdB 端强依赖租户隔离)。
*/
private requireHospitalId(actor: ActorContext): number {
if (!actor.hospitalId) {
throw new BadRequestException(MESSAGES.TASK.ACTOR_HOSPITAL_REQUIRED);
}
return actor.hospitalId;
}
}