134 lines
4.5 KiB
PHP
134 lines
4.5 KiB
PHP
|
|
<?php
|
|||
|
|
namespace app\common\utils\excel;
|
|||
|
|
|
|||
|
|
use support\Db;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* ExcelImportTrait
|
|||
|
|
*
|
|||
|
|
* 把“读取 Sheet → 映射字段 → 规范化数据 → 打时间戳 → 批量入库”
|
|||
|
|
* 这条通用导入链路抽成 Trait,便于在多个 Service 里复用。
|
|||
|
|
*
|
|||
|
|
* 依赖:
|
|||
|
|
* - ExcelReader:提供 readSheet / readSheetFromUrl / remap / toDecimal / toBool / toDate
|
|||
|
|
* - 你的 OssService:通过 generatePrivateUrl($objectKey) 生成 OSS 私有签名 URL
|
|||
|
|
*/
|
|||
|
|
trait ExcelImportTrait
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 读取某个 Sheet 的“行数组”
|
|||
|
|
* - $file 可为本地路径或 URL(由 $isUrl 控制)
|
|||
|
|
* - headerRow:表头所在行(Excel 行号,从 1 开始)
|
|||
|
|
* - startRow:数据起始行(不传则为 headerRow+1)
|
|||
|
|
*/
|
|||
|
|
protected function readSheetRows(string $file, string $sheet, bool $isUrl, int $headerRow = 1, ?int $startRow = null): array
|
|||
|
|
{
|
|||
|
|
$ret = $isUrl
|
|||
|
|
? ExcelReader::readSheetFromUrl($file, $sheet, $headerRow, $startRow)
|
|||
|
|
: ExcelReader::readSheet($file, $sheet, $headerRow, $startRow);
|
|||
|
|
return $ret['rows'] ?? [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取某个 Sheet(传入 OSS 对象键),内部会用你的 OssService 生成签名 URL 再读取
|
|||
|
|
*/
|
|||
|
|
protected function readSheetRowsFromOssKey(string $objectKey, string $sheet, int $headerRow = 1, ?int $startRow = null): array
|
|||
|
|
{
|
|||
|
|
$signedUrl = $this->ossSignedUrl($objectKey);
|
|||
|
|
$ret = ExcelReader::readSheetFromUrl($signedUrl, $sheet, $headerRow, $startRow);
|
|||
|
|
return $ret['rows'] ?? [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 使用你的 OssService 生成私有签名 URL
|
|||
|
|
* - 兼容你的现有写法:(new \app\common\service\OssService)->generatePrivateUrl($objectKey)
|
|||
|
|
*/
|
|||
|
|
protected function ossSignedUrl(string $objectKey): string
|
|||
|
|
{
|
|||
|
|
return (new \app\common\service\OssService())->generatePrivateUrl($objectKey);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将一行做字段映射 + 基础类型规范化(decimal/bool/date)
|
|||
|
|
* - $headerMap:表头映射(源列名 => 目标字段名)
|
|||
|
|
* - $decimalFields/$boolFields/$dateFields:需要做类型规范化的目标字段名列表
|
|||
|
|
*/
|
|||
|
|
protected function mapAndNormalizeRow(
|
|||
|
|
array $row,
|
|||
|
|
array $headerMap,
|
|||
|
|
array $decimalFields = [],
|
|||
|
|
array $boolFields = [],
|
|||
|
|
array $dateFields = []
|
|||
|
|
): array {
|
|||
|
|
$rec = ExcelReader::remap($row, $headerMap);
|
|||
|
|
|
|||
|
|
foreach ($decimalFields as $f) {
|
|||
|
|
if (array_key_exists($f, $rec)) {
|
|||
|
|
$rec[$f] = ExcelReader::toDecimal($rec[$f]); // 建议已升级为“去货币符号/千分位/括号负数”的版本
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
foreach ($boolFields as $f) {
|
|||
|
|
if (array_key_exists($f, $rec)) {
|
|||
|
|
$rec[$f] = ExcelReader::toBool($rec[$f]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
foreach ($dateFields as $f) {
|
|||
|
|
if (array_key_exists($f, $rec)) {
|
|||
|
|
$rec[$f] = ExcelReader::toDate($rec[$f]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return $rec;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 基于 VIN 的跳过规则:VIN 为空、或是“合计/总计”行
|
|||
|
|
* - $vinAliases:VIN 列的候选表头(如 ['车架号','VIN NO'])
|
|||
|
|
*/
|
|||
|
|
protected function shouldSkipByVinAliases(array $row, array $vinAliases = ['车架号', 'VIN NO']): bool
|
|||
|
|
{
|
|||
|
|
$vin = $this->pickFirstNonEmpty($row, $vinAliases);
|
|||
|
|
if ($vin === null) return true;
|
|||
|
|
return (bool)preg_match('/^(合计|总计)/u', $vin);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 在多个候选键里取第一个非空字符串
|
|||
|
|
*/
|
|||
|
|
protected function pickFirstNonEmpty(array $row, array $keys): ?string
|
|||
|
|
{
|
|||
|
|
foreach ($keys as $k) {
|
|||
|
|
if (isset($row[$k])) {
|
|||
|
|
$v = trim((string)$row[$k]);
|
|||
|
|
if ($v !== '') return $v;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 统一补齐 created_at / updated_at 时间戳(int(11))
|
|||
|
|
*/
|
|||
|
|
protected function stamp(array &$rec): void
|
|||
|
|
{
|
|||
|
|
$now = time();
|
|||
|
|
$rec['created_at'] = $now;
|
|||
|
|
$rec['updated_at'] = $now;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量入库
|
|||
|
|
* - 返回 ['inserted'=>N, 'error'=>null|string]
|
|||
|
|
* - 发生异常时不抛出,返回 error 字符串,便于上层统一处理
|
|||
|
|
*/
|
|||
|
|
protected function batchInsert(string $table, array $rows): array
|
|||
|
|
{
|
|||
|
|
if (!$rows) return ['inserted' => 0, 'error' => null];
|
|||
|
|
try {
|
|||
|
|
Db::table($table)->insert($rows);
|
|||
|
|
return ['inserted' => count($rows), 'error' => null];
|
|||
|
|
} catch (\Throwable $e) {
|
|||
|
|
return ['inserted' => 0, 'error' => $e->getMessage()];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|