|
|
<?php
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
/**
|
|
|
* 号源计划服务类
|
|
|
* 处理号源查询、可用号源查找等业务逻辑
|
|
|
*/
|
|
|
class PlanService
|
|
|
{
|
|
|
/**
|
|
|
* 查询医嘱的共同资源ID数组
|
|
|
*
|
|
|
* @param array $examApplicationIds 医嘱ID数组
|
|
|
* @return array 共同资源ID数组,没有共同资源返回空数组
|
|
|
*/
|
|
|
public function getCommonResourceIds(array $examApplicationIds): array
|
|
|
{
|
|
|
// 1. 查询医嘱的检查项目代码
|
|
|
$itemCodes = DB::table('exam_application')
|
|
|
->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);
|
|
|
}
|
|
|
}
|