店铺营销账户账单各平台导出 #77

Merged
chenxi merged 3 commits from feature/chenxi-marketing-bill-export-labels into main 2026-08-24 14:29:47 +08:00
9 changed files with 478 additions and 38 deletions

View File

@ -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 = [

View File

@ -103,6 +103,44 @@ class AlibabaBillRepository
];
}
/** @return \Generator<int,array<string,mixed>> */
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,

View File

@ -117,6 +117,43 @@ class JdWalletBillRepository
], 'creatTime', 'id', fn (array $row): array => $this->normalizeConsumption($row));
}
/** @return \Generator<int,array<string,mixed>> */
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
{

View File

@ -158,6 +158,44 @@ class TmallTaobaoSpreadBillRepository
];
}
/** @return \Generator<int,array<string,mixed>> */
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);
}
/**
* 当前筛选条件下的汇总(金额单位:元)。
*

View File

@ -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
<div class="w-full">
@ -91,6 +100,11 @@
<div class="flex items-end gap-2">
<button type="submit" class="alibaba-bill-primary"><span class="material-icons text-base" aria-hidden="true">search</span>查询</button>
<a href="{{ request()->url() }}" class="alibaba-bill-secondary"><span class="material-icons text-base" aria-hidden="true">clear</span>清除</a>
<a href="{{ route('business.pages.export', $exportParameters) }}"
class="alibaba-bill-secondary"
aria-label="导出当前筛选结果">
<span class="material-icons text-base" aria-hidden="true">download</span>导出
</a>
</div>
@if (($filters['shop_name'] ?? '') !== '' || ($filters['product_type'] ?? '') !== '' || ($filters['from'] ?? '') !== '' || ($filters['to'] ?? '') !== '')
<div class="ml-auto self-center text-xs text-gray-500">共匹配 {{ $bills->total() }} 条</div>

View File

@ -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
<div class="w-full" data-jd-wallet-panel>
@ -112,6 +121,11 @@
<div class="flex items-end gap-2">
<button type="submit" class="jd-wallet-button jd-wallet-button-primary"><span class="material-icons text-base" aria-hidden="true">search</span>查询</button>
<a href="{{ $currentUrl }}?jd_ledger={{ $ledger }}" data-jd-wallet-navigation class="jd-wallet-button jd-wallet-button-secondary"><span class="material-icons text-base" aria-hidden="true">clear</span>清除</a>
<a href="{{ route('business.pages.export', $exportParameters) }}"
class="jd-wallet-button jd-wallet-button-secondary"
aria-label="导出当前筛选结果">
<span class="material-icons text-base" aria-hidden="true">download</span>导出
</a>
</div>
<div class="ml-auto self-center text-xs text-gray-500">共匹配 {{ $bills->total() }} 条</div>
</div>

View File

@ -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
<form method="GET" action="{{ request()->url() }}"
class="mb-4 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3">
@ -119,6 +128,12 @@
<span class="material-icons text-base" aria-hidden="true">clear</span>
清除
</a>
<a href="{{ route('business.pages.export', $exportParameters) }}"
class="bill-filter-secondary"
aria-label="导出当前筛选结果">
<span class="material-icons text-base" aria-hidden="true">download</span>
导出
</a>
</div>
@if ($currentStore !== '' || $currentFrom !== '' || $currentTo !== '' || $currentFlowType !== '')
<div class="ml-auto self-center text-xs text-gray-500">

View File

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

View File

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