|
|
|
@ -0,0 +1,313 @@
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
|
|
|
|
|
|
use App\Models\Course;
|
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Maatwebsite\Excel\Facades\Excel;
|
|
|
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
|
|
|
|
|
|
|
|
class UpdateCourseUrls 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文件读取课程信息,匹配phome_ecms_news表的titleurl并更新courses表的url字段';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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} 个工作表");
|
|
|
|
|
|
|
|
|
|
$totalUpdated = 0;
|
|
|
|
|
|
|
|
|
|
// 处理每个工作表
|
|
|
|
|
for ($sheetIndex = 0; $sheetIndex < $sheetCount; $sheetIndex++) {
|
|
|
|
|
$worksheet = $spreadsheet->getSheet($sheetIndex);
|
|
|
|
|
$sheetName = $worksheet->getTitle();
|
|
|
|
|
|
|
|
|
|
$this->info("正在处理工作表: {$sheetName}");
|
|
|
|
|
|
|
|
|
|
$updated = $this->processWorksheet($worksheet, $sheetName);
|
|
|
|
|
$totalUpdated += $updated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->info("处理完成,总共更新了 {$totalUpdated} 条记录");
|
|
|
|
|
|
|
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->info("表头: " . implode(', ', $headers));
|
|
|
|
|
|
|
|
|
|
// 找到"课程"和"跳转链接"列的位置
|
|
|
|
|
$courseColumn = null;
|
|
|
|
|
$linkColumn = null;
|
|
|
|
|
|
|
|
|
|
foreach ($headers as $colIndex => $header) {
|
|
|
|
|
if (strpos($header, '课程') !== false) {
|
|
|
|
|
$courseColumn = $colIndex;
|
|
|
|
|
}
|
|
|
|
|
if (strpos($header, '跳转链接') !== false) {
|
|
|
|
|
$linkColumn = $colIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$courseColumn || !$linkColumn) {
|
|
|
|
|
$this->warn("工作表 {$sheetName} 中未找到'课程'或'跳转链接'列");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->info("找到课程列: {$courseColumn},跳转链接列: {$linkColumn}");
|
|
|
|
|
|
|
|
|
|
$updated = 0;
|
|
|
|
|
$failedCourses = [];
|
|
|
|
|
|
|
|
|
|
// 处理数据行
|
|
|
|
|
for ($row = 2; $row <= $highestRow; $row++) {
|
|
|
|
|
$courseName = trim($worksheet->getCellByColumnAndRow($courseColumn, $row)->getCalculatedValue());
|
|
|
|
|
$jumpLink = trim($worksheet->getCellByColumnAndRow($linkColumn, $row)->getCalculatedValue());
|
|
|
|
|
|
|
|
|
|
if (empty($courseName) || empty($jumpLink)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->info("处理行 {$row}: 课程='{$courseName}', 跳转链接='{$jumpLink}'");
|
|
|
|
|
|
|
|
|
|
// 从phome_ecms_news表获取titleurl
|
|
|
|
|
$titleUrl = $this->getTitleUrlFromNews($jumpLink);
|
|
|
|
|
|
|
|
|
|
if ($titleUrl) {
|
|
|
|
|
// 更新courses表
|
|
|
|
|
$updateCount = $this->updateCourseUrl($courseName, $titleUrl);
|
|
|
|
|
$updated += $updateCount;
|
|
|
|
|
|
|
|
|
|
if ($updateCount > 0) {
|
|
|
|
|
$this->info("✓ 成功更新课程 '{$courseName}' 的URL为: {$titleUrl}");
|
|
|
|
|
} else {
|
|
|
|
|
$this->warn("✗ 未找到匹配的课程: '{$courseName}'");
|
|
|
|
|
$failedCourses[] = $courseName;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$this->warn("✗ 未找到匹配的新闻标题: '{$jumpLink}'");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示匹配失败的课程
|
|
|
|
|
if (!empty($failedCourses)) {
|
|
|
|
|
$this->warn("工作表 {$sheetName} 中匹配失败的课程:");
|
|
|
|
|
foreach ($failedCourses as $failedCourse) {
|
|
|
|
|
$this->warn(" - {$failedCourse}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $updated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从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->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->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) {
|
|
|
|
|
$this->info("通过相似度匹配找到 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->title}' -> '{$bestMatch->titleurl}'");
|
|
|
|
|
return $bestMatch->titleurl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
$this->error("查询phome_ecms_news表时发生错误: " . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新courses表的url字段
|
|
|
|
|
*/
|
|
|
|
|
private function updateCourseUrl($courseName, $titleUrl)
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
// 直接匹配
|
|
|
|
|
$updateCount = Course::where('name', $courseName)
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
->update(['url' => $titleUrl]);
|
|
|
|
|
|
|
|
|
|
if ($updateCount > 0) {
|
|
|
|
|
return $updateCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 模糊匹配
|
|
|
|
|
$updateCount = Course::where('name', 'like', "%{$courseName}%")
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
->update(['url' => $titleUrl]);
|
|
|
|
|
|
|
|
|
|
if ($updateCount > 0) {
|
|
|
|
|
$this->info("通过模糊匹配更新了课程");
|
|
|
|
|
return $updateCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用相似度匹配
|
|
|
|
|
$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) {
|
|
|
|
|
$bestMatch->url = $titleUrl;
|
|
|
|
|
$bestMatch->save();
|
|
|
|
|
$this->info("通过相似度匹配更新了课程 (相似度: " . round($highestSimilarity * 100, 2) . "%): '{$bestMatch->name}'");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
$this->error("更新courses表时发生错误: " . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算字符串相似度
|
|
|
|
|
*/
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|