CaiWuXiTong/app/Business/CourierFreightImporter.php
qiuxiang 8c626c65de 快递运费:查询字段靠左,列名「成品配件」改「成品运费」
1) 查询栏里的空提交表单没隐藏,会占掉栅格第一格,把筛选字段整体挤到第二列;改成 hidden 后字段从最左边开始(所有用这套查询栏的页面一起受益)。
2) 快递运费页把「成品配件」列名改成「成品运费」;导入时两种表头都认(成品配件 / 成品运费)。
2026-10-08 13:55:56 +08:00

207 lines
6.6 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\Business;
use Illuminate\Database\DatabaseManager;
use InvalidArgumentException;
use RuntimeException;
/**
* 快递运费导入(.xlsx / .csv)。
*
* 源表头:来源 / 成品配件(也接受「成品运费」)/ 交易单号 / 提供成本运费(按表头名匹配,列顺序无关)。
* 快递方给的文件常见几十万行,xlsx 走 XlsxSheetReader 流式读取,不整表进内存。
*/
class CourierFreightImporter
{
/** 源表头 → 数据库列 */
private const COLUMNS = [
'来源' => 'source',
'成品配件' => 'part',
'成品运费' => 'part',
'交易单号' => 'transaction_no',
'提供成本运费' => 'freight_amount',
];
private const HEADERS = ['来源', '成品配件', '交易单号', '提供成本运费'];
/** 表头别名:源文件里这一列有时写「成品配件」、有时写「成品运费」。 */
private const HEADER_ALIASES = [
'成品配件' => ['成品配件', '成品运费'],
];
private const CHUNK = 2000;
public function __construct(
private readonly DatabaseManager $databases,
private readonly ?XlsxSheetReader $reader = null,
) {}
/**
* @return array{file:string,rows:int,inserted:int,skipped:int,amount:string,sources:array<string,int>,seconds:float}
*/
public function import(string $path, int $companyId = 1, bool $replace = false, string $source = 'courier-freight-import'): array
{
if (! is_file($path)) {
throw new InvalidArgumentException('找不到要导入的文件:' . $path);
}
$target = $this->databases->connection();
$startedAt = microtime(true);
$now = now();
if ($replace) {
$target->table('courier_freight_rows')->where('company_id', $companyId)->delete();
}
$inserted = 0;
$skipped = 0;
$amount = 0.0;
$sources = [];
$buffer = [];
$flush = function () use (&$buffer, &$inserted, $target): void {
foreach (array_chunk($buffer, self::CHUNK) as $chunk) {
$target->table('courier_freight_rows')->insert($chunk);
$inserted += count($chunk);
}
$buffer = [];
};
foreach ($this->lines($path) as $line) {
$sourceValue = $this->text($line['来源'] ?? null, 64);
$transactionNo = $this->text($line['交易单号'] ?? null, 191);
if ($sourceValue === null && $transactionNo === null) {
$skipped++;
continue;
}
$value = $this->number($line['提供成本运费'] ?? null);
$sources[$sourceValue ?? ''] = ($sources[$sourceValue ?? ''] ?? 0) + 1;
$amount += $value;
$buffer[] = [
'company_id' => $companyId,
'source' => $sourceValue ?? '',
'part' => $this->text($line['成品配件'] ?? null, 191),
'transaction_no' => $transactionNo ?? '',
'freight_amount' => number_format($value, 4, '.', ''),
'created_at' => $now,
'updated_at' => $now,
];
if (count($buffer) >= self::CHUNK * 4) {
$flush();
}
}
$flush();
arsort($sources);
return [
'file' => basename($path),
'rows' => $inserted + $skipped,
'inserted' => $inserted,
'skipped' => $skipped,
'amount' => number_format($amount, 2, '.', ''),
'sources' => $sources,
'seconds' => round(microtime(true) - $startedAt, 1),
];
}
/** @return \Generator<int, array<string,string>> */
private function lines(string $path): \Generator
{
if (mb_strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'csv') {
$handle = fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException('无法读取文件:' . $path);
}
try {
$header = null;
while (($cells = fgetcsv($handle)) !== false) {
if ($header === null) {
$cells[0] = preg_replace('/^\xEF\xBB\xBF/', '', (string) ($cells[0] ?? ''));
$header = array_map(static fn ($value): string => trim((string) $value), $cells);
$this->assertHeader($header);
continue;
}
$line = [];
foreach ($header as $index => $name) {
$line[$this->canonicalName($name)] = (string) ($cells[$index] ?? '');
}
yield $line;
}
} finally {
fclose($handle);
}
return;
}
if (mb_strtolower(pathinfo($path, PATHINFO_EXTENSION)) !== 'xlsx') {
throw new InvalidArgumentException('只支持 xlsx、csv。');
}
$columns = array_map(
static fn (string $name): array|string => self::HEADER_ALIASES[$name] ?? $name,
self::HEADERS,
);
yield from ($this->reader ?? app(XlsxSheetReader::class))->rows($path, $columns);
}
/** 把表头别名折回规范列名。 */
private function canonicalName(string $header): string
{
foreach (self::HEADER_ALIASES as $canonical => $names) {
if (in_array($header, $names, true)) {
return $canonical;
}
}
return $header;
}
/** @param list<string> $header */
private function assertHeader(array $header): void
{
foreach (self::HEADERS as $required) {
$names = self::HEADER_ALIASES[$required] ?? [$required];
if (array_intersect($names, $header) === []) {
throw new InvalidArgumentException('文件缺少列:' . $required);
}
}
}
private function text(mixed $value, int $limit): ?string
{
if ($value === null) {
return null;
}
$text = trim((string) (is_scalar($value) ? $value : ''));
return $text === '' ? null : mb_substr($text, 0, $limit);
}
private function number(mixed $value): float
{
if ($value === null || $value === '') {
return 0.0;
}
if (is_numeric($value)) {
return (float) $value;
}
$text = str_replace([',', ' ', '¥', '¥'], '', (string) $value);
return is_numeric($text) ? (float) $text : 0.0;
}
}