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. 整体校验:资源时段冲突检测 // ========================================== $this->checkResourceTimeConflict($models, $start_date, $end_date); // 3. 获取节假日列表 $holiday_list = DB::table('s_holiday') ->whereBetween('date', $dateRange) ->where(['type' => 2]) ->pluck('date') ->toArray(); $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; } // 检查当天该模板是否已有号源明细(存在即跳过) $exists = DB::table('s_source_roster_detail') ->where('roster_id', $model->id) ->where('date', $current_date_str) ->where('is_del', 0) ->exists(); if ($exists) { $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) { $startStr = $startDate->format('Y-m-d'); $endStr = $endDate->format('Y-m-d'); foreach ($models as $model) { // 查询该资源在日期范围内已存在的排班 $existingPlans = DB::table('s_source_roster_detail') ->where('resources_id', $model->resources_id) ->where('date', '>=', $startStr) ->where('date', '<=', $endStr) ->where('is_del', 0) ->get(); foreach ($existingPlans as $existing) { // 检查时间是否重叠(同一天才检查) $existingDate = $existing->date; // 使用模板的星期作为参考日期 $templateDate = $this->getTemplateDate($model); if ($existingDate === $templateDate) { if ($this->isTimeOverlap( $model->begin_time, $model->end_time, $existing->begin_time, $existing->end_time )) { throw new Exception( "资源ID [{$model->resources_id}] 在 {$existingDate} 存在时段冲突!" . "新增: {$model->begin_time}-{$model->end_time} " . "与已存在的 {$existing->begin_time}-{$existing->end_time} 重叠" ); } } } } } /** * 判断两个时段是否重叠 * 不重叠返回 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 getTemplateDate($model) { // 如果模板有固定日期,返回固定日期 if (isset($model->date)) { return $model->date; } // 否则返回 null(表示需要按循环日期比较) return null; } 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, '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("设备关联创建失败"); } } } }