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.

1056 lines
41 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;
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 号源 IDplan 表 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
]
];
});
}
}