argument('file'); $filePath = base_path($fileName); if (!file_exists($filePath)) { $this->error("文件不存在: {$filePath}"); return; } $this->info("开始处理文件: {$fileName}"); try { // 读取Excel文件 $spreadsheet = IOFactory::load($filePath); $sheetCount = $spreadsheet->getSheetCount(); $this->info("Excel文件包含 {$sheetCount} 个工作表"); $totalCreated = 0; $failedCourses = []; $failedNews = []; // 处理每个工作表 for ($sheetIndex = 0; $sheetIndex < $sheetCount; $sheetIndex++) { $worksheet = $spreadsheet->getSheet($sheetIndex); $sheetName = $worksheet->getTitle(); $this->info("正在处理工作表: {$sheetName}"); list($created, $sheetFailedCourses, $sheetFailedNews) = $this->processWorksheet($worksheet, $sheetName); $totalCreated += $created; $failedCourses = array_merge($failedCourses, $sheetFailedCourses); $failedNews = array_merge($failedNews, $sheetFailedNews); } $this->info("处理完成,总共创建了 {$totalCreated} 条日历记录"); // 显示匹配失败的课程 if (!empty($failedCourses)) { $this->warn("匹配失败的课程:"); foreach (array_unique($failedCourses) as $failedCourse) { $this->warn(" - {$failedCourse}"); } } // 显示匹配失败的新闻 if (!empty($failedNews)) { $this->warn("匹配失败的新闻:"); foreach (array_unique($failedNews) as $failedNewsItem) { $this->warn(" - {$failedNewsItem}"); } } } catch (\Exception $e) { $this->error("处理Excel文件时发生错误: " . $e->getMessage()); return; } } /** * 处理单个工作表 */ private function processWorksheet($worksheet, $sheetName) { $highestRow = $worksheet->getHighestRow(); $highestColumn = $worksheet->getHighestColumn(); $this->info("工作表 {$sheetName} 有 {$highestRow} 行,最高列为 {$highestColumn}"); // 读取第一行作为表头 $headers = []; $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); for ($col = 1; $col <= $highestColumnIndex; $col++) { $cellValue = $worksheet->getCellByColumnAndRow($col, 1)->getCalculatedValue(); $headers[$col] = trim($cellValue); } // 找到"课程"、"开始时间"、"结束时间"、"跳转链接"列的位置 $courseColumn = null; $startTimeColumn = null; $endTimeColumn = null; $linkColumn = null; foreach ($headers as $colIndex => $header) { if (strpos($header, '课程') !== false) { $courseColumn = $colIndex; } if (strpos($header, '开始时间') !== false) { $startTimeColumn = $colIndex; } if (strpos($header, '结束时间') !== false) { $endTimeColumn = $colIndex; } if (strpos($header, '跳转链接') !== false) { $linkColumn = $colIndex; } } if (!$courseColumn || !$startTimeColumn || !$endTimeColumn || !$linkColumn) { $this->warn("工作表 {$sheetName} 中未找到必要的列(课程、开始时间、结束时间、跳转链接)"); return [0, [], []]; } $this->info("找到课程列: {$courseColumn},开始时间列: {$startTimeColumn},结束时间列: {$endTimeColumn},跳转链接列: {$linkColumn}"); $created = 0; $failedCourses = []; $failedNews = []; // 处理数据行 for ($row = 2; $row <= $highestRow; $row++) { $courseName = trim($worksheet->getCellByColumnAndRow($courseColumn, $row)->getCalculatedValue()); // 获取开始时间和结束时间的原始值,避免格式化问题 $startTimeCell = $worksheet->getCellByColumnAndRow($startTimeColumn, $row); $endTimeCell = $worksheet->getCellByColumnAndRow($endTimeColumn, $row); // 优先使用原始值,如果没有则使用计算值 $startTime = $startTimeCell->getValue(); if ($startTime === null) { $startTime = trim($startTimeCell->getCalculatedValue()); } $endTime = $endTimeCell->getValue(); if ($endTime === null) { $endTime = trim($endTimeCell->getCalculatedValue()); } $jumpLink = trim($worksheet->getCellByColumnAndRow($linkColumn, $row)->getCalculatedValue()); if (empty($courseName) || empty($startTime) || empty($endTime)) { continue; } $this->info("处理行 {$row}: 课程='{$courseName}', 开始时间='{$startTime}' (类型: " . gettype($startTime) . "), 结束时间='{$endTime}' (类型: " . gettype($endTime) . "), 跳转链接='{$jumpLink}'"); // 1. 匹配courses表 $courseId = $this->matchCourse($courseName); if (!$courseId) { $this->warn("✗ 未找到匹配的课程: '{$courseName}'"); $failedCourses[] = $courseName; continue; } // 2. 匹配phome_ecms_news表获取url和title $url = null; $title = null; if (!empty($jumpLink)) { list($title, $url) = $this->getTitleUrlFromNews($jumpLink); if (!$url) { $this->warn("✗ 未找到匹配的新闻标题: '{$jumpLink}'"); $failedNews[] = $jumpLink; } } // 3. 更新courses表的url字段 if ($url && $title) { $this->updateCourseUrl($courseId, $url, $title); } // 4. 创建calendar记录 $calendarCreated = $this->createCalendarRecord($courseId, $courseName, $startTime, $endTime, $url, $title, $courseName); if ($calendarCreated) { $created++; $this->info("✓ 成功创建日历记录: '{$courseName}'"); } else { $this->warn("✗ 创建日历记录失败: '{$courseName}'"); } } return [$created, $failedCourses, $failedNews]; } /** * 匹配courses表 */ private function matchCourse($courseName) { try { // 直接匹配 $course = Course::where('name', $courseName)->first(); if ($course) { $this->info("通过直接匹配找到课程: '{$course->name}' (ID: {$course->id})"); return $course->id; } // 模糊匹配 $course = Course::where('name', 'like', "%{$courseName}%") ->whereNull('deleted_at') ->first(); if ($course) { $this->info("通过模糊匹配找到课程: '{$course->name}' (ID: {$course->id})"); return $course->id; } // 使用相似度匹配 $courses = Course::whereNotNull('name')->where('name', '!=', '')->get(); $bestMatch = null; $highestSimilarity = 0; foreach ($courses as $course) { $similarity = $this->calculateSimilarity($courseName, $course->name); if ($similarity > $highestSimilarity) { $highestSimilarity = $similarity; $bestMatch = $course; } } // 取相似度最高的作为结果,不设置阈值限制 if ($bestMatch && $highestSimilarity > 0.3) { $this->info("通过相似度匹配找到课程 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->name}' (ID: {$bestMatch->id})"); return $bestMatch->id; } } catch (\Exception $e) { $this->error("查询courses表时发生错误: " . $e->getMessage()); } return null; } /** * 从phome_ecms_news表获取titleurl */ private function getTitleUrlFromNews($title) { try { // 直接匹配 $news = DB::table('phome_ecms_news') ->where('title', $title) ->first(); if ($news && !empty($news->titleurl)) { return [$news->title, $news->titleurl]; } // 模糊匹配 $news = DB::table('phome_ecms_news') ->where('title', 'like', "%{$title}%") ->first(); if ($news && !empty($news->titleurl)) { $this->info("通过模糊匹配找到: '{$news->title}' -> '{$news->titleurl}'"); return [$news->title, $news->titleurl]; } // 使用相似度匹配 $allNews = DB::table('phome_ecms_news') ->whereNotNull('title') ->whereNotNull('titleurl') ->where('title', '!=', '') ->where('titleurl', '!=', '') ->get(); $bestMatch = null; $highestSimilarity = 0; foreach ($allNews as $news) { $similarity = $this->calculateSimilarity($title, $news->title); if ($similarity > $highestSimilarity) { $highestSimilarity = $similarity; $bestMatch = $news; } } // 取相似度最高的作为结果,不设置阈值限制 if ($bestMatch && $highestSimilarity > 0.3) { $this->info("通过相似度匹配找到新闻 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->title}' -> '{$bestMatch->titleurl}'"); return [$bestMatch->title, $bestMatch->titleurl]; } } catch (\Exception $e) { $this->error("查询phome_ecms_news表时发生错误: " . $e->getMessage()); } return null; } /** * 更新courses表的url字段 */ private function updateCourseUrl($courseId, $titleUrl, $title) { try { $course = Course::find($courseId); if ($course) { $course->url = $titleUrl; $course->url_title = $title; $course->save(); $this->info("✓ 成功更新课程URL: '{$course->name}' -> '{$titleUrl}'"); return true; } } catch (\Exception $e) { $this->error("更新courses表时发生错误: " . $e->getMessage()); } return false; } /** * 创建calendar记录 */ private function createCalendarRecord($courseId, $courseName, $startTime, $endTime, $url = null, $title = null, $calendarTitle = null) { try { // 转换时间格式 $startDateTime = $this->parseDateTime($startTime); $endDateTime = $this->parseDateTime($endTime); if (!$startDateTime || !$endDateTime) { $this->warn("时间格式解析失败: 开始时间='{$startTime}', 结束时间='{$endTime}'"); return false; } // 检查是否已存在相同的日历记录 $existingCalendar = Calendar::where('course_id', $courseId) ->where('start_time', $startDateTime) ->where('end_time', $endDateTime) ->first(); if ($existingCalendar) { $this->info("日历记录已存在,跳过创建: '{$courseName}'"); return true; } // 创建新的日历记录 $calendar = new Calendar(); $calendar->type = 1; // 课程类型 $calendar->course_id = $courseId; $calendar->title = $calendarTitle ?: $courseName; // 使用Excel中的课程名字作为title $calendar->start_time = $startDateTime; $calendar->end_time = $endDateTime; $calendar->date = $startDateTime->format('Y-m-d'); $calendar->url = $url; $calendar->is_publish = 1; // 默认发布 $calendar->save(); return true; } catch (\Exception $e) { $this->error("创建calendar记录时发生错误: " . $e->getMessage()); } return false; } /** * 解析日期时间格式 */ private function parseDateTime($dateTimeString) { try { // 处理Excel数字格式的日期时间 if (is_numeric($dateTimeString)) { $excelDate = (float)$dateTimeString; // Excel日期从1900年1月1日开始计算天数 // 需要减去2是因为Excel错误地认为1900年是闰年 $unixTimestamp = ($excelDate - 25569) * 86400; $dateTime = new \DateTime(); $dateTime->setTimestamp($unixTimestamp); $this->info("Excel数字日期转换: {$dateTimeString} -> " . $dateTime->format('Y-m-d H:i:s')); return $dateTime; } // 尝试多种日期时间格式 $formats = [ 'Y-m-d H:i:s', 'Y-m-d H:i', 'Y/m/d H:i:s', 'Y/m/d H:i', 'Y-m-d', 'Y/m/d', 'd/m/Y H:i:s', 'd/m/Y H:i', 'd-m-Y H:i:s', 'd-m-Y H:i' ]; foreach ($formats as $format) { $dateTime = \DateTime::createFromFormat($format, $dateTimeString); if ($dateTime !== false) { // 如果只有日期没有时间,设置默认时间 if (strpos($format, 'H:i') === false) { $dateTime->setTime(9, 0, 0); // 默认上午9点 } return $dateTime; } } // 尝试使用strtotime $timestamp = strtotime($dateTimeString); if ($timestamp !== false) { $dateTime = new \DateTime(); $dateTime->setTimestamp($timestamp); return $dateTime; } } catch (\Exception $e) { $this->error("解析日期时间时发生错误: " . $e->getMessage()); } return null; } /** * 计算字符串相似度 */ private function calculateSimilarity($str1, $str2) { // 移除空格并转换为小写 $str1 = strtolower(preg_replace('/\s+/', '', $str1)); $str2 = strtolower(preg_replace('/\s+/', '', $str2)); if ($str1 === $str2) { return 1.0; } if (empty($str1) || empty($str2)) { return 0.0; } // 使用Levenshtein距离计算相似度 $maxLen = max(strlen($str1), strlen($str2)); if ($maxLen == 0) { return 1.0; } $distance = levenshtein($str1, $str2); $similarity = 1 - ($distance / $maxLen); // 如果其中一个字符串包含另一个,提高相似度 if (strpos($str1, $str2) !== false || strpos($str2, $str1) !== false) { $containsSimilarity = min(strlen($str1), strlen($str2)) / $maxLen; $similarity = max($similarity, $containsSimilarity); } return max(0, $similarity); } }