Изменения в основном report

This commit is contained in:
brusnitsyn
2026-05-06 22:32:11 +09:00
parent c5da85763c
commit 723ccee8d3
56 changed files with 1911 additions and 3814 deletions

View File

@@ -2,7 +2,6 @@
namespace App\Services;
use App\Application\Reports\ReportSavePathService;
use App\Models\Department;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
@@ -19,13 +18,12 @@ class AutoReportService
public function __construct(
mixed $dateRangeService = null,
mixed $reportSavePathService = null,
?ReportSavePathService $legacyReportSavePathService = null,
) {
if ($dateRangeService instanceof ReportService) {
$this->dateRangeService = $reportSavePathService instanceof DateRangeService
? $reportSavePathService
: app(DateRangeService::class);
$this->reportSavePathService = $legacyReportSavePathService ?? app(ReportSavePathService::class);
$this->reportService = $dateRangeService;
return;
}
@@ -33,12 +31,14 @@ class AutoReportService
$this->dateRangeService = $dateRangeService instanceof DateRangeService
? $dateRangeService
: app(DateRangeService::class);
$this->reportSavePathService = $reportSavePathService ?? app(ReportSavePathService::class);
$this->reportService = $reportSavePathService instanceof ReportService
? $reportSavePathService
: app(ReportService::class);
}
protected DateRangeService $dateRangeService;
protected ReportSavePathService $reportSavePathService;
protected ReportService $reportService;
/**
* Заполнить отчеты для пользователя за период
@@ -96,9 +96,8 @@ class AutoReportService
$this->deleteExistingReport($existingReport);
}
DB::transaction(function () use ($scopedUser, $department, $dateRange) {
$this->reportSavePathService->saveAutoFill($scopedUser, $department, $dateRange);
});
$payload = $this->reportService->buildAutoFillReportPayload($scopedUser, $department, $dateRange);
$this->reportService->storeReport($payload, $scopedUser, true);
return true;
}

View File

@@ -3,7 +3,9 @@
namespace App\Services;
use App\Models\MisMigrationPatient;
use App\Models\MedicalHistory;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class CurrentPatientService
{
@@ -13,6 +15,20 @@ class CurrentPatientService
public function getCurrentMedicalHistoryIds(string $type, int $branchId, DateRange $dateRange, bool $fillableAuto = false)
{
if (! $fillableAuto && $this->useMaterializedViews()) {
return MedicalHistory::query()
->when($type === 'plan', fn ($query) => $query->urgency(1))
->when($type === 'emergency', fn ($query) => $query->whereIn('urgency_id', self::EMERGENCY_STATUSES))
->whereHas('migrations', fn ($query) => $query
->where('stationar_branch_id', $branchId)
->current($dateRange))
->pluck('original_id')
->filter()
->map(fn ($id) => (int) $id)
->values()
->all();
}
if ($fillableAuto) {
return $this->getHistoricalCurrentMedicalHistoryIds($type, $branchId, $dateRange);
}
@@ -79,4 +95,9 @@ class CurrentPatientService
->pluck('last_mp.rf_MedicalHistoryID')
->toArray();
}
private function useMaterializedViews(): bool
{
return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details');
}
}

View File

@@ -2,9 +2,10 @@
namespace App\Services;
use App\Models\MedicalHistory;
use App\Models\MedicalHistorySnapshot;
use App\Models\Report;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class MetrikaService
{
@@ -21,40 +22,58 @@ class MetrikaService
}
try {
// Получаем снапшоты с операциями
$results = DB::table('medical_history_snapshots as mhs')
->join('reports as r', 'mhs.rf_report_id', '=', 'r.report_id')
->join('stt_migrationpatient as mp', 'mhs.rf_medicalhistory_id', '=', 'mp.rf_MedicalHistoryID')
->join('stt_surgicaloperation as so', 'mhs.rf_medicalhistory_id', '=', 'so.rf_MedicalHistoryID')
->whereIn('r.rf_department_id', $departmentIds)
->where('r.period_start', '>=', $startDate)
->where('r.period_end', '<', $endDate)
->whereIn('mhs.patient_type', ['discharged', 'deceased'])
->select(
'r.rf_department_id',
'mp.rf_MedicalHistoryID',
DB::raw('MIN(mp."DateIngoing") as admission_date'),
DB::raw('MIN(so."Date") as first_operation_date')
)
->groupBy('r.rf_department_id', 'mp.rf_MedicalHistoryID')
->havingRaw('MIN(so."Date") IS NOT NULL')
->get()
$reports = Report::query()
->whereIn('rf_department_id', $departmentIds)
->where('period_start', '>=', $startDate)
->where('period_end', '<', $endDate)
->get(['report_id', 'rf_department_id'])
->groupBy('rf_department_id');
$preoperativeDays = [];
foreach ($departmentIds as $deptId) {
if (! isset($results[$deptId]) || $results[$deptId]->isEmpty()) {
$reportIds = $reports->get($deptId)?->pluck('report_id')->all() ?? [];
if ($reportIds === []) {
$preoperativeDays[$deptId] = 0;
continue;
}
$historyIds = MedicalHistorySnapshot::query()
->whereIn('rf_report_id', $reportIds)
->whereIn('patient_type', ['discharged', 'deceased'])
->pluck('rf_medicalhistory_id')
->filter()
->unique()
->values();
if ($historyIds->isEmpty()) {
$preoperativeDays[$deptId] = 0;
continue;
}
$histories = MedicalHistory::query()
->whereIn('original_id', $historyIds)
->with(['operations'])
->get();
$totalDays = 0;
$count = 0;
foreach ($results[$deptId] as $item) {
$admission = Carbon::parse($item->admission_date);
$operation = Carbon::parse($item->first_operation_date);
foreach ($histories as $history) {
$operationDate = $history->operations
->pluck('operation_date')
->filter()
->sort()
->first();
if (! $history->recipient_date || ! $operationDate) {
continue;
}
$admission = Carbon::parse($history->recipient_date);
$operation = Carbon::parse($operationDate);
$days = $admission->diffInDays($operation);
if ($days >= 0) {

View File

@@ -2,7 +2,10 @@
namespace App\Services;
use App\Models\MedicalHistory;
use App\Models\MigrationPatient;
use App\Models\MisMedicalHistory;
use Illuminate\Support\Facades\Schema;
class OutcomePatientService
{
@@ -57,6 +60,10 @@ class OutcomePatientService
string $outcomeType = 'all',
bool $onlyIds = false
) {
if ($this->useMaterializedViews()) {
return $this->getOutcomePatientsFromMaterializedViews($branchId, $dateRange, $outcomeType, $onlyIds);
}
return match ($outcomeType) {
'deceased' => $this->getBranchDeceasedPatients($branchId, $dateRange, $onlyIds),
'transferred' => $this->getBranchTransferredPatients($branchId, $dateRange, $onlyIds),
@@ -178,4 +185,62 @@ class OutcomePatientService
return $this->finalizePatientsQuery($query, $onlyIds);
}
private function getOutcomePatientsFromMaterializedViews(
int $branchId,
DateRange $dateRange,
string $outcomeType,
bool $onlyIds
) {
$migrationQuery = MigrationPatient::query()
->where('stationar_branch_id', $branchId);
$migrationQuery = match ($outcomeType) {
'deceased' => $migrationQuery->deceased($dateRange->startSql(), $dateRange->endSql()),
'transferred' => $migrationQuery->transferred($dateRange->startSql(), $dateRange->endSql()),
'discharged' => $migrationQuery->discharged($dateRange->startSql(), $dateRange->endSql()),
'without-transferred' => $migrationQuery->where(function ($query) use ($dateRange) {
$query->discharged($dateRange->startSql(), $dateRange->endSql())
->orWhere(fn ($orQuery) => $orQuery->deceased($dateRange->startSql(), $dateRange->endSql()));
}),
default => $migrationQuery->where(function ($query) use ($dateRange) {
$query->discharged($dateRange->startSql(), $dateRange->endSql())
->orWhere(fn ($orQuery) => $orQuery->deceased($dateRange->startSql(), $dateRange->endSql()))
->orWhere(fn ($orQuery) => $orQuery->transferred($dateRange->startSql(), $dateRange->endSql()));
}),
};
$historyIds = $migrationQuery
->distinct()
->pluck('medical_history_id')
->filter()
->values()
->all();
if (empty($historyIds)) {
return $onlyIds ? collect() : collect();
}
$query = MedicalHistory::query()
->whereIn('id', $historyIds)
->with([
'latestMigration' => fn ($builder) => $builder->where('stationar_branch_id', $branchId),
'migrations' => fn ($builder) => $builder
->where('stationar_branch_id', $branchId)
->orderByDesc('ingoing_date'),
'operations',
])
->orderByDesc('recipient_date');
if ($onlyIds) {
return $query->pluck('original_id');
}
return $query->get();
}
private function useMaterializedViews(): bool
{
return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details');
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use App\Models\MedicalHistory;
class RecipientPatientService
{
@@ -33,6 +35,17 @@ class RecipientPatientService
DateRange $dateRange,
bool $fillableAuto = false
) {
if (! $fillableAuto && $this->useMaterializedViews()) {
return MedicalHistory::query()
->selectRaw('original_id as "rf_MedicalHistoryID"')
->whereHas('migrations', fn ($query) => $query
->where('stationar_branch_id', $branchId)
->admitted($dateRange->startSql(), $dateRange->endSql()))
->when($type === 'plan', fn ($query) => $query->urgency(1))
->when($type === 'emergency', fn ($query) => $query->whereIn('urgency_id', [2, 4]))
->distinct();
}
$startAt = $dateRange->start()->copy()->format('Y-m-d H:i:s');
$endAt = $dateRange->end()->copy()->format('Y-m-d H:i:s');
@@ -96,4 +109,9 @@ class RecipientPatientService
return array_values(array_unique(array_merge($recipientIds, $currentIds)));
}
private function useMaterializedViews(): bool
{
return Schema::hasTable('mv_medicalhistory_summary') && Schema::hasTable('mv_migrationpatient_details');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,226 +2,22 @@
namespace App\Services;
use App\Domain\Reports\ValueObjects\MetrikaConfig;
use App\Infrastructure\Reports\Services\SnapshotPersistenceService;
use App\Infrastructure\Reports\Sources\SnapshotPatientSource;
use App\Models\Department;
use App\Models\MedicalHistorySnapshot;
use App\Models\MisStationarBranch;
use App\Models\Report;
use App\Models\User;
use Illuminate\Support\Collection;
class SnapshotService
{
public function __construct(
protected UnifiedPatientService $unifiedPatientService,
protected PatientService $patientService,
protected DateRangeService $dateRangeService,
?SnapshotPersistenceService $snapshotPersistenceService = null,
mixed $unifiedPatientService = null,
mixed $patientService = null,
mixed $dateRangeService = null,
?SnapshotPatientSource $snapshotPatientSource = null,
) {
$this->snapshotPersistenceService = $snapshotPersistenceService ?? app(SnapshotPersistenceService::class);
$this->snapshotPatientSource = $snapshotPatientSource ?? app(SnapshotPatientSource::class);
}
protected SnapshotPersistenceService $snapshotPersistenceService;
protected SnapshotPatientSource $snapshotPatientSource;
/**
* Создать снапшоты пациентов для отчета
*/
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;
}
$this->snapshotPersistenceService->clearReportSnapshots($report);
$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->snapshotPersistenceService->createSnapshotsForType($report, 'plan', $planPatients);
$this->logSnapshotMemory('snapshots:after_plan_save', ['report_id' => $report->report_id]);
$metrics[MetrikaConfig::PLAN] = $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->snapshotPersistenceService->createSnapshotsForType($report, 'emergency', $emergencyPatients);
$this->logSnapshotMemory('snapshots:after_emergency_save', ['report_id' => $report->report_id]);
$metrics[MetrikaConfig::EMERGENCY] = $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->snapshotPersistenceService->createSnapshotsForType($report, 'discharged', $dischargedPatients);
$this->logSnapshotMemory('snapshots:after_discharged_save', ['report_id' => $report->report_id]);
$metrics[MetrikaConfig::DISCHARGED] = $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->snapshotPersistenceService->createSnapshotsForType($report, 'transferred', $transferredPatients);
$this->logSnapshotMemory('snapshots:after_transferred_save', ['report_id' => $report->report_id]);
$metrics[MetrikaConfig::TRANSFERRED] = $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->snapshotPersistenceService->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->snapshotPersistenceService->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->snapshotPersistenceService->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->snapshotPersistenceService->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,
]);
}
/**
* Получить статистику из снапшотов
*/
@@ -264,23 +60,4 @@ class SnapshotService
);
}
/**
* Получить ID отделения
*/
private function getBranchId(int $misDepartmentId): ?int
{
return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId)
->value('StationarBranchID');
}
/**
* Разобрать даты
*/
private function parseDates(array $dates): array
{
return [
$this->dateRangeService->parseDate($dates[0]),
$this->dateRangeService->parseDate($dates[1]),
];
}
}

