563 lines
24 KiB
PHP
563 lines
24 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use App\Models\Auth\Permission;
|
||
use App\Models\Business\StoreReceivingAccount;
|
||
use App\Business\WithdrawalManagementReport;
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Mockery;
|
||
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
|
||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
class WithdrawalManagementImportExportTest extends FeatureTestCase
|
||
{
|
||
private const HEADERS = [
|
||
'店铺平台',
|
||
'ERP店铺名称',
|
||
'平台店铺ID',
|
||
'店铺主体',
|
||
'提现路径',
|
||
'提现账户名称',
|
||
'银行卡号(后四位)',
|
||
];
|
||
|
||
private const IMPORT_HEADERS = [...self::HEADERS, '提现金额'];
|
||
|
||
/** @var list<string> */
|
||
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);
|
||
}
|
||
|
||
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()->format('n月j日') . '店铺余额',
|
||
'预留标准',
|
||
'提现金额',
|
||
'是否达标',
|
||
], $sheet->rangeToArray('A1:K1')[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_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']);
|
||
$mirror->shouldReceive('matchesForStores')->andReturn($balances);
|
||
$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 class="biz-badge biz-badge-yes">是<\/span>/', $page->getContent());
|
||
$this->assertMatchesRegularExpression('/data-withdrawal-column="compliance">\s*<span class="biz-badge biz-badge-no">否<\/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("J{$row}")->getValue(),
|
||
$sheet->getCell("K{$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月20日店铺余额')
|
||
->assertSeeText('12,345.67')
|
||
->assertDontSeeText('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<list<mixed>> $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,
|
||
);
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|