* оптимизация обновления при редактировании спец контингента * добавил поддержку заключительных диагнозов * изменил определение законченной операции * добавил поддержку исхода операции * добавил определение отмены для операции через назначение * работа над диапазонами календарей, подсчет статистики * добавил статусы отчетов и подкорректировал привязку спец контингента к отчету * добавил новые сервисы для будущего кеширования * частичное разделение логики подсчета пациентов
500 lines
19 KiB
PHP
500 lines
19 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Data\UnifiedPatientData;
|
|
use App\Models\Department;
|
|
use App\Models\DepartmentPatientOperation;
|
|
use App\Models\MedicalHistorySnapshot;
|
|
use App\Models\MetrikaResult;
|
|
use App\Models\MisMedicalHistory;
|
|
use App\Models\MisStationarBranch;
|
|
use App\Models\Report;
|
|
use App\Models\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class SnapshotService
|
|
{
|
|
public function __construct(
|
|
protected UnifiedPatientService $unifiedPatientService,
|
|
protected PatientService $patientService,
|
|
protected DateRangeService $dateRangeService,
|
|
) {}
|
|
|
|
/**
|
|
* Создать снапшоты пациентов для отчета
|
|
*/
|
|
public function createPatientSnapshots(Report $report, User $user, array $dates, $fillableAuto = false): void
|
|
{
|
|
$this->logSnapshotMemory('snapshots:start', [
|
|
'report_id' => $report->report_id,
|
|
'department_id' => $report->rf_department_id,
|
|
'fillable_auto' => (bool) $fillableAuto,
|
|
]);
|
|
|
|
$department = Department::query()->where('department_id', $report->rf_department_id)->first() ?? $user->department;
|
|
$branchId = $department
|
|
? $this->getBranchId($department->rf_mis_department_id)
|
|
: null;
|
|
|
|
if (!$department || !$branchId) {
|
|
return;
|
|
}
|
|
|
|
MedicalHistorySnapshot::query()
|
|
->where('rf_report_id', $report->report_id)
|
|
->delete();
|
|
$this->logSnapshotMemory('snapshots:after_delete_old', [
|
|
'report_id' => $report->report_id,
|
|
]);
|
|
|
|
[$startDate, $endDate] = $this->parseDates($dates);
|
|
$dateRange = $this->dateRangeService->getNormalizedDateRange($user, $startDate, $endDate);
|
|
$metrics = [];
|
|
|
|
$this->logSnapshotMemory('snapshots:before_plan_load', ['report_id' => $report->report_id]);
|
|
$planPatients = $this->unifiedPatientService->getLivePatientsByStatus(
|
|
$department,
|
|
$user,
|
|
'plan',
|
|
$dateRange,
|
|
$branchId,
|
|
false,
|
|
!$fillableAuto,
|
|
$fillableAuto,
|
|
true
|
|
);
|
|
$this->logSnapshotMemory('snapshots:after_plan_load', [
|
|
'report_id' => $report->report_id,
|
|
'count' => $planPatients->count(),
|
|
]);
|
|
$this->createSnapshotsForType($report, 'plan', $planPatients);
|
|
$this->logSnapshotMemory('snapshots:after_plan_save', ['report_id' => $report->report_id]);
|
|
$metrics[4] = $planPatients->count();
|
|
|
|
$this->logSnapshotMemory('snapshots:before_emergency_load', ['report_id' => $report->report_id]);
|
|
$emergencyPatients = $this->unifiedPatientService->getLivePatientsByStatus(
|
|
$department,
|
|
$user,
|
|
'emergency',
|
|
$dateRange,
|
|
$branchId,
|
|
false,
|
|
!$fillableAuto,
|
|
$fillableAuto,
|
|
true
|
|
);
|
|
$this->logSnapshotMemory('snapshots:after_emergency_load', [
|
|
'report_id' => $report->report_id,
|
|
'count' => $emergencyPatients->count(),
|
|
]);
|
|
$this->createSnapshotsForType($report, 'emergency', $emergencyPatients);
|
|
$this->logSnapshotMemory('snapshots:after_emergency_save', ['report_id' => $report->report_id]);
|
|
$metrics[12] = $emergencyPatients->count();
|
|
|
|
$this->logSnapshotMemory('snapshots:before_discharged_load', ['report_id' => $report->report_id]);
|
|
$dischargedPatients = $this->unifiedPatientService->getLivePatientsByStatus(
|
|
$department,
|
|
$user,
|
|
'outcome-discharged',
|
|
$dateRange,
|
|
$branchId,
|
|
false,
|
|
null,
|
|
$fillableAuto,
|
|
true
|
|
);
|
|
$this->logSnapshotMemory('snapshots:after_discharged_load', [
|
|
'report_id' => $report->report_id,
|
|
'count' => $dischargedPatients->count(),
|
|
]);
|
|
$this->createSnapshotsForType($report, 'discharged', $dischargedPatients);
|
|
$this->logSnapshotMemory('snapshots:after_discharged_save', ['report_id' => $report->report_id]);
|
|
$metrics[15] = $dischargedPatients->count();
|
|
|
|
$this->logSnapshotMemory('snapshots:before_transferred_load', ['report_id' => $report->report_id]);
|
|
$transferredPatients = $this->unifiedPatientService->getLivePatientsByStatus(
|
|
$department,
|
|
$user,
|
|
'outcome-transferred',
|
|
$dateRange,
|
|
$branchId,
|
|
false,
|
|
null,
|
|
$fillableAuto,
|
|
true
|
|
);
|
|
$this->logSnapshotMemory('snapshots:after_transferred_load', [
|
|
'report_id' => $report->report_id,
|
|
'count' => $transferredPatients->count(),
|
|
]);
|
|
$this->createSnapshotsForType($report, 'transferred', $transferredPatients);
|
|
$this->logSnapshotMemory('snapshots:after_transferred_save', ['report_id' => $report->report_id]);
|
|
$metrics[13] = $transferredPatients->count();
|
|
|
|
$this->logSnapshotMemory('snapshots:before_deceased_load', ['report_id' => $report->report_id]);
|
|
$deceasedPatients = $this->unifiedPatientService->getLivePatientsByStatus(
|
|
$department,
|
|
$user,
|
|
'outcome-deceased',
|
|
$dateRange,
|
|
$branchId,
|
|
false,
|
|
null,
|
|
$fillableAuto,
|
|
true
|
|
);
|
|
$this->logSnapshotMemory('snapshots:after_deceased_load', [
|
|
'report_id' => $report->report_id,
|
|
'count' => $deceasedPatients->count(),
|
|
]);
|
|
$this->createSnapshotsForType($report, 'deceased', $deceasedPatients);
|
|
$this->logSnapshotMemory('snapshots:after_deceased_save', ['report_id' => $report->report_id]);
|
|
|
|
$this->logSnapshotMemory('snapshots:before_recipient_load', ['report_id' => $report->report_id]);
|
|
$recipientPatients = $this->unifiedPatientService->getLivePatientsByStatus(
|
|
$department,
|
|
$user,
|
|
'recipient',
|
|
$dateRange,
|
|
$branchId,
|
|
false,
|
|
null,
|
|
$fillableAuto,
|
|
true
|
|
);
|
|
$this->logSnapshotMemory('snapshots:after_recipient_load', [
|
|
'report_id' => $report->report_id,
|
|
'count' => $recipientPatients->count(),
|
|
]);
|
|
$this->createSnapshotsForType($report, 'recipient', $recipientPatients);
|
|
$this->logSnapshotMemory('snapshots:after_recipient_save', ['report_id' => $report->report_id]);
|
|
|
|
$this->logSnapshotMemory('snapshots:before_current_load', ['report_id' => $report->report_id]);
|
|
$currentPatients = $this->unifiedPatientService->getLivePatientsByStatus(
|
|
$department,
|
|
$user,
|
|
'current',
|
|
$dateRange,
|
|
$branchId,
|
|
false,
|
|
null,
|
|
$fillableAuto,
|
|
true
|
|
);
|
|
$this->logSnapshotMemory('snapshots:after_current_load', [
|
|
'report_id' => $report->report_id,
|
|
'count' => $currentPatients->count(),
|
|
]);
|
|
$this->createSnapshotsForType($report, 'current', $currentPatients);
|
|
$this->logSnapshotMemory('snapshots:after_current_save', ['report_id' => $report->report_id]);
|
|
|
|
$planSurgeryCount = $this->patientService->getSurgicalPatients(
|
|
'plan',
|
|
$branchId,
|
|
$dateRange,
|
|
true
|
|
);
|
|
$emergencySurgeryCount = $this->patientService->getSurgicalPatients(
|
|
'emergency',
|
|
$branchId,
|
|
$dateRange,
|
|
true
|
|
);
|
|
|
|
$this->saveMetrics($report, $metrics);
|
|
$this->logSnapshotMemory('snapshots:after_save_metrics', ['report_id' => $report->report_id]);
|
|
}
|
|
|
|
private function logSnapshotMemory(string $stage, array $context = []): void
|
|
{
|
|
\Log::info('report.snapshot.memory', [
|
|
'stage' => $stage,
|
|
'memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
|
|
'peak_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
|
|
...$context,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Сохранить метрики в базу
|
|
*/
|
|
private function saveMetrics(Report $report, array $metrics): void
|
|
{
|
|
foreach ($metrics as $metrikaItemId => $value) {
|
|
MetrikaResult::updateOrCreate(
|
|
[
|
|
'rf_report_id' => $report->report_id,
|
|
'rf_metrika_item_id' => $metrikaItemId,
|
|
],
|
|
[
|
|
'value' => $value,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить статистику из снапшотов
|
|
*/
|
|
public function getStatisticsFromSnapshots(array $reportIds): array
|
|
{
|
|
return [
|
|
'plan' => $this->getCountFromSnapshots('plan', $reportIds),
|
|
'emergency' => $this->getCountFromSnapshots('emergency', $reportIds),
|
|
'outcome' => $this->getCountFromSnapshots('outcome', $reportIds),
|
|
'deceased' => $this->getCountFromSnapshots('deceased', $reportIds),
|
|
'discharged' => $this->getCountFromSnapshots('discharged', $reportIds),
|
|
'transferred' => $this->getCountFromSnapshots('transferred', $reportIds),
|
|
'recipient' => $this->getCountFromSnapshots('recipient', $reportIds),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Получить пациентов из снапшотов по типу
|
|
*/
|
|
public function getPatientsFromSnapshots(
|
|
string $type,
|
|
array $reportIds,
|
|
?int $branchId = null,
|
|
bool $onlyIds = false,
|
|
bool $markRecipients = false,
|
|
?array $recipientReportIds = null
|
|
): Collection {
|
|
$snapshots = MedicalHistorySnapshot::query()
|
|
->whereIn('rf_report_id', $reportIds)
|
|
->where('patient_type', $type)
|
|
->get()
|
|
->unique(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"))
|
|
->values();
|
|
|
|
if ($snapshots->isEmpty()) {
|
|
return collect();
|
|
}
|
|
|
|
$recipientIds = [];
|
|
if ($markRecipients) {
|
|
$recipientReportIds ??= $reportIds;
|
|
$recipientIds = MedicalHistorySnapshot::query()
|
|
->whereIn('rf_report_id', $recipientReportIds)
|
|
->where('patient_type', 'recipient')
|
|
->get()
|
|
->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
$operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots);
|
|
$operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots);
|
|
|
|
$patients = $snapshots->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) {
|
|
$patientId = $snapshot->rf_department_patient_id
|
|
? "manual:{$snapshot->rf_department_patient_id}"
|
|
: ($snapshot->patient_uid ?: "mis:{$snapshot->rf_medicalhistory_id}");
|
|
|
|
$misOperations = $snapshot->rf_medicalhistory_id
|
|
? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? [])
|
|
: [];
|
|
$manualOperations = $snapshot->rf_department_patient_id
|
|
? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? [])
|
|
: [];
|
|
$operations = collect($misOperations)
|
|
->merge($manualOperations)
|
|
->unique(fn (array $operation) => ($operation['code'] ?? '') . '|' . ($operation['name'] ?? ''))
|
|
->values()
|
|
->all();
|
|
|
|
return UnifiedPatientData::fromSnapshot(
|
|
$snapshot,
|
|
in_array($patientId, $recipientIds, true),
|
|
$operations
|
|
);
|
|
})->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')->values();
|
|
|
|
if ($onlyIds) {
|
|
return $patients->pluck('id');
|
|
}
|
|
|
|
return $patients;
|
|
}
|
|
|
|
public function getPatientsFromOneDayCurrentSnapshots(
|
|
string $type,
|
|
array $reportIds,
|
|
bool $onlyIds = false,
|
|
?array $recipientReportIds = null
|
|
): Collection {
|
|
$snapshots = MedicalHistorySnapshot::query()
|
|
->whereIn('rf_report_id', $reportIds)
|
|
->where('patient_type', 'current')
|
|
->get();
|
|
|
|
if ($snapshots->isEmpty()) {
|
|
return $this->getPatientsFromSnapshots(
|
|
$type,
|
|
$reportIds,
|
|
null,
|
|
$onlyIds,
|
|
true,
|
|
$recipientReportIds
|
|
);
|
|
}
|
|
|
|
$recipientReportIds ??= $reportIds;
|
|
$recipientIds = MedicalHistorySnapshot::query()
|
|
->whereIn('rf_report_id', $recipientReportIds)
|
|
->where('patient_type', 'recipient')
|
|
->get()
|
|
->map(fn (MedicalHistorySnapshot $snapshot) => UnifiedPatientData::fromSnapshot($snapshot)->id)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$operationsByHistoryId = $this->getOperationsByMedicalHistoryId($snapshots);
|
|
$operationsByDepartmentPatientId = $this->getOperationsByDepartmentPatientId($snapshots);
|
|
|
|
$patients = $snapshots
|
|
->filter(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_kind === $type)
|
|
->unique(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"))
|
|
->map(function (MedicalHistorySnapshot $snapshot) use ($recipientIds, $operationsByHistoryId, $operationsByDepartmentPatientId) {
|
|
$misOperations = $snapshot->rf_medicalhistory_id
|
|
? ($operationsByHistoryId[$snapshot->rf_medicalhistory_id] ?? [])
|
|
: [];
|
|
$manualOperations = $snapshot->rf_department_patient_id
|
|
? ($operationsByDepartmentPatientId[$snapshot->rf_department_patient_id] ?? [])
|
|
: [];
|
|
$operations = collect($misOperations)
|
|
->merge($manualOperations)
|
|
->unique(fn (array $operation) => ($operation['code'] ?? '') . '|' . ($operation['name'] ?? ''))
|
|
->values()
|
|
->all();
|
|
|
|
$patient = UnifiedPatientData::fromSnapshot(
|
|
$snapshot,
|
|
false,
|
|
$operations
|
|
);
|
|
$patient->isRecipientToday = in_array($patient->id, $recipientIds, true);
|
|
|
|
return $patient;
|
|
})
|
|
->sortByDesc(fn (UnifiedPatientData $patient) => $patient->admittedAt ?? '')
|
|
->values();
|
|
|
|
if ($onlyIds) {
|
|
return $patients->pluck('id');
|
|
}
|
|
|
|
return $patients;
|
|
}
|
|
|
|
/**
|
|
* Получить количество пациентов из снапшотов
|
|
*/
|
|
private function getCountFromSnapshots(string $type, array $reportIds): int
|
|
{
|
|
$query = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds);
|
|
|
|
if ($type === 'outcome') {
|
|
$query->whereIn('patient_type', ['discharged', 'deceased']);
|
|
} else {
|
|
$query->where('patient_type', $type);
|
|
}
|
|
|
|
return $query->get()
|
|
->map(fn (MedicalHistorySnapshot $snapshot) => $snapshot->patient_uid ?: ($snapshot->rf_medicalhistory_id ? "mis:{$snapshot->rf_medicalhistory_id}" : "snapshot:{$snapshot->medical_history_snapshot_id}"))
|
|
->unique()
|
|
->count();
|
|
}
|
|
|
|
/**
|
|
* Создать снапшоты для определенного типа пациентов
|
|
*/
|
|
private function createSnapshotsForType(Report $report, string $type, Collection $patients): void
|
|
{
|
|
foreach ($patients as $patient) {
|
|
if (!$patient instanceof UnifiedPatientData) {
|
|
continue;
|
|
}
|
|
|
|
MedicalHistorySnapshot::updateOrCreate(
|
|
[
|
|
'rf_report_id' => $report->report_id,
|
|
'patient_uid' => $patient->patientUid,
|
|
'patient_type' => $type,
|
|
],
|
|
[
|
|
'rf_report_id' => $report->report_id,
|
|
'patient_type' => $type,
|
|
...$patient->toSnapshotPayload($type),
|
|
]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Получить ID отделения
|
|
*/
|
|
private function getBranchId(int $misDepartmentId): ?int
|
|
{
|
|
return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId)
|
|
->value('StationarBranchID');
|
|
}
|
|
|
|
/**
|
|
* Разобрать даты
|
|
*/
|
|
private function parseDates(array $dates): array
|
|
{
|
|
return [
|
|
Carbon::createFromTimestampMs($dates[0])->setTimezone('Asia/Yakutsk'),
|
|
Carbon::createFromTimestampMs($dates[1])->setTimezone('Asia/Yakutsk'),
|
|
];
|
|
}
|
|
|
|
private function getOperationsByMedicalHistoryId(Collection $snapshots): array
|
|
{
|
|
$historyIds = $snapshots->pluck('rf_medicalhistory_id')->filter()->unique()->values();
|
|
|
|
if ($historyIds->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
return MisMedicalHistory::query()
|
|
->whereIn('MedicalHistoryID', $historyIds)
|
|
->with(['surgicalOperations.serviceMedical'])
|
|
->get()
|
|
->mapWithKeys(function (MisMedicalHistory $history) {
|
|
return [
|
|
$history->MedicalHistoryID => $history->surgicalOperations->map(fn ($operation) => [
|
|
'code' => $operation->serviceMedical?->ServiceMedicalCode,
|
|
'name' => $operation->serviceMedical?->ServiceMedicalName,
|
|
])->values()->all()
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function getOperationsByDepartmentPatientId(Collection $snapshots): array
|
|
{
|
|
$departmentPatientIds = $snapshots->pluck('rf_department_patient_id')->filter()->unique()->values();
|
|
|
|
if ($departmentPatientIds->isEmpty()) {
|
|
return [];
|
|
}
|
|
|
|
return DepartmentPatientOperation::query()
|
|
->whereIn('rf_department_patient_id', $departmentPatientIds)
|
|
->with('serviceMedical')
|
|
->get()
|
|
->groupBy('rf_department_patient_id')
|
|
->map(fn (Collection $operations) => $operations->map(fn (DepartmentPatientOperation $operation) => [
|
|
'code' => $operation->serviceMedical?->ServiceMedicalCode ?? $operation->service_code,
|
|
'name' => $operation->serviceMedical?->ServiceMedicalName ?? $operation->service_name,
|
|
])->values()->all())
|
|
->all();
|
|
}
|
|
}
|