userService = $userService; } /** * 获取号源模板列表 * @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; $departmentId = $request->input('department_id', null); $modelTypeId = $request->input('model_type_id', null); $resourceId = $request->input('resource_id', null); $slotMode = $request->input('slot_mode', null); $weekname = $request->input('weekname', null); $page = $request->input('page', 1); $pageSize = $request->input('page_size', 10); $query = DB::table('plan_model') ->leftJoin('plan_model_type', 'plan_model.model_type_id', '=', 'plan_model_type.id') ->leftJoin('department', 'plan_model.department_id', '=', 'department.id') ->leftJoin('department_resource', 'plan_model.resource_id', '=', 'department_resource.id') ->where('plan_model.deleted', 0) ->select( 'plan_model.id', 'plan_model.model_type_id', 'plan_model_type.name as model_type_name', 'plan_model.department_id', 'department.name as department_name', 'plan_model.resource_id', 'department_resource.name as resource_name', 'department_resource.slot_mode', 'plan_model.total_quota', 'plan_model.weekname', 'plan_model.patient_type_mask', 'plan_model.start_time', 'plan_model.end_time', 'plan_model.cutoff_time', 'plan_model.status', 'plan_model.created_at', 'plan_model.updated_at' ); // 权限控制:非管理员只能查看自己科室的模板 if (!$isAdmin) { if (empty($userDeptId)) { return Rs::error('请先绑定科室'); } $query->where('plan_model.department_id', $userDeptId); // 如果用户传入了科室参数,需要验证是否属于自己科室 if ($departmentId !== null && $departmentId !== '') { if ($departmentId != $userDeptId) { return Rs::error('无权限访问其他科室的数据'); } } } if ($modelTypeId !== null && $modelTypeId !== '') { $query->where('plan_model.model_type_id', $modelTypeId); } if ($slotMode !== null && $slotMode !== '') { $query->where('plan_model.slot_mode', $slotMode); } if ($resourceId !== null && $resourceId !== '') { $query->where('plan_model.resource_id', $resourceId); } if ($weekname !== null && $weekname !== '') { $query->where('plan_model.weekname', $weekname); } if ($departmentId !== null && $departmentId !== '' && $isAdmin) { // 管理员或无科室限制的用户可以根据科室筛选 $query->where('plan_model.department_id', $departmentId); } $total = $query->count(); $models = $query->orderBy('plan_model.id', 'desc') ->skip(($page - 1) * $pageSize) ->take($pageSize) ->get(); // 获取每个模板的渠道配额信息(包含公共池) foreach ($models as $model) { $channelQuotas = DB::table('plan_model_channel_quota as pmcq') ->leftJoin('channel', 'pmcq.channel_id', '=', 'channel.id') ->where('pmcq.model_id', $model->id) ->where('pmcq.deleted', 0) ->select( 'pmcq.id', 'pmcq.channel_id', 'channel.name as channel_name', 'channel.short_name', 'pmcq.total_quota', 'pmcq.allow_overuse_public_pool' ) ->get(); $model->channel_quotas = $channelQuotas; } $data = [ 'list' => $models, 'total' => $total, 'page' => $page, 'page_size' => $pageSize, 'total_pages' => ceil($total / $pageSize) ]; return Rs::success($data); } /** * 保存号源模板 * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function Save(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' => 'nullable|integer', 'model_type_id' => 'required|integer', 'department_id' => 'required|integer', 'resource_id' => 'required|integer', 'slot_mode' => 'nullable|integer|in:1,2', 'total_quota' => 'required|integer|min:0', 'weekname' => 'required|string', 'patient_type_mask' => 'required|integer|min:1', 'start_time' => 'required|date_format:H:i:s', 'end_time' => 'required|date_format:H:i:s', 'cutoff_time' => 'nullable|date_format:H:i:s', 'allow_overuse_public_pool' => 'nullable|integer|in:0,1', 'status' => 'required|integer|in:0,1', 'channel_quotas' => 'nullable|array', 'channel_quotas.*.channel_id' => 'nullable|integer', 'channel_quotas.*.total_quota' => 'nullable|integer|min:0', 'channel_quotas.*.allow_overuse_public_pool' => 'nullable|integer|in:0,1', ]); // 如果没有 id 或 id 为空,默认为 0(新建) if (!isset($data['id']) || $data['id'] === null) { $data['id'] = 0; } // 验证时间逻辑 $startTime = strtotime('2000-01-01 ' . $data['start_time']); $endTime = strtotime('2000-01-01 ' . $data['end_time']); if ($endTime <= $startTime) { return Rs::error('结束时间必须晚于开始时间'); } // 如果没有设置截止预约时间,默认使用结束时间 if (empty($data['cutoff_time'])) { $data['cutoff_time'] = $data['end_time']; } // 如果是时间点模式,验证是否能整除 if ($data['slot_mode'] == 2) { $totalMinutes = ($endTime - $startTime) / 60; if ($totalMinutes % $data['total_quota'] !== 0) { return Rs::error('时间点模式下,时间段总时长必须能被号源数量整除'); } } // 验证科室和资源是否匹配 $resource = DB::table('department_resource') ->where('id', $data['resource_id']) ->where('department_id', $data['department_id']) ->first(); if (!$resource) { return Rs::error('资源不属于该科室'); } // 验证模板类型是否属于该科室 $modelType = DB::table('plan_model_type') ->where('id', $data['model_type_id']) ->first(); if (!$modelType) { return Rs::error('模板类型不存在'); } if ($modelType->department_id != $data['department_id']) { return Rs::error('模板类型不属于该科室'); } // 验证时间交集:同一模板分类下,同资源、相同星期的时间段不能有交集 $weeknames = explode(',', $data['weekname']); $newStartTime = strtotime('2000-01-01 ' . $data['start_time']); $newEndTime = strtotime('2000-01-01 ' . $data['end_time']); foreach ($weeknames as $weekname) { // 查询同一模板分类、同资源、同星期下已存在的模板(排除当前编辑的记录) $query = DB::table('plan_model') ->where('model_type_id', $data['model_type_id']) ->where('resource_id', $data['resource_id']) ->where('weekname', $weekname) ->where('deleted', 0); // 如果是编辑操作,排除当前记录 if ($data['id'] > 0) { $query->where('id', '!=', $data['id']); } $existingModels = $query->get(); foreach ($existingModels as $existingModel) { $existingStartTime = strtotime('2000-01-01 ' . $existingModel->start_time); $existingEndTime = strtotime('2000-01-01 ' . $existingModel->end_time); // 判断时间是否有交集:新时间段与已存在时间段重叠 if (!($newEndTime <= $existingStartTime || $newStartTime >= $existingEndTime)) { return Rs::error('该资源在周' . $weekname . '已存在冲突的时间段'); } } } // 权限控制:非管理员只能操作自己科室的模板 if (!$isAdmin) { if (empty($userDeptId)) { return Rs::error('请先绑定科室'); } if ($data['department_id'] != $userDeptId) { return Rs::error('无权限操作其他科室的数据'); } // 如果是更新操作,检查模板是否属于该科室 if ($data['id'] > 0) { $model = DB::table('plan_model') ->where('id', $data['id']) ->first(); if (!$model) { return Rs::error('模板不存在'); } if ($model->department_id != $userDeptId) { return Rs::error('无权限操作该模板'); } } } $conditions = ['id' => $data['id']]; $values = [ 'model_type_id' => $data['model_type_id'], 'department_id' => $data['department_id'], 'resource_id' => $data['resource_id'], 'slot_mode' => $data['slot_mode'] ?? null, 'total_quota' => $data['total_quota'], 'weekname' => $data['weekname'], 'patient_type_mask' => $data['patient_type_mask'], 'start_time' => $data['start_time'], 'end_time' => $data['end_time'], 'cutoff_time' => $data['cutoff_time'], 'status' => $data['status'], ]; $channelQuotas = $data['channel_quotas'] ?? []; // 校验渠道配额之和等于 total_quota if (!empty($channelQuotas)) { $channelTotal = 0; foreach ($channelQuotas as $quota) { if (isset($quota['total_quota'])) { $channelTotal += $quota['total_quota']; } } if ($channelTotal != $data['total_quota']) { return Rs::error('渠道配额之和(' . $channelTotal . ')与号源总数(' . $data['total_quota'] . ')不相等,请检查后重试'); } } try { DB::beginTransaction(); // 解析星期列表 $weeknames = explode(',', $data['weekname']); if ($data['id'] == 0) { // 新增操作:为每个星期创建一条记录 $modelIds = []; foreach ($weeknames as $weekname) { $values['weekname'] = $weekname; $values['created_at'] = now(); $values['updated_at'] = now(); $modelId = DB::table('plan_model')->insertGetId($values); $modelIds[] = $modelId; // 插入渠道配额 if (!empty($channelQuotas)) { foreach ($channelQuotas as $quota) { if (isset($quota['channel_id']) && isset($quota['total_quota']) && $quota['total_quota'] > 0) { DB::table('plan_model_channel_quota')->insert([ 'model_id' => $modelId, 'channel_id' => $quota['channel_id'], 'total_quota' => $quota['total_quota'], 'allow_overuse_public_pool' => $quota['allow_overuse_public_pool'] ?? 0, 'deleted' => 0, 'created_at' => now(), 'updated_at' => now(), ]); } } } } DB::commit(); // 返回第一个modelId return Rs::success(['id' => $modelIds[0] ?? 0, 'model_ids' => $modelIds]); } else { // 更新操作 $values['updated_at'] = now(); $updated = DB::table('plan_model') ->where('id', $data['id']) ->update($values); if ($updated === false) { DB::rollBack(); return Rs::error('操作失败:模板不存在'); } $modelId = $data['id']; // 删除旧的渠道配额 DB::table('plan_model_channel_quota') ->where('model_id', $modelId) ->update(['deleted' => 1]); // 插入新的渠道配额 if (!empty($channelQuotas)) { foreach ($channelQuotas as $quota) { if (isset($quota['channel_id']) && isset($quota['total_quota']) && $quota['total_quota'] > 0) { DB::table('plan_model_channel_quota')->insert([ 'model_id' => $modelId, 'channel_id' => $quota['channel_id'], 'total_quota' => $quota['total_quota'], 'allow_overuse_public_pool' => $quota['allow_overuse_public_pool'] ?? 0, 'deleted' => 0, 'created_at' => now(), 'updated_at' => now(), ]); } } } DB::commit(); return Rs::success(['id' => $modelId]); } } catch (\Exception $e) { DB::rollBack(); return Rs::error('保存失败:' . $e->getMessage()); } } /** * 删除号源模板 * @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([ 'id' => 'required|integer', ]); // 查询模板信息 $model = DB::table('plan_model') ->where('id', $data['id']) ->first(); if (!$model) { return Rs::error('模板不存在'); } // 权限控制:非管理员只能删除自己科室的模板 if (!$isAdmin) { if (empty($userDeptId)) { return Rs::error('请先绑定科室'); } if ($model->department_id != $userDeptId) { return Rs::error('无权限删除该模板'); } } try { DB::beginTransaction(); // 获取要删除的模板信息 $model = DB::table('plan_model') ->where('id', $data['id']) ->first(); if (!$model) { DB::rollBack(); return Rs::error('模板不存在'); } // 软删除模板(只删除这一条记录) DB::table('plan_model') ->where('id', $data['id']) ->update(['deleted' => 1, 'updated_at' => now()]); // 删除对应的渠道配额 DB::table('plan_model_channel_quota') ->where('model_id', $data['id']) ->update(['deleted' => 1]); DB::commit(); return Rs::success(); } catch (\Exception $e) { DB::rollBack(); return Rs::error('删除失败:' . $e->getMessage()); } } /** * 生成号源 * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function generatePlanBatch(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([ 'start_date' => 'required|date', 'end_date' => 'required|date|after_or_equal:start_date', 'models' => 'required|array|min:1', 'models.*.model_id' => 'required|integer', 'models.*.start_time' => 'required|date_format:H:i:s', 'models.*.end_time' => 'required|date_format:H:i:s', 'models.*.weekname' => 'required|string', ]); try { DB::beginTransaction(); $startDate = $data['start_date']; $endDate = $data['end_date']; $models = $data['models']; // ========== 1. 预加载节假日数据 ========== $holidayMap = []; $holidayRecords = DB::table('holiday') ->where('deleted', 0) ->where('status', 1) ->whereRaw('date BETWEEN ? AND ?', [$startDate, $endDate]) ->get(); foreach ($holidayRecords as $h) { $holidayMap[$h->date] = $h->type; } // ========== 2. 预加载所有模板、模板类型、渠道配额 ========== $modelIds = array_column($models, 'model_id'); $modelRecords = DB::table('plan_model') ->whereIn('id', $modelIds) ->where('deleted', 0) ->get() ->keyBy('id'); $modelTypeIds = $modelRecords->pluck('model_type_id')->unique()->toArray(); $modelTypeRecords = DB::table('plan_model_type') ->whereIn('id', $modelTypeIds) ->where('deleted', 0) ->get() ->keyBy('id'); $channelQuotaMap = []; $channelQuotaRecords = DB::table('plan_model_channel_quota') ->whereIn('model_id', $modelIds) ->where('deleted', 0) ->select('id', 'model_id', 'channel_id', 'total_quota', 'allow_overuse_public_pool') ->get(); foreach ($channelQuotaRecords as $cq) { $channelQuotaMap[$cq->model_id][] = $cq; } // ========== 3. 预校验:存在性、权限、状态、时间点整除 ========== foreach ($models as $modelInfo) { $modelId = $modelInfo['model_id']; $model = $modelRecords->get($modelId); if (!$model) { DB::rollBack(); return Rs::error('模板不存在:' . $modelId); } if ($model->status != 1) { DB::rollBack(); return Rs::error('模板 ' . $modelId . ' 已停用,不能生成号源'); } if (!$isAdmin) { if (empty($userDeptId)) { DB::rollBack(); return Rs::error('请先绑定科室'); } if ($model->department_id != $userDeptId) { DB::rollBack(); return Rs::error('无权限操作该模板'); } } if ($model->slot_mode == 2) { $modelStartTime = strtotime('2000-01-01 ' . $model->start_time); $modelEndTime = strtotime('2000-01-01 ' . $model->end_time); $totalMinutes = ($modelEndTime - $modelStartTime) / 60; if ($totalMinutes % $model->total_quota !== 0) { DB::rollBack(); return Rs::error('时间点模式下,时间段总时长必须能被号源数量整除。当前模板时长 ' . $totalMinutes . ' 分钟,号源数量 ' . $model->total_quota . ',无法整除,请修改模板后再试'); } } } // ========== 4. 预加载冲突检测所需的所有已有号源批次 ========== $resourceIds = $modelRecords->pluck('resource_id')->unique()->toArray(); $existingBatches = DB::table('plan_batch') ->whereIn('resource_id', $resourceIds) ->where('plan_date', '>=', $startDate) ->where('plan_date', '<=', $endDate) ->where('deleted', 0) ->select('id', 'resource_id', 'plan_date', 'start_time', 'end_time') ->get(); $existingByResourceAndDate = []; foreach ($existingBatches as $eb) { $key = $eb->resource_id . '_' . $eb->plan_date; $existingByResourceAndDate[$key][] = $eb; } // ========== 5. 冲突检测 ========== $allConflicts = []; foreach ($models as $modelInfo) { $model = $modelRecords->get($modelInfo['model_id']); $modelType = $modelTypeRecords->get($model->model_type_id); $allowedDateRanges = $this->resolveDateRanges($modelType); $holidayMode = $modelType->holiday_mode ?? 0; $currentDate = new \DateTime($startDate); $endDateTime = new \DateTime($endDate); while ($currentDate <= $endDateTime) { $planDate = $currentDate->format('Y-m-d'); $shouldGenerate = $this->shouldGenerateForDate($currentDate, $model, $allowedDateRanges, $holidayMode, $holidayMap); if ($shouldGenerate) { $key = $model->resource_id . '_' . $planDate; $existing = $existingByResourceAndDate[$key] ?? []; $newStartTime = strtotime('2000-01-01 ' . $model->start_time); $newEndTime = strtotime('2000-01-01 ' . $model->end_time); foreach ($existing as $plan) { $existingStartTime = strtotime('2000-01-01 ' . $plan->start_time); $existingEndTime = strtotime('2000-01-01 ' . $plan->end_time); if (!($newEndTime <= $existingStartTime || $newStartTime >= $existingEndTime)) { $allConflicts[] = $planDate . ' ' . $model->start_time . '-' . $model->end_time; } } } $currentDate->modify('+1 day'); } } if (!empty($allConflicts)) { $uniqueConflicts = array_unique($allConflicts); sort($uniqueConflicts); DB::rollBack(); return Rs::error('以下时间段已存在号源,无法生成:' . implode('、', $uniqueConflicts)); } // ========== 6. 生成号源 ========== $totalCount = 0; $skippedDates = []; foreach ($models as $modelInfo) { $model = $modelRecords->get($modelInfo['model_id']); $modelType = $modelTypeRecords->get($model->model_type_id); $allowedDateRanges = $this->resolveDateRanges($modelType); $hasDateLimit = !empty($modelType->date_info); $holidayMode = $modelType->holiday_mode ?? 0; $channelQuotas = collect($channelQuotaMap[$model->id] ?? []); $currentDate = new \DateTime($startDate); $endDateTime = new \DateTime($endDate); while ($currentDate <= $endDateTime) { $planDate = $currentDate->format('Y-m-d'); $shouldGenerate = $this->shouldGenerateForDate($currentDate, $model, $allowedDateRanges, $holidayMode, $holidayMap); if ($shouldGenerate) { $hasChannelQuota = $channelQuotas->where('channel_id', '>', 0)->isNotEmpty(); $batchId = DB::table('plan_batch')->insertGetId([ 'model_id' => $model->id, 'slot_mode' => $model->slot_mode, 'plan_date' => $planDate, 'weekname' => $currentDate->format('N'), 'department_id' => $model->department_id, 'resource_id' => $model->resource_id, 'patient_type_mask' => $model->patient_type_mask, 'total_quota' => $model->total_quota, 'used_quota' => 0, 'start_time' => $model->start_time, 'end_time' => $model->end_time, 'cutoff_time' => $model->cutoff_time, 'status' => $model->status, 'enable_channel_quota' => $hasChannelQuota ? 1 : 0, 'adduser' => $user->id, 'deleted' => 0, 'created_at' => now(), 'updated_at' => now(), ]); if ($model->slot_mode == 1) { DB::table('plan')->insert([ 'batch_id' => $batchId, 'start_time' => $model->start_time, 'end_time' => $model->end_time, 'total_quota' => $model->total_quota, 'used_quota' => 0, 'status' => $model->status, 'created_at' => now(), 'updated_at' => now(), ]); $totalCount++; } else { $modelStartTime = strtotime('2000-01-01 ' . $model->start_time); $modelEndTime = strtotime('2000-01-01 ' . $model->end_time); $totalMinutes = ($modelEndTime - $modelStartTime) / 60; $intervalMinutes = $totalMinutes / $model->total_quota; for ($i = 0; $i < $model->total_quota; $i++) { $pointStartTime = $modelStartTime + ($i * $intervalMinutes * 60); $pointEndTime = $pointStartTime + ($intervalMinutes * 60); DB::table('plan')->insert([ 'batch_id' => $batchId, 'start_time' => date('H:i:s', $pointStartTime), 'end_time' => date('H:i:s', $pointEndTime), 'total_quota' => 1, 'used_quota' => 0, 'status' => $model->status, 'created_at' => now(), 'updated_at' => now(), ]); $totalCount++; } } foreach ($channelQuotas as $quota) { DB::table('plan_batch_channel_quota')->insert([ 'batch_id' => $batchId, 'channel_id' => $quota->channel_id, 'total_quota' => $quota->total_quota, 'used_quota' => 0, 'created_at' => now(), 'updated_at' => now(), ]); } } else { $skipReason = null; $planMonthDay = $currentDate->format('m-d'); if ($hasDateLimit && !empty($allowedDateRanges)) { $dateAllowed = false; foreach ($allowedDateRanges as $range) { if ($planMonthDay >= $range[0] && $planMonthDay <= $range[1]) { $dateAllowed = true; break; } } if (!$dateAllowed) { $skipReason = '不在模板类型允许的日期范围内'; } } if (!$skipReason && !in_array($currentDate->format('N'), explode(',', $model->weekname))) { $skipReason = '星期不匹配'; } if ($skipReason) { $skippedDates[$planDate] = $skipReason; } } $currentDate->modify('+1 day'); } } DB::commit(); $resultData = [ 'count' => $totalCount, 'skipped_dates' => $skippedDates ]; $message = '成功生成 ' . $totalCount . ' 个号源'; if (!empty($skippedDates)) { $skippedCount = count($skippedDates); $message .= ",跳过 {$skippedCount} 个日期(不符合模板类型的日期范围限制)"; } return Rs::success($resultData, $message); } catch (\Exception $e) { DB::rollBack(); return Rs::error('生成号源失败:' . $e->getMessage()); } } private function resolveDateRanges($modelType): array { $allowedDateRanges = []; if ($modelType && !empty($modelType->date_info)) { $dateInfo = json_decode($modelType->date_info, true); if ($modelType->type == 1) { if (is_array($dateInfo) && count($dateInfo) > 0) { if (is_array($dateInfo[0]) && count($dateInfo[0]) == 2) { $allowedDateRanges = $dateInfo; } else { $allowedDateRanges = [$dateInfo]; } } } else { if (is_array($dateInfo) && count($dateInfo) > 0) { foreach ($dateInfo as $date) { $allowedDateRanges[] = [$date, $date]; } } } } return $allowedDateRanges; } private function shouldGenerateForDate(\DateTime $currentDate, $model, array $allowedDateRanges, int $holidayMode, array $holidayMap): bool { $planDate = $currentDate->format('Y-m-d'); $planMonthDay = $currentDate->format('m-d'); $dayOfWeek = $currentDate->format('N'); $dateAllowed = false; if (empty($allowedDateRanges)) { $dateAllowed = true; } else { foreach ($allowedDateRanges as $range) { if ($planMonthDay >= $range[0] && $planMonthDay <= $range[1]) { $dateAllowed = true; break; } } } $isHoliday = isset($holidayMap[$planDate]) && $holidayMap[$planDate] == 1; $isMakeupWork = isset($holidayMap[$planDate]) && $holidayMap[$planDate] == 2; $weeknameMatch = in_array($dayOfWeek, explode(',', $model->weekname)); if ($holidayMode == 1) { return $dateAllowed && $weeknameMatch; } elseif ($holidayMode == 2) { return $dateAllowed && $isHoliday; } else { if ($isHoliday) { return false; } elseif ($isMakeupWork) { return $dateAllowed; } else { return $dateAllowed && $weeknameMatch; } } } }