userService = $userService; $this->planService = $planService; } /** * 获取号源列表 * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function list(Request $request) { $payload = $request->attributes->get('payload'); if (!$payload) { return Rs::error('用户信息无效', 401); } $user = $this->userService->getUserInfoFromPayload($payload); if (!$user) { return Rs::error('用户不存在', 401); } $isAdmin = $this->userService->isAdmin($payload); $userDeptId = $user->dept_id ?? null; $resourceId = $request->input('resource_id', null); $startDate = $request->input('start_date', null); $endDate = $request->input('end_date', null); // 必须传入资源ID if (!$resourceId) { return Rs::error('请选择资源'); } // 必须传入日期范围 if (!$startDate || !$endDate) { return Rs::error('请选择日期范围'); } $query = DB::table('plan_batch') ->leftJoin('department_resource', 'plan_batch.resource_id', '=', 'department_resource.id') ->where('plan_batch.deleted', 0) ->where('plan_batch.resource_id', $resourceId) ->where('plan_batch.plan_date', '>=', $startDate) ->where('plan_batch.plan_date', '<=', $endDate) ->select( 'plan_batch.id', '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.weekname', 'plan_batch.patient_type_mask', '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.status', 'plan_batch.enable_channel_quota' ); // 权限控制:非管理员只能查看自己科室的号源 if (!$isAdmin) { if (empty($userDeptId)) { return Rs::error('请先绑定科室'); } $query->where('plan_batch.department_id', $userDeptId); } $planBatches = $query->orderBy('plan_batch.plan_date', 'asc') ->orderBy('plan_batch.start_time', 'asc') ->get(); if ($planBatches->isEmpty()) { return Rs::success(['list' => $planBatches]); } $allBatchIds = $planBatches->pluck('batch_id')->toArray(); // 批量预加载 locked_quota(从 plan 表按 batch_id 聚合) $lockedQuotaMap = DB::table('plan') ->whereIn('batch_id', $allBatchIds) ->where('deleted', 0) ->selectRaw('batch_id, SUM(locked_quota) as locked_quota') ->groupBy('batch_id') ->pluck('locked_quota', 'batch_id'); foreach ($planBatches as $batch) { $batch->locked_quota = $lockedQuotaMap[$batch->batch_id] ?? 0; } // 批量预加载渠道配额 $channelQuotaMap = DB::table('plan_batch_channel_quota as pbcq') ->leftJoin('channel', 'pbcq.channel_id', '=', 'channel.id') ->whereIn('pbcq.batch_id', $allBatchIds) ->where(['pbcq.deleted' => 0, 'pbcq.status' => 1]) ->select( 'pbcq.id', 'pbcq.batch_id', 'pbcq.channel_id', 'channel.name as channel_name', 'channel.short_name', 'pbcq.total_quota', 'pbcq.used_quota', 'pbcq.locked_quota', 'pbcq.allow_overuse_public_pool' ) ->get() ->groupBy('batch_id'); // 批量预加载 plan 记录 $planRecordsMap = DB::table('plan') ->whereIn('batch_id', $allBatchIds) ->where('deleted', 0) ->select('id', 'batch_id', 'start_time', 'end_time', 'total_quota', 'used_quota', 'locked_quota', 'status') ->orderBy('start_time', 'asc') ->get() ->groupBy('batch_id'); foreach ($planBatches as $batch) { $batch->channel_quotas = $channelQuotaMap[$batch->batch_id] ?? collect([]); $batchPlans = $planRecordsMap[$batch->batch_id] ?? collect([]); if ($batch->slot_mode == 2) { $batch->time_points = $batchPlans->map(function ($p) { return (object)[ 'id' => $p->id, 'start_time' => $p->start_time, 'end_time' => $p->end_time, 'total_quota' => $p->total_quota, 'used_quota' => $p->used_quota, 'locked_quota' => $p->locked_quota, 'status' => $p->status, ]; }); } else { $batch->time_points = []; $firstPlan = $batchPlans->first(); $batch->plan_id = $firstPlan ? $firstPlan->id : null; } } $data = [ 'list' => $planBatches ]; return Rs::success($data); } /** * 删除号源批次 * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function delete(Request $request) { $payload = $request->attributes->get('payload'); if (!$payload) { return Rs::error('用户信息无效', 401); } $user = $this->userService->getUserInfoFromPayload($payload); if (!$user) { return Rs::error('用户不存在', 401); } $isAdmin = $this->userService->isAdmin($payload); $userDeptId = $user->dept_id ?? null; $data = $request->validate([ 'ids' => 'required|array', 'ids.*' => 'integer' ]); try { DB::beginTransaction(); foreach ($data['ids'] as $id) { // 检查是否存在 $batch = DB::table('plan_batch') ->where('id', $id) ->where('deleted', 0) ->first(); if (!$batch) { DB::rollBack(); return Rs::error('号源批次不存在:' . $id); } // 权限控制:非管理员只能删除自己科室的号源 if (!$isAdmin) { if (empty($userDeptId)) { DB::rollBack(); return Rs::error('请先绑定科室'); } if ($batch->department_id != $userDeptId) { DB::rollBack(); return Rs::error('无权限删除该号源批次'); } } //如果被占用则不允许删除 if($batch->used_quota>0){ DB::rollBack(); return Rs::error($batch->plan_date.' '.$batch->start_time.'已有预约记录,不能删除'); } // 删除plan记录 DB::table('plan') ->where('batch_id', $batch->id) ->update([ 'deleted' => 1, 'updated_at' => now() ]); // 删除plan_batch_channel_quota记录 DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->update([ 'deleted' => 1, 'updated_at' => now() ]); // 软删除plan_batch记录 DB::table('plan_batch') ->where('id', $id) ->update([ 'deleted' => 1, 'updated_at' => now() ]); } DB::commit(); return Rs::success(null, '成功删除 ' . count($data['ids']) . ' 个号源批次'); } catch (\Exception $e) { DB::rollBack(); return Rs::error('删除号源批次失败:' . $e->getMessage()); } } /** * 更新号源批次 * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function update(Request $request) { $payload = $request->attributes->get('payload'); if (!$payload) { return Rs::error('用户信息无效', 401); } $user = $this->userService->getUserInfoFromPayload($payload); if (!$user) { return Rs::error('用户不存在', 401); } $isAdmin = $this->userService->isAdmin($payload); $userDeptId = $user->dept_id ?? null; $data = $request->validate([ 'id' => 'required|integer', 'start_time' => 'nullable|date_format:H:i:s', 'end_time' => 'nullable|date_format:H:i:s', 'status' => 'nullable|integer|in:0,1', 'total_quota' => 'nullable|integer|min:0', 'enable_channel_quota' => 'nullable|integer|in:0,1', 'channel_quotas' => 'nullable|array', 'channel_quotas.*.channel_id' => 'required|integer', 'channel_quotas.*.total_quota' => 'nullable|integer|min:0', 'channel_quotas.*.allow_overuse_public_pool' => 'nullable|integer|in:0,1', 'time_points' => 'nullable|array', 'time_points.*.id' => 'nullable|integer', 'time_points.*.time_range' => 'nullable|array', 'time_points.*.time_range.*' => 'nullable|date_format:H:i:s', 'time_points.*.status' => 'nullable|integer|in:0,1' ]); try { DB::beginTransaction(); // 检查是否存在 $batch = DB::table('plan_batch') ->where('id', $data['id']) ->where('deleted', 0) ->first(); if (!$batch) { DB::rollBack(); return Rs::error('号源批次不存在'); } // 权限控制:非管理员只能修改自己科室的号源 if (!$isAdmin) { if (empty($userDeptId)) { DB::rollBack(); return Rs::error('请先绑定科室'); } if ($batch->department_id != $userDeptId) { DB::rollBack(); return Rs::error('无权限修改该号源批次'); } } // 如果是时间点模式,验证时间段时长能否被总数整除 if ($batch->slot_mode == 2) { $startTime = strtotime('2000-01-01 ' . (isset($data['start_time']) ? $data['start_time'] : $batch->start_time)); $endTime = strtotime('2000-01-01 ' . (isset($data['end_time']) ? $data['end_time'] : $batch->end_time)); $totalMinutes = ($endTime - $startTime) / 60; $totalQuota = isset($data['total_quota']) ? $data['total_quota'] : $batch->total_quota; if ($totalMinutes % $totalQuota !== 0) { DB::rollBack(); return Rs::error('时间点模式下,时间段总时长必须能被号源数量整除。当前时长 ' . $totalMinutes . ' 分钟,号源数量 ' . $totalQuota . ',无法整除'); } } // 验证时间逻辑 if (!empty($data['start_time']) && !empty($data['end_time'])) { $startTime = strtotime('2000-01-01 ' . $data['start_time']); $endTime = strtotime('2000-01-01 ' . $data['end_time']); if ($endTime <= $startTime) { DB::rollBack(); return Rs::error('结束时间必须晚于开始时间'); } } // 验证总数与渠道配额之和是否相等 if (!empty($data['channel_quotas'])) { $channelTotal = 0; foreach ($data['channel_quotas'] as $quota) { $channelTotal += $quota['total_quota']; } $planTotal = isset($data['total_quota']) ? $data['total_quota'] : $batch->total_quota; if ($channelTotal != $planTotal) { DB::rollBack(); return Rs::error('号源总数(' . $planTotal . ')与渠道配额之和(' . $channelTotal . ')不相等,请检查后重试'); } } // 校验:总配额不能小于已预约数 if (isset($data['total_quota']) && $data['total_quota'] < $batch->used_quota) { DB::rollBack(); return Rs::error('总配额不能小于已预约数(当前已预约 ' . $batch->used_quota . ')'); } // 校验:启用渠道配额时,修改总数必须同时提交渠道配额 if (isset($data['total_quota']) && ($batch->enable_channel_quota ?? 0) == 1 && empty($data['channel_quotas'])) { DB::rollBack(); return Rs::error('当前号源已启用渠道配额,修改总数时必须同时提交渠道配额分配'); } // 记录乐观锁版本号 $batchVersion = $batch->version ?? 0; // 更新plan_batch $updateData = []; if (isset($data['status'])) { $updateData['status'] = $data['status']; } if (isset($data['total_quota'])) { $updateData['total_quota'] = $data['total_quota']; } if (isset($data['enable_channel_quota'])) { $updateData['enable_channel_quota'] = $data['enable_channel_quota']; } if (!empty($data['start_time'])) { $updateData['start_time'] = $data['start_time']; } if (!empty($data['end_time'])) { $updateData['end_time'] = $data['end_time']; $updateData['cutoff_time'] = $data['end_time']; } if (!empty($updateData)) { $updateData['updated_at'] = now(); $updateData['version'] = $batchVersion + 1; $batchAffected = DB::table('plan_batch') ->where('id', $data['id']) ->where('version', $batchVersion) ->update($updateData); if ($batchAffected === 0) { DB::rollBack(); return Rs::error('数据已被其他操作修改,请刷新后重试'); } } // 时间段模式:同步 plan.total_quota if ($batch->slot_mode == 1 && isset($data['total_quota'])) { $planRecord = DB::table('plan') ->where('batch_id', $data['id']) ->where('deleted', 0) ->first(); if ($planRecord) { $planAffected = DB::table('plan') ->where('id', $planRecord->id) ->where('version', $planRecord->version ?? 0) ->update([ 'total_quota' => $data['total_quota'], 'version' => ($planRecord->version ?? 0) + 1, 'updated_at' => now() ]); if ($planAffected === 0) { DB::rollBack(); return Rs::error('号源数据已被其他操作修改,请刷新后重试'); } } } // 时间点模式:如果调整了号源总量,需要重新生成时间点节点 if ($batch->slot_mode == 2 && isset($data['total_quota'])) { $newTotalQuota = $data['total_quota']; $oldTotalQuota = $batch->total_quota; // 只有当号源总量发生变化时才处理 if ($newTotalQuota != $oldTotalQuota) { // 获取时间范围(使用更新后的值,如果没有传则使用原值) $startTime = $batch->start_time; $endTime = $batch->end_time; // 计算时间间隔(秒) // 需要在同一天的基础上计算时间差 $baseDate = '2024-01-01'; $startTimeObj = \Carbon\Carbon::parse($baseDate . ' ' . $startTime); $endTimeObj = \Carbon\Carbon::parse($baseDate . ' ' . $endTime); $totalSeconds = $startTimeObj->diffInSeconds($endTimeObj); $intervalSeconds = $totalSeconds / $newTotalQuota; // 生成新的时间点列表 $newTimePoints = []; for ($i = 0; $i < $newTotalQuota; $i++) { $pointStartTime = $startTimeObj->copy()->addSeconds($i * $intervalSeconds); $pointEndTime = $startTimeObj->copy()->addSeconds(($i + 1) * $intervalSeconds); $newTimePoints[] = [ 'start_time' => $pointStartTime->format('H:i:s'), 'end_time' => $pointEndTime->format('H:i:s') ]; } Log::info($newTimePoints); // 获取原有的 plan 节点(只查询未删除的) $existingPlans = DB::table('plan') ->where('batch_id', $batch->id) ->where('deleted', 0) ->select('id', 'start_time', 'end_time', 'used_quota', 'status') ->get(); // 建立 start_time 到现有节点的映射(方便快速查找) $existingPlansByStartTime = []; foreach ($existingPlans as $plan) { $existingPlansByStartTime[$plan->start_time] = $plan; } // 遍历新的时间点,找出需要保留和新增的 $keepStartTimes = []; // 记录需要保留的时间点 foreach ($newTimePoints as $newPoint) { $startTimeStr = $newPoint['start_time']; $keepStartTimes[] = $startTimeStr; if (!isset($existingPlansByStartTime[$startTimeStr])) { // 时间点不存在,新增 DB::table('plan')->insert([ 'batch_id' => $batch->id, 'start_time' => $newPoint['start_time'], 'end_time' => $newPoint['end_time'], 'total_quota' => 1, 'used_quota' => 0, 'status' => 1, 'deleted' => 0, 'created_at' => now(), 'updated_at' => now() ]); } // 时间点已存在则什么都不做,保留原样 } // 处理不在新节点列表中的原有节点 foreach ($existingPlans as $plan) { if (!in_array($plan->start_time, $keepStartTimes)) { // 不在新节点列表中 if ($plan->used_quota > 0) { // 已占用,设置为不可用 DB::table('plan') ->where('id', $plan->id) ->update([ 'status' => 0, 'updated_at' => now() ]); } else { // 未占用,软删除 DB::table('plan') ->where('id', $plan->id) ->update([ 'deleted' => 1, 'updated_at' => now() ]); } } } } } // 更新渠道配额 if (!empty($data['channel_quotas'])) { // 获取前端提交的 channel_id 列表 $submittedChannelIds = array_map(function($quota) { return $quota['channel_id']; }, $data['channel_quotas']); // 停用数据库中存在但前端未提交的渠道配额(即关闭的渠道) DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('status', 1) // 只停用正常的 ->whereNotIn('channel_id', $submittedChannelIds) ->update(['status' => 0, 'updated_at' => now()]); // 更新或插入前端提交的渠道配额 foreach ($data['channel_quotas'] as $quota) { $updateQuotaData = [ 'total_quota' => $quota['total_quota'], 'status' => 1, // 恢复或设置为正常 'updated_at' => now() ]; // 如果提供了 allow_overuse_public_pool,则更新该字段 if (isset($quota['allow_overuse_public_pool'])) { $updateQuotaData['allow_overuse_public_pool'] = $quota['allow_overuse_public_pool']; } // 检查是否已存在该渠道配额(包括已停用的) $existingQuota = DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('channel_id', $quota['channel_id']) ->first(); if ($existingQuota) { // 存在则更新(恢复停用的,或更新正常的,保留 used_quota) DB::table('plan_batch_channel_quota') ->where('id', $existingQuota->id) ->update($updateQuotaData); } else { // 不存在则插入(新增渠道) DB::table('plan_batch_channel_quota')->insert([ 'batch_id' => $batch->id, 'channel_id' => $quota['channel_id'], 'total_quota' => $quota['total_quota'], 'used_quota' => 0, 'allow_overuse_public_pool' => $quota['allow_overuse_public_pool'] ?? 0, 'status' => 1, 'created_at' => now(), 'updated_at' => now() ]); } } } // 更新时间点 // 如果时间点模式且调整了号源总量,已经由上面的逻辑重新生成了时间点,这里跳过处理 $skipTimePoints = ($batch->slot_mode == 2 && isset($data['total_quota']) && $data['total_quota'] != $batch->total_quota); if (!$skipTimePoints && !empty($data['time_points'])) { foreach ($data['time_points'] as $point) { $updatePointData = []; if (!empty($point['time_range']) && is_array($point['time_range'])) { $updatePointData['start_time'] = $point['time_range'][0]; $updatePointData['end_time'] = $point['time_range'][1]; } if (isset($point['status'])) { $updatePointData['status'] = $point['status']; } if (!empty($updatePointData)) { $updatePointData['updated_at'] = now(); DB::table('plan') ->where('id', $point['id']) ->update($updatePointData); } } } DB::commit(); return Rs::success(null, '更新成功'); } catch (\Exception $e) { DB::rollBack(); return Rs::error('更新号源批次失败:' . $e->getMessage()); } } /** * 获取号源预约记录 * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function appointments(Request $request) { $payload = $request->attributes->get('payload'); if (!$payload) { return Rs::error('用户信息无效', 401); } $user = $this->userService->getUserInfoFromPayload($payload); if (!$user) { return Rs::error('用户不存在', 401); } $isAdmin = $this->userService->isAdmin($payload); $userDeptId = $user->dept_id ?? null; $planBatchId = $request->input('plan_batch_id', null); // 必须传入号源批次ID if (!$planBatchId) { return Rs::error('请提供号源批次ID'); } // 获取plan批次信息 $planBatch = DB::table('plan_batch') ->where('id', $planBatchId) ->where('deleted', 0) ->first(); if (!$planBatch) { return Rs::error('号源批次不存在'); } // 权限控制:非管理员只能查看自己科室的号源 if (!$isAdmin) { if (empty($userDeptId)) { return Rs::error('请先绑定科室'); } if ($planBatch->department_id != $userDeptId) { return Rs::error('无权限查看该号源的预约记录'); } } // 获取所有plan的ID(时间段和时间点模式都需要查询plan表) $planIds = []; $plans = DB::table('plan') ->where('batch_id', $planBatchId) ->where('deleted', 0) ->select('id') ->get(); foreach ($plans as $plan) { $planIds[] = $plan->id; } // 查询exam_appointment表 $query = DB::table('exam_appointment') ->whereIn('time_slot_id', $planIds) ->whereIn('exam_appointment.status', [1,2,3]) ->leftJoin('channel', 'exam_appointment.channel_id', '=', 'channel.id') ->leftJoin('exam_application', 'exam_appointment.exam_application_id', '=', 'exam_application.id') ->select( 'exam_appointment.id', 'exam_appointment.time_slot_id', 'exam_appointment.patient_id', 'exam_appointment.patient_name', 'exam_appointment.appointment_time', 'exam_appointment.status', 'exam_appointment.channel_id', 'channel.name as channel_name', 'exam_application.examination_item_name', 'exam_appointment.created_at' ) ->orderBy('exam_appointment.created_at', 'desc'); $appointments = $query->get(); // 批量预加载 plan → batch → resource 映射 $uniqueTimeSlotIds = $appointments->pluck('time_slot_id')->unique()->toArray(); $planMap = DB::table('plan') ->whereIn('id', $uniqueTimeSlotIds) ->where('deleted', 0) ->pluck('batch_id', 'id'); $uniqueBatchIds = $planMap->unique()->values()->toArray(); $batchMap = DB::table('plan_batch') ->whereIn('id', $uniqueBatchIds) ->pluck('resource_id', 'id'); $uniqueResourceIds = $batchMap->unique()->values()->toArray(); $resourceMap = DB::table('department_resource') ->whereIn('id', $uniqueResourceIds) ->pluck('name', 'id'); foreach ($appointments as $appointment) { $batchId = $planMap[$appointment->time_slot_id] ?? null; if ($batchId) { $resourceId = $batchMap[$batchId] ?? null; $appointment->resource_name = $resourceId ? ($resourceMap[$resourceId] ?? '') : ''; } else { $appointment->resource_name = ''; } } $data = [ 'list' => $appointments ]; return Rs::success($data); } /** * 获取可用号源(用于预约) * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function GetEnablePlan(Request $request) { $data = $request->validate([ 'exam_application_ids' => 'required|array', 'exam_application_ids.*' => 'integer', 'channel_id' => 'required|integer|min:1', 'start_date' => 'nullable|date|after_or_equal:today' ]); $examApplicationIds = $data['exam_application_ids']; $channelId = $data['channel_id']; $startDate = $data['start_date'] ?? date('Y-m-d'); try { // 使用 PlanService 获取公共数据 $planData = $this->planService->queryPlanBatchesData($examApplicationIds, $channelId, $startDate, 15); $planBatches = $planData['planBatches']; $quotaByBatch = $planData['quotaByBatch']; $totalUseSeats = $planData['totalUseSeats']; // 8. 获取当前时间用于判断截止时间 $now = now(); $currentDate = $now->format('Y-m-d'); // 8.1 批量预加载所有 plan 记录,避免循环内 N+1 查询 $allBatchIds = $planBatches->pluck('batch_id')->toArray(); $allPlanRecordsMap = DB::table('plan') ->whereIn('batch_id', $allBatchIds) ->where('deleted', 0) ->select('id', 'batch_id', 'start_time', 'end_time', 'total_quota', 'used_quota', 'locked_quota', 'status') ->orderBy('start_time') ->get() ->groupBy('batch_id'); // 9. 构建响应数据 $resourcesData = []; $batchesByResource = $planBatches->groupBy('resource_id'); foreach ($batchesByResource as $resourceId => $batches) { $resource = $batches->first(); $slotMode = $resource->slot_mode; $datesData = []; $batchesByDate = $batches->groupBy('plan_date'); foreach ($batchesByDate->keys() as $date) { if (!isset($batchesByDate[$date])) { $datesData[$date] = ['date' => $date, 'periods' => []]; continue; } $periods = []; foreach ($batchesByDate[$date] as $batch) { // 判断是否超过截止时间 $isPastCutoff = ($batch->plan_date === $currentDate) && $now->gt(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $batch->plan_date . ' ' . $batch->cutoff_time)); if ($slotMode == 1) { // ========== 时间段模式 ========== $slots = []; $quotas = $quotaByBatch[$batch->batch_id] ?? ['public' => null, 'channel' => null]; $publicQuota = $quotas['public']; $channelQuota = $quotas['channel']; if (($batch->enable_channel_quota ?? 0) == 0) { if (!$publicQuota) continue; $effectiveQuota = $publicQuota; $allowOveruse = 0; } else { if (!$channelQuota) continue; $effectiveQuota = $channelQuota; $allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0; } $totalQuota = $effectiveQuota->total_quota; $usedQuota = $effectiveQuota->used_quota; $lockedQuota = $effectiveQuota->locked_quota ?? 0; if ($allowOveruse && $publicQuota && ($batch->enable_channel_quota ?? 0) != 0) { $totalQuota += $publicQuota->total_quota; $usedQuota += $publicQuota->used_quota; $lockedQuota += $publicQuota->locked_quota ?? 0; } $remainCount = max(0, $totalQuota - $usedQuota - $lockedQuota); // 判断剩余号源是否满足所有项目的占位数,且未超过截止时间 $enable = $remainCount >= $totalUseSeats && !$isPastCutoff; // 从预加载的 plan 记录中获取(时间段模式只有1条) $batchPlans = $allPlanRecordsMap[$batch->batch_id] ?? collect([]); $planRecord = $batchPlans->first(); $slots[] = [ 'id' => $planRecord ? $planRecord->id : $batch->batch_id, 'time' => substr($batch->start_time, 0, 5), 'used_count' => $totalQuota - $remainCount, 'total_count' => $totalQuota, 'enable' => $enable ]; $periods[] = [ 'period_id' => $batch->batch_id, 'period_name' => '', 'time_range' => substr($batch->start_time, 0, 5) . '-' . substr($batch->end_time, 0, 5), 'slots' => $slots ]; } else { // ========== 时间点模式(关键修正)========== $quotas = $quotaByBatch[$batch->batch_id] ?? ['public' => null, 'channel' => null]; $publicQuota = $quotas['public']; $channelQuota = $quotas['channel']; $enableChannelQuota = ($batch->enable_channel_quota ?? 0) == 1; // 如果开启了渠道配额,但该渠道没有配额(无记录或status=0或deleted=1),则跳过该批次 if ($enableChannelQuota && !$channelQuota) { continue; } // 计算整体配额状态 $overallTotal = 0; $overallUsed = 0; $hasQuota = false; if (!$enableChannelQuota) { if ($publicQuota) { $overallTotal = $publicQuota->total_quota; $overallUsed = $publicQuota->used_quota; $hasQuota = true; } } else { if ($channelQuota) { $overallTotal = $channelQuota->total_quota; $overallUsed = $channelQuota->used_quota; $allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0; if ($allowOveruse && $publicQuota) { $overallTotal += $publicQuota->total_quota; $overallUsed += $publicQuota->used_quota; } $hasQuota = true; } } // 从预加载的 plan 记录中获取时间点 $batchPlanRecords = $allPlanRecordsMap[$batch->batch_id] ?? collect([]); $timePoints = $batchPlanRecords->filter(function ($p) { return $p->status == 1; }); if ($timePoints->isEmpty()) { continue; } $slots = []; foreach ($timePoints as $tp) { // 判断时间点是否过期(超过开始时间就不能用了) $isTimeExpired = false; if ($batch->plan_date === $currentDate) { // 当日:比较时间点的开始时间与当前时间 $tpStartTime = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', $batch->plan_date . ' ' . $tp->start_time); $isTimeExpired = $now->gt($tpStartTime); } else if ($batch->plan_date < $currentDate) { // 过去的日期:直接算过期 $isTimeExpired = true; } if (!$hasQuota || ($overallTotal - $overallUsed) <= 0) { // 无配额 或 整体已满 → 强制显示为占满 $slots[] = [ 'id' => $tp->id, 'time' => substr($tp->start_time, 0, 5), 'used_count' => $tp->used_quota, 'total_count' => $tp->total_quota, 'enable' => false ]; } else { // 有剩余配额 → 显示真实数据,同时判断是否超过截止时间或时间点过期 $availableQuota = $tp->total_quota - $tp->used_quota - ($tp->locked_quota ?? 0); $enable = $availableQuota > 0 && !$isPastCutoff && !$isTimeExpired; $slots[] = [ 'id' => $tp->id, 'time' => substr($tp->start_time, 0, 5), 'used_count' => $tp->used_quota, 'total_count' => $tp->total_quota, 'locked_count' => $tp->locked_quota ?? 0, 'enable' => $enable ]; } } $periods[] = [ 'period_id' => $batch->batch_id, 'period_name' => '', 'time_range' => substr($batch->start_time, 0, 5) . '-' . substr($batch->end_time, 0, 5), 'slots' => $slots ]; } } $datesData[$date] = ['date' => $date, 'periods' => $periods]; } $resourcesData[] = [ 'room_id' => $resourceId, 'room_name' => $resource->resource_name, 'slot_mode' => $slotMode, 'slot_data' => array_values($datesData) ]; } return Rs::success(['resources' => $resourcesData]); } catch (\Exception $e) { return Rs::error('获取可用号源失败:' . $e->getMessage()); } } /** * 获取号源日历(按月查询) * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function GetPlanCalendar(Request $request) { $data = $request->validate([ 'exam_application_ids' => 'required|array', 'exam_application_ids.*' => 'integer', 'channel_id' => 'required|integer|min:1', 'year' => 'required|integer|min:2020|max:2099', 'month' => 'required|integer|min:1|max:12' ]); $examApplicationIds = $data['exam_application_ids']; $channelId = $data['channel_id']; $year = $data['year']; $month = $data['month']; try { // 1. 生成日历显示的日期范围(覆盖整个日历网格,包括上月末尾和下月开头的日期) $firstDayOfMonth = strtotime(sprintf('%04d-%02d-01', $year, $month)); $firstDayOfWeek = date('w', $firstDayOfMonth); $startDate = date('Y-m-d', strtotime("-{$firstDayOfWeek} days", $firstDayOfMonth)); $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); $lastDayOfWeek = date('w', strtotime(sprintf('%04d-%02d-%d', $year, $month, $daysInMonth))); $totalDays = $firstDayOfWeek + $daysInMonth + (6 - $lastDayOfWeek); $totalRows = ceil($totalDays / 7); $today = date('Y-m-d'); $calendarDates = []; $currentDate = $startDate; for ($i = 0; $i < $totalRows * 7; $i++) { if ($currentDate >= $today) { $calendarDates[] = $currentDate; } $currentDate = date('Y-m-d', strtotime('+1 day', strtotime($currentDate))); } if (empty($calendarDates)) { return Rs::success(['resources' => []]); } // 2. 使用 PlanService 获取公共数据 $planData = $this->planService->queryPlanBatchesData( $examApplicationIds, $channelId, '', 0, $calendarDates ); $planBatches = $planData['planBatches']; $quotaByBatch = $planData['quotaByBatch']; if ($planBatches->isEmpty()) { return Rs::success(['resources' => []]); } // 3. 按资源分组,聚合为日历格式 $resourcesData = []; $batchesByResource = $planBatches->groupBy('resource_id'); foreach ($batchesByResource as $resourceId => $batches) { $resource = $batches->first(); $batchesByDate = $batches->groupBy('plan_date'); $datesData = []; foreach ($calendarDates as $date) { if (!isset($batchesByDate[$date])) { $datesData[$date] = [ 'date' => $date, 'total_count' => 0, 'available_count' => 0 ]; continue; } $totalCount = 0; $availableCount = 0; foreach ($batchesByDate[$date] as $batch) { $remaining = $this->planService->calculateRemainingQuota( $batch, $channelId, $quotaByBatch[$batch->batch_id]['channel'] ?? null, $quotaByBatch[$batch->batch_id]['public'] ?? null, 1 ); $quotas = $quotaByBatch[$batch->batch_id] ?? ['public' => null, 'channel' => null]; $publicQuota = $quotas['public']; $channelQuota = $quotas['channel']; $enableChannelQuota = ($batch->enable_channel_quota ?? 0) == 1; if (!$enableChannelQuota) { if (!$publicQuota) continue; $totalQuota = $publicQuota->total_quota; $usedQuota = $publicQuota->used_quota; $lockedQuota = $publicQuota->locked_quota ?? 0; } else { if (!$channelQuota) continue; $totalQuota = $channelQuota->total_quota; $usedQuota = $channelQuota->used_quota; $lockedQuota = $channelQuota->locked_quota ?? 0; $allowOveruse = $channelQuota->allow_overuse_public_pool ?? 0; if ($allowOveruse && $publicQuota) { $totalQuota += $publicQuota->total_quota; $usedQuota += $publicQuota->used_quota; $lockedQuota += $publicQuota->locked_quota ?? 0; } } $totalCount += $totalQuota; $availableCount += max(0, $totalQuota - $usedQuota - $lockedQuota); } $datesData[$date] = [ 'date' => $date, 'total_count' => $totalCount, 'available_count' => $availableCount ]; } $resourcesData[] = [ 'room_id' => $resourceId, 'room_name' => $resource->resource_name, 'slot_data' => array_values($datesData) ]; } return Rs::success([ 'resources' => $resourcesData ]); } catch (\Exception $e) { return Rs::error('获取号源日历失败:' . $e->getMessage()); } } /** * 创建占位 */ public function createLock(Request $request) { $data = $request->validate([ 'plan_ids' => 'nullable|array', 'plan_ids.*' => 'nullable|integer', 'plan_id' => 'nullable|integer', 'channel_locks' => 'nullable|array', 'channel_locks.*.channel_id' => 'required|integer', 'channel_locks.*.locked_count' => 'required|integer|min:1', 'reason' => 'required|string|max:500' ]); $payload = $request->attributes->get('payload'); if (!$payload) { return Rs::error('用户信息无效', 401); } $user = $this->userService->getUserInfoFromPayload($payload); if (!$user) { return Rs::error('用户不存在', 401); } $operatorId = $user->user_id ?? null; $operatorName = $user->name ?? ''; try { DB::beginTransaction(); $planIds = []; // 时间段模式:单个plan_id + 渠道占位 if (isset($data['plan_id'])) { $planId = $data['plan_id']; // 查询plan和batch信息 $plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first(); if (!$plan) { throw new \Exception('号源不存在'); } $batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first(); if (!$batch) { throw new \Exception('批次不存在'); } // 查询resource获取slot_mode $resource = DB::table('department_resource')->where('id', $batch->resource_id)->first(); $slotMode = $resource->slot_mode ?? 1; if (($batch->enable_channel_quota ?? 0) == 0) { // 未启用渠道配额:直接在 plan 表占位 $lockedCount = 1; if (isset($data['channel_locks']) && !empty($data['channel_locks'])) { $lockedCount = $data['channel_locks'][0]['locked_count'] ?? 1; } $availableQuota = $plan->total_quota - $plan->used_quota - ($plan->locked_quota ?? 0); if ($availableQuota < $lockedCount) { throw new \Exception('可用配额不足,剩余 ' . $availableQuota); } DB::table('plan') ->where('id', $planId) ->increment('locked_quota', $lockedCount); // 同步更新公共池 channel_quota 的 locked_quota,保持前端展示一致 DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('channel_id', -1) ->where('deleted', 0) ->increment('locked_quota', $lockedCount); DB::table('plan_locked_log')->insert([ 'plan_id' => $planId, 'batch_id' => $batch->id, 'channel_id' => null, 'locked_quota' => $lockedCount, 'reason' => $data['reason'], 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'action' => 1, 'slot_mode' => $slotMode, 'created_at' => now() ]); } else { // 启用了渠道配额:按渠道占位 if (!isset($data['channel_locks']) || empty($data['channel_locks'])) { throw new \Exception('请提供渠道占位信息'); } $channelLocks = $data['channel_locks']; $totalLockedInChannels = 0; foreach ($channelLocks as $channelLock) { $channelId = $channelLock['channel_id']; $lockedCount = $channelLock['locked_count']; $channelQuota = DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('channel_id', $channelId) ->where('deleted', 0) ->first(); if (!$channelQuota) { throw new \Exception("渠道 {$channelId} 配额不存在"); } $availableQuota = $channelQuota->total_quota - $channelQuota->used_quota - ($channelQuota->locked_quota ?? 0); if ($availableQuota < $lockedCount) { throw new \Exception("渠道 {$channelId} 可用配额不足,剩余 {$availableQuota}"); } DB::table('plan_batch_channel_quota') ->where('id', $channelQuota->id) ->increment('locked_quota', $lockedCount); $totalLockedInChannels += $lockedCount; DB::table('plan_locked_log')->insert([ 'plan_id' => $planId, 'batch_id' => $batch->id, 'channel_id' => $channelId, 'locked_quota' => $lockedCount, 'reason' => $data['reason'], 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'action' => 1, 'slot_mode' => $slotMode, 'created_at' => now() ]); } // 同步更新 plan.locked_quota,保持列表展示一致 if ($totalLockedInChannels > 0) { DB::table('plan') ->where('id', $planId) ->increment('locked_quota', $totalLockedInChannels); } } $planIds[] = $planId; } // 时间点模式:多个plan_ids(不按渠道) elseif (isset($data['plan_ids']) && !empty($data['plan_ids'])) { $planIdList = $data['plan_ids']; foreach ($planIdList as $planId) { // 查询plan和batch信息 $plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first(); if (!$plan) { throw new \Exception("号源 {$planId} 不存在"); } $batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first(); if (!$batch) { throw new \Exception("批次 {$plan->batch_id} 不存在"); } // 查询resource获取slot_mode $resource = DB::table('department_resource')->where('id', $batch->resource_id)->first(); $slotMode = $resource->slot_mode ?? 1; // 检查可用配额 $availableQuota = $plan->total_quota - $plan->used_quota - ($plan->locked_quota ?? 0); if ($availableQuota < 1) { throw new \Exception("时间点 {$plan->start_time} 可用配额不足"); } // 增加占位数量(时间点模式始终为1) DB::table('plan') ->where('id', $planId) ->increment('locked_quota', 1); // 同步更新公共池 channel_quota 的 locked_quota,保持列表展示一致 DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('channel_id', -1) ->where('deleted', 0) ->increment('locked_quota', 1); // 记录日志(时间点模式不记录channel_id) $timePointInfo = $plan->start_time ? substr($plan->start_time, 0, 5) : ''; $logReason = $timePointInfo ? "{$timePointInfo} {$data['reason']}" : $data['reason']; DB::table('plan_locked_log')->insert([ 'plan_id' => $planId, 'batch_id' => $batch->id, 'channel_id' => null, 'locked_quota' => 1, 'reason' => $logReason, 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'action' => 1, 'slot_mode' => $slotMode, 'created_at' => now() ]); $planIds[] = $planId; } } else { throw new \Exception('请提供plan_id或plan_ids'); } DB::commit(); return Rs::success(null, '占位成功'); } catch (\Exception $e) { DB::rollBack(); return Rs::error('占位失败:' . $e->getMessage()); } } /** * 释放占位 */ public function releaseLock(Request $request) { $data = $request->validate([ 'slot_mode' => 'required|integer|in:1,2', 'plan_ids' => 'nullable|array', 'plan_ids.*' => 'integer', 'channel_id' => 'nullable|integer', 'release_count' => 'nullable|integer|min:1' ]); $payload = $request->attributes->get('payload'); if (!$payload) { return Rs::error('用户信息无效', 401); } $user = $this->userService->getUserInfoFromPayload($payload); if (!$user) { return Rs::error('用户不存在', 401); } $operatorId = $user->user_id ?? null; $operatorName = $user->name ?? ''; try { DB::beginTransaction(); $slotMode = $data['slot_mode']; // 时间点模式:slot_mode=2,使用plan_ids释放 if ($slotMode == 2) { if (!isset($data['plan_ids']) || empty($data['plan_ids'])) { throw new \Exception('时间点模式请提供plan_ids'); } $planIdList = $data['plan_ids']; foreach ($planIdList as $planId) { // 查询plan信息 $plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first(); if (!$plan) { throw new \Exception("号源 {$planId} 不存在"); } $lockedQuota = $plan->locked_quota ?? 0; if ($lockedQuota <= 0) { continue; // 该时间点无占位,跳过 } // 获取batch和resource信息 $batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first(); if (!$batch) { throw new \Exception("批次 {$plan->batch_id} 不存在"); } $resource = DB::table('department_resource')->where('id', $batch->resource_id)->first(); // 减少占位数量(时间点模式始终全部释放) DB::table('plan') ->where('id', $planId) ->update(['locked_quota' => 0]); // 同步释放公共池 channel_quota 的 locked_quota,保持配额展示一致 DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('channel_id', -1) ->where('deleted', 0) ->decrement('locked_quota', $lockedQuota); // 记录日志(时间点模式不记录channel_id) $timePointInfo = $plan->start_time ? substr($plan->start_time, 0, 5) : ''; $logReason = $timePointInfo ? "{$timePointInfo} 释放占位" : '释放占位'; DB::table('plan_locked_log')->insert([ 'plan_id' => $planId, 'batch_id' => $batch->id, 'channel_id' => null, 'locked_quota' => $lockedQuota, 'reason' => $logReason, 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'action' => 2, 'slot_mode' => $slotMode, 'created_at' => now() ]); } } // 时间段模式:slot_mode=1 elseif ($slotMode == 1) { if (!isset($data['plan_ids']) || empty($data['plan_ids'])) { throw new \Exception('时间段模式请提供plan_ids'); } $planId = $data['plan_ids'][0]; $plan = DB::table('plan')->where('id', $planId)->where('deleted', 0)->first(); if (!$plan) { throw new \Exception('号源不存在'); } $batch = DB::table('plan_batch')->where('id', $plan->batch_id)->first(); if (!$batch) { throw new \Exception('批次不存在'); } if (($batch->enable_channel_quota ?? 0) == 0) { // 未启用渠道配额:直接在 plan 表释放 $releaseCount = $data['release_count'] ?? 0; $lockedQuota = $plan->locked_quota ?? 0; if (!$releaseCount) { $releaseCount = $lockedQuota; } elseif ($releaseCount > $lockedQuota) { throw new \Exception('释放数量不能超过当前占位数量'); } if ($releaseCount <= 0) { throw new \Exception('当前无占位可释放'); } DB::table('plan') ->where('id', $planId) ->decrement('locked_quota', $releaseCount); // 同步释放公共池 channel_quota 的 locked_quota DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('channel_id', -1) ->where('deleted', 0) ->decrement('locked_quota', $releaseCount); DB::table('plan_locked_log')->insert([ 'plan_id' => $planId, 'batch_id' => $batch->id, 'channel_id' => null, 'locked_quota' => $releaseCount, 'reason' => '释放占位', 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'action' => 2, 'slot_mode' => $slotMode, 'created_at' => now() ]); } else { // 启用了渠道配额:按渠道释放 if (!isset($data['channel_id'])) { throw new \Exception('时间段模式请提供channel_id'); } $channelId = $data['channel_id']; $releaseCount = $data['release_count'] ?? 0; $channelQuota = DB::table('plan_batch_channel_quota') ->where('batch_id', $batch->id) ->where('channel_id', $channelId) ->where('deleted', 0) ->first(); if (!$channelQuota) { throw new \Exception("渠道 {$channelId} 配额不存在"); } if (!$releaseCount) { $releaseCount = $channelQuota->locked_quota ?? 0; } else { if ($releaseCount > ($channelQuota->locked_quota ?? 0)) { throw new \Exception('释放数量不能超过当前占位数量'); } } if ($releaseCount <= 0) { throw new \Exception('该渠道当前无占位可释放'); } DB::table('plan_batch_channel_quota') ->where('id', $channelQuota->id) ->decrement('locked_quota', $releaseCount); // 同步释放 plan.locked_quota,保持列表展示一致 DB::table('plan') ->where('id', $planId) ->decrement('locked_quota', $releaseCount); DB::table('plan_locked_log')->insert([ 'plan_id' => $planId, 'batch_id' => $batch->id, 'channel_id' => $channelId, 'locked_quota' => $releaseCount, 'reason' => '释放占位', 'operator_id' => $operatorId, 'operator_name' => $operatorName, 'action' => 2, 'slot_mode' => $slotMode, 'created_at' => now() ]); } } DB::commit(); return Rs::success(null, '释放成功'); } catch (\Exception $e) { DB::rollBack(); return Rs::error('释放失败:' . $e->getMessage()); } } /** * 查询占位记录 */ public function getLockHistory(Request $request) { $data = $request->validate([ 'plan_id' => 'nullable|integer', 'batch_id' => 'nullable|integer' ]); try { $query = DB::table('plan_locked_log as log') ->orderBy('log.created_at', 'desc'); if (isset($data['plan_id'])) { $query->where('log.plan_id', $data['plan_id']); } if (isset($data['batch_id'])) { $query->where('log.batch_id', $data['batch_id']); } $logs = $query->limit(100)->get(); return Rs::success(['list' => $logs]); } catch (\Exception $e) { return Rs::error('查询失败:' . $e->getMessage()); } } }