diff --git a/app/Console/Commands/LinkCoursesToCalendar.php b/app/Console/Commands/LinkCoursesToCalendar.php deleted file mode 100644 index e661984..0000000 --- a/app/Console/Commands/LinkCoursesToCalendar.php +++ /dev/null @@ -1,302 +0,0 @@ -info("开始将课程关联到calendars日历表..."); - $this->info("总共需要处理 " . count($this->courseList) . " 个课程"); - - $linkedCount = 0; - $notFoundCourses = []; - $alreadyLinkedCourses = []; - - DB::beginTransaction(); - - try { - foreach ($this->courseList as $courseName) { - $this->info("正在处理课程: {$courseName}"); - - // 查找匹配的课程 - $course = $this->findCourse($courseName); - - if (!$course) { - $this->warn("✗ 未找到匹配的课程: {$courseName}"); - $notFoundCourses[] = $courseName; - continue; - } - - $this->info("✓ 找到匹配课程: {$course->name} (ID: {$course->id})"); - - // 检查是否已经存在日历记录 - $existingCalendar = Calendar::where('course_id', $course->id) - ->where('type', 1) // 类型1为课程 - ->first(); - - if ($existingCalendar) { - $this->warn("⚠ 课程已存在日历记录: {$course->name}"); - $alreadyLinkedCourses[] = $course->name; - continue; - } - - // 创建日历记录 - $calendarData = $this->createCalendarData($course); - $calendar = Calendar::create($calendarData); - - $this->info("✓ 成功创建日历记录 (ID: {$calendar->id}) 关联课程: {$course->name}"); - $linkedCount++; - } - - DB::commit(); - - $this->info("\n" . str_repeat('=', 60)); - $this->info("处理完成!"); - $this->info("成功关联课程数量: {$linkedCount}"); - $this->info("已存在日历记录: " . count($alreadyLinkedCourses)); - $this->info("未找到匹配课程: " . count($notFoundCourses)); - - // 显示未找到的课程 - if (!empty($notFoundCourses)) { - $this->warn("\n未找到匹配的课程列表:"); - foreach ($notFoundCourses as $course) { - $this->warn(" - {$course}"); - } - } - - // 显示已存在日历记录的课程 - if (!empty($alreadyLinkedCourses)) { - $this->warn("\n已存在日历记录的课程列表:"); - foreach ($alreadyLinkedCourses as $course) { - $this->warn(" - {$course}"); - } - } - - } catch (\Exception $e) { - DB::rollback(); - $this->error("处理过程中发生错误: " . $e->getMessage()); - $this->error("已回滚所有更改"); - return; - } - - $this->info("\n所有操作已完成!"); - } - - /** - * 查找匹配的课程 - */ - private function findCourse($courseName) - { - // 1. 精确匹配 - $course = Course::where('name', $courseName) - ->whereNull('deleted_at') - ->first(); - - if ($course) { - return $course; - } - - // 2. 模糊匹配 - $course = Course::where('name', 'like', "%{$courseName}%") - ->whereNull('deleted_at') - ->first(); - - if ($course) { - $this->info("通过模糊匹配找到课程: '{$course->name}'"); - return $course; - } - - // 3. 相似度匹配 - $courses = Course::whereNull('deleted_at') - ->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}'"); - return $bestMatch; - } - - return null; - } - - /** - * 创建日历数据 - */ - private function createCalendarData($course) - { - return [ - 'type' => 1, // 类型1为课程 - 'course_id' => $course->id, - 'date' => $course->start_date ?? now()->format('Y-m-d'), - 'title' => $course->name, - 'content' => $course->content ?? '', - 'start_time' => $course->start_date ? $course->start_date . ' 09:00:00' : null, - 'end_time' => $course->end_date ? $course->end_date . ' 17:00:00' : null, - 'url' => $course->url ?? '', - 'created_at' => now(), - 'updated_at' => now(), - ]; - } - - /** - * 计算字符串相似度 - */ - 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); - } -} diff --git a/app/Console/Commands/UpdateCourseUrls.php b/app/Console/Commands/UpdateCourseUrls.php index 1c276a7..80363c8 100644 --- a/app/Console/Commands/UpdateCourseUrls.php +++ b/app/Console/Commands/UpdateCourseUrls.php @@ -2,6 +2,7 @@ namespace App\Console\Commands; +use App\Models\Calendar; use App\Models\Course; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; @@ -22,7 +23,7 @@ class UpdateCourseUrls extends Command * * @var string */ - protected $description = '从Excel文件读取课程信息,匹配phome_ecms_news表的titleurl并更新courses表的url字段'; + protected $description = '从Excel文件读取课程信息,匹配courses表,获取新闻链接,并创建calendar记录'; /** * Create a new command instance. @@ -58,7 +59,9 @@ class UpdateCourseUrls extends Command $this->info("Excel文件包含 {$sheetCount} 个工作表"); - $totalUpdated = 0; + $totalCreated = 0; + $failedCourses = []; + $failedNews = []; // 处理每个工作表 for ($sheetIndex = 0; $sheetIndex < $sheetCount; $sheetIndex++) { @@ -67,11 +70,29 @@ class UpdateCourseUrls extends Command $this->info("正在处理工作表: {$sheetName}"); - $updated = $this->processWorksheet($worksheet, $sheetName); - $totalUpdated += $updated; + list($created, $sheetFailedCourses, $sheetFailedNews) = $this->processWorksheet($worksheet, $sheetName); + $totalCreated += $created; + $failedCourses = array_merge($failedCourses, $sheetFailedCourses); + $failedNews = array_merge($failedNews, $sheetFailedNews); } - $this->info("处理完成,总共更新了 {$totalUpdated} 条记录"); + $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()); @@ -98,70 +119,150 @@ class UpdateCourseUrls extends Command $headers[$col] = trim($cellValue); } - $this->info("表头: " . implode(', ', $headers)); - - // 找到"课程"和"跳转链接"列的位置 + // 找到"课程"、"开始时间"、"结束时间"、"跳转链接"列的位置 $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 || !$linkColumn) { - $this->warn("工作表 {$sheetName} 中未找到'课程'或'跳转链接'列"); - return 0; + if (!$courseColumn || !$startTimeColumn || !$endTimeColumn || !$linkColumn) { + $this->warn("工作表 {$sheetName} 中未找到必要的列(课程、开始时间、结束时间、跳转链接)"); + return [0, [], []]; } - $this->info("找到课程列: {$courseColumn},跳转链接列: {$linkColumn}"); - - $updated = 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($jumpLink)) { + if (empty($courseName) || empty($startTime) || empty($endTime)) { continue; } - $this->info("处理行 {$row}: 课程='{$courseName}', 跳转链接='{$jumpLink}'"); - - // 从phome_ecms_news表获取titleurl - list($title, $titleUrl) = $this->getTitleUrlFromNews($jumpLink); + $this->info("处理行 {$row}: 课程='{$courseName}', 开始时间='{$startTime}' (类型: " . gettype($startTime) . "), 结束时间='{$endTime}' (类型: " . gettype($endTime) . "), 跳转链接='{$jumpLink}'"); - if ($titleUrl) { - // 更新courses表 - $updateCount = $this->updateCourseUrl($courseName, $titleUrl, $title); - $updated += $updateCount; + // 1. 匹配courses表 + $courseId = $this->matchCourse($courseName); + if (!$courseId) { + $this->warn("✗ 未找到匹配的课程: '{$courseName}'"); + $failedCourses[] = $courseName; + continue; + } - if ($updateCount > 0) { - $this->info("✓ 成功更新课程 '{$courseName}' 的URL为: {$titleUrl}"); - } else { - $this->warn("✗ 未找到匹配的课程: '{$courseName}'"); - $failedCourses[] = $courseName; + // 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("✗ 未找到匹配的新闻标题: '{$jumpLink}'"); + $this->warn("✗ 创建日历记录失败: '{$courseName}'"); } } - // 显示匹配失败的课程 - if (!empty($failedCourses)) { - $this->warn("工作表 {$sheetName} 中匹配失败的课程:"); - foreach ($failedCourses as $failedCourse) { - $this->warn(" - {$failedCourse}"); + 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 $updated; + return null; } /** @@ -208,8 +309,9 @@ class UpdateCourseUrls extends Command } } - if ($bestMatch && $highestSimilarity > 0) { - $this->info("通过相似度匹配找到 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->title}' -> '{$bestMatch->titleurl}'"); + // 取相似度最高的作为结果,不设置阈值限制 + if ($bestMatch && $highestSimilarity > 0.3) { + $this->info("通过相似度匹配找到新闻 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->title}' -> '{$bestMatch->titleurl}'"); return [$bestMatch->title, $bestMatch->titleurl]; } @@ -223,58 +325,130 @@ class UpdateCourseUrls extends Command /** * 更新courses表的url字段 */ - private function updateCourseUrl($courseName, $titleUrl, $title) + private function updateCourseUrl($courseId, $titleUrl, $title) { try { - // 直接匹配 - $updateCount = Course::where('name', $courseName) - ->whereNull('deleted_at') - ->update(['url' => $titleUrl, 'url_title' => $title]); + $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 ($updateCount > 0) { - return $updateCount; + if (!$startDateTime || !$endDateTime) { + $this->warn("时间格式解析失败: 开始时间='{$startTime}', 结束时间='{$endTime}'"); + return false; } - // 模糊匹配 - $updateCount = Course::where('name', 'like', "%{$courseName}%") - ->whereNull('deleted_at') - ->update(['url' => $titleUrl, 'url_title' => $title]); + // 检查是否已存在相同的日历记录 + $existingCalendar = Calendar::where('course_id', $courseId) + ->where('start_time', $startDateTime) + ->where('end_time', $endDateTime) + ->first(); - if ($updateCount > 0) { - $this->info("通过模糊匹配更新了课程"); - return $updateCount; + if ($existingCalendar) { + $this->info("日历记录已存在,跳过创建: '{$courseName}'"); + return true; } - // 使用相似度匹配 - $courses = Course::whereNull('deleted_at') - ->whereNotNull('name') - ->where('name', '!=', '') - ->get(); + // 创建新的日历记录 + $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(); - $bestMatch = null; - $highestSimilarity = 0; + return true; - foreach ($courses as $course) { - $similarity = $this->calculateSimilarity($courseName, $course->name); - if ($similarity > $highestSimilarity) { - $highestSimilarity = $similarity; - $bestMatch = $course; + } 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; } } - if ($bestMatch && $highestSimilarity > 0) { - $bestMatch->url = $titleUrl; - $bestMatch->url_title = $title; - $bestMatch->save(); - $this->info("通过相似度匹配更新了课程 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->name}'"); - return 1; + // 尝试使用strtotime + $timestamp = strtotime($dateTimeString); + if ($timestamp !== false) { + $dateTime = new \DateTime(); + $dateTime->setTimestamp($timestamp); + return $dateTime; } } catch (\Exception $e) { - $this->error("更新courses表时发生错误: " . $e->getMessage()); + $this->error("解析日期时间时发生错误: " . $e->getMessage()); } - return 0; + return null; } /** diff --git a/app/Http/Controllers/Admin/OtherController.php b/app/Http/Controllers/Admin/OtherController.php index 035a7fd..f133af0 100755 --- a/app/Http/Controllers/Admin/OtherController.php +++ b/app/Http/Controllers/Admin/OtherController.php @@ -163,7 +163,7 @@ class OtherController extends CommonController // 培养人数 $courseTypeSignsPass = $courseSignByType->count(); // 去重培养人数 - $courseTypeSignsPassUnique = User::whereIn('id', $courseSignByType->pluck('user_id'))->groupBy('mobile')->count(); + $courseTypeSignsPassUnique = User::whereIn('id', $courseSignByType->pluck('user_id'))->distinct('mobile')->count(); foreach ($courses2 as $course) { $courseTypesSum[] = [ 'course_type' => $courseType->name, diff --git a/课程台账.xlsx b/课程台账.xlsx index 3bc5dde..4a2659a 100644 Binary files a/课程台账.xlsx and b/课程台账.xlsx differ