whereIn('id', $planModelIds) ->get(); if ($models->isEmpty()) { throw new Exception('未找到有效的排班模板'); } foreach ($models as $model) { if ($model->status != 1 || $model->is_del != 0) { throw new Exception("模板状态异常,请重新选择!异常模板Id: {$model->id}"); } } $start_date = new DateTime($dateRange[0]); $end_date = new DateTime($dateRange[1]); // ========================================== // 1. 整体校验:模板与资源时令配置匹配 // ========================================== $this->validateTemplates($models); // 2. 获取节假日列表 $holiday_list = DB::table('s_holiday') ->whereBetween('date', $dateRange) ->where(['type' => 2]) ->pluck('date') ->toArray(); // ========================================== // 3. 整体校验:资源时段冲突检测(先检查再生成) // ========================================== $conflicts = $this->checkResourceTimeConflict( $models, $start_date, $end_date, $dateType, $holidayEnable, $holiday_list ); if (!empty($conflicts)) { return [ 'success' => false, 'conflicts' => $conflicts, ]; } $generatedDates = []; $skippedDates = []; $success_count = 0; // 4. 开启事务 DB::beginTransaction(); try { $current_date = clone $start_date; while ($current_date <= $end_date) { $current_date_str = $current_date->format('Y-m-d'); // --- 逻辑判断:节假日过滤 --- // 如果是"仅节假日"模式 (date_type == 2),且当天不是节假日 -> 跳过 if ($dateType == 2 && !in_array($current_date_str, $holiday_list)) { foreach ($models as $model) { $skippedDates[] = [ 'date' => $current_date_str, 'template_id' => $model->id, 'reason' => '非节假日,跳过', ]; } $current_date->modify('+1 day'); continue; } // 如果"节假日不可用" (HolidayEnable == 0),且当天是节假日 -> 跳过 if ($holidayEnable == 0 && in_array($current_date_str, $holiday_list)) { foreach ($models as $model) { $skippedDates[] = [ 'date' => $current_date_str, 'template_id' => $model->id, 'reason' => '节假日不可用,跳过', ]; } $current_date->modify('+1 day'); continue; } // --- 获取星期 --- $weekday = (int)$current_date->format('w'); $weekname = $this->getWeekName($weekday); foreach ($models as $model) { // --- 逻辑判断:星期匹配 --- // 如果是"按星期"模式 (date_type == 1) 且模板也是按星期定义的,必须星期一致 if ($dateType == 1 && isset($model->date_type) && $model->date_type == 1) { if ($model->weekname !== $weekname) { continue; } } // 时令校验:跳过不在时令范围内的日期 $resource = $this->getCachedResource($model->resources_id); if (!$this->isDateInSeasonalRange($current_date, $model->type, $resource)) { $skippedDates[] = [ 'date' => $current_date_str, 'template_id' => $model->id, 'reason' => '不在时令范围', ]; continue; } // --- 构造插入数据 --- $data = [ 'roster_id' => $model->id, 'date' => $current_date_str, 'weekname' => $weekname, 'department_id' => $model->department_id, 'resources_id' => $model->resources_id ?? null, 'device_id' => $model->device_id ?? null, 'period_id' => $model->period_id ?? null, 'patient_type' => $model->patient_type ?? null, 'begin_time' => $model->begin_time, 'end_time' => $model->end_time, 'end_reservation_time' => $model->end_reservation_time ?? null, 'time_unit' => $model->time_unit ?? null, 'status' => 1, 'adduser' => $userId, 'is_del' => 0, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; // --- 插入主表 --- $plan_id = DB::table('s_source_roster_detail')->insertGetId($data); if (!$plan_id) { throw new Exception("号源明细插入失败,日期:{$current_date_str}, 模板ID:{$model->id}"); } $generatedDates[] = [ 'date' => $current_date_str, 'template_id' => $model->id, 'detail_id' => $plan_id, ]; $success_count++; // --- 插入关联表:数量配置 --- $this->insertCountInfo($plan_id, $model->id); // --- 插入关联表:设备配置 --- $this->insertDeviceInfo($plan_id, $model->device_id, $model->id); } $current_date->modify('+1 day'); } // 5. 提交事务 DB::commit(); return [ 'success' => true, 'count' => $success_count, 'generated_dates' => $generatedDates, 'skipped_dates' => $skippedDates, ]; } catch (Exception $e) { // 6. 异常回滚 DB::rollBack(); // 记录日志 Log::error('Roster Generation Failed: ' . $e->getMessage(), [ 'dateRange' => $dateRange, 'user_id' => $userId ]); // 重新抛出,让 Controller 处理 throw $e; } } /** * 整体校验:模板与资源时令配置必须严格匹配 * - 模板 type=1/2 → 资源 time_mode=1 * - 模板 type=0 → 资源 time_mode=0 */ private function validateTemplates($models) { foreach ($models as $model) { // 获取资源信息 $resource = $this->getCachedResource($model->resources_id); if (!$resource) { throw new Exception("模板关联的资源不存在,模板ID: {$model->id}"); } // 严格一对一匹配 if ($model->type != 0 && $resource->time_mode != 1) { $errorMsg = "模板ID {$model->id} 配置了时令(type={$model->type}),但资源未开启时令,请检查资源配置!"; Log::error($errorMsg); throw new Exception($errorMsg); } if ($model->type == 0 && $resource->time_mode != 0) { $errorMsg = "模板ID {$model->id} 未配置时令(type=0),但资源开启了时令,请检查资源配置!"; Log::error($errorMsg); throw new Exception($errorMsg); } } } /** * 资源级别冲突检测:遍历日期范围内每一天,应用与主循环一致的过滤逻辑, * 检查每个模板的资源在该日期是否存在时段重叠的号源。 * 边界判断:不重叠(一个时间点结束,另一个可以开始) * 返回冲突列表,无冲突则返回空数组 */ private function checkResourceTimeConflict($models, $startDate, $endDate, $dateType, $holidayEnable, $holidayList) { $conflicts = []; $currentDate = clone $startDate; while ($currentDate <= $endDate) { $dateStr = $currentDate->format('Y-m-d'); // --- 节假日过滤(与主循环一致) --- if ($dateType == 2 && !in_array($dateStr, $holidayList)) { $currentDate->modify('+1 day'); continue; } if ($holidayEnable == 0 && in_array($dateStr, $holidayList)) { $currentDate->modify('+1 day'); continue; } $weekday = (int)$currentDate->format('w'); $weekname = $this->getWeekName($weekday); foreach ($models as $model) { // --- 星期匹配(与主循环一致) --- if ($dateType == 1 && isset($model->date_type) && $model->date_type == 1) { if ($model->weekname !== $weekname) { continue; } } // --- 时令校验(与主循环一致) --- $resource = $this->getCachedResource($model->resources_id); if (!$this->isDateInSeasonalRange($currentDate, $model->type, $resource)) { continue; } // --- 查询该资源在该日期下已有的号源,检查时间段是否重叠 --- $existingPlans = DB::table('s_source_roster_detail') ->where('resources_id', $model->resources_id) ->where('date', $dateStr) ->where('is_del', 0) ->get(); foreach ($existingPlans as $existing) { if ($this->isTimeOverlap( $model->begin_time, $model->end_time, $existing->begin_time, $existing->end_time )) { $conflicts[] = [ 'date' => $dateStr, 'weekname' => $weekname, 'template_id' => $model->id, 'new_time' => $model->begin_time . '-' . $model->end_time, 'existing_time' => $existing->begin_time . '-' . $existing->end_time, 'existing_detail_id' => $existing->id, 'resources_id' => $model->resources_id, ]; } } } $currentDate->modify('+1 day'); } return $conflicts; } /** * 判断两个时段是否重叠 * 不重叠返回 false * 边界判断:不重叠(一个时间点结束,另一个可以开始) * 时段1: [start1, end1) 时段2: [start2, end2) * 重叠条件:时段1的结束时间 > 时段2的开始时间 && 时段2的结束时间 > 时段1的开始时间 */ private function isTimeOverlap($start1, $end1, $start2, $end2) { return ($end1 > $start2) && ($end2 > $start1); } /** * 时令日期匹配:判断日期是否在时令范围内 * - 如果资源 time_mode != 1 或模板 type == 0,不需要时令校验,返回 true * - 否则检查日期是否在对应季节的周期范围内 */ private function isDateInSeasonalRange($currentDate, $modelType, $resource) { // 不需要时令校验 if ($resource->time_mode != 1 || $modelType == 0) { return true; } $currentMMDD = $currentDate->format('m-d'); // 解析 time_range(从缓存中获取) $time_range = json_decode($resource->time_range, true); if (!$time_range || !is_array($time_range)) { return false; } // 遍历所有季节周期 foreach ($time_range as $season) { // 转换类型进行匹配(字符串 "1"/"2" vs 整数 1/2) if ((string)($season['type'] ?? '') === (string)$modelType) { // 检查是否在任意周期范围内 if (isset($season['periods']) && is_array($season['periods'])) { foreach ($season['periods'] as $period) { $start = $period['start'] ?? ''; $end = $period['end'] ?? ''; if (!empty($start) && !empty($end) && $currentMMDD >= $start && $currentMMDD <= $end) { return true; // 匹配成功 } } } } } return false; // 不在时令范围内 } /** * 获取缓存的资源信息,避免重复查询 */ private function getCachedResource($resourceId) { if (!isset($this->resourceCache[$resourceId])) { $this->resourceCache[$resourceId] = DB::table('s_department_resources') ->where('id', $resourceId) ->first(); } return $this->resourceCache[$resourceId]; } private function getWeekName($weekday) { $map = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; return $map[$weekday] ?? ''; } private function insertCountInfo($detailId, $rosterId) { $model_count_info = DB::table('s_source_roster_count')->where(['roster_id' => $rosterId])->get(); if ($model_count_info->isEmpty()) { throw new Exception("模板数量信息异常,请重新选择!异常模板Id: {$rosterId}"); } foreach ($model_count_info as $info) { $success = DB::table('s_source_roster_detail_count')->insert([ 'roster_detail_id' => $detailId, 'appointment_type_id' => $info->appointment_type_id, 'count' => $info->count ?? 0, 'locked_count' => $info->locked_count, 'max_total' => $info->max_total, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); if (!$success) { throw new Exception("渠道数量创建失败"); } } } private function insertDeviceInfo($detailId, $deviceIdStr, $modelId) { // 保持原逻辑:如果模板没配设备,视为异常 if (empty($deviceIdStr)) { throw new Exception("模板未关联设备,请重新选择!异常模板Id: {$modelId}"); } $device_ids = explode(",", $deviceIdStr); foreach ($device_ids as $dv_value) { $dv_value = trim($dv_value); if ($dv_value === '') continue; $success = DB::table('s_source_roster_detail_device')->insert([ 'roster_detail_id' => $detailId, 'device_id' => $dv_value, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); if (!$success) { throw new Exception("设备关联创建失败"); } } } }