154 lines
4.6 KiB
PHP
154 lines
4.6 KiB
PHP
<?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 文件
|
||
*
|
||
* @param string $dir 绝对目录路径
|
||
* @param int $channelId 渠道ID(写入 fb_leads.channel)
|
||
* @return int 实际导入的文件数量(按文件数计,不是行数)
|
||
* @throws BusinessException
|
||
*/
|
||
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;
|
||
}
|
||
|
||
// 这里可以再做一道保险:如果已经有同名的 .done 文件,可以认为它已经导过,直接跳过
|
||
$doneFlag = $filePath . '.done';
|
||
if (is_file($doneFlag)) {
|
||
// 理论上不会出现(因为 .xlsx 已经被改名了),但防御性写法无害
|
||
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
|
||
*
|
||
* @param string $filePath
|
||
* @param int $channelId 渠道ID
|
||
* @return int 导入的记录条数
|
||
* @throws BusinessException
|
||
*/
|
||
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();
|
||
// 如有未知字段,会在这里抛异常
|
||
$colToField = $mapper->buildColumnFieldMap($headerRow);
|
||
|
||
$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);
|
||
break;
|
||
default:
|
||
$item[$field] = $raw;
|
||
break;
|
||
}
|
||
}
|
||
|
||
$item['source_file'] = $sourceFile;
|
||
$item['channel'] = $channelId; // 👈 渠道ID记录到 fb_leads
|
||
|
||
$item['created_at'] = $now;
|
||
$item['updated_at'] = $now;
|
||
$item['sync_status'] = 0; // 未同步到 inquiry_log
|
||
|
||
$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((string)$raw);
|
||
if ($str === '') {
|
||
return 0;
|
||
}
|
||
return in_array($str, ['1', 'true', 'yes', 'organic'], true) ? 1 : 0;
|
||
}
|
||
|
||
/**
|
||
* FB 创建时间目前按原始字符串存储
|
||
*/
|
||
protected function normalizeCreatedTime($raw)
|
||
{
|
||
return $raw;
|
||
}
|
||
}
|