Files
HealthCarePregnant/pcm-platform/backend/src/modules/disposition/disposition.service.ts
T
2026-06-18 09:48:05 +08:00

191 lines
6.4 KiB
TypeScript

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<string, unknown>;
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<Disposition> {
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<Disposition[]> {
return this.repo.findByPatient(patientId);
}
async getById(id: string): Promise<Disposition> {
return this.require(id);
}
/** 人工确认中/高风险处置单(医生/管理师兜底),确认后方可执行。 */
async confirm(id: string, confirmedBy: string): Promise<Disposition> {
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<Disposition> {
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<Disposition> {
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<Disposition> {
const d = await this.repo.findById(id);
if (!d) throw new NotFoundException('处置单不存在');
return d;
}
}