修复进销存滚动月份导出超时 #261
@ -1320,6 +1320,7 @@ class Pages extends Controller
|
||||
$columns = $inventoryPage['directory']['fields'] ?? [];
|
||||
|
||||
return response()->streamDownload(function () use ($filters, $columns): void {
|
||||
set_time_limit(0);
|
||||
$csv = fopen('php://output', 'w');
|
||||
if ($csv === false) {
|
||||
throw new RuntimeException('无法创建进销存报表导出流。');
|
||||
@ -1327,6 +1328,7 @@ class Pages extends Controller
|
||||
|
||||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
fputcsv($csv, array_column($columns, 'label'));
|
||||
$written = 0;
|
||||
foreach ($this->thirdPartyWarehouseStorage->exportMonthlyInventoryReportRows($filters) as $row) {
|
||||
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
||||
$value = data_get($row, $column['name']);
|
||||
@ -1337,6 +1339,10 @@ class Pages extends Controller
|
||||
? ($value === null || $value === '' ? '' : $value)
|
||||
: $this->csvText($value));
|
||||
}, $columns));
|
||||
$written++;
|
||||
if ($written % 500 === 0) {
|
||||
fflush($csv);
|
||||
}
|
||||
}
|
||||
fclose($csv);
|
||||
}, '进销存报表_' . str_replace('-', '', $filters['month']) . '_' . now()->format('Ymd_His') . '.csv', [
|
||||
|
||||
@ -585,12 +585,10 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
* @param array<string,mixed> $filters
|
||||
* @return Generator<int,object>
|
||||
*/
|
||||
public function exportMonthlyInventoryReportRows(array $filters, int $batchSize = 2000): Generator
|
||||
public function exportMonthlyInventoryReportRows(array $filters, int $batchSize = 500): Generator
|
||||
{
|
||||
$batchSize = min(max($batchSize, 1), 5000);
|
||||
$page = 1;
|
||||
$exported = 0;
|
||||
$total = null;
|
||||
$state = $this->requireReadyState();
|
||||
|
||||
while (true) {
|
||||
@ -598,12 +596,9 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
$filters,
|
||||
$page,
|
||||
$batchSize,
|
||||
$total === null,
|
||||
false,
|
||||
$state,
|
||||
);
|
||||
if ($total === null) {
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
}
|
||||
|
||||
$rows = $result['data'] ?? [];
|
||||
if ($rows === []) {
|
||||
@ -613,8 +608,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
yield $row;
|
||||
}
|
||||
|
||||
$exported += count($rows);
|
||||
if ($exported >= $total || count($rows) < $batchSize) {
|
||||
if (count($rows) < $batchSize) {
|
||||
return;
|
||||
}
|
||||
$page++;
|
||||
@ -779,23 +773,64 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
array_merge([$owner], $inventoryWarehouseBindings),
|
||||
);
|
||||
|
||||
$candidateUnion = $authorityCandidates->unionAll($inventoryDimensions);
|
||||
$candidates = $this->connection->query()->fromSub($candidateUnion, 'candidate_rows');
|
||||
|
||||
$pageCandidates = (clone $candidates)
|
||||
->orderBy('warehouse_provider')->orderBy('wh_id')->orderBy('sku_code')
|
||||
->forPage($page, $perPage);
|
||||
if ($includeTotal) {
|
||||
$pageCandidates->select('candidate_rows.*')->selectRaw('COUNT(*) OVER() AS report_total');
|
||||
}
|
||||
$pairRows = $this->boundedGet(clone $pageCandidates);
|
||||
$total = $includeTotal ? (int) ($pairRows->first()->report_total ?? 0) : 0;
|
||||
if ($pairRows->isEmpty()) {
|
||||
if ($includeTotal && $page > 1) {
|
||||
$total = $this->boundedCount(clone $authorityRows)
|
||||
+ $this->boundedCount(clone $inventoryDimensions);
|
||||
$candidateUnion = $authorityCandidates->unionAll($inventoryDimensions);
|
||||
$candidates = $this->connection->query()->fromSub($candidateUnion, 'candidate_rows');
|
||||
$pageCandidates = (clone $candidates)
|
||||
->orderBy('warehouse_provider')->orderBy('wh_id')->orderBy('sku_code')
|
||||
->forPage($page, $perPage)
|
||||
->select('candidate_rows.*')->selectRaw('COUNT(*) OVER() AS report_total');
|
||||
$pairRows = $this->boundedGet(clone $pageCandidates);
|
||||
$total = (int) ($pairRows->first()->report_total ?? 0);
|
||||
} else {
|
||||
// Export does not need a total. Page the authoritative baseline and newly
|
||||
// discovered inventory dimensions separately so every batch avoids the
|
||||
// expensive full UNION count/window query.
|
||||
$authorityTotal = $this->boundedCount(clone $authorityRows);
|
||||
$total = 0;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$remaining = $perPage;
|
||||
$candidatePages = [];
|
||||
|
||||
if ($offset < $authorityTotal) {
|
||||
$authorityLimit = min($remaining, $authorityTotal - $offset);
|
||||
$candidatePages[] = (clone $authorityCandidates)
|
||||
->orderBy('authority.warehouse_provider')
|
||||
->orderBy('wh_id')
|
||||
->orderBy('authority.product_code')
|
||||
->offset($offset)
|
||||
->limit($authorityLimit);
|
||||
$remaining -= $authorityLimit;
|
||||
$inventoryOffset = 0;
|
||||
} else {
|
||||
$inventoryOffset = $offset - $authorityTotal;
|
||||
}
|
||||
|
||||
if ($remaining > 0) {
|
||||
$candidatePages[] = (clone $inventoryDimensions)
|
||||
->orderBy('w.name')
|
||||
->orderBy('inventory.wh_id')
|
||||
->orderBy('inventory.sku_code')
|
||||
->offset($inventoryOffset)
|
||||
->limit($remaining);
|
||||
}
|
||||
|
||||
if ($candidatePages === []) {
|
||||
return [
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'state' => $state,
|
||||
'sync' => $this->syncStatus($state),
|
||||
];
|
||||
}
|
||||
|
||||
$pageCandidates = array_shift($candidatePages);
|
||||
foreach ($candidatePages as $candidatePage) {
|
||||
$pageCandidates->unionAll($candidatePage);
|
||||
}
|
||||
$pairRows = $this->boundedGet(clone $pageCandidates);
|
||||
}
|
||||
if ($pairRows->isEmpty()) {
|
||||
return [
|
||||
'data' => [],
|
||||
'total' => $total,
|
||||
|
||||
@ -447,6 +447,35 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
|
||||
$this->assertNotContains('LEGACY-CODE-NOT-IN-CATALOG', $codes);
|
||||
}
|
||||
|
||||
public function test_inventory_report_export_reads_roll_forward_rows_without_total_count(): void
|
||||
{
|
||||
$this->markReady();
|
||||
$this->insertProduct('SKU-EXPORT');
|
||||
$this->insertInventory('SKU-EXPORT', 999, '2026-09-11 03:21:00');
|
||||
$this->insertInventoryReportAuthoritativeRow([
|
||||
'source_month' => '2026-07-01',
|
||||
'report_month' => '2026-07-01',
|
||||
'warehouse_provider' => '三方-南昌樵舍仓',
|
||||
'warehouse' => '三方-南昌樵舍仓主仓',
|
||||
'product_code' => 'SKU-EXPORT',
|
||||
'opening_quantity' => 10,
|
||||
'closing_quantity' => 12,
|
||||
]);
|
||||
$this->insertMovement('SKU-EXPORT', '采购进仓', 3, 1, '2026-08-02 10:00:00');
|
||||
|
||||
$rows = iterator_to_array($this->repository->exportMonthlyInventoryReportRows([
|
||||
'month' => '2026-08',
|
||||
'product_code' => 'SKU-EXPORT',
|
||||
'warehouse_provider' => '',
|
||||
'category' => '',
|
||||
], 1), false);
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame('SKU-EXPORT', $rows[0]->product_code);
|
||||
$this->assertSame(12.0, (float) $rows[0]->opening_quantity);
|
||||
$this->assertSame(15.0, (float) $rows[0]->closing_quantity);
|
||||
}
|
||||
|
||||
public function test_third_party_page_remains_ready_when_only_the_all_warehouse_dimension_is_missing(): void
|
||||
{
|
||||
$this->markReady();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user