importOneFile($filePath, $channelId); $count++; // 导入成功后,把文件重命名为 .xlsx.done,避免下次重复导入 $newPath = $filePath . '.done'; if (!@rename($filePath, $newPath)) { Log::warning("FbFileImportService: 文件导入成功,但重命名失败:{$filePath} -> {$newPath}"); } else { Log::info("FbFileImportService: 文件导入并标记完成:{$newPath}(行数:{$rows})"); } } return $count; } /** * 导入单个 Excel 到 fb_leads */ public function importOneFile(string $filePath, int $channelId): int { if (!is_file($filePath)) { throw new BusinessException("Excel 文件不存在:{$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(); $res = $mapper->buildColumnFieldMap($headerRow); $colToField = $res['colToField']; $unknownCols = $res['unknownCols']; $insertData = []; $now = time(); $sourceFile = basename($filePath); foreach ($rows as $row) { $item = []; foreach ($colToField as $colIndex => $field) { $raw = $row[$colIndex] ?? null; switch ($field) { case 'is_organic': $item['is_organic'] = $this->normalizeIsOrganic($raw); break; case 'fb_created_time': // ✅ 方案B:写入 DATETIME 字符串 $item['fb_created_time'] = $this->normalizeCreatedTime($raw); break; default: // 小增强:字符串统一 trim(避免 " xxx ") if (is_string($raw)) { $raw = trim($raw); } $item[$field] = $raw; break; } } // 未知列收集到 extra_data(宽松模式) $extra = []; foreach ($unknownCols as $colIndex => $rawHeader) { $val = $row[$colIndex] ?? null; if ($val === null || $val === '') continue; $extra[$rawHeader] = is_string($val) ? trim($val) : $val; } $item['extra_data'] = $extra ? json_encode($extra, JSON_UNESCAPED_UNICODE) : null; $item['source_file'] = $sourceFile; $item['channel'] = $channelId; $item['created_at'] = $now; $item['updated_at'] = $now; $item['sync_status'] = 0; $insertData[] = $item; } if ($insertData) { Db::table('fb_leads')->insert($insertData); } return count($insertData); } /** * 规范化 is_organic 字段为 0/1 */ protected function normalizeIsOrganic($raw): int { if (is_bool($raw)) { return $raw ? 1 : 0; } $str = strtolower(trim((string)$raw)); if ($str === '') { return 0; } return in_array($str, ['1', 'true', 'yes', 'organic'], true) ? 1 : 0; } /** * ✅ 方案B:把 FB 创建时间统一转成 MySQL DATETIME(Y-m-d H:i:s) * * 支持: * - "10/13/25" / "12/8/25" * - "2025-12-08 13:22:11" * - Excel 日期序列(数字) * * 解析失败:返回 null(让 DB 用默认值/允许为空) * 注意:如果你的 fb_created_time 列是 NOT NULL 且无默认值,请把 null 改成 '1970-01-01 00:00:00' */ protected function normalizeCreatedTime($raw): ?string { if ($raw === null || $raw === '') { return null; } // Excel 日期序列:数字 if (is_numeric($raw)) { $n = (float)$raw; // Excel serial date -> unix timestamp $ts = (int)round(($n - 25569) * 86400); if ($ts > 0) { return date('Y-m-d H:i:s', $ts); } return null; } $s = trim((string)$raw); if ($s === '') { return null; } // 1) strtotime 能解析很多格式 $ts = strtotime($s); if ($ts !== false) { return date('Y-m-d H:i:s', $ts); } // 2) 兼容 m/d/yy(如 10/13/25) if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{2})$#', $s, $m)) { $mm = (int)$m[1]; $dd = (int)$m[2]; $yy = (int)$m[3]; $year = 2000 + $yy; // FB 导出通常是 20xx $ts2 = mktime(0, 0, 0, $mm, $dd, $year); return date('Y-m-d H:i:s', $ts2); } // 3) 兜底:无法解析 return null; } }