277 lines
9.6 KiB
PHP
277 lines
9.6 KiB
PHP
<?php
|
|
|
|
namespace App\Business\KukaPostingBase;
|
|
|
|
use App\Business\XlsxSheetReader;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Database\ConnectionInterface;
|
|
use Illuminate\Database\DatabaseManager;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
class KukaPostingBaseImporter
|
|
{
|
|
private const INSERT_CHUNK = 500;
|
|
|
|
public function __construct(
|
|
private readonly DatabaseManager $databases,
|
|
private readonly XlsxSheetReader $reader,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* period: string,
|
|
* dry_run: bool,
|
|
* rows: int,
|
|
* stores: array<string, int>,
|
|
* store_count: int,
|
|
* order_count: int,
|
|
* skipped_store_rows: int,
|
|
* file_sha256: string
|
|
* }
|
|
*/
|
|
public function import(
|
|
string $path,
|
|
string $period,
|
|
?string $sheetName = null,
|
|
int $companyId = 1,
|
|
bool $dryRun = false,
|
|
): array {
|
|
if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) !== 1) {
|
|
throw new InvalidArgumentException('账期格式必须为 YYYY-MM。');
|
|
}
|
|
if (! is_file($path) || ! is_readable($path)) {
|
|
throw new InvalidArgumentException('聚水潭导出文件不存在或不可读。');
|
|
}
|
|
|
|
$connection = $this->connection();
|
|
if (! Schema::connection($this->connectionName())->hasTable('kuka_posting_base_imports')
|
|
|| ! Schema::connection($this->connectionName())->hasTable('kuka_posting_base_rows')) {
|
|
throw new RuntimeException('顾家过账基础数据表尚未建立,请先执行数据库迁移。');
|
|
}
|
|
|
|
$fields = (array) config('kuka_posting.fields', []);
|
|
$allowedStores = array_values((array) config('kuka_posting.stores', []));
|
|
if ($fields === [] || $allowedStores === []) {
|
|
throw new RuntimeException('顾家过账基础数据字段或店铺范围未配置。');
|
|
}
|
|
|
|
$sourceColumns = array_values(array_filter(array_map(
|
|
static fn (array $field): string => trim((string) ($field['source'] ?? '')),
|
|
$fields,
|
|
)));
|
|
if (count($sourceColumns) !== count($fields)) {
|
|
throw new RuntimeException('顾家过账基础数据字段配置不完整。');
|
|
}
|
|
|
|
$sheetName = trim((string) ($sheetName ?: config('kuka_posting.source_sheet', '发货明细表')));
|
|
$fileHash = hash_file('sha256', $path);
|
|
if (! is_string($fileHash)) {
|
|
throw new RuntimeException('无法计算聚水潭导出文件摘要。');
|
|
}
|
|
|
|
$storeCounts = array_fill_keys($allowedStores, 0);
|
|
$orders = [];
|
|
$rowCount = 0;
|
|
$skippedStoreRows = 0;
|
|
$sourceRowNumber = 1;
|
|
$now = now();
|
|
|
|
$write = function () use (
|
|
$connection,
|
|
$path,
|
|
$period,
|
|
$sheetName,
|
|
$companyId,
|
|
$fields,
|
|
$sourceColumns,
|
|
$allowedStores,
|
|
&$storeCounts,
|
|
&$orders,
|
|
&$rowCount,
|
|
&$skippedStoreRows,
|
|
&$sourceRowNumber,
|
|
$fileHash,
|
|
$now,
|
|
$dryRun,
|
|
): void {
|
|
if (! $dryRun) {
|
|
$connection->table('kuka_posting_base_rows')
|
|
->where('company_id', $companyId)
|
|
->where('period', $period)
|
|
->delete();
|
|
}
|
|
|
|
$batch = [];
|
|
$flush = static function () use (&$batch, $connection): void {
|
|
if ($batch === []) {
|
|
return;
|
|
}
|
|
$connection->table('kuka_posting_base_rows')->insert($batch);
|
|
$batch = [];
|
|
};
|
|
|
|
foreach ($this->reader->rows($path, $sourceColumns, [], $sheetName) as $sourceValues) {
|
|
$sourceRowNumber++;
|
|
$storeName = trim((string) ($sourceValues['店铺'] ?? ''));
|
|
if ($storeName === '' || ! in_array($storeName, $allowedStores, true)) {
|
|
$skippedStoreRows++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$payload = [];
|
|
foreach ($fields as $field) {
|
|
$source = (string) $field['source'];
|
|
$value = trim((string) ($sourceValues[$source] ?? ''));
|
|
if (($field['kind'] ?? null) === 'date') {
|
|
$value = $this->normalizeDate($value);
|
|
}
|
|
$payload[$source] = $value;
|
|
}
|
|
|
|
$rowCount++;
|
|
$storeCounts[$storeName]++;
|
|
$internalOrderNo = trim((string) ($payload['内部订单号'] ?? ''));
|
|
if ($internalOrderNo !== '') {
|
|
$orders[$internalOrderNo] = true;
|
|
}
|
|
|
|
if ($dryRun) {
|
|
continue;
|
|
}
|
|
|
|
$batch[] = [
|
|
'company_id' => $companyId,
|
|
'period' => $period,
|
|
'store_name' => $storeName,
|
|
'internal_order_no' => $this->limit($internalOrderNo, 191),
|
|
'online_order_no' => $this->limit((string) ($payload['线上订单号'] ?? ''), 191),
|
|
'product_code' => $this->limit((string) ($payload['商品编码'] ?? ''), 191),
|
|
'order_status' => $this->limit((string) ($payload['订单状态'] ?? ''), 64),
|
|
'order_date' => $this->nullableValue($payload['订单日期'] ?? ''),
|
|
'send_date' => $this->nullableValue($payload['发货日期'] ?? ''),
|
|
'pay_date' => $this->nullableValue($payload['付款日期'] ?? ''),
|
|
'confirm_date' => $this->nullableValue($payload['确认收货日期'] ?? ''),
|
|
'row_hash' => hash('sha256', json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)),
|
|
'source_values' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
|
'source_row_number' => $sourceRowNumber,
|
|
'imported_at' => $now,
|
|
];
|
|
|
|
if (count($batch) >= self::INSERT_CHUNK) {
|
|
$flush();
|
|
}
|
|
}
|
|
|
|
$flush();
|
|
|
|
if ($rowCount === 0) {
|
|
throw new RuntimeException('导出文件中没有指定店铺的数据,已保留原有基础数据。');
|
|
}
|
|
|
|
$missingStores = array_keys(array_filter(
|
|
$storeCounts,
|
|
static fn (int $count): bool => $count === 0,
|
|
));
|
|
if ($missingStores !== []) {
|
|
throw new RuntimeException('导出文件缺少指定店铺:' . implode('、', $missingStores));
|
|
}
|
|
|
|
if ($dryRun) {
|
|
return;
|
|
}
|
|
|
|
$import = [
|
|
'company_id' => $companyId,
|
|
'period' => $period,
|
|
'row_count' => $rowCount,
|
|
'store_count' => count(array_filter($storeCounts)),
|
|
'order_count' => count($orders),
|
|
'store_counts' => json_encode($storeCounts, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
|
'file_name' => basename($path),
|
|
'file_sha256' => $fileHash,
|
|
'source_sheet' => $sheetName,
|
|
'completed_at' => $now,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
$connection->table('kuka_posting_base_imports')->upsert(
|
|
[$import],
|
|
['company_id', 'period'],
|
|
[
|
|
'row_count',
|
|
'store_count',
|
|
'order_count',
|
|
'store_counts',
|
|
'file_name',
|
|
'file_sha256',
|
|
'source_sheet',
|
|
'completed_at',
|
|
'updated_at',
|
|
],
|
|
);
|
|
};
|
|
|
|
if ($dryRun) {
|
|
$write();
|
|
} else {
|
|
$connection->transaction($write);
|
|
}
|
|
|
|
return [
|
|
'period' => $period,
|
|
'dry_run' => $dryRun,
|
|
'rows' => $rowCount,
|
|
'stores' => $storeCounts,
|
|
'store_count' => count(array_filter($storeCounts)),
|
|
'order_count' => count($orders),
|
|
'skipped_store_rows' => $skippedStoreRows,
|
|
'file_sha256' => strtolower($fileHash),
|
|
];
|
|
}
|
|
|
|
private function connection(): ConnectionInterface
|
|
{
|
|
return $this->databases->connection($this->connectionName());
|
|
}
|
|
|
|
private function connectionName(): string
|
|
{
|
|
return (string) config('upstream_mirrors.connection', 'business_mirror');
|
|
}
|
|
|
|
private function normalizeDate(string $value): string
|
|
{
|
|
if ($value === '') {
|
|
return '';
|
|
}
|
|
|
|
if (is_numeric($value) && (float) $value > 20000 && (float) $value < 90000) {
|
|
return gmdate('Y-m-d H:i:s', (int) round(((float) $value - 25569) * 86400));
|
|
}
|
|
|
|
try {
|
|
return CarbonImmutable::parse($value, 'Asia/Shanghai')->format('Y-m-d H:i:s');
|
|
} catch (Throwable) {
|
|
throw new InvalidArgumentException('聚水潭日期格式无法识别:' . $value);
|
|
}
|
|
}
|
|
|
|
private function nullableValue(string $value): ?string
|
|
{
|
|
return $value === '' ? null : $value;
|
|
}
|
|
|
|
private function limit(string $value, int $length): ?string
|
|
{
|
|
$value = trim($value);
|
|
|
|
return $value === '' ? null : mb_substr($value, 0, $length);
|
|
}
|
|
}
|