Files
fdesk/app/inquiry/service/FbFileImportService.php
T
2025-12-12 18:11:44 +08:00

264 lines
7.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\inquiry\service;
use PhpOffice\PhpSpreadsheet\IOFactory;
use support\Db;
use app\inquiry\utils\FbLeadHeaderMapper;
use support\exception\BusinessException;
use support\Log;
class FbFileImportService
{
/**
* 导入指定目录下所有 .xlsx 文件
*/
public function importDir(string $dir, int $channelId): int
{
if (!is_dir($dir)) {
throw new BusinessException("目录不存在:{$dir}");
}
// 只扫描 .xlsx,之前处理过的 .xlsx.done 不会被匹配到
$files = glob($dir . DIRECTORY_SEPARATOR . '*.xlsx');
if (!$files) {
return 0;
}
$count = 0;
foreach ($files as $filePath) {
if (!is_file($filePath)) {
continue;
}
$doneFlag = $filePath . '.done';
if (is_file($doneFlag)) {
continue;
}
$rows = $this->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':
$item['fb_created_time'] = $this->normalizeCreatedTime($raw); // 你方案B的datetime
break;
// ✅ 这些是数值ID字段:可能出现 ag:/as:/c:/f:/l: 前缀
case 'ad_id':
case 'adset_id':
case 'campaign_id':
case 'form_id':
$item[$field] = $this->normalizeNumericId($raw);
break;
// ✅ lead id 也经常是 l:xxxx
case 'fb_lead_id':
$item['fb_lead_id'] = $this->normalizeNumericId($raw);
break;
// ✅ 电话可能是 p:+233xxxx
case 'phone':
$item['phone'] = $this->normalizePhone($raw);
break;
default:
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);
}
/**
* 把 "ag:120..." / "l:831..." / "120..." 这种值清洗成纯数字(int 或 string数字)
* - 如果你的 DB 字段是 BIGINT UNSIGNED,PHP int 在 64 位环境下是够用的
* - 解析失败返回 0(避免 insert 失败)
*/
protected function normalizeNumericId($raw)
{
if ($raw === null || $raw === '') return 0;
// 数字直接返回
if (is_int($raw)) return $raw;
if (is_float($raw)) return (int)$raw;
$s = trim((string)$raw);
if ($s === '') return 0;
// 取出所有数字拼成一段(ag:120... -> 120..., l:xxx -> xxx)
$digits = preg_replace('/\D+/', '', $s);
if ($digits === '') return 0;
// 这里返回字符串数字也可以,MySQL 会转;返回 int 在 64 位一般没问题
// 若你担心超出 int,可直接 return $digits;
return $digits;
}
/**
* 电话清洗:去掉 "p:" 前缀,保留 + 和数字
*/
protected function normalizePhone($raw): string
{
if ($raw === null) return '';
$s = trim((string)$raw);
if ($s === '') return '';
// 去掉类似 "p:" "tel:" 等前缀
$s = preg_replace('/^[a-zA-Z]+:\s*/', '', $s);
// 保留 + 和数字
$s = preg_replace('/[^\d\+]+/', '', $s);
return $s;
}
/**
* 规范化 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;
}
}