|
|
<?php
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
use Illuminate\Support\Facades\Request;
|
|
|
use App\Services\ComprehensiveValidationService;
|
|
|
|
|
|
/**
|
|
|
* 预约服务类
|
|
|
* 处理预约、取消预约、改约等业务逻辑
|
|
|
*/
|
|
|
class AppointmentService
|
|
|
{
|
|
|
private ComprehensiveValidationService $validationService;
|
|
|
private BlockService $blockService;
|
|
|
|
|
|
public function __construct(ComprehensiveValidationService $validationService, BlockService $blockService)
|
|
|
{
|
|
|
$this->validationService = $validationService;
|
|
|
$this->blockService = $blockService;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 记录操作日志
|
|
|
*
|
|
|
* @param int $applicationId 医嘱 ID
|
|
|
* @param int $operationType 操作类型 (1:创建,2:更新,3:作废,4:取消)
|
|
|
* @param int $entityType 操作实体 (1:检查申请单,2:预约记录)
|
|
|
* @param int $entityId 操作实体 ID
|
|
|
* @param array|null $oldValues 操作前值
|
|
|
* @param array|null $newValues 操作后值
|
|
|
* @param string|null $operationReason 操作原因
|
|
|
* @return void
|
|
|
*/
|
|
|
private function logOperation(
|
|
|
int $applicationId,
|
|
|
int $operationType,
|
|
|
int $entityType,
|
|
|
int $entityId,
|
|
|
int $operatorType,
|
|
|
int $operatorId,
|
|
|
?array $oldValues = null,
|
|
|
?array $newValues = null,
|
|
|
?string $operationReason = null
|
|
|
): void {
|
|
|
try {
|
|
|
DB::table('operation_log')->insert([
|
|
|
'application_id' => $applicationId,
|
|
|
'operator_type' => $operatorType,
|
|
|
'operator_id' => $operatorId,
|
|
|
'operation_type' => $operationType,
|
|
|
'entity_type' => $entityType,
|
|
|
'entity_id' => $entityId,
|
|
|
'old_values' => $oldValues ? json_encode($oldValues, JSON_UNESCAPED_UNICODE) : null,
|
|
|
'new_values' => $newValues ? json_encode($newValues, JSON_UNESCAPED_UNICODE) : null,
|
|
|
'operation_time' => now(),
|
|
|
'operation_reason' => $operationReason,
|
|
|
'client_ip' => Request::ip(),
|
|
|
]);
|
|
|
} catch (\Exception $e) {
|
|
|
Log::error('记录操作日志失败:' . $e->getMessage());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 创建预约
|
|
|
*
|
|
|
* @param array $applicationIds 医嘱 ID 数组
|
|
|
* @param int $channelId 渠道 ID
|
|
|
* @param int $planId 号源 ID(plan 表 ID)
|
|
|
* @return array
|
|
|
*/
|
|
|
public function createAppointment($userId, array $applicationIds, int $channelId, int $planId): array
|
|
|
{
|
|
|
$now = now();
|
|
|
|
|
|
return DB::transaction(function () use ($userId, $applicationIds, $channelId, $planId, $now) {
|
|
|
// 1. 查询号源信息(加行锁)
|
|
|
$plan = DB::table('plan')
|
|
|
->where('id', $planId)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$plan) {
|
|
|
return ['success' => false, 'message' => '号源不存在'];
|
|
|
}
|
|
|
|
|
|
// 2. 查询批次信息(加行锁)
|
|
|
$batch = DB::table('plan_batch')
|
|
|
->where('id', $plan->batch_id)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$batch) {
|
|
|
return ['success' => false, 'message' => '批次信息不存在'];
|
|
|
}
|
|
|
|
|
|
// 3. 检查是否开启了渠道配额
|
|
|
$enableChannelQuota = ($batch->enable_channel_quota ?? 0) == 1;
|
|
|
|
|
|
// 4. 查询医嘱信息
|
|
|
$applications = DB::table('exam_application')
|
|
|
->whereIn('id', $applicationIds)
|
|
|
->where('status', 0)
|
|
|
->lockForUpdate()
|
|
|
->get();
|
|
|
|
|
|
if ($applications->isEmpty()) {
|
|
|
return ['success' => false, 'message' => '未找到可预约的医嘱'];
|
|
|
}
|
|
|
|
|
|
// 4.5 黑名单检测
|
|
|
$patientIdCardNo = $applications->first()->patient_id_card_no ?? null;
|
|
|
if ($patientIdCardNo) {
|
|
|
$blockCheck = $this->blockService->checkAppointmentPermission($channelId, $patientIdCardNo);
|
|
|
if (!$blockCheck['status']) {
|
|
|
return ['success' => false, 'message' => $blockCheck['msg']];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if ($applications->count() != count($applicationIds)) {
|
|
|
return ['success' => false, 'message' => '部分医嘱状态不符合预约条件'];
|
|
|
}
|
|
|
|
|
|
// 5. 检查渠道
|
|
|
$channel = DB::table('channel')
|
|
|
->where('id', $channelId)
|
|
|
->where('status', 1)
|
|
|
->first();
|
|
|
|
|
|
if (!$channel) {
|
|
|
return ['success' => false, 'message' => '渠道不存在或已停用'];
|
|
|
}
|
|
|
|
|
|
// 6. 检查患者 ID 是否一致
|
|
|
$patientId = $applications->first()->patient_id;
|
|
|
foreach ($applications as $app) {
|
|
|
if ($app->patient_id != $patientId) {
|
|
|
return ['success' => false, 'message' => '预约医嘱必须属于同一患者'];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 7. 检查截止时间
|
|
|
$planDate = $batch->plan_date;
|
|
|
$cutoffTime = $batch->cutoff_time;
|
|
|
$cutoffDateTime = $planDate . ' ' . $cutoffTime;
|
|
|
|
|
|
if ($now > $cutoffDateTime) {
|
|
|
return ['success' => false, 'message' => '已过预约截止时间'];
|
|
|
}
|
|
|
|
|
|
// 7.5 检查患者类型
|
|
|
$patientType = $applications->first()->patient_type;
|
|
|
$patientTypeMask = $batch->patient_type_mask ?? 0;
|
|
|
|
|
|
if ($patientTypeMask > 0 && ($patientTypeMask & (1 << ($patientType - 1))) == 0) {
|
|
|
$patientTypeNames = [1 => '门诊', 2 => '住院', 3 => '急诊', 4 => '体检'];
|
|
|
$typeName = $patientTypeNames[$patientType] ?? '未知';
|
|
|
return ['success' => false, 'message' => "该号源不支持{$typeName}患者"];
|
|
|
}
|
|
|
|
|
|
// 8. 获取所有项目的 use_seats
|
|
|
$itemCodes = $applications->pluck('examination_item_code')->toArray();
|
|
|
|
|
|
$itemSeats = DB::table('exam_item')
|
|
|
->whereIn('code', $itemCodes)
|
|
|
->where('deleted', 0)
|
|
|
->select('code', 'use_seats')
|
|
|
->get()
|
|
|
->keyBy('code');
|
|
|
|
|
|
// 9. 计算占位数量
|
|
|
$slotMode = $batch->slot_mode;
|
|
|
|
|
|
if ($slotMode == 1) {
|
|
|
$totalSeats = 0;
|
|
|
foreach ($itemCodes as $itemCode) {
|
|
|
$useSeats = $itemSeats->get($itemCode)->use_seats ?? 1;
|
|
|
$totalSeats += $useSeats;
|
|
|
}
|
|
|
} else {
|
|
|
$totalSeats = 1;
|
|
|
}
|
|
|
|
|
|
// 10. 查询渠道配额(加行锁)
|
|
|
$channelQuota = null;
|
|
|
$publicQuota = null;
|
|
|
|
|
|
if ($enableChannelQuota) {
|
|
|
$channelQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $plan->batch_id)
|
|
|
->where('channel_id', $channelId)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$channelQuota) {
|
|
|
return ['success' => false, 'message' => '该渠道无可配额'];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$publicQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $plan->batch_id)
|
|
|
->where('channel_id', -1)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
// 11. 在事务内检查配额(包含 locked_quota)
|
|
|
if ($enableChannelQuota && $channelQuota) {
|
|
|
$channelRemain = $channelQuota->total_quota - $channelQuota->used_quota - ($channelQuota->locked_quota ?? 0);
|
|
|
$allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0;
|
|
|
|
|
|
if ($channelRemain >= $totalSeats) {
|
|
|
$channelQuotaUsed = $totalSeats;
|
|
|
$publicQuotaUsed = 0;
|
|
|
} elseif ($allowOveruse && $publicQuota) {
|
|
|
$channelQuotaUsed = $channelRemain;
|
|
|
$publicQuotaUsed = $totalSeats - $channelRemain;
|
|
|
|
|
|
$publicRemain = $publicQuota->total_quota - $publicQuota->used_quota - ($publicQuota->locked_quota ?? 0);
|
|
|
if ($publicRemain < $publicQuotaUsed) {
|
|
|
return ['success' => false, 'message' => '配额不足'];
|
|
|
}
|
|
|
} else {
|
|
|
return ['success' => false, 'message' => '配额不足'];
|
|
|
}
|
|
|
} else {
|
|
|
if (!$publicQuota) {
|
|
|
return ['success' => false, 'message' => '无可用配额'];
|
|
|
}
|
|
|
|
|
|
$remain = $publicQuota->total_quota - $publicQuota->used_quota - ($publicQuota->locked_quota ?? 0);
|
|
|
if ($remain < $totalSeats) {
|
|
|
return ['success' => false, 'message' => '配额不足'];
|
|
|
}
|
|
|
|
|
|
$channelQuotaUsed = 0;
|
|
|
$publicQuotaUsed = $totalSeats;
|
|
|
}
|
|
|
|
|
|
// 额外检查 plan 表的可用配额(包含 locked_quota)
|
|
|
$planAvailable = $plan->total_quota - $plan->used_quota - ($plan->locked_quota ?? 0);
|
|
|
if ($planAvailable < $totalSeats) {
|
|
|
return ['success' => false, 'message' => '号源配额不足'];
|
|
|
}
|
|
|
|
|
|
// 11.5 为每个医嘱计算配额分配
|
|
|
$quotaAllocation = [];
|
|
|
$allocatedChannel = 0;
|
|
|
$allocatedPublic = 0;
|
|
|
|
|
|
foreach ($applications as $app) {
|
|
|
$itemSeatVal = DB::table('exam_item')
|
|
|
->where('code', $app->examination_item_code)
|
|
|
->where('deleted', 0)
|
|
|
->value('use_seats') ?? 1;
|
|
|
|
|
|
$itemTotalSeats = ($slotMode == 1) ? $itemSeatVal : 1;
|
|
|
|
|
|
if ($enableChannelQuota) {
|
|
|
$remainingChannel = $channelQuotaUsed - $allocatedChannel;
|
|
|
$itemChannelUsed = min($remainingChannel, $itemTotalSeats);
|
|
|
$itemPublicUsed = $itemTotalSeats - $itemChannelUsed;
|
|
|
|
|
|
$allocatedChannel += $itemChannelUsed;
|
|
|
$allocatedPublic += $itemPublicUsed;
|
|
|
|
|
|
$quotaAllocation[$app->id] = [
|
|
|
'channel_used' => $itemChannelUsed,
|
|
|
'public_used' => $itemPublicUsed
|
|
|
];
|
|
|
} else {
|
|
|
$quotaAllocation[$app->id] = [
|
|
|
'channel_used' => 0,
|
|
|
'public_used' => $itemTotalSeats
|
|
|
];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 12. 调用综合校验服务
|
|
|
$validationResult = $this->validationService->validate($patientId, $itemCodes, $planId);
|
|
|
if (!$validationResult['valid']) {
|
|
|
$conflictMessages = collect($validationResult['conflicts'])
|
|
|
->pluck('message')
|
|
|
->implode('; ');
|
|
|
return [
|
|
|
'success' => false,
|
|
|
'message' => $conflictMessages,
|
|
|
'conflicts' => $validationResult['conflicts']
|
|
|
];
|
|
|
}
|
|
|
|
|
|
// 13. 计算预约时间
|
|
|
$appointmentTime = $planDate . ' ' . ($plan->start_time ?? $batch->start_time);
|
|
|
|
|
|
// 14. 更新渠道配额(乐观锁)
|
|
|
if ($channelQuotaUsed > 0 && $channelQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $channelQuota->id)
|
|
|
->where('version', $channelQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $channelQuotaUsed),
|
|
|
'version' => $channelQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发更新渠道配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 更新公共池配额(乐观锁)
|
|
|
if ($publicQuotaUsed > 0 && $publicQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $publicQuota->id)
|
|
|
->where('version', $publicQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $publicQuotaUsed),
|
|
|
'version' => $publicQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发更新公共池配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 更新批次配额(乐观锁)
|
|
|
$batchAffected = DB::table('plan_batch')
|
|
|
->where('id', $batch->id)
|
|
|
->where('version', $batch->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $totalSeats),
|
|
|
'version' => ($batch->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($batchAffected == 0) {
|
|
|
throw new \Exception('并发更新批次配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 更新号源配额(乐观锁)
|
|
|
$planAffected = DB::table('plan')
|
|
|
->where('id', $plan->id)
|
|
|
->where('version', $plan->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $totalSeats),
|
|
|
'version' => ($plan->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($planAffected == 0) {
|
|
|
throw new \Exception('并发更新号源配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 创建预约记录
|
|
|
$appointmentIds = [];
|
|
|
foreach ($applications as $app) {
|
|
|
$allocation = $quotaAllocation[$app->id] ?? ['channel_used' => 0, 'public_used' => 0];
|
|
|
|
|
|
$appointmentId = DB::table('exam_appointment')->insertGetId([
|
|
|
'time_slot_id' => $plan->id,
|
|
|
'channel_id' => $channelId,
|
|
|
'exam_application_id' => $app->id,
|
|
|
'patient_id' => $app->patient_id,
|
|
|
'patient_name' => $app->patient_name,
|
|
|
'status' => 1,
|
|
|
'department_resource_id' => $batch->resource_id,
|
|
|
'appointment_time' => $appointmentTime,
|
|
|
'channel_quota_used' => $enableChannelQuota ? round($allocation['channel_used'], 4) : 0,
|
|
|
'public_quota_used' => round($allocation['public_used'], 4),
|
|
|
'created_at' => now(),
|
|
|
'updated_at' => now()
|
|
|
]);
|
|
|
$appointmentIds[] = $appointmentId;
|
|
|
|
|
|
$this->logOperation(
|
|
|
$app->id,
|
|
|
1,
|
|
|
2,
|
|
|
$appointmentId,
|
|
|
2,
|
|
|
$userId,
|
|
|
null,
|
|
|
[
|
|
|
'appointment_time' => $appointmentTime,
|
|
|
'channel_id' => $channelId,
|
|
|
'plan_id' => $plan->id,
|
|
|
'status' => 1
|
|
|
],
|
|
|
null
|
|
|
);
|
|
|
|
|
|
DB::table('exam_application')
|
|
|
->where('id', $app->id)
|
|
|
->update(['status' => 1]);
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
'success' => true,
|
|
|
'message' => '预约成功',
|
|
|
'data' => [
|
|
|
'appointment_ids' => $appointmentIds,
|
|
|
'appointment_time' => $appointmentTime
|
|
|
]
|
|
|
];
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 取消预约
|
|
|
*
|
|
|
* @param int $appointmentId 预约 ID
|
|
|
* @param string|null $cancelReason 取消原因
|
|
|
* @return array
|
|
|
*/
|
|
|
public function cancelAppointment($userId, int $appointmentId, ?string $cancelReason = null): array
|
|
|
{
|
|
|
return DB::transaction(function () use ($userId, $appointmentId, $cancelReason) {
|
|
|
// 1. 查询预约记录(加行锁)
|
|
|
$appointment = DB::table('exam_appointment')
|
|
|
->where('id', $appointmentId)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$appointment) {
|
|
|
return ['success' => false, 'message' => '预约记录不存在'];
|
|
|
}
|
|
|
|
|
|
if ($appointment->status != 1) {
|
|
|
return ['success' => false, 'message' => '只能取消预约成功的记录'];
|
|
|
}
|
|
|
|
|
|
// 2. 查询号源信息(加行锁)
|
|
|
$plan = DB::table('plan')
|
|
|
->where('id', $appointment->time_slot_id)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$plan) {
|
|
|
return ['success' => false, 'message' => '号源不存在'];
|
|
|
}
|
|
|
|
|
|
// 3. 查询批次信息(加行锁)
|
|
|
$batch = DB::table('plan_batch')
|
|
|
->where('id', $plan->batch_id)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$batch) {
|
|
|
return ['success' => false, 'message' => '批次信息不存在'];
|
|
|
}
|
|
|
|
|
|
// 4. 查询医嘱信息
|
|
|
$application = DB::table('exam_application')
|
|
|
->where('id', $appointment->exam_application_id)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$application) {
|
|
|
return ['success' => false, 'message' => '医嘱信息不存在'];
|
|
|
}
|
|
|
|
|
|
// 5. 获取项目的 use_seats
|
|
|
$itemCode = $application->examination_item_code;
|
|
|
$itemSeat = DB::table('exam_item')
|
|
|
->where('code', $itemCode)
|
|
|
->where('deleted', 0)
|
|
|
->select('use_seats')
|
|
|
->first();
|
|
|
|
|
|
// 6. 计算占位数量
|
|
|
$slotMode = $batch->slot_mode;
|
|
|
|
|
|
if ($slotMode == 1) {
|
|
|
$totalSeats = $itemSeat->use_seats ?? 1;
|
|
|
} else {
|
|
|
$totalSeats = 1;
|
|
|
}
|
|
|
|
|
|
// 7. 查询渠道配额(加行锁)
|
|
|
$enableChannelQuota = ($batch->enable_channel_quota ?? 0) == 1;
|
|
|
$channelId = $appointment->channel_id;
|
|
|
|
|
|
$channelQuota = null;
|
|
|
$publicQuota = null;
|
|
|
|
|
|
if ($enableChannelQuota) {
|
|
|
$channelQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $plan->batch_id)
|
|
|
->where('channel_id', $channelId)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
}
|
|
|
|
|
|
$publicQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $plan->batch_id)
|
|
|
->where('channel_id', -1)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
// 记录取消前的预约状态
|
|
|
$oldValues = [
|
|
|
'appointment_time' => $appointment->appointment_time,
|
|
|
'channel_id' => $appointment->channel_id,
|
|
|
'status' => $appointment->status
|
|
|
];
|
|
|
|
|
|
// 更新预约记录状态
|
|
|
$updateData = [
|
|
|
'status' => 4,
|
|
|
'cancel_time' => now(),
|
|
|
'updated_at' => now()
|
|
|
];
|
|
|
|
|
|
if ($cancelReason !== null && $cancelReason !== '') {
|
|
|
$updateData['cancel_reason'] = $cancelReason;
|
|
|
}
|
|
|
|
|
|
DB::table('exam_appointment')
|
|
|
->where('id', $appointment->id)
|
|
|
->update($updateData);
|
|
|
|
|
|
$this->logOperation(
|
|
|
$appointment->exam_application_id,
|
|
|
4,
|
|
|
2,
|
|
|
$appointment->id,
|
|
|
2,
|
|
|
$userId,
|
|
|
$oldValues,
|
|
|
[
|
|
|
'status' => 4,
|
|
|
'cancel_reason' => $cancelReason
|
|
|
],
|
|
|
$cancelReason
|
|
|
);
|
|
|
|
|
|
// 根据预约记录中记录的配额使用量精确恢复
|
|
|
$channelQuotaUsed = $appointment->channel_quota_used ?? 0;
|
|
|
$publicQuotaUsed = $appointment->public_quota_used ?? 0;
|
|
|
|
|
|
// 恢复渠道配额(乐观锁)
|
|
|
if ($channelQuotaUsed > 0 && $channelQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $channelQuota->id)
|
|
|
->where('version', $channelQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $channelQuotaUsed . ')'),
|
|
|
'version' => $channelQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发恢复渠道配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 恢复公共池配额(乐观锁)
|
|
|
if ($publicQuotaUsed > 0 && $publicQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $publicQuota->id)
|
|
|
->where('version', $publicQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $publicQuotaUsed . ')'),
|
|
|
'version' => $publicQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发恢复公共池配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 恢复批次配额(乐观锁 + GREATEST 保护)
|
|
|
$batchAffected = DB::table('plan_batch')
|
|
|
->where('id', $batch->id)
|
|
|
->where('version', $batch->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $totalSeats . ')'),
|
|
|
'version' => ($batch->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($batchAffected == 0) {
|
|
|
throw new \Exception('并发恢复批次配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 恢复号源配额(乐观锁 + GREATEST 保护)
|
|
|
$planAffected = DB::table('plan')
|
|
|
->where('id', $plan->id)
|
|
|
->where('version', $plan->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $totalSeats . ')'),
|
|
|
'version' => ($plan->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($planAffected == 0) {
|
|
|
throw new \Exception('并发恢复号源配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 恢复医嘱状态为申请中
|
|
|
DB::table('exam_application')
|
|
|
->where('id', $appointment->exam_application_id)
|
|
|
->update(['status' => 0]);
|
|
|
|
|
|
return [
|
|
|
'success' => true,
|
|
|
'message' => '取消成功'
|
|
|
];
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 改约
|
|
|
*
|
|
|
* @param int $appointmentId 原预约 ID
|
|
|
* @param string $cancelReason 取消原因
|
|
|
* @param int $newPlanId 新号源 ID
|
|
|
* @return array
|
|
|
*/
|
|
|
public function rescheduleAppointment($userId, int $appointmentId, string $cancelReason, int $newPlanId): array
|
|
|
{
|
|
|
$now = now();
|
|
|
|
|
|
return DB::transaction(function () use ($userId, $appointmentId, $cancelReason, $newPlanId, $now) {
|
|
|
// 1. 查询原预约记录(加行锁)
|
|
|
$oldAppointment = DB::table('exam_appointment')
|
|
|
->where('id', $appointmentId)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$oldAppointment) {
|
|
|
return ['success' => false, 'message' => '预约记录不存在'];
|
|
|
}
|
|
|
|
|
|
if ($oldAppointment->status != 1) {
|
|
|
return ['success' => false, 'message' => '只能改约预约成功的记录'];
|
|
|
}
|
|
|
|
|
|
// 2. 查询原医嘱信息
|
|
|
$oldApplication = DB::table('exam_application')
|
|
|
->where('id', $oldAppointment->exam_application_id)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$oldApplication) {
|
|
|
return ['success' => false, 'message' => '医嘱信息不存在'];
|
|
|
}
|
|
|
|
|
|
// 2.5 黑名单检测
|
|
|
$patientIdCardNo = $oldApplication->patient_id_card_no ?? null;
|
|
|
if ($patientIdCardNo) {
|
|
|
$blockCheck = $this->blockService->checkAppointmentPermission($oldAppointment->channel_id, $patientIdCardNo);
|
|
|
if (!$blockCheck['status']) {
|
|
|
return ['success' => false, 'message' => $blockCheck['msg']];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 3. 查询新号源信息(加行锁)
|
|
|
$newPlan = DB::table('plan')
|
|
|
->where('id', $newPlanId)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$newPlan) {
|
|
|
return ['success' => false, 'message' => '新号源不存在'];
|
|
|
}
|
|
|
|
|
|
// 3.5 检查新号源是否与原号源相同
|
|
|
if ($newPlanId == $oldAppointment->time_slot_id) {
|
|
|
return ['success' => false, 'message' => '新号源不能与原号源相同'];
|
|
|
}
|
|
|
|
|
|
// 4. 查询新批次信息(加行锁)
|
|
|
$newBatch = DB::table('plan_batch')
|
|
|
->where('id', $newPlan->batch_id)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$newBatch) {
|
|
|
return ['success' => false, 'message' => '新批次信息不存在'];
|
|
|
}
|
|
|
|
|
|
// 5. 检查是否开启了渠道配额
|
|
|
$enableChannelQuota = ($newBatch->enable_channel_quota ?? 0) == 1;
|
|
|
|
|
|
// 6. 检查新号源截止时间
|
|
|
$newPlanDate = $newBatch->plan_date;
|
|
|
$cutoffTime = $newBatch->cutoff_time;
|
|
|
$cutoffDateTime = $newPlanDate . ' ' . $cutoffTime;
|
|
|
|
|
|
if ($now > $cutoffDateTime) {
|
|
|
return ['success' => false, 'message' => '已过预约截止时间'];
|
|
|
}
|
|
|
|
|
|
// 6.5 检查患者类型
|
|
|
$patientType = $oldApplication->patient_type;
|
|
|
$patientTypeMask = $newBatch->patient_type_mask ?? 0;
|
|
|
|
|
|
// 校验患者类型有效范围,避免位运算越界
|
|
|
if ($patientType < 1 || $patientType > 4) {
|
|
|
return ['success' => false, 'message' => '患者类型无效'];
|
|
|
}
|
|
|
|
|
|
if ($patientTypeMask > 0 && ($patientTypeMask & (1 << ($patientType - 1))) == 0) {
|
|
|
$patientTypeNames = [1 => '门诊', 2 => '住院', 3 => '急诊', 4 => '体检'];
|
|
|
$typeName = $patientTypeNames[$patientType];
|
|
|
return ['success' => false, 'message' => "该号源不支持{$typeName}患者"];
|
|
|
}
|
|
|
|
|
|
// 7. 检查渠道(改约时渠道不变,沿用原预约渠道)
|
|
|
$channelId = $oldAppointment->channel_id;
|
|
|
$channel = DB::table('channel')
|
|
|
->where('id', $channelId)
|
|
|
->where('status', 1)
|
|
|
->first();
|
|
|
|
|
|
if (!$channel) {
|
|
|
return ['success' => false, 'message' => '渠道不存在或已停用'];
|
|
|
}
|
|
|
|
|
|
// 8. 获取项目的 use_seats
|
|
|
$itemCode = $oldApplication->examination_item_code;
|
|
|
$itemSeat = DB::table('exam_item')
|
|
|
->where('code', $itemCode)
|
|
|
->where('deleted', 0)
|
|
|
->select('use_seats')
|
|
|
->first();
|
|
|
|
|
|
if (!$itemSeat) {
|
|
|
return ['success' => false, 'message' => '检查项目信息不存在'];
|
|
|
}
|
|
|
|
|
|
// 9. 计算占位数量
|
|
|
$slotMode = $newBatch->slot_mode;
|
|
|
|
|
|
if ($slotMode == 1) {
|
|
|
$totalSeats = $itemSeat->use_seats ?? 1;
|
|
|
} else {
|
|
|
$totalSeats = 1;
|
|
|
}
|
|
|
|
|
|
// 10. 查询新号源渠道配额(加行锁)
|
|
|
$newChannelQuota = null;
|
|
|
$newPublicQuota = null;
|
|
|
|
|
|
if ($enableChannelQuota) {
|
|
|
$newChannelQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $newPlan->batch_id)
|
|
|
->where('channel_id', $channelId)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$newChannelQuota) {
|
|
|
return ['success' => false, 'message' => '该渠道无可配额'];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$newPublicQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $newPlan->batch_id)
|
|
|
->where('channel_id', -1)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
// 11. 在事务内检查新号源配额(包含 locked_quota)
|
|
|
$newChannelQuotaUsed = 0;
|
|
|
$newPublicQuotaUsed = 0;
|
|
|
|
|
|
if ($enableChannelQuota && $newChannelQuota) {
|
|
|
$channelRemain = $newChannelQuota->total_quota - $newChannelQuota->used_quota - ($newChannelQuota->locked_quota ?? 0);
|
|
|
$allowOveruse = $newChannelQuota->allow_overuse_public_pool ?? 0;
|
|
|
|
|
|
if ($channelRemain >= $totalSeats) {
|
|
|
$newChannelQuotaUsed = $totalSeats;
|
|
|
} elseif ($allowOveruse && $newPublicQuota) {
|
|
|
$newChannelQuotaUsed = $channelRemain;
|
|
|
$newPublicQuotaUsed = $totalSeats - $channelRemain;
|
|
|
|
|
|
$publicRemain = $newPublicQuota->total_quota - $newPublicQuota->used_quota - ($newPublicQuota->locked_quota ?? 0);
|
|
|
if ($publicRemain < $newPublicQuotaUsed) {
|
|
|
return ['success' => false, 'message' => '新号源配额不足'];
|
|
|
}
|
|
|
} else {
|
|
|
return ['success' => false, 'message' => '新号源配额不足'];
|
|
|
}
|
|
|
} else {
|
|
|
if (!$newPublicQuota) {
|
|
|
return ['success' => false, 'message' => '无可用配额'];
|
|
|
}
|
|
|
|
|
|
$remain = $newPublicQuota->total_quota - $newPublicQuota->used_quota - ($newPublicQuota->locked_quota ?? 0);
|
|
|
if ($remain < $totalSeats) {
|
|
|
return ['success' => false, 'message' => '新号源配额不足'];
|
|
|
}
|
|
|
|
|
|
$newPublicQuotaUsed = $totalSeats;
|
|
|
}
|
|
|
|
|
|
// 额外检查新 plan 表的可用配额
|
|
|
$newPlanAvailable = $newPlan->total_quota - $newPlan->used_quota - ($newPlan->locked_quota ?? 0);
|
|
|
if ($newPlanAvailable < $totalSeats) {
|
|
|
return ['success' => false, 'message' => '新号源配额不足'];
|
|
|
}
|
|
|
|
|
|
// 12. 调用综合校验服务
|
|
|
$validationResult = $this->validationService->validate(
|
|
|
$oldApplication->patient_id,
|
|
|
[$itemCode],
|
|
|
$newPlanId
|
|
|
);
|
|
|
if (!$validationResult['valid']) {
|
|
|
$conflictMessages = collect($validationResult['conflicts'])
|
|
|
->pluck('message')
|
|
|
->implode('; ');
|
|
|
return [
|
|
|
'success' => false,
|
|
|
'message' => $conflictMessages,
|
|
|
'conflicts' => $validationResult['conflicts']
|
|
|
];
|
|
|
}
|
|
|
|
|
|
// 13. 计算新预约时间
|
|
|
$newAppointmentTime = $newPlanDate . ' ' . ($newPlan->start_time ?? $newBatch->start_time);
|
|
|
|
|
|
// 14. 查询原预约信息(加行锁,不限制 deleted 条件,已删除的号源也需要恢复配额)
|
|
|
$oldPlan = DB::table('plan')
|
|
|
->where('id', $oldAppointment->time_slot_id)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$oldPlan) {
|
|
|
return ['success' => false, 'message' => '原号源信息不存在'];
|
|
|
}
|
|
|
|
|
|
$oldBatch = DB::table('plan_batch')
|
|
|
->where('id', $oldPlan->batch_id)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
if (!$oldBatch) {
|
|
|
return ['success' => false, 'message' => '原批次信息不存在'];
|
|
|
}
|
|
|
|
|
|
$oldEnableChannelQuota = ($oldBatch->enable_channel_quota ?? 0) == 1;
|
|
|
$oldChannelId = $oldAppointment->channel_id;
|
|
|
|
|
|
$oldChannelQuota = null;
|
|
|
$oldPublicQuota = null;
|
|
|
|
|
|
if ($oldEnableChannelQuota) {
|
|
|
$oldChannelQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $oldPlan->batch_id)
|
|
|
->where('channel_id', $oldChannelId)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
}
|
|
|
|
|
|
$oldPublicQuota = DB::table('plan_batch_channel_quota')
|
|
|
->where('batch_id', $oldPlan->batch_id)
|
|
|
->where('channel_id', -1)
|
|
|
->where('status', 1)
|
|
|
->where('deleted', 0)
|
|
|
->lockForUpdate()
|
|
|
->first();
|
|
|
|
|
|
// 更新旧预约记录状态
|
|
|
DB::table('exam_appointment')
|
|
|
->where('id', $oldAppointment->id)
|
|
|
->update([
|
|
|
'status' => 4,
|
|
|
'cancel_time' => now(),
|
|
|
'cancel_reason' => $cancelReason,
|
|
|
'updated_at' => now()
|
|
|
]);
|
|
|
|
|
|
// 根据原预约记录中记录的配额使用量精确恢复
|
|
|
$oldChannelQuotaUsed = $oldAppointment->channel_quota_used ?? 0;
|
|
|
$oldPublicQuotaUsed = $oldAppointment->public_quota_used ?? 0;
|
|
|
$oldTotalQuotaUsed = $oldChannelQuotaUsed + $oldPublicQuotaUsed;
|
|
|
|
|
|
// 恢复原渠道配额(乐观锁)
|
|
|
if ($oldChannelQuotaUsed > 0 && $oldChannelQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $oldChannelQuota->id)
|
|
|
->where('version', $oldChannelQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $oldChannelQuotaUsed . ')'),
|
|
|
'version' => $oldChannelQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发恢复原渠道配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 恢复原公共池配额(乐观锁)
|
|
|
if ($oldPublicQuotaUsed > 0 && $oldPublicQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $oldPublicQuota->id)
|
|
|
->where('version', $oldPublicQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $oldPublicQuotaUsed . ')'),
|
|
|
'version' => $oldPublicQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发恢复原公共池配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 恢复原批次配额(乐观锁 + GREATEST,使用原预约记录的精确配额总量)
|
|
|
$oldBatchAffected = DB::table('plan_batch')
|
|
|
->where('id', $oldBatch->id)
|
|
|
->where('version', $oldBatch->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $oldTotalQuotaUsed . ')'),
|
|
|
'version' => ($oldBatch->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($oldBatchAffected == 0) {
|
|
|
throw new \Exception('并发恢复原批次配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 恢复原号源配额(乐观锁 + GREATEST,使用原预约记录的精确配额总量)
|
|
|
$oldPlanAffected = DB::table('plan')
|
|
|
->where('id', $oldPlan->id)
|
|
|
->where('version', $oldPlan->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('GREATEST(0, used_quota - ' . $oldTotalQuotaUsed . ')'),
|
|
|
'version' => ($oldPlan->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($oldPlanAffected == 0) {
|
|
|
throw new \Exception('并发恢复原号源配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 更新新渠道配额(乐观锁)
|
|
|
if ($newChannelQuotaUsed > 0 && $newChannelQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $newChannelQuota->id)
|
|
|
->where('version', $newChannelQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $newChannelQuotaUsed),
|
|
|
'version' => $newChannelQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发更新新渠道配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 更新新公共池配额(乐观锁)
|
|
|
if ($newPublicQuotaUsed > 0 && $newPublicQuota) {
|
|
|
$affected = DB::table('plan_batch_channel_quota')
|
|
|
->where('id', $newPublicQuota->id)
|
|
|
->where('version', $newPublicQuota->version)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $newPublicQuotaUsed),
|
|
|
'version' => $newPublicQuota->version + 1
|
|
|
]);
|
|
|
|
|
|
if ($affected == 0) {
|
|
|
throw new \Exception('并发更新新公共池配额失败,请重试');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 更新新批次配额(乐观锁)
|
|
|
$newBatchAffected = DB::table('plan_batch')
|
|
|
->where('id', $newBatch->id)
|
|
|
->where('version', $newBatch->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $totalSeats),
|
|
|
'version' => ($newBatch->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($newBatchAffected == 0) {
|
|
|
throw new \Exception('并发更新新批次配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 更新新号源配额(乐观锁)
|
|
|
$newPlanAffected = DB::table('plan')
|
|
|
->where('id', $newPlan->id)
|
|
|
->where('version', $newPlan->version ?? 0)
|
|
|
->update([
|
|
|
'used_quota' => DB::raw('used_quota + ' . $totalSeats),
|
|
|
'version' => ($newPlan->version ?? 0) + 1
|
|
|
]);
|
|
|
|
|
|
if ($newPlanAffected == 0) {
|
|
|
throw new \Exception('并发更新新号源配额失败,请重试');
|
|
|
}
|
|
|
|
|
|
// 创建新预约记录
|
|
|
$newAppointmentId = DB::table('exam_appointment')->insertGetId([
|
|
|
'time_slot_id' => $newPlan->id,
|
|
|
'channel_id' => $oldAppointment->channel_id,
|
|
|
'exam_application_id' => $oldAppointment->exam_application_id,
|
|
|
'patient_id' => $oldAppointment->patient_id,
|
|
|
'patient_name' => $oldAppointment->patient_name,
|
|
|
'status' => 1,
|
|
|
'department_resource_id' => $newBatch->resource_id,
|
|
|
'appointment_time' => $newAppointmentTime,
|
|
|
'channel_quota_used' => $enableChannelQuota ? round($newChannelQuotaUsed, 4) : 0,
|
|
|
'public_quota_used' => round($newPublicQuotaUsed, 4),
|
|
|
'created_at' => now(),
|
|
|
'updated_at' => now()
|
|
|
]);
|
|
|
|
|
|
$this->logOperation(
|
|
|
$oldAppointment->exam_application_id,
|
|
|
2,
|
|
|
2,
|
|
|
$newAppointmentId,
|
|
|
2,
|
|
|
$userId,
|
|
|
[
|
|
|
'appointment_id' => $oldAppointment->id,
|
|
|
'appointment_time' => $oldAppointment->appointment_time,
|
|
|
'plan_id' => $oldAppointment->time_slot_id
|
|
|
],
|
|
|
[
|
|
|
'appointment_id' => $newAppointmentId,
|
|
|
'appointment_time' => $newAppointmentTime,
|
|
|
'plan_id' => $newPlan->id
|
|
|
],
|
|
|
$cancelReason
|
|
|
);
|
|
|
|
|
|
return [
|
|
|
'success' => true,
|
|
|
'message' => '改约成功',
|
|
|
'data' => [
|
|
|
'new_appointment_id' => $newAppointmentId,
|
|
|
'old_appointment_id' => $oldAppointment->id,
|
|
|
'new_appointment_time' => $newAppointmentTime
|
|
|
]
|
|
|
];
|
|
|
});
|
|
|
}
|
|
|
}
|