addOption( 'file', null, InputOption::VALUE_OPTIONAL, '要导入的单个 Excel 文件名(相对 public/excel 的路径,或绝对路径)' ) ->addOption( 'dir', null, InputOption::VALUE_OPTIONAL, 'Excel 目录(相对 public/ 的路径)', 'excel' // 默认就是 public/excel ); } protected function execute(InputInterface $input, OutputInterface $output): int { $fileOption = $input->getOption('file'); $dirOption = $input->getOption('dir') ?? 'excel'; // 基准目录:public/{$dirOption} $baseDir = rtrim(public_path($dirOption), DIRECTORY_SEPARATOR); if ($fileOption) { // 如果传的是绝对路径,直接用;否则认为是相对 baseDir 的文件名 if ($this->isAbsolutePath($fileOption)) { $filePath = $fileOption; } else { $filePath = $baseDir . DIRECTORY_SEPARATOR . $fileOption; } $output->writeln("开始导入单个文件: {$filePath}"); $this->importOneFile($filePath, $output); } else { // 不指定 file 时,默认导入目录下所有 .xlsx if (!is_dir($baseDir)) { throw new BusinessException("目录不存在:{$baseDir}"); } $pattern = $baseDir . DIRECTORY_SEPARATOR . '*.xlsx'; $files = glob($pattern); if (empty($files)) { $output->writeln("目录 {$baseDir} 下没有找到 .xlsx 文件"); return self::SUCCESS; } $output->writeln("开始批量导入目录: {$baseDir}"); foreach ($files as $filePath) { $output->writeln(" -> 文件:{$filePath}"); try { $this->importOneFile($filePath, $output); $output->writeln(" 导入成功"); } catch (\Throwable $e) { // 某个文件失败,不影响后面的文件 $output->writeln(" 导入失败:{$e->getMessage()}"); } } } return self::SUCCESS; } /** * 导入单个 Excel 文件 */ protected function importOneFile(string $filePath, OutputInterface $output): void { if (!is_file($filePath)) { throw new BusinessException("文件不存在:{$filePath}"); } $spreadsheet = IOFactory::load($filePath); $sheet = $spreadsheet->getActiveSheet(); $rows = $sheet->toArray(null, true, true, true); if (empty($rows)) { throw new BusinessException('Excel 内容为空:' . $filePath); } // 取第一行表头 $headerRow = array_shift($rows); // 表头映射器 $mapper = new FbLeadHeaderMapper(); // 如果有未知表头,这里会直接抛异常中断当前文件 $colToField = $mapper->buildColumnFieldMap($headerRow); $insertData = []; $now = time(); foreach ($rows as $row) { $item = []; foreach ($colToField as $colIndex => $field) { $raw = $row[$colIndex] ?? null; switch ($field) { // 1) is_organic: Excel 里是 true/false,这里转成 0/1 case 'is_organic': $item['is_organic'] = $this->normalizeIsOrganic($raw); break; // 2) fb_created_time: 统一转成 Y-m-d H:i:s case 'fb_created_time': $item['fb_created_time'] = $this->normalizeFbCreatedTime($raw); break; default: if (is_string($raw)) { $raw = trim($raw); } $item[$field] = $raw; break; } } // 补充公共字段 $item['source_file'] = basename($filePath); $item['created_at'] = $now; $item['updated_at'] = $now; // 整行都空就跳过 $allNull = true; foreach ($item as $v) { if ($v !== null && $v !== '') { $allNull = false; break; } } if ($allNull) { continue; } $insertData[] = $item; } if ($insertData) { Db::table('fb_leads')->insert($insertData); } else { $output->writeln(" 文件 {$filePath} 没有有效数据行"); } } /** * 将 Excel 里的 is_organic 值转换为 0/1 */ protected function normalizeIsOrganic($value): int { if ($value === null) { return 0; } $v = strtolower(trim((string)$value)); // 认为这些是 “true” $trueValues = ['1', 'true', 'yes', 'y']; return in_array($v, $trueValues, true) ? 1 : 0; } /** * 统一处理 fb_created_time * Excel 目前是类似 '12/8/25',这里尽量转成 'Y-m-d H:i:s' * 解析失败时返回 null */ protected function normalizeFbCreatedTime($value): ?string { if ($value === null || $value === '') { return null; } if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d H:i:s'); } $v = trim((string)$value); $formats = [ 'Y-m-d H:i:s', 'Y-m-d', 'n/j/y', // 12/8/25 'n/j/Y', 'd/m/Y', 'd.m.Y', ]; foreach ($formats as $fmt) { $dt = \DateTime::createFromFormat($fmt, $v); if ($dt !== false) { return $dt->format('Y-m-d H:i:s'); } } // 纯数字当时间戳试一下 if (is_numeric($v) && strlen($v) >= 9 && strlen($v) <= 11) { $ts = (int)$v; if ($ts > 0) { return date('Y-m-d H:i:s', $ts); } } return null; } /** * 判断是否为绝对路径(简单兼容 Windows / Linux) */ protected function isAbsolutePath(string $path): bool { // Windows: X:\ 或 \\server\share if (preg_match('/^[a-zA-Z]:\\\\/', $path) || str_starts_with($path, '\\\\')) { return true; } // *nix: / 开头 return str_starts_with($path, '/'); } } /* php webman tb-db # 等价于 php webman tb-db --dir=excel # 会遍历 public/excel/*.xlsx 逐个导入 # 相对路径(相对 public/excel) php webman tb-db --file="BYD tang_Leads_2025-12-04_2025-12-09.xlsx" # 或给绝对路径 php webman tb-db --file="/www/wwwroot/xxx/public/excel/BYD tang_Leads_2025-12-04_2025-12-09.xlsx" */