argument('file'); $period = trim((string) $this->option('period')); if (! is_file($path)) { $this->error('文件不存在:' . $path); return self::FAILURE; } if ($period !== '' && preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) !== 1) { $this->error('数据月份格式必须是 YYYY-MM:' . $period); return self::FAILURE; } $headers = (array) config('order_after_sales_cost.headers', []); $numberKeys = array_flip((array) config('order_after_sales_cost.number_keys', [])); if ($headers === []) { $this->error('缺少表头映射:config/order_after_sales_cost.php'); return self::FAILURE; } $this->info('流式读取文件:' . $path); $chunkSize = max((int) $this->option('chunk'), 100); $dryRun = (bool) $this->option('dry-run'); $buffer = []; $total = 0; $written = 0; $skipped = 0; $shops = []; $mappedRows = 0; $minDate = null; $maxDate = null; $importedAt = now()->toDateTimeString(); try { foreach ($reader->rows($path, [], array_keys($headers)) as $row) { $total++; $payload = []; foreach ($headers as $header => $key) { if (! array_key_exists($header, $row)) { continue; } $value = trim((string) $row[$header]); if ($value === '') { continue; } if (isset($numberKeys[$key])) { $value = is_numeric($value) ? (float) $value : 0.0; } elseif (self::isDateKey($key)) { $value = self::dateValue($value); } $payload[$key] = $value; } if ($payload !== []) { $mappedRows++; } $shop = trim((string) ($payload['shop_name'] ?? '')); $productCode = trim((string) ($payload['product_code'] ?? '')); if ($shop === '' || $productCode === '') { $skipped++; continue; } $buffer[] = AfterSalesCostRowRepository::toRecord( $payload, $importedAt, $period !== '' ? $period : null, ); $shops[$shop] = true; $orderTime = $payload['order_time'] ?? null; if (is_string($orderTime) && $orderTime !== '') { $minDate = $minDate === null || $orderTime < $minDate ? $orderTime : $minDate; $maxDate = $maxDate === null || $orderTime > $maxDate ? $orderTime : $maxDate; } if (! $dryRun && count($buffer) >= $chunkSize) { $written += $this->flush($buffer); $buffer = []; $this->output->write("\r写入 " . $written . ' 行'); } if ($dryRun && $buffer !== []) { $buffer = []; } } } catch (\Throwable $exception) { if (! $dryRun) { Cache::forget('order-data.after-sales.imported-shop-options'); } $this->error('读取失败:' . $exception->getMessage()); return self::FAILURE; } if ($mappedRows === 0) { $this->error('表头和配置的映射对不上,请检查文件或 config/order_after_sales_cost.php'); return self::FAILURE; } if (! $dryRun && $buffer !== []) { $written += $this->flush($buffer); } if (! $dryRun) { Cache::forget('order-data.after-sales.imported-shop-options'); } $this->newLine(2); $this->info(sprintf( '完成:读取 %d 行,写入 %d 行,跳过 %d 行;店铺 %d 个;数据月份 %s;订单时间 %s ~ %s;峰值内存 %.1f MB', $total, $written, $skipped, count($shops), $period !== '' ? $period : '-', $minDate ?? '-', $maxDate ?? '-', memory_get_peak_usage(true) / 1048576, )); return self::SUCCESS; } /** @param list> $rows */ private function flush(array $rows): int { DB::table('ops_after_sales_cost_rows')->upsert( $rows, ['row_key'], [ 'shop_name', 'shop_code', 'online_order_no', 'internal_order_no', 'product_code', 'style_code', 'shop_product_code', 'shop_style_code', 'order_time', 'source_period', 'product_quantity', 'product_amount', 'product_cost', 'cost_source', 'combine_type', 'payload', 'imported_at', 'updated_at', ], ); return count($rows); } private static function isDateKey(string $key): bool { return in_array($key, [ 'pay_time', 'order_complete_time', 'send_time', 'order_time', 'registration_time', 'confirmation_time', 'warehouse_entry_time', ], true); } private static function dateValue(mixed $value): ?string { if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d H:i:s'); } if (is_numeric($value) && (float) $value > 20000 && (float) $value < 80000) { $timestamp = ((float) $value - 25569) * 86400; return gmdate('Y-m-d H:i:s', (int) $timestamp); } $timestamp = strtotime((string) $value); return $timestamp === false ? null : date('Y-m-d H:i:s', $timestamp); } }