diff --git a/app/Console/Commands/AutoSchoolmate.php b/app/Console/Commands/AutoSchoolmate.php index 2015621..c08343a 100755 --- a/app/Console/Commands/AutoSchoolmate.php +++ b/app/Console/Commands/AutoSchoolmate.php @@ -41,38 +41,67 @@ class AutoSchoolmate extends Command */ public function handle() { - // 获取所有已开始且需要自动加入校友库的课程 + // 获取所有已开始且需要自动加入校友库的课程(只处理存在 start_date 的课程) $today = date('Y-m-d'); $courses = Course::where('auto_schoolmate', 1) - ->where(function ($query) use ($today) { - // 方式1: start_date 已填写且 <= 今天 - $query->where(function ($q) use ($today) { - $q->whereNotNull('start_date') - ->where('start_date', '<=', $today); - }) - // 方式2: 或者课程状态为进行中(即使 start_date 未及时填写) - ->orWhere('course_status', 10); - }) + ->whereNotNull('start_date') + ->where('start_date', '<=', $today) ->get(); + if ($courses->isEmpty()) { + return $this->info('没有需要处理的课程'); + } + + $this->info("找到 {$courses->count()} 个需要处理的课程"); + $totalUpdated = 0; + $totalSkipped = 0; + $processedUserIds = []; // 记录已处理的用户ID,避免重复处理 + foreach ($courses as $course) { - // 获取报名通过的学员 - $courseSigns = CourseSign::where('course_id', $course->id)->where('status', 1)->get(); + // 获取报名通过的学员,并排除已经是校友的用户 + $courseSigns = CourseSign::where('course_id', $course->id) + ->where('status', 1) + ->whereHas('user', function ($query) { + // 排除已经是校友的用户(is_schoolmate = 1) + $query->whereRaw('COALESCE(is_schoolmate, 0) != 1'); + })->get(); + if ($courseSigns->isEmpty()) { continue; } - // 只更新还不是校友的学员;从 非校友→校友 时顺带写入 schoolmate_time - $userIds = $courseSigns->pluck('user_id')->unique()->values(); - $updated = User::whereIn('id', $userIds) - ->whereRaw('COALESCE(is_schoolmate, 0) != 1') - ->update(['is_schoolmate' => 1, 'schoolmate_time' => now()]); + // 获取需要更新的用户ID,并排除已经在本脚本中处理过的用户 + $userIds = $courseSigns->pluck('user_id') + ->unique() + ->filter(function ($userId) use (&$processedUserIds) { + // 如果已经处理过,跳过 + if (in_array($userId, $processedUserIds)) { + return false; + } + $processedUserIds[] = $userId; + return true; + }) + ->values() + ->toArray(); + + if (empty($userIds)) { + continue; + } + + // 批量更新:只更新还不是校友的学员;使用课程开课时间作为 schoolmate_time + $updated = User::batchUpdateToSchoolmate($userIds, $course->start_date); $totalUpdated += $updated; + $skipped = count($userIds) - $updated; + $totalSkipped += $skipped; + + if ($updated > 0) { + $this->info("课程【{$course->name}】: 更新 {$updated} 位学员,跳过 {$skipped} 位已处理学员"); + } } - return $this->info("更新完成,共处理 {$totalUpdated} 位学员"); + return $this->info("更新完成,共处理 {$totalUpdated} 位学员,跳过 {$totalSkipped} 位已处理学员"); } } 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/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php index 2cfed43..764e7df 100644 --- a/app/Http/Controllers/Admin/CompanyController.php +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -223,6 +223,20 @@ class CompanyController extends BaseController } continue; } + if ($key == 'ranking_tag') { + $valueArray = explode(',', $value); + if (!empty($valueArray)) { + $query->where(function ($q) use ($valueArray) { + foreach ($valueArray as $item) { + $item = trim($item); + if (!empty($item)) { + $q->orWhere('ranking_tag', 'like', '%' . $item . '%'); + } + } + }); + } + continue; + } // 等于 if ($op == 'eq') { diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 1efdf1e..12f5dc6 100755 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -263,6 +263,25 @@ class UserController extends BaseController }); } + // 榜单标签查询 + if (isset($all['ranking_tag'])) { + $list = $list->whereHas('company', function ($query) use ($all) { + $string = explode(',', $all['ranking_tag']); + $query->where(function ($q) use ($string) { + foreach ($string as $index => $v) { + $trimmed = trim($v); + if (!empty($trimmed)) { + if ($index === 0) { + $q->where('ranking_tag', 'like', '%' . $trimmed . '%'); + } else { + $q->orWhere('ranking_tag', 'like', '%' . $trimmed . '%'); + } + } + } + }); + }); + } + $list = $list->whereHas('courseSigns', function ($query) use ($all) { if (isset($all['course_id'])) { $query->where('course_id', $all['course_id']); diff --git a/app/Models/User.php b/app/Models/User.php index 5c6d362..f0266a4 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -243,4 +243,28 @@ class User extends Authenticatable implements Auditable return $user->appointment_total - $useTotal >= 0 ? $user->appointment_total - $useTotal : 0; } + /** + * 批量将用户更新为校友 + * @param array $userIds 用户ID数组 + * @param string $courseStartDate 课程开课时间(用于设置 schoolmate_time) + * @return int 更新的用户数量 + */ + public static function batchUpdateToSchoolmate(array $userIds, string $courseStartDate) + { + if (empty($userIds)) { + return 0; + } + + // 将课程开课时间转换为 datetime 格式 + $schoolmateTime = Carbon::parse($courseStartDate)->format('Y-m-d H:i:s'); + + // 批量更新:只更新还不是校友的学员;从 非校友→校友 时使用课程开课时间作为 schoolmate_time + return self::whereIn('id', $userIds) + ->whereRaw('COALESCE(is_schoolmate, 0) != 1') + ->update([ + 'is_schoolmate' => 1, + 'schoolmate_time' => $schoolmateTime + ]); + } + } diff --git a/科技商学院学员信息-匹配人才标签_匹配是否.xlsx b/科技商学院学员信息-匹配人才标签_匹配是否.xlsx new file mode 100644 index 0000000..103a04b Binary files /dev/null and b/科技商学院学员信息-匹配人才标签_匹配是否.xlsx differ