feat: merge promotion bills into marketing account bills

This commit is contained in:
404877757@qq.com 2026-09-07 12:27:46 +08:00
parent a56328750e
commit d46ed01a81
6 changed files with 225 additions and 148 deletions

View File

@ -90,6 +90,12 @@ class Pages extends Controller
protected const MARKETING_PLATFORMS = [
'taobao-alipay', 'taobao-wechat', 'pinduoduo',
'jd-wallet', 'douyin', 'dewu', 'vipshop',
'jd-promotion', 'douyin-promotion', 'dewu-promotion', 'vip-promotion',
];
/** @var list<string> 推广账户流水已合并到营销账户账单页的四个平台页签 */
protected const PROMOTION_MARKETING_TABS = [
'jd-promotion', 'douyin-promotion', 'dewu-promotion', 'vip-promotion',
];
public function home()
@ -501,7 +507,6 @@ class Pages extends Controller
'supplier-payables-report' => $this->contentForSupplierPayablesReport(),
'payable-cloud-warehouse-vehicle-fee' => $this->contentForDingPayableWorkflows(),
'courier-surcharge' => $this->contentForCourierSurcharge(),
'platform-promotion-accounts' => app(PlatformPromotionAccountPageData::class)->content($tab),
default => null,
};
}
@ -1071,11 +1076,11 @@ class Pages extends Controller
$pageConfig = collect($sectionConfig['pages'] ?? [])->firstWhere('slug', $page);
abort_if(empty($pageConfig), 403, '当前账号没有该页面权限。');
if ($page === 'platform-promotion-accounts') {
return $this->exportPlatformPromotionAccounts($request);
}
if ($page === 'marketing-account-bills') {
if (in_array($tab, self::PROMOTION_MARKETING_TABS, true)) {
return $this->exportPlatformPromotionAccounts($request, $section, $page, $tab);
}
return $this->exportMarketingAccountBills($request, $section, $page, $tab);
}
@ -1144,8 +1149,16 @@ class Pages extends Controller
]);
}
private function exportPlatformPromotionAccounts(Request $request)
private function exportPlatformPromotionAccounts(
Request $request,
string $section = 'billing-transactions',
string $page = 'marketing-account-bills',
?string $tab = null,
)
{
$platform = PlatformPromotionAccountPageData::platformForTab($tab);
abort_if($platform === null, 404);
$month = trim((string) $request->query('month', ''));
$month = preg_match('/^\d{4}-\d{2}$/', $month) === 1 ? $month : null;
$filters = [
@ -1161,13 +1174,15 @@ class Pages extends Controller
} catch (Throwable $exception) {
report($exception);
return redirect()->route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'platform-promotion-accounts',
])->with('error', '推广账户数据导出暂时无法完成,请稍后重试。');
return redirect()->route('business.pages.show', array_filter([
'section' => $section,
'page' => $page,
'tab' => $tab,
], static fn (mixed $value): bool => $value !== null && $value !== ''))
->with('error', '推广账户数据导出暂时无法完成,请稍后重试。');
}
return response()->streamDownload(function () use ($data): void {
return response()->streamDownload(function () use ($data, $platform): void {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, ['平台', '店铺/账户', '日期', '类型', '金额(元)', '余额(元)', '备注']);
@ -1184,12 +1199,17 @@ class Pages extends Controller
]);
}
};
$emit('京东京准通', $data['jd'] ?? [], ['name' => fn ($row) => $row['shop_name'] . ' / ' . $row['account'], 'date' => fn ($row) => $row['show_date'] ?? $row['settle_datetime'], 'type' => fn ($row) => $row['trade_type_name'] ?? $row['in_out_type_name'], 'amount' => fn ($row) => $row['amount'] ?? '', 'balance' => fn ($row) => $row['balance_amount'] ?? '', 'remark' => fn ($row) => $row['remark'] ?? '']);
$emit('抖音千川', $data['dy'] ?? [], ['name' => fn ($row) => $row['shopName'] . ' / ' . $row['account'] . ' / ' . $row['qianchuanAavid'], 'date' => fn ($row) => $row['statDate'] ?? '', 'type' => fn () => '花费', 'amount' => fn ($row) => $row['cost'] ?? '', 'balance' => fn ($row) => $row['balance'] ?? '', 'remark' => fn ($row) => $row['remark'] ?? '']);
$emit('得物推广', $data['dw'] ?? [], ['name' => fn ($row) => $row['shop_name'] . ' / ' . $row['account'], 'date' => fn ($row) => $row['trade_date'] ?? $row['date_time'], 'type' => fn ($row) => $row['flow_type'] ?? '', 'amount' => fn ($row) => $row['trade_amount_yuan'] ?? '', 'balance' => fn ($row) => $row['balance_yuan'] ?? '', 'remark' => fn ($row) => $row['remark'] ?? '']);
$emit('VIP营销', $data['vipMarketing'] ?? [], ['name' => fn ($row) => $row['accountName'] . ' / ' . $row['advertiserId'], 'date' => fn ($row) => $row['dataMonth'], 'type' => fn () => '月度花费', 'amount' => fn ($row) => $row['totalCostYuan'] ?? '', 'balance' => fn () => '', 'remark' => fn ($row) => '曝光 ' . $row['totalImpressionCount']]);
if ($platform === 'jd') {
$emit('京东京准通', $data['jd'] ?? [], ['name' => fn ($row) => $row['shop_name'] . ' / ' . $row['account'], 'date' => fn ($row) => $row['show_date'] ?? $row['settle_datetime'], 'type' => fn ($row) => $row['trade_type_name'] ?? $row['in_out_type_name'], 'amount' => fn ($row) => $row['amount'] ?? '', 'balance' => fn ($row) => $row['balance_amount'] ?? '', 'remark' => fn ($row) => $row['remark'] ?? '']);
} elseif ($platform === 'douyin') {
$emit('抖音千川', $data['dy'] ?? [], ['name' => fn ($row) => $row['shopName'] . ' / ' . $row['account'] . ' / ' . $row['qianchuanAavid'], 'date' => fn ($row) => $row['statDate'] ?? '', 'type' => fn () => '花费', 'amount' => fn ($row) => $row['cost'] ?? '', 'balance' => fn ($row) => $row['balance'] ?? '', 'remark' => fn ($row) => $row['remark'] ?? '']);
} elseif ($platform === 'dewu') {
$emit('得物推广', $data['dw'] ?? [], ['name' => fn ($row) => $row['shop_name'] . ' / ' . $row['account'], 'date' => fn ($row) => $row['trade_date'] ?? $row['date_time'], 'type' => fn ($row) => $row['flow_type'] ?? '', 'amount' => fn ($row) => $row['trade_amount_yuan'] ?? '', 'balance' => fn ($row) => $row['balance_yuan'] ?? '', 'remark' => fn ($row) => $row['remark'] ?? '']);
} else {
$emit('VIP营销', $data['vipMarketing'] ?? [], ['name' => fn ($row) => $row['accountName'] . ' / ' . $row['advertiserId'], 'date' => fn ($row) => $row['dataMonth'], 'type' => fn () => '月度花费', 'amount' => fn ($row) => $row['totalCostYuan'] ?? '', 'balance' => fn () => '', 'remark' => fn ($row) => '曝光 ' . $row['totalImpressionCount']]);
}
fclose($csv);
}, '平台推广账户流水_' . $filters['month'] . '.csv', [
}, '店铺营销账户账单_' . $platform . '_' . $filters['month'] . '.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
@ -2289,6 +2309,10 @@ class Pages extends Controller
return null;
}
if (in_array($tab, self::PROMOTION_MARKETING_TABS, true)) {
return app(PlatformPromotionAccountPageData::class)->content($tab);
}
// Alibaba 页面沿用历史 taobao-wechat 路由,数据来自 JianSun.dbo.alibaba_bill。
if ($tab === 'taobao-wechat') {
$request = request();

View File

@ -359,16 +359,10 @@ return [
['slug' => 'douyin', 'title' => '抖音账户'],
['slug' => 'dewu', 'title' => '得物账户'],
['slug' => 'vipshop', 'title' => '唯品会账户'],
],
],
[
'slug' => 'platform-promotion-accounts',
'title' => '平台推广账户流水',
'tabs' => [
['slug' => 'jd', 'title' => '京东京准通'],
['slug' => 'douyin', 'title' => '抖音千川'],
['slug' => 'dewu', 'title' => '得物推广账户'],
['slug' => 'vip', 'title' => 'VIP(唯品会)'],
['slug' => 'jd-promotion', 'title' => '京东京准通'],
['slug' => 'douyin-promotion', 'title' => '抖音千川'],
['slug' => 'dewu-promotion', 'title' => '得物推广账户'],
['slug' => 'vip-promotion', 'title' => '唯品会推广'],
],
],
['slug' => 'deposit-account-balance', 'title' => '店铺保证金账户余额'],

