422 lines
15 KiB
PHP
422 lines
15 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Business;
|
||
|
||
use App\Models\Banking\MarketingAccountBill;
|
||
use App\Models\Banking\MarketingTaobaoAlipayBill;
|
||
use App\Models\StoreReceivables\StoreZhongcaiReconciliation;
|
||
use App\Utilities\SidebarPermissions;
|
||
use Illuminate\Routing\Controller;
|
||
use Illuminate\Pagination\LengthAwarePaginator;
|
||
use Illuminate\Support\Collection;
|
||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||
use Throwable;
|
||
|
||
class Pages extends Controller
|
||
{
|
||
private const EDGE_BANDING_SOURCE = 'jiansun';
|
||
|
||
public function __construct(
|
||
private readonly EdgeBandingRepository $edgeBanding,
|
||
private readonly UpstreamQueryCacheFactory $upstreamCaches,
|
||
) {
|
||
}
|
||
|
||
/**
|
||
* 支持的营销账户平台列表
|
||
*/
|
||
protected const MARKETING_PLATFORMS = [
|
||
'taobao-alipay', 'taobao-wechat', 'pinduoduo',
|
||
'jd-wallet', 'douyin', 'dewu', 'vipshop',
|
||
];
|
||
public function home()
|
||
{
|
||
$section = collect(SidebarPermissions::navigationFor(user()))->first();
|
||
|
||
abort_if(empty($section), 403, '当前账号没有已授权的侧边栏功能。');
|
||
|
||
$page = collect($section['pages'] ?? [])->first();
|
||
$parameters = ['section' => $section['slug']];
|
||
|
||
if ($page) {
|
||
$parameters['page'] = $page['slug'];
|
||
|
||
if (! empty($page['tabs'])) {
|
||
$parameters['tab'] = $page['tabs'][0]['slug'];
|
||
}
|
||
}
|
||
|
||
return redirect()->route('business.pages.show', $parameters);
|
||
}
|
||
|
||
public function show(string $section, ?string $page = null, ?string $tab = null)
|
||
{
|
||
$sectionConfig = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', $section);
|
||
|
||
abort_if(empty($sectionConfig), 403, '当前账号没有该页面权限。');
|
||
|
||
$pages = collect($sectionConfig['pages'] ?? []);
|
||
|
||
if ($pages->isEmpty()) {
|
||
abort_if($page !== null || $tab !== null, 404);
|
||
|
||
return view('business.pages.show', [
|
||
'section' => $sectionConfig,
|
||
'page' => null,
|
||
'tabs' => collect(),
|
||
'activeTab' => null,
|
||
]);
|
||
}
|
||
|
||
if ($page === null) {
|
||
return $this->redirectToPage($sectionConfig, $pages->first());
|
||
}
|
||
|
||
$pageConfig = $pages
|
||
->flatMap(fn (array $configuredPage) => [
|
||
$configuredPage,
|
||
...($configuredPage['children'] ?? []),
|
||
])
|
||
->firstWhere('slug', $page);
|
||
|
||
abort_if(empty($pageConfig), 404);
|
||
|
||
// 如果该 page 是一个 children-only 的父级分组,访问它时跳到第一个子页面
|
||
if (! empty($pageConfig['children']) && empty($pageConfig['tabs'])) {
|
||
return $this->redirectToPage($sectionConfig, $pageConfig['children'][0]);
|
||
}
|
||
|
||
$tabs = collect($pageConfig['tabs'] ?? []);
|
||
|
||
if ($tabs->isEmpty()) {
|
||
abort_if($tab !== null, 404);
|
||
|
||
$content = $this->resolveContent($page, null);
|
||
|
||
return view('business.pages.show', [
|
||
'section' => $sectionConfig,
|
||
'page' => $pageConfig,
|
||
'tabs' => $tabs,
|
||
'activeTab' => null,
|
||
'content' => $content,
|
||
'edgeBanding' => $this->edgeBandingContent($page),
|
||
]);
|
||
}
|
||
|
||
if ($tab === null) {
|
||
return $this->redirectToPage($sectionConfig, $pageConfig);
|
||
}
|
||
|
||
$activeTab = $tabs->firstWhere('slug', $tab);
|
||
|
||
abort_if(empty($activeTab), 404);
|
||
|
||
$content = $this->resolveContent($page, $tab);
|
||
|
||
return view('business.pages.show', [
|
||
'section' => $sectionConfig,
|
||
'page' => $pageConfig,
|
||
'tabs' => $tabs,
|
||
'activeTab' => $activeTab,
|
||
'content' => $content,
|
||
'edgeBanding' => $this->edgeBandingContent($page),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 根据页面和标签加载对应的业务内容数据
|
||
*/
|
||
protected function resolveContent(string $page, ?string $tab): ?array
|
||
{
|
||
return match ($page) {
|
||
'marketing-account-bills' => $this->contentForMarketingAccountBills($tab),
|
||
'zhongcai-reconciliation' => $this->contentForZhongcaiReconciliation(),
|
||
default => null,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 封边条页面只读取上游快照;未启用或无快照时将错误交给页面显示,
|
||
* 避免工作台因上游暂不可用直接返回 500。
|
||
*/
|
||
protected function edgeBandingContent(string $page): ?array
|
||
{
|
||
$scope = match ($page) {
|
||
'edge-banding-prices' => 'edge-banding.prices',
|
||
'details' => 'edge-banding.details',
|
||
'edge-banding-payables' => 'edge-banding.payables',
|
||
default => null,
|
||
};
|
||
|
||
if ($scope === null) {
|
||
return null;
|
||
}
|
||
|
||
$filters = match ($page) {
|
||
'edge-banding-prices' => $this->edgeBandingFilters([
|
||
'raw_name', 'raw_code', 'production_code', 'supplier',
|
||
]),
|
||
'details' => $this->edgeBandingFilters([
|
||
'date', 'production_code', 'raw_name', 'return_flag',
|
||
'outbound_supplier', 'arrival_supplier', 'outbound_no',
|
||
]),
|
||
'edge-banding-payables' => $this->edgeBandingFilters(['period', 'supplier']),
|
||
default => [],
|
||
};
|
||
|
||
if ($page === 'edge-banding-payables' && ! isset($filters['period'])) {
|
||
$filters['period'] = '2026-06';
|
||
}
|
||
|
||
$pageNumber = max((int) request('data_page', 1), 1);
|
||
$limit = min(max((int) request('data_limit', 25), 1), 100);
|
||
$cache = $this->upstreamCaches->forSource(self::EDGE_BANDING_SOURCE);
|
||
|
||
try {
|
||
$result = $cache->remember(
|
||
$scope,
|
||
['filters' => $filters, 'page' => $pageNumber, 'limit' => $limit],
|
||
fn () => match ($scope) {
|
||
'edge-banding.prices' => $this->edgeBanding->prices($filters, $pageNumber, $limit),
|
||
'edge-banding.details' => $this->edgeBanding->details($filters, $pageNumber, $limit),
|
||
'edge-banding.payables' => $this->edgeBanding->payables($filters, $pageNumber, $limit),
|
||
},
|
||
);
|
||
|
||
return [
|
||
'filters' => $filters,
|
||
'result' => $result,
|
||
'page' => $pageNumber,
|
||
'limit' => $limit,
|
||
'last_page' => max((int) ceil(max((int) ($result['total'] ?? 0), 0) / $limit), 1),
|
||
'status' => $cache->responseHeaders(),
|
||
'error' => null,
|
||
];
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
return [
|
||
'filters' => $filters,
|
||
'result' => ['data' => [], 'total' => 0],
|
||
'page' => $pageNumber,
|
||
'limit' => $limit,
|
||
'last_page' => 1,
|
||
'status' => [],
|
||
'error' => $exception->getMessage(),
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return [
|
||
'filters' => $filters,
|
||
'result' => ['data' => [], 'total' => 0],
|
||
'page' => $pageNumber,
|
||
'limit' => $limit,
|
||
'last_page' => 1,
|
||
'status' => [],
|
||
'error' => '封边条数据暂时无法读取,请稍后重试。',
|
||
];
|
||
}
|
||
}
|
||
|
||
private function edgeBandingFilters(array $fields): array
|
||
{
|
||
$filters = [];
|
||
|
||
foreach ($fields as $field) {
|
||
$value = trim((string) request($field, ''));
|
||
|
||
if ($value !== '') {
|
||
$filters[$field] = $value;
|
||
}
|
||
}
|
||
|
||
return $filters;
|
||
}
|
||
|
||
/**
|
||
* 种菜应收对账 - 加载对账数据(21字段)
|
||
*/
|
||
protected function contentForZhongcaiReconciliation(): ?array
|
||
{
|
||
return [
|
||
'view' => 'store_receivables.zhongcai_reconciliations.partials.embedded-table',
|
||
'records' => StoreZhongcaiReconciliation::orderBy('bill_date', 'desc')
|
||
->orderBy('id', 'desc')
|
||
->paginate(setting('default.list_limit', 25)),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 营销账户账单 - 加载指定平台的数据
|
||
* 淘系支付宝、拼多多使用独立的流水账单表,其他平台使用通用汇总账单表
|
||
*/
|
||
protected function contentForMarketingAccountBills(string $tab): ?array
|
||
{
|
||
if (!in_array($tab, self::MARKETING_PLATFORMS, true)) {
|
||
return null;
|
||
}
|
||
|
||
// 淘系支付宝 - 使用独立的流水账单表(8字段)
|
||
if ($tab === 'taobao-alipay') {
|
||
$request = request();
|
||
$storeName = mb_substr(trim((string) $request->query('store_name')), 0, 191);
|
||
$fromDate = $this->dateFilter($request->query('from'));
|
||
$toDate = $this->dateFilter($request->query('to'));
|
||
|
||
$query = MarketingTaobaoAlipayBill::query();
|
||
|
||
if ($storeName !== '') {
|
||
$query->where('store_name', $storeName);
|
||
}
|
||
if ($fromDate !== '') {
|
||
$query->where('transaction_date', '>=', $fromDate);
|
||
}
|
||
if ($toDate !== '') {
|
||
$query->where('transaction_date', '<=', $toDate);
|
||
}
|
||
|
||
// 筛选项下拉用的店铺列表(去重)
|
||
$storeOptions = MarketingTaobaoAlipayBill::select('store_name')
|
||
->whereNotNull('store_name')
|
||
->distinct()
|
||
->orderBy('store_name')
|
||
->pluck('store_name')
|
||
->toArray();
|
||
|
||
return [
|
||
'view' => 'banking.marketing_taobao_alipay_bills.partials.embedded-table',
|
||
'bills' => $query->orderBy('transaction_date', 'desc')
|
||
->orderBy('id', 'desc')
|
||
->paginate(setting('default.list_limit', 25))
|
||
->appends([
|
||
'store_name' => $storeName,
|
||
'from' => $fromDate,
|
||
'to' => $toDate,
|
||
]),
|
||
'filters' => [
|
||
'store_name' => $storeName,
|
||
'from' => $fromDate,
|
||
'to' => $toDate,
|
||
'store_options' => $storeOptions,
|
||
],
|
||
];
|
||
}
|
||
|
||
// 拼多多 - 从上游 JianSun.pdd_spread_bills 读取并按表1字段展示(7列)
|
||
// 表1列:店铺名称 | 金额 | 余额 | 摘要 | 收支类型 | 资金类型 | 费用时间
|
||
if ($tab === 'pinduoduo') {
|
||
$request = request();
|
||
$storeName = mb_substr(trim((string) $request->query('store_name')), 0, 191);
|
||
$fromDate = $this->dateFilter($request->query('from'));
|
||
$toDate = $this->dateFilter($request->query('to'));
|
||
$flowType = trim((string) $request->query('flow_type'));
|
||
$flowType = in_array($flowType, ['income', 'expense'], true) ? $flowType : '';
|
||
$page = max(1, (int) $request->query('page', 1));
|
||
$pageSize = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
|
||
$repository = app(PinduoduoSpreadBillRepository::class);
|
||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||
$filters = [
|
||
'store_name' => $storeName !== '' ? $storeName : null,
|
||
'from' => $fromDate !== '' ? $fromDate : null,
|
||
'to' => $toDate !== '' ? $toDate : null,
|
||
'flow_type' => $flowType !== '' ? $flowType : null,
|
||
];
|
||
|
||
// 页面只读取共享查询快照;冷查询由统一限流和熔断链路访问上游。
|
||
$result = $cache->remember(
|
||
'pinduoduo-bills.list',
|
||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||
fn () => $repository->paginate(
|
||
$filters['store_name'],
|
||
$filters['from'],
|
||
$filters['to'],
|
||
$filters['flow_type'],
|
||
$page,
|
||
$pageSize
|
||
)
|
||
);
|
||
$summary = $cache->remember(
|
||
'pinduoduo-bills.summary',
|
||
['filters' => $filters],
|
||
fn () => $repository->summary(
|
||
$filters['store_name'],
|
||
$filters['from'],
|
||
$filters['to'],
|
||
$filters['flow_type']
|
||
)
|
||
);
|
||
|
||
$storeOptions = $cache->remember(
|
||
'pinduoduo-bills.options',
|
||
[],
|
||
fn () => $repository->storeOptions()
|
||
);
|
||
|
||
// 包装成 Laravel 分页器,便于 Blade 用 $bills->links()
|
||
$bills = new LengthAwarePaginator(
|
||
Collection::make($result['rows']),
|
||
$result['total'],
|
||
$pageSize,
|
||
$page,
|
||
[
|
||
'path' => $request->url(),
|
||
'query' => $request->query(),
|
||
]
|
||
);
|
||
|
||
return [
|
||
'view' => 'banking.marketing_pinduoduo_bills.partials.embedded-table',
|
||
'bills' => $bills,
|
||
'filters' => [
|
||
'store_name' => $storeName,
|
||
'from' => $fromDate,
|
||
'to' => $toDate,
|
||
'flow_type' => $flowType,
|
||
'store_options' => $storeOptions,
|
||
],
|
||
'summary' => $summary,
|
||
];
|
||
}
|
||
|
||
// 其他平台 - 使用通用汇总账单表(15字段)
|
||
return [
|
||
'view' => 'banking.marketing_account_bills.partials.embedded-table',
|
||
'platform' => $tab,
|
||
'accounts' => MarketingAccountBill::platform($tab)
|
||
->orderBy('bill_date', 'desc')
|
||
->orderBy('id', 'desc')
|
||
->paginate(setting('default.list_limit', 25)),
|
||
];
|
||
}
|
||
|
||
protected function redirectToPage(array $section, array $page)
|
||
{
|
||
$parameters = [
|
||
'section' => $section['slug'],
|
||
'page' => $page['slug'],
|
||
];
|
||
|
||
if (! empty($page['tabs'])) {
|
||
$parameters['tab'] = $page['tabs'][0]['slug'];
|
||
}
|
||
|
||
return redirect()->route('business.pages.show', $parameters);
|
||
}
|
||
|
||
private function dateFilter(mixed $value): string
|
||
{
|
||
$date = trim((string) $value);
|
||
|
||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
|
||
return '';
|
||
}
|
||
|
||
[$year, $month, $day] = array_map('intval', explode('-', $date));
|
||
|
||
return checkdate($month, $day, $year) ? $date : '';
|
||
}
|
||
}
|