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