Initial commit: HealthCarePregnant project documentation and platform
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user