*/ private array $temporaryFiles = []; protected function setUp(): void { parent::setUp(); $this->user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail(), ); $this->withoutMiddleware(ThrottleRequests::class); } public function test_page_exposes_import_and_filtered_export_actions(): void { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '操作按钮测试店', 'store_entity_name' => '按钮测试主体', ]); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '天猫', ])) ->assertOk() ->assertSeeText('导入') ->assertSeeText('导出') ->assertSee('data-withdrawal-import-open', false) ->assertSee('class="withdrawal-management__actions"', false) ->assertSee('data-withdrawal-column-trigger', false) ->assertSee('data-withdrawal-column-toggle', false) ->assertSee('data-withdrawal-sort-asc', false) ->assertSee('data-withdrawal-sort-desc', false) ->assertSee(route('business.withdrawal-management.export', ['platform' => '天猫']), false) ->assertSee(route('business.withdrawal-management.template'), false) ->assertSee(route('business.withdrawal-management.template.withdrawals', ['platform' => '天猫']), false) ->assertSeeText('下载提现金额模板') ->assertSeeText('按大笔提现处理'); } public function test_page_sorts_all_filtered_rows_before_pagination(): void { foreach (['B排序测试店', 'A排序测试店'] as $name) { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '排序测试平台', 'erp_store_name' => $name, 'store_entity_name' => '排序测试主体', ]); } $ascending = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '排序测试平台', 'sort' => 'erp_store_name', 'direction' => 'asc', ])); $descending = $this->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '排序测试平台', 'sort' => 'erp_store_name', 'direction' => 'desc', ])); $ascending ->assertOk() ->assertSeeInOrder(['A排序测试店', 'B排序测试店']) ->assertSee('name="sort" value="erp_store_name"', false) ->assertSee('name="direction" value="asc"', false) ->assertSee('aria-sort="ascending"', false); $descending ->assertOk() ->assertSeeInOrder(['B排序测试店', 'A排序测试店']) ->assertSee('aria-sort="descending"', false); } public function test_export_contains_all_filtered_rows_and_excludes_passwords(): void { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '导出目标店', 'store_id' => '00001234', 'store_entity_name' => '导出主体', 'login_password' => 'never-export-this-password', 'withdrawal_path' => '支付宝', 'withdrawal_account_name' => '目标账户', 'bank_card_last_four' => '0098', ]); StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '不应导出的店铺', 'store_entity_name' => '其他主体', ]); $response = $this->loginAs()->get(route('business.withdrawal-management.export', [ 'platform' => '天猫', ])); $response->assertOk()->assertDownload(); $path = $this->temporaryPath('withdrawal-export-'); file_put_contents($path, $response->streamedContent()); $spreadsheet = IOFactory::load($path); try { $sheet = $spreadsheet->getActiveSheet(); $this->assertSame([ ...self::HEADERS, now()->subDays(3)->format('n月j日') . '店铺余额', now()->subDays(2)->format('n月j日') . '店铺余额', now()->subDay()->format('n月j日') . '店铺余额', '余额来源', '余额预警', '预留标准', '提现金额', '是否达标', ], $sheet->rangeToArray('A1:O1')[0]); $this->assertSame(2, $sheet->getHighestDataRow()); $this->assertSame('导出目标店', $sheet->getCell('B2')->getValue()); $this->assertSame('00001234', $sheet->getCell('C2')->getValue()); $this->assertSame(DataType::TYPE_STRING, $sheet->getCell('C2')->getDataType()); $this->assertSame('0098', $sheet->getCell('G2')->getValue()); $this->assertSame(DataType::TYPE_STRING, $sheet->getCell('G2')->getDataType()); $values = array_merge(...$sheet->toArray(null, false, false, false)); $this->assertNotContains('不应导出的店铺', $values); $this->assertNotContains('never-export-this-password', $values); } finally { $spreadsheet->disconnectWorksheets(); } } public function test_template_has_import_headers_and_instructions(): void { $response = $this->loginAs()->get(route('business.withdrawal-management.template')); $response->assertOk()->assertDownload('withdrawal_management_import_template.xlsx'); $path = $this->temporaryPath('withdrawal-template-'); file_put_contents($path, $response->streamedContent()); $spreadsheet = IOFactory::load($path); try { $this->assertSame(self::IMPORT_HEADERS, $spreadsheet->getSheet(0)->rangeToArray('A1:H1')[0]); $this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle()); $this->assertStringContainsString('[清空]', (string) $spreadsheet->getSheet(1)->getCell('A4')->getValue()); } finally { $spreadsheet->disconnectWorksheets(); } } public function test_withdrawal_amount_template_prefills_current_filtered_store_and_balance(): void { Carbon::setTestNow('2026-09-24 09:00:00'); try { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '金额模板目标店', 'store_id' => 'TM-AMOUNT-TEMPLATE', 'store_entity_name' => '模板测试主体', ]); $this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:00:00', '金额模板目标店', 25000); $response = $this->loginAs()->get(route('business.withdrawal-management.template.withdrawals', [ 'platform' => '天猫', 'keyword' => '金额模板目标店', ])); $response->assertOk()->assertDownload(); $path = $this->temporaryPath('withdrawal-amount-template-'); file_put_contents($path, $response->streamedContent()); $spreadsheet = IOFactory::load($path); try { $sheet = $spreadsheet->getSheet(0); $this->assertSame('提现金额', $sheet->getTitle()); $this->assertSame(self::WITHDRAWAL_TEMPLATE_HEADERS, $sheet->rangeToArray('A1:I1')[0]); $this->assertSame('天猫', $sheet->getCell('A2')->getValue()); $this->assertSame('金额模板目标店', $sheet->getCell('B2')->getValue()); $this->assertSame('TM-AMOUNT-TEMPLATE', $sheet->getCell('C2')->getValue()); $this->assertSame('2026-09-23', $sheet->getCell('D2')->getValue()); $this->assertSame(25000.0, (float) $sheet->getCell('E2')->getValue()); $this->assertSame(20000.0, (float) $sheet->getCell('F2')->getValue()); $this->assertNull($sheet->getCell('G2')->getValue()); $this->assertStringContainsString('E2-G2', (string) $sheet->getCell('H2')->getValue()); $this->assertStringContainsString('H2<=F2', (string) $sheet->getCell('I2')->getValue()); $this->assertSame(2, $sheet->getHighestDataRow()); $this->assertStringContainsString('大笔提现', (string) $spreadsheet->getSheetByName('填写说明')?->getCell('A5')->getValue()); } finally { $spreadsheet->disconnectWorksheets(); } } finally { Carbon::setTestNow(); } } public function test_withdrawal_amount_template_imports_only_filled_amounts_and_previews_compliance(): void { Carbon::setTestNow('2026-09-24 09:00:00'); try { $qualified = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '大笔达标店', 'store_id' => 'TM-LARGE-YES', 'store_entity_name' => '模板测试主体', ]); $unqualified = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '大笔未达标店', 'store_id' => 'TM-LARGE-NO', 'store_entity_name' => '模板测试主体', ]); $this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:00:00', '大笔达标店', 25000); $this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:01:00', '大笔未达标店', 25000); $this->loginAs(); $previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->withdrawalWorkbook([ ['天猫', '大笔达标店', 'TM-LARGE-YES', '2026-09-23', 25000, 20000, '5000.00'], ['天猫', '大笔未达标店', 'TM-LARGE-NO', '2026-09-23', 25000, 20000, '4999.99'], ]), ]); $previewResponse->assertSessionHas('withdrawalImportPreview', function (array $preview): bool { return $preview['total'] === 2 && $preview['changed'] === 2 && $preview['preview'][0]['withdrawal_amount'] === 5000.0 && $preview['preview'][0]['post_withdrawal_balance'] === 20000.0 && $preview['preview'][0]['compliance'] === 'yes' && $preview['preview'][1]['withdrawal_amount'] === 4999.99 && $preview['preview'][1]['post_withdrawal_balance'] === 20000.01 && $preview['preview'][1]['compliance'] === 'no'; }); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); $this->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', ])) ->assertOk() ->assertSeeText('提现后余额') ->assertSeeText('是否达标(大笔)') ->assertSeeText('是(大笔)'); $this->post(route('business.withdrawal-management.import.commit'), [ 'preview_token' => $preview['token'], ])->assertSessionHasNoErrors(); $this->assertSame('5000.00', $qualified->fresh()->withdrawal_amount); $this->assertSame('2026-09-23', $qualified->fresh()->withdrawal_balance_date); $this->assertSame('4999.99', $unqualified->fresh()->withdrawal_amount); $this->assertSame('2026-09-23', $unqualified->fresh()->withdrawal_balance_date); } finally { Carbon::setTestNow(); } } public function test_withdrawal_amount_template_rejects_a_stale_balance_date(): void { Carbon::setTestNow('2026-09-24 09:00:00'); try { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '过期金额模板店', 'store_id' => 'JD-STALE-AMOUNT', 'store_entity_name' => '模板测试主体', ]); $this->loginAs(); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->withdrawalWorkbook([ ['京东', '过期金额模板店', 'JD-STALE-AMOUNT', '2026-09-22', 10000, 5000, '5000.00'], ]), ]) ->assertSessionHasErrors('import'); $this->assertNull($store->fresh()->withdrawal_amount); } finally { Carbon::setTestNow(); } } public function test_valid_import_is_previewed_before_allowed_fields_are_committed(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '导入更新店', 'store_id' => 'TM-IMPORT-1', 'store_entity_name' => '原主体', 'login_password' => 'password-remains-private', 'withdrawal_path' => '银行卡', 'withdrawal_account_name' => '原账户', 'bank_card_last_four' => '1111', ]); $this->loginAs(); $file = $this->workbook([[ '天猫', '导入更新店', 'TM-IMPORT-1', '新主体', '支付宝', '新账户', '0022', ]]); $previewResponse = $this->post(route('business.withdrawal-management.import.preview', [ 'platform' => '天猫', ]), ['import' => $file]); $previewResponse ->assertRedirect(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '天猫', ])) ->assertSessionHas('withdrawalImportPreview', function (array $preview): bool { return $preview['total'] === 1 && $preview['changed'] === 1 && $preview['unchanged'] === 0 && $preview['preview'][0]['changes'] === '店铺主体、提现路径、提现账户名称、银行卡号后四位'; }); $this->assertSame('原主体', $store->fresh()->store_entity_name); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); $this->post(route('business.withdrawal-management.import.commit', [ 'platform' => '天猫', ]), ['preview_token' => $preview['token']]) ->assertRedirect() ->assertSessionHas('success', '提现配置导入完成,共更新 1 家店铺。'); $store = $store->fresh(); $this->assertSame('新主体', $store->store_entity_name); $this->assertSame('支付宝', $store->withdrawal_path); $this->assertSame('新账户', $store->withdrawal_account_name); $this->assertSame('0022', $store->bank_card_last_four); $this->assertSame('password-remains-private', $store->login_password); } public function test_invalid_bank_card_rejects_the_entire_preview_without_writes(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '非法卡号测试店', 'store_id' => 'JD-INVALID-1', 'store_entity_name' => '原主体', 'withdrawal_path' => '银行卡', ]); $this->loginAs(); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.preview'), ['import' => $this->workbook([[ '京东', '非法卡号测试店', 'JD-INVALID-1', '不应写入的主体', '', '', '12ab', ]])]) ->assertRedirect() ->assertSessionHasErrors('import'); $this->assertSame('原主体', $store->fresh()->store_entity_name); } public function test_clear_marker_clears_nullable_and_required_configuration_fields(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '拼多多', 'erp_store_name' => '清空配置测试店', 'store_id' => 'PDD-CLEAR-1', 'store_entity_name' => '待清空主体', 'withdrawal_path' => '银行卡', 'withdrawal_account_name' => '待清空账户', 'bank_card_last_four' => '8899', ]); $this->loginAs(); $previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->workbook([[ '拼多多', '清空配置测试店', 'PDD-CLEAR-1', '[清空]', '[清空]', '[清空]', '[清空]', ]]), ]); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); $this->post(route('business.withdrawal-management.import.commit'), [ 'preview_token' => $preview['token'], ])->assertRedirect()->assertSessionHasNoErrors(); $store = $store->fresh(); $this->assertSame('', $store->getRawOriginal('store_entity_name')); $this->assertSame('', $store->getRawOriginal('receiving_method')); $this->assertNull($store->withdrawal_account_name); $this->assertNull($store->bank_card_last_four); } public function test_broken_workbook_is_reported_as_an_import_error(): void { $this->loginAs(); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.preview'), [ 'import' => UploadedFile::fake()->createWithContent('损坏文件.xlsx', "PK\x03\x04broken"), ]) ->assertRedirect() ->assertSessionHasErrors([ 'import' => 'Excel 文件无法读取,请确认文件未损坏并使用最新模板。', ]); } public function test_commit_rejects_a_preview_when_store_changed_after_preview(): void { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '抖音', 'erp_store_name' => '并发修改测试店', 'store_id' => 'DY-STALE-1', 'store_entity_name' => '预览前主体', ]); $this->loginAs(); $previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->workbook([[ '抖音', '并发修改测试店', 'DY-STALE-1', '导入主体', '', '', '', ]]), ]); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); DB::table('business_store_receiving_accounts')->where('id', $store->id)->update([ 'store_entity_name' => '他人已修改主体', 'updated_at' => now()->addMinute(), ]); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.commit'), [ 'preview_token' => $preview['token'], ]) ->assertRedirect() ->assertSessionHasErrors('import'); $this->assertSame('他人已修改主体', $store->fresh()->store_entity_name); } public function test_compliance_uses_balance_minus_current_date_withdrawal_and_matches_export(): void { Carbon::setTestNow('2026-09-24 09:00:00'); try { $stores = []; foreach ([ ['达标测试店', '5000.00', '2026-09-23'], ['未达标测试店', '4999.99', '2026-09-23'], ['过期金额测试店', '5000.00', '2026-09-22'], ['缺少金额测试店', null, null], ['缺少余额测试店', '5000.00', '2026-09-23'], ] as [$name, $amount, $date]) { $stores[$name] = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => $name, 'store_entity_name' => '测试主体', 'withdrawal_amount' => $amount, 'withdrawal_balance_date' => $date, ]); } $stores['缺少标准测试店'] = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '唯品会', 'erp_store_name' => '缺少标准测试店', 'store_entity_name' => '测试主体', 'withdrawal_amount' => '5000.00', 'withdrawal_balance_date' => '2026-09-23', ]); $balances = []; foreach ($stores as $store) { $balances[(int) $store->id] = [ 'status' => $store->erp_store_name === '缺少余额测试店' ? 'unmatched' : 'matched', 'balance' => $store->erp_store_name === '缺少余额测试店' ? null : 10000.00, 'source_store_name' => $store->erp_store_name, 'source_platform' => '京东', ]; } $mirror = Mockery::mock(JushuitanShopBalanceRepository::class); $mirror->shouldReceive('latestBalanceDate')->andReturn('2026-09-23'); $mirror->shouldReceive('availableDates')->andReturn(['2026-09-23', '2026-09-22']); $historyDates = ['2026-09-21', '2026-09-22', '2026-09-23']; $balancesByDate = []; foreach (array_keys($balances) as $storeId) { foreach ($historyDates as $date) { $balancesByDate[$storeId][$date] = $balances[$storeId]; } } $mirror->shouldReceive('matchesForStoresOnDates')->andReturn($balancesByDate); $mirror->shouldReceive('countForDate')->andReturn(6); $this->app->instance(JushuitanShopBalanceRepository::class, $mirror); $report = $this->app->make(WithdrawalManagementReport::class)->results(request()); $this->assertSame('yes', $report['compliance'][(int) $stores['达标测试店']->id]); $this->assertSame('no', $report['compliance'][(int) $stores['未达标测试店']->id]); $this->assertNull($report['compliance'][(int) $stores['过期金额测试店']->id]); $this->assertNull($report['compliance'][(int) $stores['缺少金额测试店']->id]); $this->assertNull($report['compliance'][(int) $stores['缺少余额测试店']->id]); $this->assertNull($report['compliance'][(int) $stores['缺少标准测试店']->id]); $page = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '京东', ])); $page->assertOk() ->assertSeeText('待核验'); $this->assertMatchesRegularExpression('/data-withdrawal-column="compliance">\s*是<\/span>/', $page->getContent()); $this->assertMatchesRegularExpression('/data-withdrawal-column="compliance">\s*否<\/span>/', $page->getContent()); $response = $this->get(route('business.withdrawal-management.export', ['platform' => '京东'])); $path = $this->temporaryPath('withdrawal-compliance-export-'); file_put_contents($path, $response->streamedContent()); $spreadsheet = IOFactory::load($path); try { $sheet = $spreadsheet->getActiveSheet(); $values = []; for ($row = 2; $row <= $sheet->getHighestDataRow(); $row++) { $values[(string) $sheet->getCell("B{$row}")->getValue()] = [ $sheet->getCell("N{$row}")->getValue(), $sheet->getCell("O{$row}")->getValue(), ]; } $this->assertSame([5000.0, '是'], $values['达标测试店']); $this->assertSame([4999.99, '否'], $values['未达标测试店']); $this->assertSame(['待接入', '待核验'], $values['过期金额测试店']); $this->assertSame(['待接入', '待核验'], $values['缺少金额测试店']); $this->assertSame([5000.0, '待核验'], $values['缺少余额测试店']); } finally { $spreadsheet->disconnectWorksheets(); } } finally { Carbon::setTestNow(); } } public function test_withdrawal_amount_import_is_validated_and_bound_to_current_balance_date(): void { Carbon::setTestNow('2026-09-24 09:00:00'); try { $store = StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '京东', 'erp_store_name' => '金额导入测试店', 'store_entity_name' => '测试主体', ]); $this->loginAs(); $this->withExceptionHandling() ->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->workbook([['京东', '金额导入测试店', '', '', '', '', '', '-1']]), ]) ->assertSessionHasErrors('import'); $this->assertNull($store->fresh()->withdrawal_amount); $previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [ 'import' => $this->workbook([['京东', '金额导入测试店', '', '', '', '', '', '5000.00']]), ]); $previewResponse->assertSessionHas('withdrawalImportPreview', function (array $preview): bool { return $preview['preview'][0]['changes'] === '提现金额'; }); $preview = $previewResponse->getSession()->get('withdrawalImportPreview'); $this->post(route('business.withdrawal-management.import.commit'), [ 'preview_token' => $preview['token'], ])->assertSessionHasNoErrors(); $this->assertSame('5000.00', $store->fresh()->withdrawal_amount); $this->assertSame('2026-09-23', $store->fresh()->withdrawal_balance_date); } finally { Carbon::setTestNow(); } } public function test_page_defaults_to_the_latest_balance_and_supports_a_date_filter(): void { Carbon::setTestNow('2026-10-20 10:00:00'); try { StoreReceivingAccount::query()->create([ 'company_id' => $this->company->id, 'store_platform' => '天猫', 'erp_store_name' => '五天保留测试店', 'shop_name' => '五天保留测试店', 'store_entity_name' => '五天保留测试主体', ]); $this->insertBalanceSnapshot('2026-10-15', '2026-10-16 18:31:00', '天猫-五天保留测试店', 12345.67); $this->insertBalanceSnapshot('2026-10-14', '2026-10-15 18:31:00', '天猫-五天保留测试店', 99999.99); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '天猫', ])); $historyResponse = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'withdrawal-management', 'platform' => '天猫', 'balance_date' => '2026-10-14', ])); $response->assertOk() ->assertSeeText('10月14日店铺余额') ->assertSeeText('10月15日店铺余额') ->assertSeeText('12,345.67') ->assertSeeText('99,999.99') ->assertSee('余额日期:2026-10-15', false); $historyResponse->assertOk() ->assertSeeText('10月14日店铺余额') ->assertSeeText('99,999.99') ->assertDontSeeText('12,345.67') ->assertSee('余额日期:2026-10-14', false); } finally { Carbon::setTestNow(); } } private function insertBalanceSnapshot( string $balanceDate, string $importedAt, string $storeName, float $balance, ): void { DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([ 'balance_date' => $balanceDate, 'source_scope' => 'all', 'source_platform' => '天猫', 'store_name' => $storeName, 'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName), 'platform_account_id' => 'feature-' . $balanceDate, 'balance' => $balance, 'source_row_key' => hash('sha256', $balanceDate . '|' . $storeName), 'source_file_name' => 'feature-fixture.json', 'source_sha256' => str_repeat('d', 64), 'imported_at' => Carbon::parse($importedAt), 'created_at' => Carbon::parse($importedAt), 'updated_at' => Carbon::parse($importedAt), ]); } /** @param list> $rows */ private function workbook(array $rows): UploadedFile { $path = $this->temporaryPath('withdrawal-import-'); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('提现管理'); $sheet->fromArray(self::IMPORT_HEADERS, null, 'A1'); if ($rows !== []) { $sheet->fromArray($rows, null, 'A2'); } (new Xlsx($spreadsheet))->save($path); $spreadsheet->disconnectWorksheets(); return new UploadedFile( $path, '提现管理.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', null, true, ); } /** @param list> $rows */ private function withdrawalWorkbook(array $rows): UploadedFile { $path = $this->temporaryPath('withdrawal-amount-import-'); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('提现金额'); $sheet->fromArray(self::WITHDRAWAL_TEMPLATE_HEADERS, null, 'A1'); if ($rows !== []) { $sheet->fromArray($rows, null, 'A2'); } (new Xlsx($spreadsheet))->save($path); $spreadsheet->disconnectWorksheets(); return new UploadedFile( $path, '提现金额.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', null, true, ); } private function temporaryPath(string $prefix): string { $path = tempnam(sys_get_temp_dir(), $prefix); $this->temporaryFiles[] = $path; return $path; } protected function tearDown(): void { foreach ($this->temporaryFiles as $path) { @unlink($path); } parent::tearDown(); } }