Files
onboard/app/Console/Commands/RecalculatePreoperativeMetric.php
2026-03-25 17:37:32 +09:00

245 lines
8.4 KiB
PHP
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.

<?php
// app/Console/Commands/RecalculatePreoperativeMetric.php
namespace App\Console\Commands;
use App\Models\Report;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class RecalculatePreoperativeMetric extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'metrics:recalculate-preoperative
{--report= : ID конкретного отчета}
{--department= : ID отделения}
{--from= : Начальная дата (Y-m-d)}
{--to= : Конечная дата (Y-m-d)}
{--force : Принудительно перезаписать даже если есть значение}
{--chunk=100 : Количество отчетов за один раз}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Пересчет предоперационного койко-дня (метрика 25) для отчетов';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🚀 Начинаем пересчет предоперационного койко-дня...');
$query = Report::query();
// Фильтр по конкретному отчету
if ($reportId = $this->option('report')) {
$query->where('report_id', $reportId);
$this->info("📋 Фильтр: отчет ID {$reportId}");
}
// Фильтр по отделению
if ($departmentId = $this->option('department')) {
$query->where('rf_department_id', $departmentId);
$this->info("🏥 Фильтр: отделение ID {$departmentId}");
}
// Фильтр по дате
if ($from = $this->option('from')) {
$query->whereDate('created_at', '>=', $from);
$this->info("📅 Фильтр: с {$from}");
}
if ($to = $this->option('to')) {
$query->whereDate('created_at', '<=', $to);
$this->info("📅 Фильтр: по {$to}");
}
$force = $this->option('force');
$chunkSize = (int)$this->option('chunk');
$totalReports = $query->count();
if ($totalReports === 0) {
$this->warn('❌ Отчеты не найдены');
return 0;
}
$this->info("📊 Найдено отчетов: {$totalReports}");
if ($totalReports > 1000 && !$this->confirm("⚠️ Обработка {$totalReports} отчетов может занять время. Продолжить?")) {
$this->info('❌ Операция отменена');
return 0;
}
$bar = $this->output->createProgressBar($totalReports);
$bar->start();
$updated = 0;
$skipped = 0;
$errors = 0;
$noPatients = 0;
$query->orderBy('report_id')->chunk($chunkSize, function ($reports) use ($force, &$updated, &$skipped, &$errors, &$noPatients, $bar) {
foreach ($reports as $report) {
try {
$result = $this->processReport($report, $force);
match ($result) {
'updated' => $updated++,
'skipped' => $skipped++,
'no_patients' => $noPatients++,
default => null
};
} catch (\Exception $e) {
$errors++;
Log::error("Ошибка обработки отчета {$report->report_id}: " . $e->getMessage());
}
$bar->advance();
}
});
$bar->finish();
$this->newLine(2);
// Итоговая таблица
$this->table(
['Статус', 'Количество'],
[
['✅ Обновлено', $updated],
['⏭️ Пропущено (уже есть)', $skipped],
['👤 Нет пациентов', $noPatients],
['❌ Ошибок', $errors],
['📊 Всего', $totalReports],
]
);
// Покажем примеры обновленных отчетов
if ($updated > 0) {
$this->newLine();
$this->info('📋 Примеры обновленных отчетов:');
$samples = Report::whereHas('metrikaResults', function($q) {
$q->where('rf_metrika_item_id', 21);
})
->orderBy('report_id', 'desc')
->limit(5)
->get()
->map(function($report) {
$metric = $report->metrikaResults
->where('rf_metrika_item_id', 21)
->first();
return [
'report_id' => $report->report_id,
'department' => $report->rf_department_id,
'date' => $report->created_at->format('Y-m-d'),
'preoperative_days' => $metric ? $metric->value : 'N/A',
];
});
$this->table(['ID отчета', 'Отделение', 'Дата', 'Предоп. дни'], $samples->toArray());
}
$this->newLine();
$this->info('✅ Готово!');
return 0;
}
/**
* Обработка одного отчета
*/
private function processReport(Report $report, bool $force): string
{
// Проверяем, есть ли уже метрика
$existing = MetrikaResult::where('rf_report_id', $report->report_id)
->where('rf_metrika_item_id', 21)
->first();
if ($existing && !$force) {
return 'skipped';
}
// Получаем пациентов из снапшотов
$snapshots = MedicalHistorySnapshot::where('rf_report_id', $report->report_id)
->whereIn('patient_type', ['discharged', 'deceased'])
->pluck('rf_medicalhistory_id');
if ($snapshots->isEmpty()) {
// Сохраняем 0 если нет пациентов
MetrikaResult::updateOrCreate(
[
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => 21,
],
['value' => 0]
);
return 'no_patients';
}
// Получаем первые операции и первые поступления
$operations = DB::table('stt_surgicaloperation as so')
->join('stt_migrationpatient as mp', 'so.rf_MedicalHistoryID', '=', 'mp.rf_MedicalHistoryID')
->whereIn('so.rf_MedicalHistoryID', $snapshots)
->whereNotNull('so.Date')
->whereNotNull('mp.DateIngoing')
->select(
'so.rf_MedicalHistoryID',
DB::raw('MIN(so."Date") as first_operation'),
DB::raw('MIN(mp."DateIngoing") as first_admission')
)
->groupBy('so.rf_MedicalHistoryID')
->get();
if ($operations->isEmpty()) {
// Сохраняем 0 если нет операций
MetrikaResult::updateOrCreate(
[
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => 21,
],
['value' => 0]
);
return 'no_patients';
}
$totalDays = 0;
$count = 0;
foreach ($operations as $op) {
$days = Carbon::parse($op->first_admission)
->diffInDays(Carbon::parse($op->first_operation));
if ($days >= 0) {
$totalDays += $days;
$count++;
}
}
$avgDays = $count > 0 ? round($totalDays / $count, 1) : 0;
// Сохраняем метрику
MetrikaResult::updateOrCreate(
[
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => 21,
],
['value' => $avgDays]
);
return 'updated';
}
}