copyCoursesToFutureDays(7); Log::write('自动排课任务执行完成,插入:' . $result['inserted'] . ',跳过:' . $result['skipped']); return true; } catch (\Exception $e) { Log::write('自动排课任务执行失败:' . $e->getMessage()); throw $e; } } /** * 将自动排课模板复制到未来指定天数,生成可预约的课程时间段 * @param int $days 未来天数,默认7天 * @return array 处理结果 */ public function copyCoursesToFutureDays($days = 7) { // 获取所有auto_schedule=1的课程作为模板(不限制日期) $autoSchedules = CourseSchedule::where('auto_schedule', 1) ->select(); Log::write('找到' . count($autoSchedules) . '个自动排课模板'); $results = [ 'status' => 'success', 'total' => count($autoSchedules), 'inserted' => 0, 'skipped' => 0, 'user_modified' => 0, 'details' => [] ]; if (count($autoSchedules) == 0) { Log::write('没有找到自动排课模板,跳过执行'); return $results; } // 遍历每个自动排课模板,为未来指定天数生成可预约时间段 foreach ($autoSchedules as $schedule) { $courseResults = $this->copyCourseToFutureDays($schedule, $days); $results['inserted'] += $courseResults['inserted']; $results['skipped'] += $courseResults['skipped']; $results['user_modified'] += $courseResults['user_modified']; $results['details'][] = $courseResults; } Log::write('自动排课完成,共插入' . $results['inserted'] . '个课程,跳过' . $results['skipped'] . '个已存在课程,用户干预' . $results['user_modified'] . '个课程'); return $results; } /** * 将单个课程复制到未来指定天数 * @param CourseSchedule $schedule 课程安排 * @param int $days 未来天数 * @return array 处理结果 */ protected function copyCourseToFutureDays(CourseSchedule $schedule, $days) { $result = [ 'course_id' => $schedule->course_id, 'campus_id' => $schedule->campus_id, 'venue_id' => $schedule->venue_id, 'time_slot' => $schedule->time_slot, 'inserted' => 0, 'skipped' => 0, 'user_modified' => 0, 'dates' => [] ]; // 从今天开始,复制到未来指定天数 for ($i = 0; $i <= $days; $i++) { // 计算目标日期 $targetDate = date('Y-m-d', strtotime("+{$i} days")); // 检查该日期是否有相同的课程安排 $checkResult = $this->checkCourseExists($schedule, $targetDate); if (!$checkResult['exists']) { // 如果不存在,则插入新的课程安排 $newSchedule = $this->createNewSchedule($schedule, $targetDate); $result['inserted']++; $result['dates'][] = [ 'date' => $targetDate, 'status' => 'inserted', 'id' => $newSchedule->id ]; } elseif ($checkResult['skip_reason'] === 'user_modified') { // 用户已手动修改此课程,跳过并记录 $result['user_modified']++; $result['dates'][] = [ 'date' => $targetDate, 'status' => 'user_modified', 'reason' => '用户已取消自动排课', 'existing_id' => $checkResult['existing_id'] ]; // 记录用户干预的日志 Log::write("用户干预检测:课程ID {$schedule->course_id} 在 {$targetDate} 已被用户手动修改,跳过自动排课"); } else { // 课程已存在(自动排课生成的) $result['skipped']++; $result['dates'][] = [ 'date' => $targetDate, 'status' => 'auto_exists', 'reason' => '自动排课已存在', 'existing_id' => $checkResult['existing_id'] ]; } } // 记录用户干预统计 if ($result['user_modified'] > 0) { Log::write("课程ID {$schedule->course_id}:用户干预了 {$result['user_modified']} 个时间段,尊重用户选择跳过自动排课"); } return $result; } /** * 处理自动排课数据,按月份检查并插入不存在的课程 * @param int $month 月份(1-12) * @param int $year 年份 * @return array 处理结果 */ public function processAutoSchedule($month, $year) { // 获取所有auto_schedule=1的课程作为模板 $autoSchedules = CourseSchedule::where('auto_schedule', 1)->select(); $results = []; foreach ($autoSchedules as $schedule) { // 获取该月份的所有日期 $daysInMonth = date('t', mktime(0, 0, 0, $month, 1, $year)); for ($day = 1; $day <= $daysInMonth; $day++) { $courseDate = sprintf('%04d-%02d-%02d', $year, $month, $day); // 检查该日期是否有相同的课程安排 $checkResult = $this->checkCourseExists($schedule, $courseDate); if (!$checkResult['exists']) { // 如果不存在,则插入新的课程安排 $newSchedule = $this->createNewSchedule($schedule, $courseDate); $results[] = [ 'status' => 'inserted', 'schedule' => $newSchedule, 'date' => $courseDate ]; } elseif ($checkResult['skip_reason'] === 'user_modified') { // 用户已手动修改此课程 $results[] = [ 'status' => 'user_modified', 'schedule' => $schedule, 'date' => $courseDate, 'reason' => '用户已取消自动排课' ]; } else { // 课程已存在(自动排课生成的) $results[] = [ 'status' => 'auto_exists', 'schedule' => $schedule, 'date' => $courseDate ]; } } } return $results; } /** * 检查指定日期是否有相同的课程安排,或者用户已取消自动排课 * @param CourseSchedule $schedule 原始课程安排 * @param string $courseDate 课程日期 * @return array 检查结果 ['exists' => bool, 'auto_schedule' => bool, 'skip_reason' => string] */ protected function checkCourseExists(CourseSchedule $schedule, $courseDate) { $existing = CourseSchedule::where(function ($query) use ($schedule, $courseDate) { $query->where('campus_id', $schedule->campus_id) ->where('venue_id', $schedule->venue_id) ->where('course_date', $courseDate) ->where('time_slot', $schedule->time_slot) ->where('course_id', $schedule->course_id); })->find(); if (!$existing) { return ['exists' => false, 'auto_schedule' => false, 'skip_reason' => '']; } // 如果课程存在但auto_schedule=0,说明用户已手动修改,跳过自动排课 if ($existing->auto_schedule == 0) { return [ 'exists' => true, 'auto_schedule' => false, 'skip_reason' => 'user_modified', 'existing_id' => $existing->id ]; } // 课程存在且仍是自动排课状态 return [ 'exists' => true, 'auto_schedule' => true, 'skip_reason' => 'auto_exists', 'existing_id' => $existing->id ]; } /** * 创建新的课程安排 * @param CourseSchedule $schedule 原始课程安排 * @param string $courseDate 课程日期 * @return CourseSchedule 新创建的课程安排 */ protected function createNewSchedule(CourseSchedule $schedule, $courseDate) { $newSchedule = new CourseSchedule(); $newSchedule->campus_id = $schedule->campus_id; $newSchedule->venue_id = $schedule->venue_id; $newSchedule->course_date = $courseDate; $newSchedule->time_slot = $schedule->time_slot; $newSchedule->course_id = $schedule->course_id; $newSchedule->auto_schedule = 1; $newSchedule->created_by = 'system'; // 解析时间段并填充start_time和end_time $timeData = $this->parseTimeSlot($schedule->time_slot); $newSchedule->start_time = $timeData ? $timeData['start_time'] : null; $newSchedule->end_time = $timeData ? $timeData['end_time'] : null; // 根据课程日期确定初始状态 $newSchedule->status = $this->determineInitialStatus($courseDate, $timeData); // 复制其他所有字段(除了id和主键相关字段) $attributes = $schedule->toArray(); foreach ($attributes as $key => $value) { // 跳过id、主键相关字段和我们已经设置的字段 if (!in_array($key, ['id', 'course_date', 'auto_schedule', 'start_time', 'end_time', 'status', 'created_by'])) { $newSchedule->$key = $value; } } $newSchedule->save(); return $newSchedule; } /** * 解析time_slot字符串,提取开始和结束时间 * @param string $timeSlot 格式如 "09:00-10:30" * @return array|null */ private function parseTimeSlot($timeSlot) { if (empty($timeSlot)) { return null; } // 支持多种格式:09:00-10:30, 09:00~10:30, 9:00-10:30 if (preg_match('/(\d{1,2}:\d{2})\s*[-~~]\s*(\d{1,2}:\d{2})/', $timeSlot, $matches)) { return [ 'start_time' => $matches[1], 'end_time' => $matches[2] ]; } return null; } /** * 根据课程日期和时间段确定初始状态 * @param string $courseDate 课程日期 * @param array|null $timeData 时间数据 * @return string */ private function determineInitialStatus($courseDate, $timeData) { $currentDate = date('Y-m-d'); $currentTime = date('H:i:s'); // 如果是过去的日期,直接标记为已完成 if ($courseDate < $currentDate) { return 'completed'; } // 如果是未来的日期,标记为待安排 if ($courseDate > $currentDate) { return 'pending'; } // 如果是今天且有时间数据,根据时间判断状态 if ($courseDate === $currentDate && $timeData) { $startTime = $timeData['start_time']; $endTime = $timeData['end_time']; $currentTimestamp = strtotime($currentTime); $startTimestamp = strtotime($startTime); $endTimestamp = strtotime($endTime); // 如果当前时间在课程时间段内,状态为进行中 if ($currentTimestamp >= $startTimestamp && $currentTimestamp <= $endTimestamp) { return 'ongoing'; } // 如果课程已结束,状态为已完成 if ($currentTimestamp > $endTimestamp) { return 'completed'; } // 如果距离开始时间不足6小时,状态为即将开始 $timeDiff = $startTimestamp - $currentTimestamp; if ($timeDiff <= 6 * 3600 && $timeDiff > 0) { return 'upcoming'; } } // 其他情况默认为待安排 return 'pending'; } }