diff --git a/app/Console/Commands/UpdateCalendar.php b/app/Console/Commands/UpdateCalendar.php deleted file mode 100644 index c5d2647..0000000 --- a/app/Console/Commands/UpdateCalendar.php +++ /dev/null @@ -1,488 +0,0 @@ -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); - } -} diff --git a/app/Console/Commands/UpdatePosition.php b/app/Console/Commands/UpdatePosition.php deleted file mode 100755 index 2540d2a..0000000 --- a/app/Console/Commands/UpdatePosition.php +++ /dev/null @@ -1,63 +0,0 @@ -option('field'); - if (empty($field)) return self::FAILURE; - $users = User::get(); - foreach ($users as $user) { - $item = str_replace(',', ',', $user->$field); - $tempArray = explode(',', $item); - $tempArray = array_unique(array_filter($tempArray)); - $user->$field = implode(',', $tempArray); - $user->save(); - $this->info($user->username.'-处理完成'); - } - return self::SUCCESS; - } - - -} diff --git a/app/Models/User.php b/app/Models/User.php index dd23587..fd37771 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -64,6 +64,7 @@ class User extends Authenticatable implements Auditable 'is_import', 'is_vip', 'is_schoolmate', + 'schoolmate_time', 'appointment_total', 'letter', 'code', @@ -107,9 +108,41 @@ class User extends Authenticatable implements Auditable 'is_vip' ]; - public function getNameAttribute($value) + /** + * 数据类型转换,数字转字符串。 + * 值是id则是数据字典的顶级id,或者是枚举型数组 + * @var array[] + */ + public static $intToString = [ + 'company_position' => 1, + 'company_area' => 1, + 'company_industry' => 1, + 'company_type' => 1, + 'is_vip' => ['普通学员', 'VIP学员'], + 'is_schoolmate' => ['否', '是'], + ]; + + /** + * Boot 方法 - 处理模型事件 + */ + protected static function boot() { - return $this->username; + parent::boot(); + + static::saving(function ($user) { + // 当 is_schoolmate 被设置为 1 时,自动设置成为校友时间 + if ($user->isDirty('is_schoolmate')) { + if ($user->is_schoolmate == 1) { + // 如果是从非校友变成校友,设置成为校友时间为当前时间 + if ($user->getOriginal('is_schoolmate') != 1) { + $user->schoolmate_time = now(); + } + } elseif ($user->is_schoolmate == 0 || $user->is_schoolmate === null) { + // 如果设置为 0 或 null,清空成为校友时间 + $user->schoolmate_time = null; + } + } + }); } public function getMobileAttribute($value) @@ -173,19 +206,7 @@ class User extends Authenticatable implements Auditable return $this->is_company_schoolmate == 1 ? '是' : '否'; } - /** - * 数据类型转换,数字转字符串。 - * 值是id则是数据字典的顶级id,或者是枚举型数组 - * @var array[] - */ - public static $intToString = [ - 'company_position' => 1, - 'company_area' => 1, - 'company_industry' => 1, - 'company_type' => 1, - 'is_vip' => ['普通学员', 'VIP学员'], - 'is_schoolmate' => ['否', '是'], - ]; + public function courses() { @@ -227,6 +248,8 @@ class User extends Authenticatable implements Auditable return $this->hasOne(Company::class, 'id', 'company_id'); } + + /** * 获取预约剩余次数 */ diff --git a/database/migrations/2026_01_17_110058_add_schoolmate_date_to_users_table.php b/database/migrations/2026_01_17_110058_add_schoolmate_date_to_users_table.php new file mode 100644 index 0000000..148212c --- /dev/null +++ b/database/migrations/2026_01_17_110058_add_schoolmate_date_to_users_table.php @@ -0,0 +1,31 @@ +dateTime('schoolmate_time')->nullable()->comment('成为校友时间')->after('is_schoolmate'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('schoolmate_time'); + }); + } +};