* переписал функции прототипов в сервисы

* оптимизация доставки контента до клиента
* переписал запросы выборок
* убрал из подсчета переведенных
* добавил сохранение метрикам для вывода в дашборд
This commit is contained in:
brusnitsyn
2026-02-04 17:05:13 +09:00
parent 9ee33bc517
commit eab78a0291
16 changed files with 1644 additions and 737 deletions

View File

@@ -2,41 +2,65 @@
namespace App\Services;
use App\Models\Report;
use App\Models\MetrikaResult;
use App\Models\UnwantedEvent;
use App\Models\ObservationPatient;
use App\Models\MedicalHistorySnapshot;
use App\Models\MetrikaResult;
use App\Models\MisLpuDoctor;
use App\Models\MisMigrationPatient;
use App\Models\MisMedicalHistory;
use App\Models\MisStationarBranch;
use App\Models\ObservationPatient;
use App\Models\Report;
use App\Models\UnwantedEvent;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class ReportService
{
private PatientService $patientService;
public function __construct(
protected DateRangeService $dateRangeService,
protected PatientService $patientQueryService,
protected SnapshotService $snapshotService
) {}
public function __construct(PatientService $patientService)
/**
* Получить статистику для отчета
*/
public function getReportStatistics(User $user, DateRange $dateRange): array
{
$this->patientService = $patientService;
$department = $user->department;
$misDepartmentId = $department->rf_mis_department_id;
$branchId = $this->getBranchId($misDepartmentId);
// Определяем, используем ли мы снапшоты
$useSnapshots = $this->shouldUseSnapshots($user, $dateRange);
if ($useSnapshots) {
return $this->getStatisticsFromSnapshots($user, $dateRange, $branchId);
}
return $this->getStatisticsFromReplica($user, $dateRange, $branchId);
}
public function createReport(array $data): Report
/**
* Создать или обновить отчет
*/
public function storeReport(array $data, User $user): Report
{
DB::beginTransaction();
try {
$report = Report::create([
'rf_department_id' => $data['departmentId'],
'rf_user_id' => Auth::id(),
'sent_at' => now()
]);
$report = $this->createOrUpdateReport($data, $user);
$this->saveMetrics($report, $data['metrics'] ?? []);
$this->saveUnwantedEvents($report, $data['unwantedEvents'] ?? []);
$this->saveObservationPatients($report, $data['observationPatients'] ?? []);
$this->savePatientSnapshots($report, $data);
$this->saveObservationPatients($report, $data['observationPatients'] ?? [], $user->rf_department_id);
// Сохраняем снапшоты пациентов
$this->snapshotService->createPatientSnapshots($report, $user, $data['dates']);
DB::commit();
return $report;
} catch (\Exception $e) {
DB::rollBack();
@@ -44,82 +68,707 @@ class ReportService
}
}
/**
* Получить пациентов по статусу
*/
public function getPatientsByStatus(
User $user,
string $status,
DateRange $dateRange
) {
$branchId = $this->getBranchId($user->department->rf_mis_department_id);
$useSnapshots = $this->shouldUseSnapshots($user, $dateRange);
if ($useSnapshots) {
return $this->getPatientsFromSnapshots($user, $status, $dateRange, $branchId);
}
return $this->getPatientsFromReplica($user, $status, $dateRange, $branchId);
}
/**
* Получить количество пациентов по статусу
*/
public function getPatientsCountByStatus(
User $user,
string $status,
DateRange $dateRange
): int {
$branchId = $this->getBranchId($user->department->rf_mis_department_id);
$useSnapshots = $this->shouldUseSnapshots($user, $dateRange);
if ($useSnapshots) {
return $this->getPatientsCountFromSnapshots($user, $status, $dateRange);
}
return $this->getPatientsCountFromReplica($user, $status, $dateRange, $branchId);
}
/**
* Получить ID отделения из стационарного отделения
*/
private function getBranchId(int $misDepartmentId): ?int
{
return MisStationarBranch::where('rf_DepartmentID', $misDepartmentId)
->value('StationarBranchID');
}
/**
* Определить, нужно ли использовать снапшоты
*/
private function shouldUseSnapshots(User $user, DateRange $dateRange): bool
{
if ($user->isAdmin() || $user->isHeadOfDepartment()) {
return true;
}
// Проверяем, есть ли отчет на сегодня
$reportToday = Report::whereDate('sent_at', $dateRange->end())
->whereDate('created_at', $dateRange->end())
->first();
return !$dateRange->isEndDateToday() || $reportToday;
}
/**
* Создать или обновить отчет
*/
private function createOrUpdateReport(array $data, User $user): Report
{
$reportData = [
'rf_department_id' => $data['departmentId'],
'rf_user_id' => $user->id,
'rf_lpudoctor_id' => $data['userId'],
'sent_at' => now(),
];
if (isset($data['reportId']) && $data['reportId']) {
$report = Report::updateOrCreate(
['report_id' => $data['reportId']],
$reportData
);
} else {
$report = Report::create($reportData);
}
return $report;
}
/**
* Сохранить метрики отчета
*/
private function saveMetrics(Report $report, array $metrics): void
{
foreach ($metrics as $key => $value) {
MetrikaResult::create([
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => (int) str_replace('metrika_item_', '', $key),
'value' => $value
]);
$metrikaId = (int)str_replace('metrika_item_', '', $key);
MetrikaResult::updateOrCreate(
[
'rf_report_id' => $report->report_id,
'rf_metrika_item_id' => $metrikaId,
],
[
'value' => $value,
]
);
}
}
/**
* Сохранить нежелательные события
*/
private function saveUnwantedEvents(Report $report, array $unwantedEvents): void
{
if (empty($unwantedEvents)) {
$report->unwantedEvents()->delete();
return;
}
foreach ($unwantedEvents as $event) {
if (isset($event['unwanted_event_id'])) {
if (isset($event['unwanted_event_id']) && $event['unwanted_event_id']) {
UnwantedEvent::updateOrCreate(
['unwanted_event_id' => $event['unwanted_event_id']],
$this->formatUnwantedEventData($report, $event)
[
'rf_report_id' => $report->report_id,
'comment' => $event['comment'] ?? '',
'title' => $event['title'] ?? '',
'is_visible' => $event['is_visible'] ?? true,
]
);
} else {
UnwantedEvent::create($this->formatUnwantedEventData($report, $event));
}
}
}
private function formatUnwantedEventData(Report $report, array $event): array
{
return [
'rf_report_id' => $report->report_id,
'comment' => $event['comment'] ?? '',
'title' => $event['title'] ?? '',
'is_visible' => $event['is_visible'] ?? true,
];
}
private function saveObservationPatients(Report $report, array $observationPatients): void
{
foreach ($observationPatients as $patient) {
ObservationPatient::create([
'rf_department_id' => $report->rf_department_id,
'rf_report_id' => $report->report_id,
'rf_medicalhistory_id' => $patient['id'],
'comment' => $patient['comment'] ?? null
]);
}
}
private function savePatientSnapshots(Report $report, array $data): void
{
$snapshotTypes = [
'plan' => $this->patientService->getPlanPatients(
false,
$data['branchId'],
$data['startDate'],
$data['endDate'],
false,
true
),
'emergency' => $this->patientService->getEmergencyPatients(
false,
$data['branchId'],
$data['startDate'],
$data['endDate'],
false,
true
)
];
foreach ($snapshotTypes as $type => $patientIds) {
foreach ($patientIds as $patientId) {
MedicalHistorySnapshot::create([
UnwantedEvent::create([
'rf_report_id' => $report->report_id,
'rf_medicalhistory_id' => $patientId,
'patient_type' => $type
'comment' => $event['comment'] ?? '',
'title' => $event['title'] ?? '',
'is_visible' => $event['is_visible'] ?? true,
]);
}
}
}
/**
* Сохранить пациентов под наблюдением
*/
private function saveObservationPatients(
Report $report,
array $observationPatients,
int $departmentId
): void {
if (empty($observationPatients)) {
ObservationPatient::where('rf_department_id', $departmentId)
->where('rf_report_id', $report->report_id)
->delete();
return;
}
foreach ($observationPatients as $patient) {
ObservationPatient::updateOrCreate(
[
'rf_medicalhistory_id' => $patient['id'],
'rf_department_id' => $departmentId,
],
[
'rf_report_id' => $report->report_id,
'rf_mkab_id' => null,
'comment' => $patient['comment'] ?? null,
]
);
}
}
/**
* Получить информацию о текущем отчете
*/
public function getCurrentReportInfo(User $user, DateRange $dateRange): array
{
$department = $user->department;
$reportToday = Report::whereDate('sent_at', $dateRange->endSql())
->whereDate('created_at', $dateRange->endSql())
->first();
$isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
$useSnapshots = $isHeadOrAdmin || !$dateRange->isEndDateToday() || $reportToday;
// Получаем ID пользователя для заполнения отчета
if ($useSnapshots && $isHeadOrAdmin && $reportToday) {
$fillableUserId = $reportToday->rf_lpudoctor_id ?? null;
} else {
$fillableUserId = request()->query('userId', $user->rf_lpudoctor_id);
}
// Получаем нежелательные события
$unwantedEvents = $this->getUnwantedEvents($user, $dateRange);
// Определяем активность кнопки отправки
$isActiveSendButton = $this->isSendButtonActive($user, $dateRange, $reportToday, $fillableUserId);
$message = null;
if ($reportToday && $reportToday->rf_lpudoctor_id !== intval($fillableUserId)) {
$reportDoctor = $reportToday->lpuDoctor;
$message = "Отчет уже создан пользователем: $reportDoctor->FAM_V $reportDoctor->IM_V $reportDoctor->OT_V";
}
// Получаем информацию о враче
$lpuDoctor = $this->getDoctorInfo($fillableUserId, $dateRange);
// Проверяем, является ли диапазон одним днем
// $isRangeOneDay = $this->dateRangeService->isRangeOneDay(
// $endDate->copy()->subDay()->format('Y-m-d H:i:s'),
// $endDate->format('Y-m-d H:i:s')
// );
// Формируем даты для ответа
// $date = $isHeadOrAdmin ? [
// $endDate->copy()->subDay()->getTimestampMs(),
// $endDate->getTimestampMs()
// ] : $endDate->getTimestampMs();
$date = $isHeadOrAdmin ? [
$dateRange->startDate->getTimestampMs(),
$dateRange->endDate->getTimestampMs()
] : $dateRange->endDate->getTimestampMs();
return [
'report_id' => $reportToday?->report_id,
'unwantedEvents' => $unwantedEvents,
'isActiveSendButton' => $isActiveSendButton,
'message' => $message,
'isOneDay' => $dateRange->isOneDay,
'isHeadOrAdmin' => $isHeadOrAdmin,
'dates' => $date,
'userId' => $fillableUserId,
'userName' => $lpuDoctor ? "$lpuDoctor->FAM_V $lpuDoctor->IM_V $lpuDoctor->OT_V" : null
];
}
/**
* Удалить пациента из наблюдения
*/
public function removeObservationPatient(int $medicalHistoryId): void
{
ObservationPatient::where('rf_medicalhistory_id', $medicalHistoryId)->delete();
}
/**
* Получить статистику из снапшотов
*/
private function getStatisticsFromSnapshots(User $user, DateRange $dateRange, int $branchId): array
{
// Получаем отчеты за период
$reports = $this->getReportsForDateRange(
$user->rf_department_id,
$dateRange
);
$reportIds = $reports->pluck('report_id')->toArray();
// Получаем статистику из снапшотов
$snapshotStats = [
'plan' => $this->getMetrikaResultCount(4, $reportIds),
'emergency' => $this->getMetrikaResultCount(12, $reportIds),
'outcome' => $this->getMetrikaResultCount(7, $reportIds),
'deceased' => $this->getMetrikaResultCount(9, $reportIds),
// 'discharged' => $this->getMetrikaResultCount('discharged', $reportIds),
'transferred' => $this->getMetrikaResultCount(13, $reportIds),
'recipient' => $this->getMetrikaResultCount(3, $reportIds),
];
// Получаем ID поступивших пациентов
$recipientIds = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
->where('patient_type', 'recipient')
->pluck('rf_medicalhistory_id')
->unique()
->toArray();
// Получаем количество операций из метрик
$surgicalCount = [
$this->getMetrikaResultCount(10, $reportIds), // экстренные операции
$this->getMetrikaResultCount(11, $reportIds) // плановые операции
];
return [
'recipientCount' => $snapshotStats['recipient'] ?? 0,
'extractCount' => $snapshotStats['outcome'] ?? 0,
'currentCount' => $this->calculateCurrentPatientsFromSnapshots($reportIds, $branchId),
'deadCount' => $snapshotStats['deceased'] ?? 0,
'surgicalCount' => $surgicalCount,
'recipientIds' => $recipientIds,
];
}
/**
* Получить статистику из реплики БД
*/
private function getStatisticsFromReplica(User $user, DateRange $dateRange, int $branchId): array
{
$isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
// Плановые: поступившие сегодня + уже лечащиеся
$planCount = $this->patientQueryService->getPatientsCountWithCurrent(
'plan',
$isHeadOrAdmin,
$branchId,
$dateRange
);
// Экстренные: поступившие сегодня + уже лечащиеся
$emergencyCount = $this->patientQueryService->getPatientsCountWithCurrent(
'emergency',
$isHeadOrAdmin,
$branchId,
$dateRange
);
// Все пациенты в отделении: поступившие + лечащиеся
$currentCount = $this->patientQueryService->getAllPatientsInDepartment(
$isHeadOrAdmin,
$branchId,
$dateRange,
true
);
// Поступившие сегодня (только новые поступления)
$recipientCount = $this->patientQueryService->getPlanOrEmergencyPatients(
null, // все типы
$isHeadOrAdmin,
$branchId,
$dateRange,
true,
false,
false // не включаем уже лечащихся
);
// Выбывшие за период
$outcomeCount = $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'all'
)->count();
// Умершие за период
$deadCount = $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'deceased'
)->count();
// Операции
$surgicalCount = [
$this->patientQueryService->getSurgicalPatients(
'emergency',
$branchId,
$dateRange,
true
),
$this->patientQueryService->getSurgicalPatients(
'plan',
$branchId,
$dateRange,
true
)
];
// ID поступивших сегодня (для отметки в таблице)
$recipientIds = $this->patientQueryService->getPlanOrEmergencyPatients(
null,
$isHeadOrAdmin,
$branchId,
$dateRange,
false,
true,
false // только поступившие сегодня
);
return [
'recipientCount' => $recipientCount, // только поступившие сегодня
'extractCount' => $outcomeCount,
'currentCount' => $currentCount, // все в отделении
'deadCount' => $deadCount,
'surgicalCount' => $surgicalCount,
'recipientIds' => $recipientIds, // ID поступивших сегодня
'planCount' => $planCount, // плановые (поступившие + уже лечащиеся)
'emergencyCount' => $emergencyCount, // экстренные (поступившие + уже лечащиеся)
];
}
/**
* Получить пациентов из снапшотов
*/
private function getPatientsFromSnapshots(User $user, string $status, DateRange $dateRange, int $branchId)
{
$reports = $this->getReportsForDateRange(
$user->rf_department_id,
$dateRange
);
$reportIds = $reports->pluck('report_id')->toArray();
$patientTypeMap = [
'plan' => 'plan',
'emergency' => 'emergency',
'outcome-discharged' => 'discharged',
'outcome-transferred' => 'transferred',
'outcome-deceased' => 'deceased',
'observation' => 'observation'
];
$patientType = $patientTypeMap[$status] ?? null;
if ($patientType === 'observation') {
return $this->getObservationPatientsFromSnapshots($user->rf_department_id, $reportIds);
}
return $this->snapshotService->getPatientsFromSnapshots($patientType, $reportIds, $branchId);
}
/**
* Получить пациентов из реплики БД
*/
private function getPatientsFromReplica(User $user, string $status, DateRange $dateRange, int $branchId)
{
$isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
// Для плановых и экстренных включаем уже лечащихся
$includeCurrent = in_array($status, ['plan', 'emergency']);
return match($status) {
'plan', 'emergency' => $this->patientQueryService->getPlanOrEmergencyPatients(
$status,
$isHeadOrAdmin,
$branchId,
$dateRange,
false,
false,
$includeCurrent
),
'observation' => $this->patientQueryService->getObservationPatients($user->rf_department_id),
'outcome-discharged' => $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'discharged'
),
'outcome-transferred' => $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'transferred'
),
'outcome-deceased' => $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'deceased'
),
'current' => $this->patientQueryService->getAllPatientsInDepartment(
$isHeadOrAdmin,
$branchId,
$dateRange
),
'recipient' => $this->patientQueryService->getPlanOrEmergencyPatients(
null,
$isHeadOrAdmin,
$branchId,
$dateRange,
false,
false,
false // только поступившие
),
default => collect()
};
}
/**
* Получить количество пациентов из снапшотов
*/
private function getPatientsCountFromSnapshots(User $user, string $status, DateRange $dateRange): int
{
$reports = $this->getReportsForDateRange(
$user->rf_department_id,
$dateRange
);
$reportIds = $reports->pluck('report_id')->toArray();
if ($status === 'outcome') {
return MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
->whereIn('patient_type', ['discharged', 'transferred', 'deceased'])
->distinct('rf_medicalhistory_id')
->count('rf_medicalhistory_id');
}
$patientTypeMap = [
'plan' => 'plan',
'emergency' => 'emergency',
'observation' => 'observation',
'outcome-discharged' => 'discharged',
'outcome-transferred' => 'transferred',
'outcome-deceased' => 'deceased'
];
$patientType = $patientTypeMap[$status] ?? null;
if (!$patientType) {
return 0;
}
if ($patientType === 'observation') {
return ObservationPatient::whereIn('rf_report_id', $reportIds)
->distinct('rf_medicalhistory_id')
->count('rf_medicalhistory_id');
}
return MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
->where('patient_type', $patientType)
->distinct('rf_medicalhistory_id')
->count('rf_medicalhistory_id');
}
/**
* Получить количество пациентов из реплики БД
*/
private function getPatientsCountFromReplica(User $user, string $status, DateRange $dateRange, int $branchId): int
{
$isHeadOrAdmin = $user->isHeadOfDepartment() || $user->isAdmin();
return match($status) {
'plan', 'emergency' => $this->patientQueryService->getPatientsCountWithCurrent(
$status,
$isHeadOrAdmin,
$branchId,
$dateRange,
),
'observation' => ObservationPatient::where('rf_department_id', $user->rf_department_id)->count(),
'outcome' => $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'all'
)->count(),
'outcome-discharged' => $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'discharged'
)->count(),
'outcome-transferred' => $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'transferred'
)->count(),
'outcome-deceased' => $this->patientQueryService->getOutcomePatients(
$branchId,
$dateRange,
'deceased'
)->count(),
default => 0
};
}
/**
* Получить нежелательные события за дату
*/
private function getUnwantedEvents(User $user, DateRange $dateRange)
{
return UnwantedEvent::whereHas('report', function ($query) use ($user, $dateRange) {
$query->where('rf_department_id', $user->rf_department_id)
->whereDate('created_at', $dateRange->endSql());
})
->get()
->map(function ($item) {
return [
...$item->toArray(),
'created_at' => Carbon::parse($item->created_at)->format('Создано d.m.Y в H:i'),
];
});
}
/**
* Проверить активность кнопки отправки отчета
*/
private function isSendButtonActive(User $user, DateRange $dateRange, ?Report $reportToday, ?int $fillableUserId): bool
{
// Для врача: только сегодня и если отчета еще нет
if (!$user->isHeadOfDepartment() && !$user->isAdmin()) {
return $dateRange->isEndDateToday() && !$reportToday;
}
// Для заведующего/админа: если есть отчет и он заполнен текущим пользователем
if ($reportToday && $reportToday->rf_lpudoctor_id === intval($fillableUserId)) {
return true;
}
return false;
}
/**
* Получить информацию о враче
*/
private function getDoctorInfo(?int $doctorId, DateRange $dateRange): ?MisLpuDoctor
{
if (!$doctorId) {
return null;
}
// Если дата не сегодня, не показываем врача
if (!$dateRange->isEndDateToday()) {
return null;
}
return MisLpuDoctor::where('LPUDoctorID', $doctorId)->first();
}
/**
* Получить отчеты за диапазон дат
*/
private function getReportsForDateRange(int $departmentId, DateRange $dateRange)
{
if ($dateRange->isOneDay) {
return Report::where('rf_department_id', $departmentId)
->whereDate('created_at', $dateRange->endSql())
->orderBy('created_at', 'ASC')
->get();
}
return Report::where('rf_department_id', $departmentId)
->whereBetween('created_at', [$dateRange->startSql(), $dateRange->endSql()])
->orderBy('created_at', 'ASC')
->get();
}
/**
* Получить количество из метрик
*/
private function getMetrikaResultCount(int $metrikaItemId, array $reportIds): int
{
$count = 0;
$reports = Report::whereIn('report_id', $reportIds)
->with('metrikaResults')
->get();
foreach ($reports as $report) {
foreach ($report->metrikaResults as $metrikaResult) {
if ($metrikaResult->rf_metrika_item_id === $metrikaItemId) {
$count += intval($metrikaResult->value) ?? 0;
}
}
}
return $count;
}
/**
* Рассчитать текущих пациентов из снапшотов
*/
private function calculateCurrentPatientsFromSnapshots(array $reportIds, int $branchId): int
{
// Получаем ID всех пациентов из снапшотов
$allPatientIds = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
->pluck('rf_medicalhistory_id')
->unique()
->toArray();
if (empty($allPatientIds)) {
return 0;
}
// Получаем ID выбывших пациентов
$outcomePatientIds = MedicalHistorySnapshot::whereIn('rf_report_id', $reportIds)
->whereIn('patient_type', ['discharged', 'transferred', 'deceased'])
->pluck('rf_medicalhistory_id')
->unique()
->toArray();
// Текущие = все - выбывшие
$currentPatientIds = array_diff($allPatientIds, $outcomePatientIds);
return count($currentPatientIds);
}
/**
* Получить пациентов под наблюдением из снапшотов
*/
private function getObservationPatientsFromSnapshots(int $departmentId, array $reportIds)
{
$medicalHistoryIds = ObservationPatient::whereIn('rf_report_id', $reportIds)
->where('rf_department_id', $departmentId)
->pluck('rf_medicalhistory_id')
->unique()
->toArray();
if (empty($medicalHistoryIds)) {
return collect();
}
return MisMedicalHistory::whereIn('MedicalHistoryID', $medicalHistoryIds)
->with(['observationPatient' => function($query) use ($departmentId) {
$query->where('rf_department_id', $departmentId)
->select(['rf_medicalhistory_id', 'observation_patient_id', 'comment']);
}])
->orderBy('DateRecipient', 'DESC')
->get()
->map(function ($patient) {
$patient->comment = $patient->observationPatient
->pluck('comment')
->filter()
->implode('; ');
return $patient;
});
}
}