master
cody 4 months ago
parent a26decca9c
commit 626afe5484

@ -1,302 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Course;
use App\Models\Calendar;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class LinkCoursesToCalendar extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'link:courses-to-calendar';
/**
* The console command description.
*
* @var string
*/
protected $description = '将指定的课程列表关联到calendars日历表';
/**
* 课程列表
*
* @var array
*/
protected $courseList = [
'第三期:张平院士— 6G通信与AI融合',
'高研班|第四期高级科创人才研修班-第七模块',
'高研班|第五期高级科创人才研修班-第五模块',
'校友返校日AI+产业融合创新论坛',
'高研班|第六期高级科创人才研修班-第三模块',
'第二课堂|走进珂玛科技',
'人才培训|省科技厅高级技术经理人专班-开学模块',
'初创班|首期技术经理人领航班-第一模块',
'专题培训|苏州市科技企业资本运作公开课',
'初创班|首期高校科技成果转化班-第一模块',
'高研班|第五期高级科创人才研修班-毕业模块',
'高研班|第四期高级科创人才研修班-第八模块',
'第二课堂|走进世华科技',
'初创班|首期高校科技成果转化班-第二模块',
'高研班|第六期高级科创人才研修班-第四模块',
'人才培训|省科技厅高级技术经理人专班-实践模块',
'初创班|首期技术经理人领航班-第二模块',
'攀峰班|首期苏州科技企业资本运作研修班-开学模块',
'初创班|首期技术经理人领航班-结业模块',
'高研班|第七期高级科创人才研修班-开学模块',
'产业加速营|具身智能极客营-开学模块',
'产业加速营|苏州市人工智能潜在独角兽训练营-开学模块',
'初创班|首期高校科技成果转化班-结业模块',
'第二课堂|走进姑苏区',
'高研班|第六期高级科创人才研修班-第五模块',
'产业加速营|苏州市人工智能潜在独角兽训练营-第二模块',
'夏令营2025年度小科学家夏令营',
'攀峰班|首期苏州科技企业资本运作研修班-第二模块',
'产业加速营|苏州市人工智能潜在独角兽训练营-第三模块',
'人才培训|江苏青年科技人才"U35青创学院"培训',
'高研班|第七期高级科创人才研修班-第二模块',
'第二课堂|走进科沃斯',
'高研班|第四期高级科创人才研修班-开学模块',
'高研班|第三期高级科创人才研修班-结业模块',
'第二课堂|走进永鼎',
'第二课堂|走进旭创',
'高研班|第五期高级科创人才研修班-开学模块',
'高研班|第四期高级科创人才研修班-第二模块',
'第二课堂|走进亨通',
'第二课堂|走进企查查',
'科技大讲堂|第一期: 凯文凯利、丁文江院士领衔',
'高研班|第四期高级科创人才研修班-第三模块',
'第二课堂|走进华为苏研所',
'人才培训2024年姑苏领军人才培育营',
'专题培训|苏州市标杆孵化器培训',
'高研班|第五期高级科创人才研修班-第一模块',
'第二课堂|走进亚盛医药',
'第二课堂|新加坡海外游学',
'第二课堂|走进苏州市市场监督管理局',
'第二课堂|走进信达生物',
'人才培训|江苏省高层次人才专题培训',
'夏令营2024年度小科学家夏令营',
'高研班|第五期高级科创人才研修班-第二模块',
'高研班|第四期高级科创人才研修班-第四模块',
'专题培训|太仓市国资审计高质量发展专题培训',
'第二课堂|走进天准科技',
'高研班|第五期高级科创人才研修班-第三模块',
'科技金融沙龙|上海交通大学新能源沙龙',
'科技大讲堂|第二期:伊雷娜·克罗宁解析空间计算',
'人才培训|苏州市科技企业孵化器沙龙',
'高研班|第四期高级科创人才研修班-第五模块',
'人才培训|苏州乡镇党委书记专题研修班',
'科技金融沙龙|资本市场新机遇研讨沙龙',
'产业加速营|人工智能产业加速营',
'高研班|第六期高级科创人才研修班-开学模块',
'第二课堂|走进苏州市低空经济发展展示馆',
'高研班|第四期高级科创人才研修班-第六模块',
'人才培训2024姑苏领军人才创业营',
'产业加速营|集成电路产业专班',
'专题培训关税与出海应对2025美国新政',
'高研班|第五期高级科创人才研修班-第四模块',
'高研班|第六期高级科创人才研修班-第二模块',
];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->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);
}
}

@ -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;
}
/**

@ -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,

Binary file not shown.
Loading…
Cancel
Save