Files
fdesk/app/inquiry/service/FbFileImportService.php
T
mzl92 ebcabb23df Merge branch 'master' into develop
# Conflicts:
#	app/inquiry/service/FbFileImportService.php
2025-12-22 16:23:05 +08:00

282 lines
8.7 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:写 DATETIME,且永不为 null(因为 fb_created_time NOT NULL)
* 兼容:
* - Excel 数字日期
* - 10/13/25
* - 2025-12-10 04:25:09
* - 25年12月21日 / 2025年12月21日
* - 25年12月21日 13:05 / 25年12月21日13:05:09
* 为空/解析失败:用当前时间兜底
*/
protected function normalizeCreatedTime($raw): string
{
$fallback = date('Y-m-d H:i:s');
if ($raw === null) return $fallback;
// Excel 日期序列:数字
if (is_numeric($raw)) {
$n = (float)$raw;
$ts = (int)round(($n - 25569) * 86400);
if ($ts > 0) return date('Y-m-d H:i:s', $ts);
return $fallback;
}
$s = trim((string)$raw);
if ($s === '') return $fallback;
// 先尝试 strtotime(能覆盖很多格式:2025-12-10 04:25:09 / 2025/12/10 等)
$ts = strtotime($s);
if ($ts !== false) {
return date('Y-m-d H:i:s', $ts);
}
// 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;
$ts2 = mktime(0, 0, 0, $mm, $dd, $year);
return date('Y-m-d H:i:s', $ts2);
}
// ✅ 中文日期:25年12月21日 / 2025年12月21日(可带时间)
// 支持:
// - 25年12月21日
// - 2025年12月21日
// - 25年12月21日 13:05
// - 25年12月21日13:05:09
$cn = preg_replace('/\s+/', '', $s); // 去空格方便匹配
if (preg_match('/^(\d{2,4})年(\d{1,2})月(\d{1,2})日(?:(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/u', $cn, $m)) {
$y = (int)$m[1];
$month = (int)$m[2];
$day = (int)$m[3];
// 2位年 -> 2000+(你这里是 25年 = 2025)
if ($y < 100) $y += 2000;
$hh = isset($m[4]) && $m[4] !== '' ? (int)$m[4] : 0;
$ii = isset($m[5]) && $m[5] !== '' ? (int)$m[5] : 0;
$ss = isset($m[6]) && $m[6] !== '' ? (int)$m[6] : 0;
$ts3 = mktime($hh, $ii, $ss, $month, $day, $y);
return date('Y-m-d H:i:s', $ts3);
}
// 解析失败兜底:当前时间
return $fallback;
}
}