whereIn('id', $examApplicationIds) ->where('deleted', 0) ->pluck('examination_item_code') ->unique() ->toArray(); if (empty($itemCodes)) { return []; } // 2. 查询检查项目ID $examItemIds = DB::table('exam_item') ->whereIn('code', $itemCodes) ->where('deleted', 0) ->pluck('id') ->toArray(); if (empty($examItemIds)) { return []; } // 3. 查询项目-资源关联 $itemResources = DB::table('exam_item_resource') ->whereIn('item_id', $examItemIds) ->where('deleted', 0) ->select('item_id', 'resource_id') ->get(); if ($itemResources->isEmpty()) { return []; } // 4. 按 item_id 分组资源 $resourcesByItem = []; foreach ($itemResources as $ir) { $resourcesByItem[$ir->item_id][] = $ir->resource_id; } // 5. 计算所有项目的资源交集 if (count($resourcesByItem) === 1) { return reset($resourcesByItem); } else { return array_values(call_user_func_array('array_intersect', array_values($resourcesByItem))); } } /** * 计算医嘱的总占位数 * * @param array $examApplicationIds 医嘱ID数组 * @return int 总占位数 */ public function calculateTotalSeats(array $examApplicationIds): int { $itemCodes = DB::table('exam_application') ->whereIn('id', $examApplicationIds) ->where('deleted', 0) ->pluck('examination_item_code') ->unique() ->toArray(); if (empty($itemCodes)) { return 0; } $useSeatsByItemCode = DB::table('exam_item') ->whereIn('code', $itemCodes) ->where('deleted', 0) ->pluck('use_seats', 'code') ->toArray(); $applications = DB::table('exam_application') ->whereIn('id', $examApplicationIds) ->where('deleted', 0) ->select('examination_item_code') ->get(); $totalSeats = 0; foreach ($applications as $app) { $useSeats = $useSeatsByItemCode[$app->examination_item_code] ?? 1; $totalSeats += $useSeats; } return $totalSeats; } /** * 查询号源批次数据(GetEnablePlan、GetPlanCalendar 和 findEarliestAvailableSlot 的公共逻辑) * * @param array $examApplicationIds 医嘱ID数组 * @param int $channelId 渠道ID * @param string $startDate 开始日期 * @param int $days 查询天数 * @param array|null $dates 指定日期数组,传入时忽略 startDate 和 days * @return array * @throws \Exception */ public function queryPlanBatchesData(array $examApplicationIds, int $channelId, string $startDate, int $days = 15, ?array $dates = null): array { // 1. 查询医嘱信息 $examApplications = DB::table('exam_application') ->whereIn('id', $examApplicationIds) ->select('id', 'patient_id', 'patient_type', 'examination_item_code', 'examination_item_name') ->get(); if ($examApplications->isEmpty()) { throw new \Exception('医嘱不存在'); } // 2. 获取所有检查项目代码 $itemCodes = $examApplications->pluck('examination_item_code')->unique()->toArray(); // 3. 查询检查项目 $examItems = DB::table('exam_item') ->whereIn('code', $itemCodes) ->where('deleted', 0) ->select('id', 'code', 'name', 'use_seats') ->get(); if ($examItems->isEmpty()) { throw new \Exception('检查项目不存在'); } $examItemIds = $examItems->pluck('id')->toArray(); // 4. 建立 use_seats 映射 $useSeatsByItemCode = []; foreach ($examItems as $item) { $useSeatsByItemCode[$item->code] = $item->use_seats ?? 1; } // 5. 计算所有项目的总占位数 $totalUseSeats = 0; foreach ($examApplications as $app) { $useSeats = $useSeatsByItemCode[$app->examination_item_code] ?? 1; $totalUseSeats += $useSeats; } // 6. 查询项目-资源关联 $itemResources = DB::table('exam_item_resource') ->whereIn('item_id', $examItemIds) ->where('deleted', 0) ->select('item_id', 'resource_id') ->get(); if ($itemResources->isEmpty()) { throw new \Exception('检查项目未关联资源'); } // 7. 按 item_id 分组资源 $resourcesByItem = []; foreach ($itemResources as $ir) { $resourcesByItem[$ir->item_id][] = $ir->resource_id; } // 8. 计算所有项目的资源交集 if (count($resourcesByItem) === 1) { $commonResourceIds = reset($resourcesByItem); } else { $commonResourceIds = array_values(call_user_func_array('array_intersect', array_values($resourcesByItem))); } if (empty($commonResourceIds)) { throw new \Exception('这些医嘱项目没有共同可用的资源'); } // 9. 计算 patient_type 交集(位掩码 AND) $patientTypes = $examApplications->pluck('patient_type')->unique()->toArray(); $commonPatientTypeMask = array_reduce($patientTypes, function ($carry, $type) { return $carry === null ? $type : ($carry & $type); }, null); if ($commonPatientTypeMask === 0) { throw new \Exception('医嘱患者类型无共同支持的号源类型'); } // 10. 生成日期范围 if ($dates === null) { $dates = []; for ($i = 0; $i < $days; $i++) { $dates[] = date('Y-m-d', strtotime($startDate . ' +' . $i . ' days')); } } // 11. 查询符合条件的号源批次 $planBatches = DB::table('plan_batch') ->leftJoin('department_resource', 'plan_batch.resource_id', '=', 'department_resource.id') ->whereIn('plan_batch.resource_id', $commonResourceIds) ->whereIn('plan_batch.plan_date', $dates) ->where('plan_batch.deleted', 0) ->where('plan_batch.status', 1) ->where(function ($query) use ($commonPatientTypeMask) { $query->whereNull('plan_batch.patient_type_mask') ->orWhereRaw('(plan_batch.patient_type_mask & ?) != 0', [$commonPatientTypeMask]); }) ->select( 'plan_batch.id as batch_id', 'plan_batch.resource_id', 'department_resource.name as resource_name', 'department_resource.slot_mode', 'plan_batch.plan_date', 'plan_batch.start_time', 'plan_batch.end_time', 'plan_batch.cutoff_time', 'plan_batch.total_quota', 'plan_batch.used_quota', 'plan_batch.slot_mode', 'plan_batch.enable_channel_quota' ) ->orderBy('plan_batch.plan_date') ->orderBy('plan_batch.start_time') ->get(); if ($planBatches->isEmpty()) { return [ 'planBatches' => $planBatches, 'quotaByBatch' => [], 'totalUseSeats' => $totalUseSeats, 'commonResourceIds' => $commonResourceIds, 'commonPatientTypeMask' => $commonPatientTypeMask ]; } // 12. 批量预加载渠道配额 $allBatchIds = $planBatches->pluck('batch_id')->toArray(); $channelQuotaRecords = DB::table('plan_batch_channel_quota') ->whereIn('batch_id', $allBatchIds) ->whereIn('channel_id', [$channelId, -1]) ->where(['deleted' => 0, 'status' => 1]) ->select('batch_id', 'channel_id', 'total_quota', 'used_quota', 'locked_quota', 'allow_overuse_public_pool') ->get(); $quotaByBatch = []; foreach ($channelQuotaRecords as $record) { if (!isset($quotaByBatch[$record->batch_id])) { $quotaByBatch[$record->batch_id] = ['public' => null, 'channel' => null]; } if ($record->channel_id == -1) { $quotaByBatch[$record->batch_id]['public'] = $record; } elseif ($record->channel_id == $channelId) { $quotaByBatch[$record->batch_id]['channel'] = $record; } } return [ 'planBatches' => $planBatches, 'quotaByBatch' => $quotaByBatch, 'totalUseSeats' => $totalUseSeats, 'commonResourceIds' => $commonResourceIds, 'commonPatientTypeMask' => $commonPatientTypeMask ]; } /** * 从批次数组中查找最早的可用号源 * * @param \Illuminate\Support\Collection $planBatches 号源批次数组 * @param array $quotaByBatch 批次配额映射 * @param int $requiredSeats 需要的占位数 * @param int $channelId 渠道ID * @return array|null 最早可用号源信息 */ public function findEarliestSlot($planBatches, array $quotaByBatch, int $requiredSeats, int $channelId): ?array { $now = now(); $currentDate = $now->format('Y-m-d'); foreach ($planBatches as $batch) { // 跳过今天之前的日期 if ($batch->plan_date < $currentDate) { continue; } // 判断是否超过截止时间 $isPastCutoff = ($batch->plan_date === $currentDate) && $now->gt(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $batch->plan_date . ' ' . $batch->cutoff_time)); if ($isPastCutoff) { continue; } // 获取配额信息 $quotas = $quotaByBatch[$batch->batch_id] ?? ['public' => null, 'channel' => null]; $publicQuota = $quotas['public']; $channelQuota = $quotas['channel']; $slotMode = $batch->slot_mode; // 检查配额 $remainingQuota = $this->calculateRemainingQuota($batch, $channelId, $channelQuota, $publicQuota, $requiredSeats); if ($remainingQuota >= $requiredSeats) { // 配额充足,查询 plan 记录 $planRecord = null; if ($slotMode == 1) { // 时间段模式:只有1条 plan 记录 $planRecord = DB::table('plan') ->where('batch_id', $batch->batch_id) ->where('deleted', 0) ->select('id', 'start_time', 'total_quota', 'used_quota') ->first(); } else { // 时间点模式:找最早的时间点 $planRecord = DB::table('plan') ->where('batch_id', $batch->batch_id) ->where('status', 1) ->where('deleted', 0) ->orderBy('start_time') ->select('id', 'start_time', 'total_quota', 'used_quota') ->first(); // 时间点模式需要额外检查时间点是否过期 if ($planRecord && $batch->plan_date === $currentDate) { $tpStartTime = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $batch->plan_date . ' ' . $planRecord->start_time); $isTimeExpired = $now->gt($tpStartTime); if ($isTimeExpired) { continue; } } } if ($planRecord) { return [ 'plan_id' => $planRecord->id, 'batch_id' => $batch->batch_id, 'resource_id' => $batch->resource_id, 'resource_name' => $batch->resource_name, 'plan_date' => $batch->plan_date, 'start_time' => $planRecord->start_time ?? $batch->start_time, 'end_time' => $batch->end_time, 'slot_mode' => $slotMode, 'total_quota' => $batch->total_quota, 'remaining_quota' => $remainingQuota, 'required_seats' => $requiredSeats ]; } } } return null; } /** * 查找指定医嘱的最早可用号源(封装方法) * * @param array $examApplicationIds 医嘱ID数组 * @param int $channelId 渠道ID * @param string $startDate 开始日期 * @param int $days 查询天数 * @return array|null 最早可用号源信息,无可用号源返回null */ public function findEarliestAvailableSlot(array $examApplicationIds, int $channelId, string $startDate, int $days = 15): ?array { try { $data = $this->queryPlanBatchesData($examApplicationIds, $channelId, $startDate, $days); return $this->findEarliestSlot($data['planBatches'], $data['quotaByBatch'], $data['totalUseSeats'], $channelId); } catch (\Exception $e) { return null; } } /** * 计算批次的剩余配额 * * @param object $batch 批次对象 * @param int $channelId 渠道ID * @param object|null $channelQuota 渠道配额 * @param object|null $publicQuota 公共配额 * @param int $requiredSeats 需要的占位数 * @return int 剩余配额 */ public function calculateRemainingQuota($batch, int $channelId, $channelQuota, $publicQuota, int $requiredSeats): int { $enableChannelQuota = ($batch->enable_channel_quota ?? 0) == 1; if ($enableChannelQuota) { if (!$channelQuota) { return 0; } $effectiveQuota = $channelQuota; $allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0; } else { if (!$publicQuota) { return 0; } $effectiveQuota = $publicQuota; $allowOveruse = 0; } $totalQuota = $effectiveQuota->total_quota; $usedQuota = $effectiveQuota->used_quota; $lockedQuota = $effectiveQuota->locked_quota ?? 0; if ($allowOveruse && $publicQuota && $enableChannelQuota) { $totalQuota += $publicQuota->total_quota; $usedQuota += $publicQuota->used_quota; $lockedQuota += $publicQuota->locked_quota ?? 0; } return max(0, $totalQuota - $usedQuota - $lockedQuota); } }