From 0dc45608178c5c7f8e24021035f5db5740f85acd Mon Sep 17 00:00:00 2001 From: chenxi Date: Tue, 18 Aug 2026 10:09:43 +0800 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20=E5=8D=A0=E7=94=A8=E8=90=A5?= =?UTF-8?q?=E9=94=80=E8=B4=A6=E6=88=B7=E8=B4=A6=E5=8D=95=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E4=B8=8E=E6=A0=87=E7=AD=BE=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -- 2.47.2 From 90366c7cd28865367875342b5bcd5cebe10254dd Mon Sep 17 00:00:00 2001 From: chenxi Date: Mon, 24 Aug 2026 14:27:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=BA=97=E9=93=BA=E8=90=A5=E9=94=80?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E8=B4=A6=E5=8D=95=E5=A2=9E=E5=8A=A0=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Business/Pages.php | 230 +++++++++++++++--- .../Repositories/AlibabaBillRepository.php | 38 +++ .../Repositories/JdWalletBillRepository.php | 37 +++ .../TmallTaobaoSpreadBillRepository.php | 38 +++ .../partials/embedded-table.blade.php | 14 ++ .../partials/embedded-table.blade.php | 14 ++ .../partials/embedded-table.blade.php | 15 ++ .../MarketingAccountBillsTabsTest.php | 120 +++++++++ ...rketingAccountBillMirrorRepositoryTest.php | 10 + 9 files changed, 478 insertions(+), 38 deletions(-) diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 26e6c3b1..20046603 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -483,61 +483,203 @@ class Pages extends Controller string $page, string $tab, ) { - abort_unless($tab === 'pinduoduo', 404); + abort_unless(in_array($tab, ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet'], true), 404); + return match ($tab) { + 'taobao-alipay' => $this->exportTaobaoAccountBills($request, $section, $page, $tab), + 'taobao-wechat' => $this->exportAlibabaAccountBills($request, $section, $page, $tab), + 'jd-wallet' => $this->exportJdAccountBills($request, $section, $page, $tab), + default => $this->exportPinduoduoAccountBills($request, $section, $page, $tab), + }; + } + + private function exportTaobaoAccountBills(Request $request, string $section, string $page, string $tab) + { $storeName = mb_substr(trim((string) $request->query('store_name', '')), 0, 191); $fromDate = $this->dateFilter($request->query('from')); $toDate = $this->dateFilter($request->query('to')); $flowType = in_array($request->query('flow_type'), ['income', 'expense'], true) ? (string) $request->query('flow_type') : ''; - $filters = [ - 'store_name' => $storeName !== '' ? $storeName : null, - 'from' => $fromDate, - 'to' => $toDate, - 'flow_type' => $flowType !== '' ? $flowType : null, - ]; - $redirectParameters = array_filter([ - 'section' => $section, - 'page' => $page, - 'tab' => $tab, + $redirectParameters = $this->marketingBillRedirectParameters($section, $page, $tab, [ 'store_name' => $storeName, 'from' => $fromDate, 'to' => $toDate, 'flow_type' => $flowType, - ], static fn (mixed $value): bool => $value !== '' && $value !== null); + ]); + $repository = app(TmallTaobaoSpreadBillRepository::class); + + if (! $repository->usesBusinessMirror()) { + return redirect()->route('business.pages.show', $redirectParameters) + ->with('error', '淘系账单共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'); + } + + return $this->streamMarketingAccountBillCsv( + $repository->exportRows( + $storeName !== '' ? $storeName : null, + $fromDate !== '' ? $fromDate : null, + $toDate !== '' ? $toDate : null, + $flowType !== '' ? $flowType : null, + ), + ['店铺名称', '记账日期', '交易日期', '金额(元)', '余额(元)', '备注', 'finType', '收支类型', '交易类型'], + static fn (array $row): array => [ + $row['store_name'] ?? '', $row['bookkeeping_time'] ?? '', $row['transaction_date'] ?? '', + $row['amount'] ?? 0, $row['balance'] ?? 0, $row['comment'] ?? '', $row['fin_type'] ?? '', + $row['income_expense_type'] ?? '', $row['transaction_type'] ?? '', + ], + 'taobao_account_bills_' . $this->marketingBillRange($fromDate, $toDate) . '.csv', + $redirectParameters, + '淘系账单导出暂时无法完成,请稍后重试。', + ); + } + + private function exportAlibabaAccountBills(Request $request, string $section, string $page, string $tab) + { + $shopName = mb_substr(trim((string) $request->query('shop_name', '')), 0, 191); + $productType = mb_substr(trim((string) $request->query('product_type', '')), 0, 191); + $fromDate = $this->dateFilter($request->query('from')); + $toDate = $this->dateFilter($request->query('to')); + $redirectParameters = $this->marketingBillRedirectParameters($section, $page, $tab, [ + 'shop_name' => $shopName, + 'product_type' => $productType, + 'from' => $fromDate, + 'to' => $toDate, + ]); + $repository = app(AlibabaBillRepository::class); + + if (! $repository->usesBusinessMirror()) { + return redirect()->route('business.pages.show', $redirectParameters) + ->with('error', '阿里巴巴账单共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'); + } + + return $this->streamMarketingAccountBillCsv( + $repository->exportRows( + $shopName !== '' ? $shopName : null, + $productType !== '' ? $productType : null, + $fromDate !== '' ? $fromDate : null, + $toDate !== '' ? $toDate : null, + ), + ['店铺名称', '交易时间', '产品类型', '入账金额(元)', '出帐金额(元)'], + static fn (array $row): array => [ + $row['shop_name'] ?? '', $row['trade_day'] ?? '', $row['product_type'] ?? '', + $row['income_amount'] ?? 0, $row['expense_amount'] ?? 0, + ], + 'alibaba_account_bills_' . $this->marketingBillRange($fromDate, $toDate) . '.csv', + $redirectParameters, + '阿里巴巴账单导出暂时无法完成,请稍后重试。', + ); + } + + private function exportJdAccountBills(Request $request, string $section, string $page, string $tab) + { + $ledger = $request->query('jd_ledger') === 'consumption' ? 'consumption' : 'recharge'; + $storeName = mb_substr(trim((string) $request->query('store_name', '')), 0, 191); + $fromDate = $this->dateFilter($request->query('from')); + $toDate = $this->dateFilter($request->query('to')); + $redirectParameters = $this->marketingBillRedirectParameters($section, $page, $tab, [ + 'jd_ledger' => $ledger, + 'store_name' => $storeName, + 'from' => $fromDate, + 'to' => $toDate, + ]); + $repository = app(JdWalletBillRepository::class); + + if (! $repository->usesBusinessMirror($ledger)) { + return redirect()->route('business.pages.show', $redirectParameters) + ->with('error', '京东账单共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'); + } + + $headers = $ledger === 'consumption' + ? ['店铺名称', '金额(元)', '余额(元)', '交易类型', '投放日期', '结算日期', '流水号'] + : ['店铺名称', '金额(元)', '余额(元)', '交易类型', '商户订单号', '充值时间', '到账时间', '流水号', '状态', '备注']; + $rowValues = $ledger === 'consumption' + ? static fn (array $row): array => [ + $row['store_name'] ?? '', $row['amount'] ?? 0, $row['balance'] ?? 0, + $row['transaction_type'] ?? '', $row['investment_date'] ?? '', + $row['settlement_date'] ?? '', $row['serial_number'] ?? '', + ] + : static fn (array $row): array => [ + $row['store_name'] ?? '', $row['amount'] ?? 0, $row['balance'] ?? 0, + $row['transaction_type'] ?? '', $row['order_number'] ?? '', $row['recharge_time'] ?? '', + $row['arrival_time'] ?? '', $row['serial_number'] ?? '', $row['status'] ?? '', $row['notes'] ?? '', + ]; + + return $this->streamMarketingAccountBillCsv( + $repository->exportRows( + $ledger, + $storeName !== '' ? $storeName : null, + $fromDate !== '' ? $fromDate : null, + $toDate !== '' ? $toDate : null, + ), + $headers, + $rowValues, + 'jd_' . $ledger . '_account_bills_' . $this->marketingBillRange($fromDate, $toDate) . '.csv', + $redirectParameters, + '京东账单导出暂时无法完成,请稍后重试。', + ); + } + + private function exportPinduoduoAccountBills(Request $request, string $section, string $page, string $tab) + { + $storeName = mb_substr(trim((string) $request->query('store_name', '')), 0, 191); + $fromDate = $this->dateFilter($request->query('from')); + $toDate = $this->dateFilter($request->query('to')); + $flowType = in_array($request->query('flow_type'), ['income', 'expense'], true) + ? (string) $request->query('flow_type') + : ''; + $redirectParameters = $this->marketingBillRedirectParameters($section, $page, $tab, [ + 'store_name' => $storeName, + 'from' => $fromDate, + 'to' => $toDate, + 'flow_type' => $flowType, + ]); $repository = app(PinduoduoSpreadBillRepository::class); + + if (! $repository->usesBusinessMirror()) { + return redirect()->route('business.pages.show', $redirectParameters) + ->with('error', '拼多多账单共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'); + } + + return $this->streamMarketingAccountBillCsv( + $repository->exportRows( + $storeName !== '' ? $storeName : null, + $fromDate !== '' ? $fromDate : null, + $toDate !== '' ? $toDate : null, + $flowType !== '' ? $flowType : null, + ), + ['店铺名称', '金额(元)', '余额(元)', '摘要', '收支类型', '资金类型', '费用时间'], + static fn (array $row): array => [ + $row['store_name'] ?? '', $row['amount'] ?? 0, $row['balance'] ?? 0, + $row['summary'] ?? '', $row['income_expense_type'] ?? '', + $row['fund_type'] ?? '', $row['cost_time'] ?? '', + ], + 'pinduoduo_account_bills_' . $this->marketingBillRange($fromDate, $toDate) . '.csv', + $redirectParameters, + '拼多多账单导出暂时无法完成,请稍后重试。', + ); + } + + private function streamMarketingAccountBillCsv( + iterable $rows, + array $headers, + callable $rowValues, + string $filename, + array $redirectParameters, + string $errorMessage, + ) { $csv = null; try { - if (! $repository->usesBusinessMirror()) { - return redirect()->route('business.pages.show', $redirectParameters) - ->with('error', '拼多多账单共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'); - } - $csv = fopen('php://temp/maxmemory:5242880', 'w+b'); if ($csv === false) { - throw new \RuntimeException('无法创建拼多多账单导出临时文件。'); + throw new \RuntimeException('无法创建账单导出临时文件。'); } fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); - fputcsv($csv, ['店铺名称', '金额(元)', '余额(元)', '摘要', '收支类型', '资金类型', '费用时间']); + fputcsv($csv, $headers); - foreach ($repository->exportRows( - $filters['store_name'], - $filters['from'], - $filters['to'], - $filters['flow_type'], - ) as $row) { - fputcsv($csv, [ - $row['store_name'] ?? '', - $row['amount'] ?? 0, - $row['balance'] ?? 0, - $row['summary'] ?? '', - $row['income_expense_type'] ?? '', - $row['fund_type'] ?? '', - $row['cost_time'] ?? '', - ]); + foreach ($rows as $row) { + fputcsv($csv, $rowValues($row)); } rewind($csv); @@ -548,20 +690,32 @@ class Pages extends Controller report($exception); return redirect()->route('business.pages.show', $redirectParameters) - ->with('error', '拼多多账单导出暂时无法完成,请稍后重试。'); + ->with('error', $errorMessage); } - $range = ($fromDate ?: 'all') . '_' . ($toDate ?: 'all'); - return response()->streamDownload(function () use ($csv): void { fpassthru($csv); fclose($csv); - }, 'pinduoduo_account_bills_' . $range . '.csv', [ + }, $filename, [ 'Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store', ]); } + private function marketingBillRedirectParameters(string $section, string $page, string $tab, array $filters): array + { + return array_filter(array_merge([ + 'section' => $section, + 'page' => $page, + 'tab' => $tab, + ], $filters), static fn (mixed $value): bool => $value !== '' && $value !== null); + } + + private function marketingBillRange(string $fromDate, string $toDate): string + { + return ($fromDate ?: 'all') . '_' . ($toDate ?: 'all'); + } + protected function contentForBoardDeductions(): array { $filters = [ diff --git a/modules/EcommerceFinance/Repositories/AlibabaBillRepository.php b/modules/EcommerceFinance/Repositories/AlibabaBillRepository.php index cb5b8296..f04d9b8d 100644 --- a/modules/EcommerceFinance/Repositories/AlibabaBillRepository.php +++ b/modules/EcommerceFinance/Repositories/AlibabaBillRepository.php @@ -103,6 +103,44 @@ class AlibabaBillRepository ]; } + /** @return \Generator> */ + public function exportRows( + ?string $shopName = null, + ?string $productType = null, + ?string $fromDate = null, + ?string $toDate = null, + int $batchSize = 100, + ): \Generator { + if (! $this->usesBusinessMirror()) { + throw new \RuntimeException('阿里巴巴账单共享业务镜像尚未完成同步。'); + } + + $batchSize = max(1, min(100, $batchSize)); + $page = 1; + $exported = 0; + + do { + $result = $this->mirror->paginateAlibaba( + $shopName, + $productType, + $fromDate, + $toDate, + $page, + $batchSize, + ); + $rows = is_array($result['rows'] ?? null) ? $result['rows'] : []; + $total = max(0, (int) ($result['total'] ?? 0)); + + foreach ($rows as $row) { + yield $this->normalize($row); + } + + $count = count($rows); + $exported += $count; + $page++; + } while ($count > 0 && $exported < $total); + } + /** @return array{income_total: float, expense_total: float, net: float, count: int} */ public function summary( ?string $shopName = null, diff --git a/modules/EcommerceFinance/Repositories/JdWalletBillRepository.php b/modules/EcommerceFinance/Repositories/JdWalletBillRepository.php index 4c22d7d4..5201cac2 100644 --- a/modules/EcommerceFinance/Repositories/JdWalletBillRepository.php +++ b/modules/EcommerceFinance/Repositories/JdWalletBillRepository.php @@ -117,6 +117,43 @@ class JdWalletBillRepository ], 'creatTime', 'id', fn (array $row): array => $this->normalizeConsumption($row)); } + /** @return \Generator> */ + public function exportRows( + string $ledger, + ?string $shopName = null, + ?string $fromDate = null, + ?string $toDate = null, + int $batchSize = 100, + ): \Generator { + $ledger = $ledger === 'consumption' ? 'consumption' : 'recharge'; + + if (! $this->usesBusinessMirror($ledger)) { + throw new \RuntimeException('京东账单共享业务镜像尚未完成同步。'); + } + + $batchSize = max(1, min(100, $batchSize)); + $page = 1; + $exported = 0; + + do { + $result = $ledger === 'consumption' + ? $this->mirror->paginateJdConsumption($shopName, $fromDate, $toDate, $page, $batchSize) + : $this->mirror->paginateJdRecharge($shopName, $fromDate, $toDate, $page, $batchSize); + $rows = is_array($result['rows'] ?? null) ? $result['rows'] : []; + $total = max(0, (int) ($result['total'] ?? 0)); + + foreach ($rows as $row) { + yield $ledger === 'consumption' + ? $this->normalizeConsumption($row) + : $this->normalizeRecharge($row); + } + + $count = count($rows); + $exported += $count; + $page++; + } while ($count > 0 && $exported < $total); + } + /** @return array{amount_total: float, balance_total: float, count: int} */ public function summaryRecharge(?string $shopName = null, ?string $fromDate = null, ?string $toDate = null): array { diff --git a/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php b/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php index ccbf69dc..c7022165 100644 --- a/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php +++ b/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php @@ -158,6 +158,44 @@ class TmallTaobaoSpreadBillRepository ]; } + /** @return \Generator> */ + public function exportRows( + ?string $storeName = null, + ?string $fromDate = null, + ?string $toDate = null, + ?string $flowType = null, + int $batchSize = 200, + ): \Generator { + if (! $this->usesBusinessMirror()) { + throw new \RuntimeException('淘系账单共享业务镜像尚未完成同步。'); + } + + $batchSize = max(1, min(200, $batchSize)); + $page = 1; + $exported = 0; + + do { + $result = $this->mirror->paginateTaobao( + $storeName, + $fromDate, + $toDate, + $flowType, + $page, + $batchSize, + ); + $rows = is_array($result['rows'] ?? null) ? $result['rows'] : []; + $total = max(0, (int) ($result['total'] ?? 0)); + + foreach ($rows as $row) { + yield $this->normalize($row); + } + + $count = count($rows); + $exported += $count; + $page++; + } while ($count > 0 && $exported < $total); + } + /** * 当前筛选条件下的汇总(金额单位:元)。 * diff --git a/resources/views/banking/marketing_alibaba_bills/partials/embedded-table.blade.php b/resources/views/banking/marketing_alibaba_bills/partials/embedded-table.blade.php index cd5983c7..2134e202 100644 --- a/resources/views/banking/marketing_alibaba_bills/partials/embedded-table.blade.php +++ b/resources/views/banking/marketing_alibaba_bills/partials/embedded-table.blade.php @@ -35,6 +35,15 @@ $incomeWan = ((float) ($summary['income_total'] ?? 0)) / 10000; $expenseWan = ((float) ($summary['expense_total'] ?? 0)) / 10000; $netWan = ((float) ($summary['net'] ?? 0)) / 10000; + $exportParameters = array_filter([ + 'section' => request()->route('section') ?? 'billing-transactions', + 'page' => request()->route('page') ?? 'marketing-account-bills', + 'tab' => 'taobao-wechat', + 'shop_name' => (string) ($filters['shop_name'] ?? ''), + 'product_type' => (string) ($filters['product_type'] ?? ''), + 'from' => (string) ($filters['from'] ?? ''), + 'to' => (string) ($filters['to'] ?? ''), + ], static fn ($value) => $value !== ''); @endphp
@@ -91,6 +100,11 @@ @if (($filters['shop_name'] ?? '') !== '' || ($filters['product_type'] ?? '') !== '' || ($filters['from'] ?? '') !== '' || ($filters['to'] ?? '') !== '')
共匹配 {{ $bills->total() }} 条
diff --git a/resources/views/banking/marketing_jd_wallet_bills/partials/embedded-table.blade.php b/resources/views/banking/marketing_jd_wallet_bills/partials/embedded-table.blade.php index 39825cc3..45307a97 100644 --- a/resources/views/banking/marketing_jd_wallet_bills/partials/embedded-table.blade.php +++ b/resources/views/banking/marketing_jd_wallet_bills/partials/embedded-table.blade.php @@ -49,6 +49,15 @@ $balanceWan = (float) ($summary['balance_total'] ?? 0) / 10000; $isConsumption = $ledger === 'consumption'; $currentUrl = request()->url(); + $exportParameters = array_filter([ + 'section' => request()->route('section') ?? 'billing-transactions', + 'page' => request()->route('page') ?? 'marketing-account-bills', + 'tab' => 'jd-wallet', + 'jd_ledger' => $ledger, + 'store_name' => (string) ($filters['store_name'] ?? ''), + 'from' => (string) ($filters['from'] ?? ''), + 'to' => (string) ($filters['to'] ?? ''), + ], static fn ($value) => $value !== ''); @endphp
@@ -112,6 +121,11 @@
共匹配 {{ $bills->total() }} 条
diff --git a/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php b/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php index ab6da197..0333008b 100644 --- a/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php +++ b/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php @@ -77,6 +77,15 @@ $currentFrom = (string) ($filters['from'] ?? ''); $currentTo = (string) ($filters['to'] ?? ''); $currentFlowType = (string) ($filters['flow_type'] ?? ''); + $exportParameters = array_filter([ + 'section' => request()->route('section') ?? 'billing-transactions', + 'page' => request()->route('page') ?? 'marketing-account-bills', + 'tab' => 'taobao-alipay', + 'store_name' => $currentStore, + 'from' => $currentFrom, + 'to' => $currentTo, + 'flow_type' => $currentFlowType, + ], static fn ($value) => $value !== ''); @endphp
@@ -119,6 +128,12 @@ 清除 + + + 导出 +
@if ($currentStore !== '' || $currentFrom !== '' || $currentTo !== '' || $currentFlowType !== '')
diff --git a/tests/Feature/Business/MarketingAccountBillsTabsTest.php b/tests/Feature/Business/MarketingAccountBillsTabsTest.php index 721b84fd..fe3d9525 100644 --- a/tests/Feature/Business/MarketingAccountBillsTabsTest.php +++ b/tests/Feature/Business/MarketingAccountBillsTabsTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Business; use App\Models\Auth\Permission; use Mockery; use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException; +use Modules\EcommerceFinance\Repositories\AlibabaBillRepository; use Modules\EcommerceFinance\Repositories\JdWalletBillRepository; use Modules\EcommerceFinance\Repositories\MarketingAccountBillMirrorRepository; use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository; @@ -82,6 +83,12 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase ->assertSeeText('淘系支付宝账户') ->assertSeeText('Alibaba') ->assertSeeText('京东') + ->assertSeeText('导出') + ->assertSee(e(route('business.pages.export', [ + 'section' => 'billing-transactions', + 'page' => 'marketing-account-bills', + 'tab' => 'taobao-wechat', + ])), false) ->assertDontSeeText('淘系微信账户') ->assertDontSeeText('京东网银钱包账户') ->assertSee('class="my-6 business-page--marketing-account-bills"', false); @@ -184,6 +191,117 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase ->assertSessionHas('error', '拼多多账单共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'); } + public function testTaobaoBillsExportUsesTheCurrentMirrorFilters(): void + { + $this->allowMarketingAccountBillsPage(); + $repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class); + $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); + $repository->shouldReceive('exportRows') + ->once() + ->with('淘系筛选店铺', '2026-07-01', '2026-07-31', 'income') + ->andReturn((function (): \Generator { + yield [ + 'store_name' => '淘系筛选店铺', + 'bookkeeping_time' => '2026-07-31 09:00:00', + 'transaction_date' => '2026-07-31', + 'amount' => 12.34, + 'balance' => 56.78, + 'comment' => '充值', + 'fin_type' => 'DEBIT', + 'income_expense_type' => '收入', + 'transaction_type' => '充值', + ]; + })()); + $this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository); + + $response = $this->loginAs()->get(route('business.pages.export', [ + 'section' => 'billing-transactions', + 'page' => 'marketing-account-bills', + 'tab' => 'taobao-alipay', + 'store_name' => '淘系筛选店铺', + 'from' => '2026-07-01', + 'to' => '2026-07-31', + 'flow_type' => 'income', + ])); + + $response->assertOk()->assertDownload('taobao_account_bills_2026-07-01_2026-07-31.csv'); + $content = $response->streamedContent(); + $this->assertStringContainsString('店铺名称,记账日期,交易日期,金额(元),余额(元),备注,finType,收支类型,交易类型', $content); + $this->assertStringContainsString('淘系筛选店铺,"2026-07-31 09:00:00",2026-07-31,12.34,56.78,充值,DEBIT,收入,充值', $content); + } + + public function testAlibabaBillsExportUsesTheCurrentMirrorFilters(): void + { + $this->allowMarketingAccountBillsPage(); + $repository = Mockery::mock(AlibabaBillRepository::class); + $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); + $repository->shouldReceive('exportRows') + ->once() + ->with('阿里筛选店铺', '全站推广', '2026-07-01', '2026-07-31') + ->andReturn((function (): \Generator { + yield [ + 'shop_name' => '阿里筛选店铺', + 'trade_day' => '2026-07-31 10:00:00', + 'product_type' => '全站推广', + 'income_amount' => 100.25, + 'expense_amount' => 20.5, + ]; + })()); + $this->app->instance(AlibabaBillRepository::class, $repository); + + $response = $this->loginAs()->get(route('business.pages.export', [ + 'section' => 'billing-transactions', + 'page' => 'marketing-account-bills', + 'tab' => 'taobao-wechat', + 'shop_name' => '阿里筛选店铺', + 'product_type' => '全站推广', + 'from' => '2026-07-01', + 'to' => '2026-07-31', + ])); + + $response->assertOk()->assertDownload('alibaba_account_bills_2026-07-01_2026-07-31.csv'); + $content = $response->streamedContent(); + $this->assertStringContainsString('店铺名称,交易时间,产品类型,入账金额(元),出帐金额(元)', $content); + $this->assertStringContainsString('阿里筛选店铺,"2026-07-31 10:00:00",全站推广,100.25,20.5', $content); + } + + public function testJdConsumptionBillsExportUsesTheCurrentMirrorFilters(): void + { + $this->allowMarketingAccountBillsPage(); + $repository = Mockery::mock(JdWalletBillRepository::class); + $repository->shouldReceive('usesBusinessMirror')->once()->with('consumption')->andReturnTrue(); + $repository->shouldReceive('exportRows') + ->once() + ->with('consumption', '京东筛选店铺', '2026-07-01', '2026-07-31') + ->andReturn((function (): \Generator { + yield [ + 'store_name' => '京东筛选店铺', + 'amount' => 88.6, + 'balance' => 10.4, + 'transaction_type' => '推广消耗', + 'investment_date' => '2026-07-30 12:00:00', + 'settlement_date' => '2026-07-31 00:00:00', + 'serial_number' => 'JD-1', + ]; + })()); + $this->app->instance(JdWalletBillRepository::class, $repository); + + $response = $this->loginAs()->get(route('business.pages.export', [ + 'section' => 'billing-transactions', + 'page' => 'marketing-account-bills', + 'tab' => 'jd-wallet', + 'jd_ledger' => 'consumption', + 'store_name' => '京东筛选店铺', + 'from' => '2026-07-01', + 'to' => '2026-07-31', + ])); + + $response->assertOk()->assertDownload('jd_consumption_account_bills_2026-07-01_2026-07-31.csv'); + $content = $response->streamedContent(); + $this->assertStringContainsString('店铺名称,金额(元),余额(元),交易类型,投放日期,结算日期,流水号', $content); + $this->assertStringContainsString('京东筛选店铺,88.6,10.4,推广消耗,"2026-07-30 12:00:00","2026-07-31 00:00:00",JD-1', $content); + } + public function testJdWalletHasRechargeAndConsumptionViewsWithoutChangingTheDataSource(): void { $this->allowMarketingAccountBillsPage(); @@ -201,6 +319,8 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase ->assertSeeText('消耗账单') ->assertSeeText('金额(元)') ->assertSeeText('充值时间') + ->assertSeeText('导出') + ->assertSee('aria-label="导出当前筛选结果"', false) ->assertDontSeeText('投放日期') ->assertSee('data-jd-wallet-panel', false) ->assertSee('data-jd-wallet-navigation', false); diff --git a/tests/Unit/EcommerceFinance/MarketingAccountBillMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/MarketingAccountBillMirrorRepositoryTest.php index 609ae15b..19d6b789 100644 --- a/tests/Unit/EcommerceFinance/MarketingAccountBillMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/MarketingAccountBillMirrorRepositoryTest.php @@ -90,6 +90,10 @@ class MarketingAccountBillMirrorRepositoryTest extends TestCase $pddMonthly = $pinduoduo->paginateMonthlyBalances('镜像店铺', '2026-06', 1, 25); $alibabaPage2 = $alibaba->paginate('阿里店', '全站', '2026-06-01', '2026-06-30', 2, 1); $jdRechargePage2 = $jd->paginateRecharge('京东店', '2026-06-01', '2026-06-30', 2, 1); + $taobaoExport = iterator_to_array($taobao->exportRows('镜像店铺', '2026-06-01', '2026-06-30', null, 1)); + $alibabaExport = iterator_to_array($alibaba->exportRows('阿里店', '全站', '2026-06-01', '2026-06-30', 1)); + $jdRechargeExport = iterator_to_array($jd->exportRows('recharge', '京东店', '2026-06-01', '2026-06-30', 1)); + $jdConsumptionExport = iterator_to_array($jd->exportRows('consumption', '京东店', '2026-06-01', '2026-06-30', 1)); $this->assertSame(2, $taobaoPage2['total']); $this->assertSame(50.0, $taobaoPage2['rows'][0]['amount']); @@ -116,6 +120,12 @@ class MarketingAccountBillMirrorRepositoryTest extends TestCase $this->assertSame(['京东店'], $jd->shopOptions()); $this->assertSame(300.0, $jd->summaryRecharge('京东店')['amount_total']); $this->assertSame(30.0, $jd->summaryConsumption('京东店')['amount_total']); + $this->assertCount(2, $taobaoExport); + $this->assertSame([20.0, 50.0], array_column($taobaoExport, 'amount')); + $this->assertCount(2, $alibabaExport); + $this->assertSame([20.0, 10.0], array_column($alibabaExport, 'income_amount')); + $this->assertSame(['R2', 'R1'], array_column($jdRechargeExport, 'order_number')); + $this->assertSame(['C1'], array_column($jdConsumptionExport, 'serial_number')); } public function test_unready_dataset_keeps_the_existing_upstream_repository_path(): void -- 2.47.2