import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { randomUUID } from 'node:crypto'; import { DispositionRepository } from './disposition.repository'; import { ActionKind, ClosureOutcome, Disposition, DispositionSourceType, DispositionStatus, RiskLevel, } from './disposition.types'; import { canTransition, initialStatus } from './disposition-state-machine'; import { FollowupService } from '../followup/followup.service'; import { TargetOperator } from '../followup/followup.types'; export interface CreateActionInput { kind: ActionKind; params?: Record; assigneeId?: string | null; dueAt?: string | null; } export interface CreateDispositionInput { caseId: string; patientId: string; sourceType: DispositionSourceType; sourceId?: string | null; title: string; riskLevelAtCreation: RiskLevel; actions: CreateActionInput[]; supersedesId?: string | null; } export interface ExecuteActionPatch { linkedEntityId?: string | null; resultNote?: string | null; } /** * 处置单服务(REQ-D1)。 * 负责处置单创建(含动作组)、风险门控确认、动作执行与回执、闭环。 * 安全:中/高风险须人工确认方可执行(REQ-D1.2 / REQ-10.3)。 */ @Injectable() export class DispositionService { constructor( private readonly repo: DispositionRepository, private readonly followup: FollowupService, ) {} async create(input: CreateDispositionInput, createdBy: string): Promise { if (!input.title?.trim()) { throw new BadRequestException('处置单需标题'); } if (!input.actions?.length) { throw new BadRequestException('处置单需至少一个处置动作'); } const now = new Date().toISOString(); const requiresConfirmation = input.riskLevelAtCreation !== 'low'; const d: Disposition = { id: randomUUID(), caseId: input.caseId, patientId: input.patientId, sourceType: input.sourceType, sourceId: input.sourceId ?? null, title: input.title.trim(), riskLevelAtCreation: input.riskLevelAtCreation, requiresConfirmation, status: initialStatus(requiresConfirmation), closureOutcome: null, supersedesId: input.supersedesId ?? null, createdBy, confirmedBy: null, createdAt: now, updatedAt: now, closedAt: null, actions: input.actions.map((a) => ({ id: randomUUID(), kind: a.kind, params: a.params ?? {}, status: 'planned' as const, assigneeId: a.assigneeId ?? null, dueAt: a.dueAt ?? null, executedAt: null, resultNote: null, linkedEntityId: null, })), }; return this.repo.save(d); } listByPatient(patientId: string): Promise { return this.repo.findByPatient(patientId); } async getById(id: string): Promise { return this.require(id); } /** 人工确认中/高风险处置单(医生/管理师兜底),确认后方可执行。 */ async confirm(id: string, confirmedBy: string): Promise { const d = await this.require(id); if (d.status !== 'pending_confirmation') { throw new BadRequestException('仅「待确认」状态的处置单可确认'); } this.setStatus(d, 'in_progress'); d.confirmedBy = confirmedBy; return this.repo.save(d); } /** 执行单个处置动作并回执;中/高风险未确认前禁止执行。 */ async executeAction( dispositionId: string, actionId: string, patch: ExecuteActionPatch = {}, ): Promise { const d = await this.require(dispositionId); if (d.status === 'pending_confirmation') { throw new BadRequestException('处置单需先经人工确认方可执行(REQ-10.3 人工兜底)'); } if (d.status === 'closed') { throw new BadRequestException('处置单已闭环'); } const action = d.actions.find((a) => a.id === actionId); if (!action) { throw new NotFoundException('处置动作不存在'); } action.status = 'executed'; action.executedAt = new Date().toISOString(); if (patch.linkedEntityId !== undefined) action.linkedEntityId = patch.linkedEntityId; if (patch.resultNote !== undefined) action.resultNote = patch.resultNote; // 复测动作执行 → 自动生成跟进项(REQ-D2),并回填 linkedEntityId if (action.kind === 'recheck' && !action.linkedEntityId) { const indicator = String(action.params?.indicator ?? ''); if (indicator) { const fu = await this.followup.createForRecheck({ dispositionId: d.id, patientId: d.patientId, indicator, targetOperator: action.params?.targetOperator as TargetOperator | undefined, targetValue: typeof action.params?.targetValue === 'number' ? (action.params.targetValue as number) : undefined, windowDays: typeof action.params?.windowDays === 'number' ? (action.params.windowDays as number) : undefined, }); if (fu) action.linkedEntityId = fu.id; } } // 全部动作落地(执行或跳过)→ 进入「跟进中」 const allSettled = d.actions.every((a) => a.status === 'executed' || a.status === 'skipped'); if (allSettled && d.status === 'in_progress') { this.setStatus(d, 'following_up'); } else { d.updatedAt = new Date().toISOString(); } return this.repo.save(d); } /** 闭环处置单并记录结果(达标/未达标/升级)。 */ async close(id: string, outcome: ClosureOutcome): Promise { const d = await this.require(id); if (d.status !== 'in_progress' && d.status !== 'following_up') { throw new BadRequestException('仅「执行中/跟进中」的处置单可闭环'); } this.setStatus(d, 'closed'); d.closureOutcome = outcome; d.closedAt = new Date().toISOString(); return this.repo.save(d); } private setStatus(d: Disposition, to: DispositionStatus): void { if (!canTransition(d.status, to)) { throw new BadRequestException(`非法状态流转:${d.status} → ${to}`); } d.status = to; d.updatedAt = new Date().toISOString(); } private async require(id: string): Promise { const d = await this.repo.findById(id); if (!d) throw new NotFoundException('处置单不存在'); return d; } }