diff --git a/app/Console/Commands/UpdateUserTalentTags.php b/app/Console/Commands/UpdateUserTalentTags.php new file mode 100644 index 0000000..35cd98b --- /dev/null +++ b/app/Console/Commands/UpdateUserTalentTags.php @@ -0,0 +1,398 @@ +info('开始读取Excel文件并更新用户人才标签...'); + + // Excel文件路径 + $excelPath = base_path('科技商学院学员信息-匹配人才标签_匹配是否.xlsx'); + + // 检查文件是否存在 + if (!file_exists($excelPath)) { + $this->error('Excel文件不存在: ' . $excelPath); + return 1; + } + + try { + // 读取Excel文件 + $dataArray = (new FastExcel)->import($excelPath)->toArray(); + + if (empty($dataArray)) { + $this->warn('Excel文件为空或无法读取'); + return 1; + } + + $this->info('成功读取Excel文件,共 ' . count($dataArray) . ' 行数据'); + + $successCount = 0; + $notFoundCount = 0; + $errorCount = 0; + $skippedCount = 0; + $notFoundUsers = []; // 收集所有未匹配上的用户信息 + $skippedData = []; // 收集所有被跳过的数据信息 + + DB::beginTransaction(); + + foreach ($dataArray as $index => $row) { + $rowNum = $index + 2; // Excel行号(从第2行开始,第1行是表头) + + // 获取必要字段 + $courseNameRaw = trim($row['课程名称'] ?? ''); + $userName = trim($row['姓名'] ?? ''); + $talentTag = trim($row['人才标签'] ?? ''); + + // 跳过空行 + if (empty($courseNameRaw) && empty($userName)) { + continue; + } + + // 验证必要字段 + if (empty($courseNameRaw)) { + $this->warn("第 {$rowNum} 行: 课程名称为空,跳过"); + $skippedCount++; + $skippedData[] = [ + 'row' => $rowNum, + 'reason' => '课程名称为空', + 'name' => $userName ?? '', + 'course' => '', + 'talent_tag' => $talentTag ?? '', + ]; + continue; + } + + // 提取所有课程名称(可能包含多个课程,用换行符、顿号、逗号等分隔) + $courseNames = $this->extractAllCourseNames($courseNameRaw); + + // 如果提取到多个课程名称,记录日志 + if (count($courseNames) > 1) { + $this->info("第 {$rowNum} 行: 检测到多个课程名称,将按顺序尝试匹配: " . implode(', ', $courseNames)); + } + + if (empty($userName)) { + $this->warn("第 {$rowNum} 行: 姓名为空,跳过"); + $skippedCount++; + $skippedData[] = [ + 'row' => $rowNum, + 'reason' => '姓名为空', + 'name' => '', + 'course' => $courseNameRaw, + 'talent_tag' => $talentTag ?? '', + ]; + continue; + } + + if (empty($talentTag)) { + $this->warn("第 {$rowNum} 行: 人才标签为空,跳过"); + $skippedCount++; + $skippedData[] = [ + 'row' => $rowNum, + 'reason' => '人才标签为空', + 'name' => $userName, + 'course' => $courseNameRaw, + 'talent_tag' => '', + ]; + continue; + } + + // 按顺序尝试匹配每个课程,直到找到有对应报名用户的课程 + $courseSign = null; + $matchedCourseName = null; + $matchedCourse = null; + + foreach ($courseNames as $courseName) { + // 通过课程名称查找课程 + $course = Course::where('name', $courseName)->first(); + + if (!$course) { + $this->info("第 {$rowNum} 行: 未找到课程 '{$courseName}',尝试下一个..."); + continue; + } + + // 通过课程ID和姓名查找用户(通过CourseSign关联) + $courseSign = CourseSign::where('course_id', $course->id) + ->whereHas('user', function ($query) use ($userName) { + $query->where('name', $userName); + }) + ->with('user') + ->first(); + + if ($courseSign && $courseSign->user) { + // 找到匹配的课程和用户,停止循环 + $matchedCourseName = $courseName; + $matchedCourse = $course; + if (count($courseNames) > 1) { + $this->info("第 {$rowNum} 行: 在课程 '{$matchedCourseName}' 中找到用户 '{$userName}'"); + } + break; + } else { + $this->info("第 {$rowNum} 行: 课程 '{$courseName}' 中未找到用户 '{$userName}',尝试下一个..."); + } + } + + // 如果所有课程都找不到匹配的用户,直接通过姓名在用户表中查找 + if (!$courseSign || !$courseSign->user) { + $this->warn("第 {$rowNum} 行: 在所有课程中都未找到用户 '{$userName}' 的报名记录,尝试直接通过姓名匹配用户表..."); + + // 直接通过姓名在用户表中查找 + $user = User::where('name', $userName)->first(); + + if (!$user) { + $this->warn("第 {$rowNum} 行: 在用户表中也未找到用户 '{$userName}'(原始课程值: '{$courseNameRaw}'),跳过"); + $notFoundCount++; + // 记录未匹配的用户信息 + $notFoundUsers[] = [ + 'row' => $rowNum, + 'name' => $userName, + 'course' => $courseNameRaw, + ]; + continue; + } + + $this->info("第 {$rowNum} 行: 通过姓名在用户表中找到用户 '{$userName}' (ID: {$user->id})"); + } else { + $user = $courseSign->user; + } + + // 保存旧值用于显示 + $oldTalentTags = $user->talent_tags ?? ''; + $oldTalentTagsDisplay = empty($oldTalentTags) ? '(空)' : $oldTalentTags; + + // 处理人才标签,避免重复 + $existingTags = $user->talent_tags; + $existingTagsArray = []; + + if (!empty($existingTags)) { + // 将现有标签转换为数组,并先进行去重处理(防止数据库中已有重复数据) + $existingTagsArray = array_filter(array_map('trim', explode(',', $existingTags))); + // 对现有标签数组进行去重(防止数据库中已存在重复标签) + $existingTagsArray = array_values(array_unique($existingTagsArray)); + } + + // 检查新标签是否已存在(使用严格比较,确保大小写敏感) + if (in_array($talentTag, $existingTagsArray, true)) { + $this->info("第 {$rowNum} 行: 用户 '{$userName}' (ID: {$user->id}) 已存在标签 '{$talentTag}',跳过"); + $this->info(" 当前人才标签: {$oldTalentTagsDisplay}"); + $skippedCount++; + $skippedData[] = [ + 'row' => $rowNum, + 'reason' => '标签已存在', + 'name' => $userName, + 'course' => $courseNameRaw, + 'talent_tag' => $talentTag, + 'user_id' => $user->id, + 'current_tags' => $oldTalentTagsDisplay, + ]; + continue; + } + + // 添加新标签 + $existingTagsArray[] = $talentTag; + // 再次去重并重新索引(双重保险,确保不会有重复) + $existingTagsArray = array_values(array_unique($existingTagsArray)); + $newTalentTags = implode(',', $existingTagsArray); + + // 更新用户 + try { + $user->talent_tags = $newTalentTags; + $user->save(); + + $this->info("第 {$rowNum} 行: 成功更新用户 '{$userName}' (ID: {$user->id}) 的人才标签"); + $this->info(" 从: {$oldTalentTagsDisplay}"); + $this->info(" 到: {$newTalentTags}"); + $successCount++; + } catch (\Exception $e) { + $this->error("第 {$rowNum} 行: 更新用户 '{$userName}' 失败: " . $e->getMessage()); + $errorCount++; + $skippedData[] = [ + 'row' => $rowNum, + 'reason' => '更新失败: ' . $e->getMessage(), + 'name' => $userName, + 'course' => $courseNameRaw, + 'talent_tag' => $talentTag, + 'user_id' => $user->id ?? '', + ]; + } + } + + DB::commit(); + + // 输出统计信息 + $this->info(''); + $this->info('========================================'); + $this->info('更新完成!统计信息:'); + $this->info("成功更新: {$successCount} 条"); + $this->info("未找到记录: {$notFoundCount} 条"); + $this->info("跳过(已存在/空数据): {$skippedCount} 条"); + $this->info("更新失败: {$errorCount} 条"); + $this->info('========================================'); + + // 输出所有未匹配上的用户列表 + if (!empty($notFoundUsers)) { + $this->info(''); + $this->info('========================================'); + $this->info('未匹配上的用户列表:'); + $this->info('========================================'); + foreach ($notFoundUsers as $notFoundUser) { + $this->warn("第 {$notFoundUser['row']} 行: 姓名: {$notFoundUser['name']}, 课程: {$notFoundUser['course']}"); + } + $this->info('========================================'); + $this->info("共 {$notFoundCount} 个用户未匹配上"); + $this->info('========================================'); + } + + // 输出所有被跳过的数据详情 + if (!empty($skippedData)) { + $this->info(''); + $this->info('========================================'); + $this->info('被跳过的数据详情:'); + $this->info('========================================'); + foreach ($skippedData as $skipped) { + $this->warn("第 {$skipped['row']} 行: {$skipped['reason']}"); + $this->warn(" 姓名: " . ($skipped['name'] ?: '(空)')); + $this->warn(" 课程: " . ($skipped['course'] ?: '(空)')); + $this->warn(" 人才标签: " . ($skipped['talent_tag'] ?: '(空)')); + if (isset($skipped['user_id']) && !empty($skipped['user_id'])) { + $this->warn(" 用户ID: {$skipped['user_id']}"); + } + if (isset($skipped['current_tags']) && !empty($skipped['current_tags'])) { + $this->warn(" 当前标签: {$skipped['current_tags']}"); + } + $this->info(''); + } + $this->info('========================================'); + $this->info("共 {$skippedCount} 条数据被跳过"); + $this->info('========================================'); + } + + return 0; + } catch (\Exception $e) { + DB::rollBack(); + $this->error('处理Excel文件时发生错误: ' . $e->getMessage()); + $this->error('错误堆栈: ' . $e->getTraceAsString()); + return 1; + } + } + + /** + * 清理所有用户中重复的人才标签 + */ + protected function cleanDuplicateTags() + { + $this->info('开始清理重复的人才标签...'); + + $users = User::whereNotNull('talent_tags') + ->where('talent_tags', '!=', '') + ->get(); + + $cleanedCount = 0; + $totalCleaned = 0; + + DB::beginTransaction(); + + try { + foreach ($users as $user) { + $originalTags = $user->talent_tags; + + // 将标签转换为数组 + $tagsArray = array_filter(array_map('trim', explode(',', $originalTags))); + + // 去重 + $uniqueTagsArray = array_values(array_unique($tagsArray)); + + // 如果去重后数量减少,说明有重复 + if (count($tagsArray) > count($uniqueTagsArray)) { + $cleanedTags = implode(',', $uniqueTagsArray); + $user->talent_tags = $cleanedTags; + $user->save(); + + $this->info("用户 ID {$user->id} ({$user->name}): 清理重复标签"); + $this->info(" 从: {$originalTags}"); + $this->info(" 到: {$cleanedTags}"); + + $cleanedCount++; + $totalCleaned += (count($tagsArray) - count($uniqueTagsArray)); + } + } + + DB::commit(); + + $this->info(''); + $this->info('========================================'); + $this->info('清理完成!'); + $this->info("清理了 {$cleanedCount} 个用户的重复标签"); + $this->info("共移除 {$totalCleaned} 个重复标签"); + $this->info('========================================'); + $this->info(''); + } catch (\Exception $e) { + DB::rollBack(); + $this->error('清理重复标签时发生错误: ' . $e->getMessage()); + throw $e; + } + } + + /** + * 从可能包含多个课程名称的字符串中提取所有课程名称 + * + * @param string $courseNameRaw 原始课程名称字符串 + * @return array 所有课程名称数组 + */ + protected function extractAllCourseNames($courseNameRaw) + { + $courseNames = []; + + // 先按换行符分割(\n, \r\n, \r) + $parts = preg_split('/[\r\n]+/', $courseNameRaw); + + // 如果只有一个部分,尝试按顿号、逗号等分割 + if (count($parts) === 1 || (count($parts) === 1 && empty(trim($parts[0])))) { + $parts = preg_split('/[、,,]+/', $courseNameRaw); + } + + // 提取所有非空部分 + foreach ($parts as $part) { + $part = trim($part); + // 去除末尾可能的标点符号(顿号、逗号等) + $part = rtrim($part, '、,,'); + if (!empty($part)) { + $courseNames[] = $part; + } + } + + // 如果所有部分都为空,返回原始值作为单个元素 + if (empty($courseNames)) { + $courseNames[] = trim($courseNameRaw); + } + + return $courseNames; + } +} diff --git a/科技商学院学员信息-匹配人才标签_匹配是否.xlsx b/科技商学院学员信息-匹配人才标签_匹配是否.xlsx new file mode 100644 index 0000000..103a04b Binary files /dev/null and b/科技商学院学员信息-匹配人才标签_匹配是否.xlsx differ