You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

489 lines
16 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Console\Commands;
use App\Models\Calendar;
use App\Models\Course;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use PhpOffice\PhpSpreadsheet\IOFactory;
class UpdateCalendar extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'update:course-urls {file=课程台账.xlsx}';
/**
* The console command description.
*
* @var string
*/
protected $description = '从Excel文件读取课程信息匹配courses表获取新闻链接并创建calendar记录';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$fileName = $this->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);
}
}