View File

@ -10,12 +10,22 @@ use Throwable;
class PlatformPromotionAccountPageData
{
private const TAB_PLATFORMS = [
'jd-promotion' => 'jd',
'douyin-promotion' => 'douyin',
'dewu-promotion' => 'dewu',
'vip-promotion' => 'vip',
];
public function __construct(private readonly PlatformPromotionAccountRepository $repository)
{
}
public function content(?string $tab = null): array
{
$platform = self::platformForTab($tab);
abort_if($platform === null, 404);
$requestedMonth = $this->month(request('month'));
$month = $requestedMonth ?: now()->format('Y-m');
$filters = [
@ -23,8 +33,6 @@ class PlatformPromotionAccountPageData
'shop_name' => $this->text(request('shop_name')),
'account' => $this->text(request('account')),
];
$tab = in_array($tab, ['jd', 'douyin', 'dewu', 'vip'], true) ? $tab : 'jd';
try {
if ($requestedMonth === '') {
$month = $this->repository->latestMonth() ?: $month;
@ -33,19 +41,21 @@ class PlatformPromotionAccountPageData
$data = $this->repository->dashboard($filters, 5000);
return [
'view' => 'business.pages.billing-transactions.platform-promotion-accounts',
'view' => 'banking.marketing_platform_promotion_bills.partials.embedded-table',
'filters' => $filters,
'data' => $data,
'platform' => $tab,
'rows' => $this->paginateRows($data, $tab),
'platform' => $platform,
'marketingTab' => $tab,
'rows' => $this->paginateRows($data, $platform),
'error' => null,
];
} catch (ServiceUnavailableHttpException $exception) {
return [
'view' => 'business.pages.billing-transactions.platform-promotion-accounts',
'view' => 'banking.marketing_platform_promotion_bills.partials.embedded-table',
'filters' => $filters,
'data' => [],
'platform' => $tab,
'platform' => $platform,
'marketingTab' => $tab,
'rows' => $this->emptyPaginator(),
'error' => $exception->getMessage(),
];
@ -53,16 +63,22 @@ class PlatformPromotionAccountPageData
report($exception);
return [
'view' => 'business.pages.billing-transactions.platform-promotion-accounts',
'view' => 'banking.marketing_platform_promotion_bills.partials.embedded-table',
'filters' => $filters,
'data' => [],
'platform' => $tab,
'platform' => $platform,
'marketingTab' => $tab,
'rows' => $this->emptyPaginator(),
'error' => '推广账户数据暂时无法读取,请稍后重试。',
];
}
}
public static function platformForTab(?string $tab): ?string
{
return self::TAB_PLATFORMS[$tab] ?? null;
}
private function paginateRows(array $data, string $tab): LengthAwarePaginator
{
$key = ['jd' => 'jd', 'douyin' => 'dy', 'dewu' => 'dw', 'vip' => 'vipSales'][$tab];

View File

@ -0,0 +1,108 @@
@php
$data = $data ?? [];
$platform = $platform ?? 'jd';
$marketingTab = $marketingTab ?? null;
$summary = match ($platform) {
'jd' => $data['jdSummary'] ?? [],
'douyin' => $data['dySummary'] ?? [],
'dewu' => $data['dwSummary'] ?? [],
default => $data['vipSummary'] ?? [],
};
$titles = ['jd' => '京东京准通', 'douyin' => '抖音千川', 'dewu' => '得物推广账户', 'vip' => '唯品会推广'];
$columns = match ($platform) {
'jd' => [['shop_name', '店铺'], ['account', '账户'], ['show_date', '日期'], ['trade_type_name', '交易类型'], ['amount', '金额'], ['balance_amount', '余额'], ['remark', '备注']],
'douyin' => [['shopName', '店铺'], ['account', '账户'], ['qianchuanAavid', '广告账户'], ['statDate', '日期'], ['cost', '花费'], ['income', '收入'], ['transferIn', '转入'], ['transferOut', '转出'], ['balance', '余额'], ['status', '状态'], ['remark', '备注']],
'dewu' => [['shop_name', '店铺'], ['account', '账户'], ['trade_date', '日期'], ['amount_type', '金额类型'], ['flow_type', '收支类型'], ['trade_amount_yuan', '金额'], ['balance_yuan', '余额'], ['remark', '备注']],
default => [['shopName', '店铺'], ['storeName', '平台店铺'], ['accountName', '营销账户'], ['dataMonth', '月份'], ['actualSalesAmount', '实际销售额'], ['reportAmount', '申报金额'], ['totalCostYuan', '营销花费']],
};
$moneyFields = ['amount', 'balance_amount', 'cost', 'income', 'transferIn', 'transferOut', 'balance', 'trade_amount_yuan', 'balance_yuan', 'actualSalesAmount', 'reportAmount', 'totalCostYuan'];
@endphp
<style>
.marketing-platform-promotion-page { color: #171717; }
.marketing-platform-promotion-page .promotion-toolbar { display:flex; flex-wrap:wrap; align-items:flex-end; gap:.75rem; margin-bottom:1rem; padding-bottom:1rem; border-bottom:1px solid var(--vip-border); }
.marketing-platform-promotion-page .promotion-toolbar label { display:flex; align-items:center; gap:.4rem; color:#595959; font-size:.75rem; font-weight:500; }
.marketing-platform-promotion-page .promotion-toolbar input { height:2.15rem; width:11rem; padding:.35rem .55rem; border:1px solid var(--vip-border); border-radius:.45rem; background:#fff; color:#171717; font-size:.8125rem; }
.marketing-platform-promotion-page .promotion-toolbar input:focus { border-color:var(--vip-content-accent-dark); outline:2px solid rgba(166,124,61,.18); }
.marketing-platform-promotion-page .promotion-toolbar .biz-btn { min-height:2.15rem; }
.marketing-platform-promotion-page .promotion-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; margin:14px 0; }
.marketing-platform-promotion-page .promotion-card { padding:16px 18px; }
.marketing-platform-promotion-page .promotion-card-title { color:#6b6255; font-size:13px; font-weight:600; }
.marketing-platform-promotion-page .promotion-kpi { margin-top:7px; color:#171717; font-size:22px; font-weight:700; }
.marketing-platform-promotion-page .promotion-pagination { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:14px 4px 2px; color:#6b6255; font-size:13px; }
.marketing-platform-promotion-page .promotion-pagination nav { display:flex; align-items:center; justify-content:flex-end; min-width:0; }
.marketing-platform-promotion-page .promotion-pagination__buttons { display:flex !important; flex-direction:row !important; flex-wrap:nowrap; align-items:center; gap:8px; white-space:nowrap; }
.marketing-platform-promotion-page .promotion-pagination__buttons a,
.marketing-platform-promotion-page .promotion-pagination__buttons span { display:inline-flex !important; min-width:32px; height:32px; align-items:center; justify-content:center; padding:0 10px; border:1px solid var(--vip-border); border-radius:7px; background:#fff; color:#433f39; text-decoration:none; }
.marketing-platform-promotion-page .promotion-pagination__buttons a.is-current { border-color:var(--vip-content-accent-dark); background:var(--vip-content-accent-dark); color:#fff; }
.marketing-platform-promotion-page .promotion-pagination__buttons span.is-disabled { color:#a7a095; background:#f8f6f2; }
.marketing-platform-promotion-page .promotion-pagination__buttons a:hover { border-color:var(--vip-content-accent-dark); color:var(--vip-content-accent-dark); }
@media (max-width: 900px) {
.marketing-platform-promotion-page .promotion-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
.marketing-platform-promotion-page .promotion-pagination { align-items:flex-start; flex-direction:column; }
}
@media (max-width: 640px) {
.marketing-platform-promotion-page .promotion-toolbar label { width:100%; justify-content:space-between; }
.marketing-platform-promotion-page .promotion-toolbar input { flex:1; width:auto; }
}
</style>
<div class="marketing-platform-promotion-page">
<form method="GET" class="promotion-toolbar" aria-label="{{ $titles[$platform] }}推广账户筛选">
<label>数据月份 <input type="month" name="month" value="{{ $filters['month'] ?? now()->format('Y-m') }}" required></label>
<label>店铺 <input type="text" name="shop_name" value="{{ $filters['shop_name'] ?? '' }}" placeholder="店铺名称"></label>
<label>账户 <input type="text" name="account" value="{{ $filters['account'] ?? '' }}" placeholder="账户名称"></label>
<button class="biz-btn biz-btn-primary" type="submit">查询</button>
<a class="biz-btn biz-btn-ghost" href="{{ route('business.pages.export', [
'section' => request()->route('section') ?? 'billing-transactions',
'page' => request()->route('page') ?? 'marketing-account-bills',
'tab' => $marketingTab,
] + request()->only(['month', 'shop_name', 'account'])) }}">导出 CSV</a>
</form>
@if (! empty($error))
<div class="biz-alert biz-alert-error" role="alert">{{ $error }}</div>
@endif
<div class="promotion-grid">
@if ($platform === 'jd')
<div class="biz-card promotion-card"><div class="promotion-card-title">入账金额(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['income'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">出账金额(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['expense'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">净额(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['income'] ?? 0) - (float) ($summary['expense'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">流水笔数</div><div class="promotion-kpi">{{ number_format((int) ($summary['record_count'] ?? 0)) }}</div></div>
@elseif ($platform === 'douyin')
<div class="biz-card promotion-card"><div class="promotion-card-title">总花费(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['cost'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">收入(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['income'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">转入(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['transfer_in'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">账户数</div><div class="promotion-kpi">{{ number_format((int) ($summary['account_count'] ?? 0)) }}</div></div>
@elseif ($platform === 'dewu')
<div class="biz-card promotion-card"><div class="promotion-card-title">总收入(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['income'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">总支出(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['expense'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">净额(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['net'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">流水笔数</div><div class="promotion-kpi">{{ number_format((int) ($summary['record_count'] ?? 0)) }}</div></div>
@else
<div class="biz-card promotion-card"><div class="promotion-card-title">实际销售额(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['sales'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">销售目标(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['target'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">涉税申报(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['tax'] ?? 0), 2) }}</div></div>
<div class="biz-card promotion-card"><div class="promotion-card-title">营销花费(元)</div><div class="promotion-kpi">{{ number_format((float) ($summary['marketing_cost'] ?? 0), 2) }}</div></div>
@endif
</div>
<section class="biz-card">
<div class="biz-card-head"><div class="biz-card-title">{{ $titles[$platform] }}明细</div><span class="biz-badge">{{ number_format($rows->total()) }} 条</span></div>
<div class="biz-table-wrap"><table class="biz-table"><thead><tr>@foreach ($columns as $column)<th>{{ $column[1] }}</th>@endforeach</tr></thead><tbody>
@forelse ($rows as $row)
<tr>@foreach ($columns as $column)
@php($value = $row[$column[0]] ?? null)
<td>{{ in_array($column[0], $moneyFields, true) && is_numeric($value) ? number_format((float) $value, 2) : ($value ?? '—') }}</td>
@endforeach</tr>
@empty
<tr><td colspan="{{ count($columns) }}">该月份暂无数据</td></tr>
@endforelse
</tbody></table></div>
<div class="promotion-pagination"><span>每页最多 50 条 · 共 {{ number_format($rows->total()) }} 条</span><nav aria-label="{{ $titles[$platform] }}分页"><div class="promotion-pagination__buttons">
@if ($rows->onFirstPage())<span class="is-disabled">上一页</span>@else<a href="{{ $rows->previousPageUrl() }}">上一页</a>@endif
<span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页</span>
@if ($rows->hasMorePages())<a href="{{ $rows->nextPageUrl() }}">下一页</a>@else<span class="is-disabled">下一页</span>@endif
</div></nav></div>
</section>
</div>

View File

@ -1,89 +0,0 @@
@php
$data = $data ?? [];
$platform = $platform ?? 'jd';
$summary = match ($platform) {
'jd' => $data['jdSummary'] ?? [],
'douyin' => $data['dySummary'] ?? [],
'dewu' => $data['dwSummary'] ?? [],
default => $data['vipSummary'] ?? [],
};
$titles = ['jd' => '京东京准通', 'douyin' => '抖音千川', 'dewu' => '得物推广账户', 'vip' => 'VIP(唯品会)'];
$columns = match ($platform) {
'jd' => [['shop_name', '店铺'], ['account', '账户'], ['show_date', '日期'], ['trade_type_name', '交易类型'], ['amount', '金额'], ['balance_amount', '余额'], ['remark', '备注']],
'douyin' => [['shopName', '店铺'], ['account', '账户'], ['qianchuanAavid', '广告账户'], ['statDate', '日期'], ['cost', '花费'], ['balance', '余额'], ['status', '状态'], ['remark', '备注']],
'dewu' => [['shop_name', '店铺'], ['account', '账户'], ['trade_date', '日期'], ['flow_type', '收支类型'], ['trade_amount_yuan', '金额'], ['balance_yuan', '余额'], ['remark', '备注']],
default => [['shopName', '店铺'], ['storeName', '平台店铺'], ['accountName', '营销账户'], ['dataMonth', '月份'], ['actualSalesAmount', '实际销售额'], ['reportAmount', '申报金额'], ['totalCostYuan', '营销花费']],
};
$moneyFields = ['amount', 'balance_amount', 'cost', 'balance', 'trade_amount_yuan', 'balance_yuan', 'actualSalesAmount', 'reportAmount', 'totalCostYuan'];
@endphp
<style>
.platform-promotion-page .biz-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; margin:14px 0; }
.platform-promotion-page .biz-card { padding:16px 18px; }
.platform-promotion-page .biz-card-title { color:#6b6255; font-size:13px; font-weight:600; }
.platform-promotion-page .biz-kpi { margin-top:7px; color:#171717; font-size:22px; font-weight:700; }
.platform-promotion-page .platform-pagination { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:14px 4px 2px; color:#6b6255; font-size:13px; }
.platform-promotion-page .platform-pagination nav { display:flex; align-items:center; justify-content:flex-end; min-width:0; }
.platform-promotion-page .platform-pagination__buttons { display:flex !important; flex-direction:row !important; flex-wrap:nowrap; align-items:center; gap:8px; white-space:nowrap; }
.platform-promotion-page .platform-pagination__buttons a,
.platform-promotion-page .platform-pagination__buttons span { display:inline-flex !important; min-width:32px; height:32px; align-items:center; justify-content:center; padding:0 10px; border:1px solid #ded7ca; border-radius:7px; background:#fff; color:#433f39; text-decoration:none; }
.platform-promotion-page .platform-pagination__buttons a.is-current { border-color:#856632; background:#856632; color:#fff; }
.platform-promotion-page .platform-pagination__buttons span.is-disabled { color:#a7a095; background:#f8f6f2; }
.platform-promotion-page .platform-pagination__buttons a:hover { border-color:#856632; color:#856632; }
@media (max-width: 900px) { .platform-promotion-page .biz-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } .platform-promotion-page .platform-pagination { align-items:flex-start; flex-direction:column; } }
</style>
<div class="platform-promotion-page">
<form method="GET" class="upstream-data-toolbar" aria-label="平台推广账户筛选">
<label>数据月份 <input type="month" name="month" value="{{ $filters['month'] ?? now()->format('Y-m') }}" required></label>
<label>店铺 <input type="text" name="shop_name" value="{{ $filters['shop_name'] ?? '' }}" placeholder="店铺名称"></label>
<label>账户 <input type="text" name="account" value="{{ $filters['account'] ?? '' }}" placeholder="账户名称"></label>
<button class="biz-btn biz-btn-primary" type="submit">查询</button>
<a class="biz-btn biz-btn-ghost" href="{{ route('business.pages.export', ['section' => 'billing-transactions', 'page' => 'platform-promotion-accounts', 'tab' => $platform] + request()->only(['month', 'shop_name', 'account'])) }}">导出 CSV</a>
</form>
@if (! empty($error))
<div class="biz-alert biz-alert-error" role="alert">{{ $error }}</div>
@endif
<div class="biz-grid biz-grid-4">
@if ($platform === 'jd')
<div class="biz-card"><div class="biz-card-title">入账金额</div><div class="biz-kpi">{{ number_format((float) ($summary['income'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">出账金额</div><div class="biz-kpi">{{ number_format((float) ($summary['expense'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">净额</div><div class="biz-kpi">{{ number_format((float) ($summary['income'] ?? 0) - (float) ($summary['expense'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">流水笔数</div><div class="biz-kpi">{{ number_format((int) ($summary['record_count'] ?? 0)) }}</div></div>
@elseif ($platform === 'douyin')
<div class="biz-card"><div class="biz-card-title">总花费</div><div class="biz-kpi">{{ number_format((float) ($summary['cost'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">收入</div><div class="biz-kpi">{{ number_format((float) ($summary['income'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">转入</div><div class="biz-kpi">{{ number_format((float) ($summary['transfer_in'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">账户数</div><div class="biz-kpi">{{ number_format((int) ($summary['account_count'] ?? 0)) }}</div></div>
@elseif ($platform === 'dewu')
<div class="biz-card"><div class="biz-card-title">总收入</div><div class="biz-kpi">{{ number_format((float) ($summary['income'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">总支出</div><div class="biz-kpi">{{ number_format((float) ($summary['expense'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">净额</div><div class="biz-kpi">{{ number_format((float) ($summary['net'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">流水笔数</div><div class="biz-kpi">{{ number_format((int) ($summary['record_count'] ?? 0)) }}</div></div>
@else
<div class="biz-card"><div class="biz-card-title">实际销售额</div><div class="biz-kpi">{{ number_format((float) ($summary['sales'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">销售目标</div><div class="biz-kpi">{{ number_format((float) ($summary['target'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">涉税申报</div><div class="biz-kpi">{{ number_format((float) ($summary['tax'] ?? 0), 2) }}</div></div>
<div class="biz-card"><div class="biz-card-title">营销花费</div><div class="biz-kpi">{{ number_format((float) ($summary['marketing_cost'] ?? 0), 2) }}</div></div>
@endif
</div>
<section class="biz-card mt-4">
<div class="biz-card-head"><div class="biz-card-title">{{ $titles[$platform] }}明细</div><span class="biz-badge">{{ number_format($rows->total()) }} 条</span></div>
<div class="biz-table-wrap"><table class="biz-table"><thead><tr>@foreach ($columns as $column)<th>{{ $column[1] }}</th>@endforeach</tr></thead><tbody>
@forelse ($rows as $row)
<tr>@foreach ($columns as $column)
@php($value = $row[$column[0]] ?? null)
<td>{{ in_array($column[0], $moneyFields, true) && is_numeric($value) ? number_format((float) $value, 2) : ($value ?? '—') }}</td>
@endforeach</tr>
@empty
<tr><td colspan="{{ count($columns) }}">该月份暂无数据</td></tr>
@endforelse
</tbody></table></div>
<div class="platform-pagination"><span>每页最多 50 条 · 共 {{ number_format($rows->total()) }} 条</span><nav aria-label="{{ $titles[$platform] }}分页"><div class="platform-pagination__buttons">
@if ($rows->onFirstPage())<span class="is-disabled">上一页</span>@else<a href="{{ $rows->previousPageUrl() }}">上一页</a>@endif
<span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页</span>
@if ($rows->hasMorePages())<a href="{{ $rows->nextPageUrl() }}">下一页</a>@else<span class="is-disabled">下一页</span>@endif
</div></nav></div>
</section>
</div>

View File

@ -7,10 +7,12 @@ use Mockery;
use Modules\EcommerceFinance\Repositories\PlatformPromotionAccountRepository;
use Tests\Feature\FeatureTestCase;
class PlatformPromotionAccountPageTest extends FeatureTestCase
class MarketingPlatformPromotionBillsTest extends FeatureTestCase
{
public function test_platform_promotion_page_remains_available_with_bill_offset_navigation(): void
public function test_promotion_platform_is_a_tab_inside_marketing_account_bills(): void
{
$this->allowMarketingAccountBillsPage();
$repository = Mockery::mock(PlatformPromotionAccountRepository::class);
$repository->shouldReceive('latestMonth')->once()->andReturn('2026-08');
$repository->shouldReceive('dashboard')->once()->with([
@ -20,29 +22,30 @@ class PlatformPromotionAccountPageTest extends FeatureTestCase
], 5000)->andReturn($this->dashboardData());
$this->app->instance(PlatformPromotionAccountRepository::class, $repository);
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'platform-promotion-accounts',
'tab' => 'jd',
'page' => 'marketing-account-bills',
'tab' => 'jd-promotion',
]))
->assertOk()
->assertSeeText('平台推广账户流水')
->assertSeeText('京东京准通')
->assertSee('aria-label="京东京准通推广账户筛选"', false)
->assertSeeText('测试店铺')
->assertSeeText('导出 CSV')
->assertSee('class="my-6 business-page--marketing-account-bills"', false)
->assertSee(route('business.pages.show', [
'section' => 'order-data',
'page' => 'bill-offset',
]), false);
'section' => 'billing-transactions',
'page' => 'marketing-account-bills',
'tab' => 'taobao-alipay',
]), false)
->assertDontSeeText('平台推广账户流水');
}
public function test_platform_promotion_export_keeps_the_complete_filtered_month(): void
public function test_promotion_platform_export_uses_merged_marketing_account_bills_route(): void
{
$this->allowMarketingAccountBillsPage();
$repository = Mockery::mock(PlatformPromotionAccountRepository::class);
$repository->shouldReceive('latestMonth')->never();
$repository->shouldReceive('dashboard')->once()->with([
@ -52,27 +55,34 @@ class PlatformPromotionAccountPageTest extends FeatureTestCase
], 5000)->andReturn($this->dashboardData());
$this->app->instance(PlatformPromotionAccountRepository::class, $repository);
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$response = $this->loginAs()->get(route('business.pages.export', [
'section' => 'billing-transactions',
'page' => 'platform-promotion-accounts',
'tab' => 'jd',
'page' => 'marketing-account-bills',
'tab' => 'jd-promotion',
'month' => '2026-08',
'shop_name' => '测试',
'account' => 'account-01',
]));
$response->assertOk();
$response->assertHeader('Content-Type', 'text/csv; charset=UTF-8');
$disposition = (string) $response->headers->get('Content-Disposition');
$this->assertStringContainsString("filename*=utf-8''", $disposition);
$this->assertStringContainsString('2026-08.csv', $disposition);
$response->assertOk()
->assertHeader('Content-Type', 'text/csv; charset=UTF-8')
->assertDownload('店铺营销账户账单_jd_2026-08.csv');
$this->assertStringContainsString('测试店铺', $response->streamedContent());
}
public function test_removed_platform_promotion_directory_is_not_available(): void
{
$this->allowMarketingAccountBillsPage();
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'platform-promotion-accounts',
'tab' => 'jd',
]))
->assertNotFound();
}
private function dashboardData(): array
{
return [
@ -97,4 +107,18 @@ class PlatformPromotionAccountPageTest extends FeatureTestCase
'vipSummary' => [],
];
}
private function allowMarketingAccountBillsPage(): void
{
$this->user->update(['locale' => 'zh-CN']);
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$subject = 'user_' . $this->user->id;
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['billing-transactions/marketing-account-bills'],
],
], JSON_UNESCAPED_UNICODE));
}
}