|
|
|
|
@ -0,0 +1,398 @@
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
|
|
|
|
|
|
use App\Models\Course;
|
|
|
|
|
use App\Models\CourseSign;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Rap2hpoutre\FastExcel\FastExcel;
|
|
|
|
|
|
|
|
|
|
class UpdateUserTalentTags extends Command
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* The name and signature of the console command.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $signature = 'update:user-talent-tags';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The console command description.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $description = '从Excel文件读取人才标签并更新到用户表';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute the console command.
|
|
|
|
|
*
|
|
|
|
|
* @return mixed
|
|
|
|
|
*/
|
|
|
|
|
public function handle()
|
|
|
|
|
{
|
|
|
|
|
$this->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;
|
|
|
|
|
}
|
|
|
|
|
}
|