You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

425 lines
15 KiB
PHP

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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);
}
}