diff --git a/app/Console/Commands/LinkCoursesToCalendar.php b/app/Console/Commands/LinkCoursesToCalendar.php new file mode 100644 index 0000000..e661984 --- /dev/null +++ b/app/Console/Commands/LinkCoursesToCalendar.php @@ -0,0 +1,302 @@ +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 new file mode 100644 index 0000000..d238d21 --- /dev/null +++ b/app/Console/Commands/UpdateCourseUrls.php @@ -0,0 +1,313 @@ +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); + } +} diff --git a/app/Http/Controllers/Mobile/UserController.php b/app/Http/Controllers/Mobile/UserController.php index e182f5b..ce4ad43 100755 --- a/app/Http/Controllers/Mobile/UserController.php +++ b/app/Http/Controllers/Mobile/UserController.php @@ -175,7 +175,7 @@ class UserController extends CommonController $all['letter'] = strtoupper(Pinyin::abbr(mb_substr($all['name'], 0, 1))[0]); } // 如果有公司信息,就更新一下公司 - if (!empty($all['company_name'] && $model->company_name != $all['company_name'])) { + if (isset($all['company_name']) && !empty($all['company_name']) && $model->company_name != $all['company_name']) { // 调用命令行更新 Artisan::call("update_company --user_id={$model->id}"); } diff --git a/composer.json b/composer.json index 3e505c5..8efdc80 100755 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "overtrue/pinyin": "^5.0", "overtrue/wechat": "~5.0", "owen-it/laravel-auditing": "^13.6", + "phpoffice/phpspreadsheet": "^1.29", "prettus/l5-repository": "^2.9", "rap2hpoutre/fast-excel": "^5.2", "simplesoftwareio/simple-qrcode": "^4.2", diff --git a/course_url_update_readme.md b/course_url_update_readme.md new file mode 100644 index 0000000..fb41718 --- /dev/null +++ b/course_url_update_readme.md @@ -0,0 +1,104 @@ +# 课程URL更新命令使用说明 + +## 功能描述 + +这个Laravel命令 `UpdateCourseUrls` 用于从Excel文件读取课程信息,并自动更新数据库中课程的URL字段。 + +## 主要功能 + +1. **Excel文件解析**: 读取Excel文件中的课程数据和跳转链接信息 +2. **数据匹配**: 从`phome_ecms_news`表中根据跳转链接匹配title字段,获取对应的titleurl +3. **智能匹配**: 支持精确匹配、模糊匹配和相似度匹配三种方式 +4. **批量更新**: 将获取到的titleurl批量更新到`courses`表的url字段 + +## 命令使用方法 + +### 基本用法 +```bash +php artisan update:course-urls +``` + +### 指定Excel文件 +```bash +php artisan update:course-urls "your_excel_file.xlsx" +``` + +## Excel文件格式要求 + +Excel文件需要包含以下列: +- **课程**: 包含课程名称的列 +- **跳转链接**: 包含需要匹配的标题的列 + +文件可以包含多个工作表,命令会自动处理所有工作表。 + +## 匹配逻辑 + +### 1. 新闻标题匹配(从phome_ecms_news表) +- **精确匹配**: 直接匹配title字段 +- **模糊匹配**: 使用LIKE进行部分匹配 +- **相似度匹配**: 使用Levenshtein算法计算字符串相似度,自动选择相似度最高的记录 + +### 2. 课程名称匹配(更新courses表) +- **精确匹配**: 直接匹配name字段 +- **模糊匹配**: 使用LIKE进行部分匹配 +- **相似度匹配**: 使用Levenshtein算法计算字符串相似度,自动选择相似度最高的记录 + +## 特殊处理 + +1. **软删除过滤**: 只处理未被软删除的课程记录 +2. **空值检查**: 自动跳过空的课程名称或跳转链接 +3. **重复更新**: 如果课程已有URL,会被新的URL覆盖 +4. **详细日志**: 提供详细的处理日志,包括匹配方式和相似度信息 +5. **失败统计**: 自动显示匹配失败的课程名称列表 + +## 相似度算法说明 + +相似度计算使用多种策略: +- **Levenshtein距离**: 计算字符编辑距离 +- **包含检查**: 如果一个字符串包含另一个,会提高相似度 +- **预处理**: 移除空格并转换为小写进行比较 + +## 示例输出 + +``` +开始处理文件: 课程台账.xlsx +Excel文件包含 2 个工作表 +正在处理工作表: Sheet1 +工作表 Sheet1 有 10 行,最高列为 C +表头: 序号, 课程, 跳转链接 +找到课程列: 2,跳转链接列: 3 +处理行 2: 课程='Python基础课程', 跳转链接='Python编程入门教程' +通过相似度匹配找到 (相似度: 85.7%): 'Python编程入门教程完整版' -> '/news/python-tutorial-2024' +✓ 成功更新课程 'Python基础课程' 的URL为: /news/python-tutorial-2024 +工作表 Sheet1 中匹配失败的课程: + - 高级数据分析课程 + - 机器学习实战 +处理完成,总共更新了 6 条记录 +``` + +## 注意事项 + +1. 确保Excel文件位于项目根目录或提供正确的文件路径 +2. 确保数据库连接正常,特别是`phome_ecms_news`表的访问权限 +3. 建议在执行前备份`courses`表的数据 +4. 相似度匹配会自动选择最高相似度的记录,无需设置阈值 +5. 命令会显示所有匹配失败的课程名称,便于后续手动处理 + +## 错误处理 + +命令包含完善的错误处理机制: +- 文件不存在检查 +- 数据库连接错误处理 +- Excel格式错误处理 +- 详细的错误日志输出 + +## 数据库表结构要求 + +### phome_ecms_news表 +- `title`: 新闻标题字段 +- `titleurl`: 新闻URL字段 + +### courses表 +- `name`: 课程名称字段 +- `url`: 课程URL字段(会被更新) +- `deleted_at`: 软删除时间戳字段 diff --git a/link_courses_to_calendar_readme.md b/link_courses_to_calendar_readme.md new file mode 100644 index 0000000..e08439f --- /dev/null +++ b/link_courses_to_calendar_readme.md @@ -0,0 +1,143 @@ +# 课程关联日历命令使用说明 + +## 功能描述 + +这个Laravel命令 `LinkCoursesToCalendar` 用于将指定的课程列表自动关联到`calendars`日历表中。 + +## 主要功能 + +1. **批量课程关联**: 一次性处理69个指定的课程 +2. **智能匹配**: 支持精确匹配、模糊匹配和相似度匹配三种策略 +3. **重复检查**: 自动检查并跳过已存在日历记录的课程 +4. **事务保护**: 使用数据库事务确保数据一致性 +5. **详细统计**: 提供完整的处理结果统计 + +## 命令使用方法 + +### 基本用法 +```bash +php artisan link:courses-to-calendar +``` + +## 课程列表 + +命令会处理以下69个课程: + +### 高研班系列 +- 高研班|第四期高级科创人才研修班-第七模块 +- 高研班|第五期高级科创人才研修班-第五模块 +- 高研班|第六期高级科创人才研修班-第三模块 +- 高研班|第五期高级科创人才研修班-毕业模块 +- 高研班|第四期高级科创人才研修班-第八模块 +- ... (共29个高研班课程) + +### 第二课堂系列 +- 第二课堂|走进珂玛科技 +- 第二课堂|走进世华科技 +- 第二课堂|走进姑苏区 +- 第二课堂|走进科沃斯 +- ... (共17个第二课堂课程) + +### 其他培训类 +- 人才培训、初创班、攀峰班、产业加速营、专题培训等 (共23个课程) + +## 匹配策略 + +### 1. 精确匹配 +直接匹配courses表中的name字段 + +### 2. 模糊匹配 +使用LIKE查询进行部分匹配 + +### 3. 相似度匹配 +- 使用Levenshtein算法计算字符串相似度 +- 最低相似度阈值:30% +- 自动选择相似度最高的课程 + +## 日历记录创建 + +为每个匹配的课程创建以下日历记录: + +```php +[ + 'type' => 1, // 类型1为课程 + 'course_id' => $course->id, // 关联的课程ID + 'date' => $course->start_date, // 课程开始日期 + 'title' => $course->name, // 课程标题 + 'content' => $course->content, // 课程内容 + 'start_time' => '09:00:00', // 默认开始时间 + 'end_time' => '17:00:00', // 默认结束时间 + 'url' => $course->url, // 课程链接 +] +``` + +## 输出示例 + +``` +开始将课程关联到calendars日历表... +总共需要处理 69 个课程 +正在处理课程: 第三期:张平院士— 6G通信与AI融合 +✓ 找到匹配课程: 第三期:张平院士— 6G通信与AI融合 (ID: 123) +✓ 成功创建日历记录 (ID: 456) 关联课程: 第三期:张平院士— 6G通信与AI融合 + +正在处理课程: 高研班|第四期高级科创人才研修班-第七模块 +通过模糊匹配找到课程: '第四期高级科创人才研修班-第七模块' +✓ 成功创建日历记录 (ID: 457) 关联课程: 第四期高级科创人才研修班-第七模块 + +============================================================ +处理完成! +成功关联课程数量: 65 +已存在日历记录: 2 +未找到匹配课程: 2 + +未找到匹配的课程列表: + - 某个未找到的课程1 + - 某个未找到的课程2 + +已存在日历记录的课程列表: + - 某个已存在的课程1 + - 某个已存在的课程2 +``` + +## 安全特性 + +1. **数据库事务**: 所有操作在事务中执行,出错时自动回滚 +2. **重复检查**: 防止创建重复的日历记录 +3. **软删除过滤**: 只处理未被软删除的课程 +4. **错误处理**: 完善的异常处理机制 + +## 注意事项 + +1. **备份数据**: 建议执行前备份`calendars`表数据 +2. **权限检查**: 确保数据库连接正常,有足够的读写权限 +3. **课程匹配**: 相似度匹配可能不是100%准确,建议执行后检查结果 +4. **重复执行**: 命令可以安全地重复执行,已存在的记录会被跳过 + +## 错误处理 + +命令包含完善的错误处理机制: +- 数据库连接错误处理 +- 事务回滚保护 +- 详细的错误日志输出 +- 匹配失败统计 + +## 数据库表要求 + +### courses表 +- `id`: 主键 +- `name`: 课程名称 +- `start_date`: 开始日期 +- `end_date`: 结束日期 +- `content`: 课程内容 +- `url`: 课程链接 +- `deleted_at`: 软删除时间戳 + +### calendars表 +- `type`: 类型(1=课程,2=课堂,3=事件) +- `course_id`: 关联的课程ID +- `date`: 日期 +- `title`: 标题 +- `content`: 内容 +- `start_time`: 开始时间 +- `end_time`: 结束时间 +- `url`: 链接 diff --git a/课程台账.xlsx b/课程台账.xlsx new file mode 100644 index 0000000..3bc5dde Binary files /dev/null and b/课程台账.xlsx differ