View File

@@ -7,6 +7,7 @@ use App\Infrastructure\Reports\Sources\MisPatientSource;
use App\Infrastructure\Reports\Sources\SpecialPatientSource;
use App\Models\Department;
use App\Models\DepartmentPatient;
use App\Models\MedicalHistory;
use App\Models\MisMedicalHistory;
use App\Models\ObservationPatient;
use App\Models\Report;
@@ -192,13 +193,15 @@ class UnifiedPatientService
public function linkManualPatientToMis(DepartmentPatient $patient, int $medicalHistoryId): DepartmentPatient
{
$misPatient = MisMedicalHistory::where('MedicalHistoryID', $medicalHistoryId)->firstOrFail();
$history = MedicalHistory::query()
->where('original_id', $medicalHistoryId)
->firstOrFail();
$patient->update([
'rf_medicalhistory_id' => $misPatient->MedicalHistoryID,
'rf_medicalhistory_id' => $history->original_id,
'linked_to_mis_at' => now(),
'full_name' => $patient->full_name ?: trim("{$misPatient->FAMILY} {$misPatient->Name} {$misPatient->OT}"),
'birth_date' => $patient->birth_date ?: $misPatient->BD,
'full_name' => $patient->full_name ?: $history->full_name,
'birth_date' => $patient->birth_date ?: $history->birth_date,
]);
return $patient->fresh();
@@ -206,20 +209,13 @@ class UnifiedPatientService
public function searchMisPatients(Department $department, string $query): Collection
{
$branchId = \App\Models\MisStationarBranch::where('rf_DepartmentID', $department->rf_mis_department_id)
->value('StationarBranchID');
return MisMedicalHistory::query()
->whereHas('migrations', fn ($builder) => $builder->where('rf_StationarBranchID', $branchId))
->where(function ($builder) use ($query) {
$builder->where('FAMILY', 'like', "%{$query}%")
->orWhere('Name', 'like', "%{$query}%")
->orWhere('OT', 'like', "%{$query}%");
})
->with(['outcomeMigration.mainDiagnosis.mkb'])
return MedicalHistory::query()
->whereLike('full_name', "%{$query}%")
->whereHas('migrations', fn ($builder) => $builder->department($department->rf_mis_department_id))
->with(['latestMigration', 'operations'])
->limit(20)
->get()
->map(fn (MisMedicalHistory $patient) => UnifiedPatientData::fromMisMedicalHistory($patient));
->map(fn (MedicalHistory $patient) => UnifiedPatientData::fromMedicalHistory($patient));
}
public function getObservationPatients(
@@ -232,10 +228,11 @@ class UnifiedPatientService
$misIds = $observationPatients->pluck('rf_medicalhistory_id')->filter()->unique()->values();
$manualIds = $observationPatients->pluck('rf_department_patient_id')->filter()->unique()->values();
$misPatients = MisMedicalHistory::whereIn('MedicalHistoryID', $misIds)
->with(['outcomeMigration.mainDiagnosis.mkb'])
$misPatients = MedicalHistory::query()
->whereIn('original_id', $misIds)
->with(['latestMigration', 'operations'])
->get()
->keyBy('MedicalHistoryID');
->keyBy('original_id');
$manualPatients = DepartmentPatient::whereIn('department_patient_id', $manualIds)->get()->keyBy('department_patient_id');
@@ -258,7 +255,7 @@ class UnifiedPatientService
return null;
}
return UnifiedPatientData::fromMisMedicalHistory(
return UnifiedPatientData::fromMedicalHistory(
$misPatients[$observation->rf_medicalhistory_id],
false,
null,
@@ -290,14 +287,24 @@ class UnifiedPatientService
$linkedManualPatients = $this->specialPatientSource->getLinkedManualPatientsForPeriod($department, $dateRange);
$mergedMisPatients = $misPatients->map(function ($patient) use ($linkedManualPatients) {
$linkedManual = $linkedManualPatients->get($patient->MedicalHistoryID);
$medicalHistoryId = $patient instanceof MedicalHistory
? ($patient->original_id ?? $patient->id)
: $patient->MedicalHistoryID;
$linkedManual = $linkedManualPatients->get($medicalHistoryId);
return UnifiedPatientData::fromMisMedicalHistory(
$patient,
(bool) ($patient->is_recipient_today ?? false),
$linkedManual,
$this->resolveObservationComment($patient->MedicalHistoryID, null)
);
return $patient instanceof MedicalHistory
? UnifiedPatientData::fromMedicalHistory(
$patient,
(bool) ($patient->is_recipient_today ?? false),
$linkedManual,
$this->resolveObservationComment($medicalHistoryId, null)
)
: UnifiedPatientData::fromMisMedicalHistory(
$patient,
(bool) ($patient->is_recipient_today ?? false),
$linkedManual,
$this->resolveObservationComment($medicalHistoryId, null)
);
});
$manualDtos = $this->specialPatientSource->getDtos($department, $status, $dateRange, self::SPECIAL_SOURCE_TYPES, $forSnapshots);