修复进销存滚动月份导出超时 #261

Merged
chenxi merged 1 commits from fix/chenxi-inventory-report-export-timeout into main 2026-09-24 10:38:41 +08:00
3 changed files with 93 additions and 23 deletions

View File

@ -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', [

View File

@ -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,

View File

@ -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();