4807 lines
212 KiB
PHP
4807 lines
212 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Business;
|
||
|
||
use App\Business\BusinessTableImporter;
|
||
use App\Business\InventoryWarehouseNameMappingWorkbookImporter;
|
||
use App\Business\InventoryWarehouseShortNames;
|
||
use App\Business\CourierPayableSummaryFilter;
|
||
use App\Business\CourierPayableSummaryStore;
|
||
use App\Business\FundForecastTemplate;
|
||
use App\Business\TableRegistry;
|
||
use App\Business\UpstreamBusinessInformation;
|
||
use App\Business\StoreReceivingAccountTemplate;
|
||
use App\Business\StoreReceivingAccountWorkbookImporter;
|
||
use App\Business\WithdrawalManagementReport;
|
||
use App\Jobs\Common\InventoryReportExport;
|
||
use App\Models\Banking\MarketingAccountBill;
|
||
use App\Models\Business\CourierAgreementSignature;
|
||
use App\Models\Business\CourierQuoteSignature;
|
||
use App\Models\Business\CourierQuoteVersion;
|
||
use App\Models\Business\CourierSigningSubject;
|
||
use App\Models\Business\InstallationDetail;
|
||
use App\Models\Business\InventoryWarehouseNameMapping;
|
||
use App\Business\CourierSurchargeAuditLogRepository;
|
||
use App\Business\CourierSurchargeQuoteRepository;
|
||
use App\Business\CourierSurchargeTypeRepository;
|
||
use App\Models\StoreReceivables\StoreZhongcaiReconciliation;
|
||
use App\Utilities\SidebarPermissions;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Routing\Controller;
|
||
use Illuminate\Pagination\LengthAwarePaginator;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Pagination\Paginator;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Str;
|
||
use Illuminate\Database\QueryException;
|
||
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
|
||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||
use Modules\EcommerceFinance\Repositories\AlibabaAccountBalanceRepository;
|
||
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
|
||
use Modules\EcommerceFinance\View\BillOffsetPageData;
|
||
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\ShippingOrderRepository;
|
||
use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||
use Modules\EcommerceFinance\Repositories\InventoryRepository;
|
||
use Modules\EcommerceFinance\Repositories\InstallationDetailMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\JdPromotionAccountBalanceRepository;
|
||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||
use Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\ProductPriceMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\RawBoardPayablesRepository;
|
||
use Modules\EcommerceFinance\Repositories\DingPayableWorkflowMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\DingOnlineDaifaPriceMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\SmallPaymentWorkOrderMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\SupplierPayableReportRepository;
|
||
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
|
||
use Modules\EcommerceFinance\Services\AccountMappingStore;
|
||
use Modules\EcommerceFinance\Services\AccountMappingValidator;
|
||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||
use Modules\EcommerceFinance\Services\SupplierPayableMaintenanceRegistry;
|
||
use Modules\EcommerceFinance\Services\PayablesFinalRegistry;
|
||
use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore;
|
||
use Modules\EcommerceFinance\View\PlatformPromotionAccountPageData;
|
||
use Modules\EcommerceFinance\View\OrderDataPageData;
|
||
use RuntimeException;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||
use Throwable;
|
||
|
||
class Pages extends Controller
|
||
{
|
||
private const EDGE_BANDING_SOURCE = 'jiansun';
|
||
|
||
private const INVENTORY_SOURCE = 'jiansun';
|
||
|
||
private const RAW_BOARD_SOURCE = 'jiansun';
|
||
|
||
/**
|
||
* 「终版」目录只提供空的表结构,数据由各自按表导入,不解析其他目录的既有页面内容。
|
||
*/
|
||
private const PAYABLES_FINAL_SECTIONS = ['supplier-payables-final', 'courier-payables-final'];
|
||
|
||
public function __construct(
|
||
private readonly EdgeBandingRepository $edgeBanding,
|
||
private readonly InventoryRepository $inventory,
|
||
private readonly InstallationDetailMirrorRepository $installationDetails,
|
||
private readonly NonJingxiRefundMirrorRepository $nonJingxiRefunds,
|
||
private readonly SmallPaymentWorkOrderMirrorRepository $smallPaymentWorkOrders,
|
||
private readonly ProductCatalogMirrorRepository $productCatalog,
|
||
private readonly ProductPriceMirrorRepository $productPrices,
|
||
private readonly CourierInformationMirrorRepository $courierInformation,
|
||
private readonly CourierFreightReconciliationMirrorRepository $courierFreightReconciliations,
|
||
private readonly ThirdPartyWarehouseStorageMirrorRepository $thirdPartyWarehouseStorage,
|
||
private readonly RawBoardPayablesRepository $rawBoardPayables,
|
||
private readonly DingPayableWorkflowMirrorRepository $dingPayableWorkflows,
|
||
private readonly DingOnlineDaifaPriceMirrorRepository $onlineDaifaPrices,
|
||
private readonly SupplierPayableReportRepository $supplierPayableReports,
|
||
private readonly SupplierPayableOverrideStore $supplierPayableOverrides,
|
||
private readonly SupplierPayableMaintenanceRegistry $supplierPayableMaintenance,
|
||
private readonly UpstreamQueryCacheFactory $upstreamCaches,
|
||
private readonly CourierSurchargeQuoteRepository $courierSurchargeQuotes,
|
||
private readonly CourierSurchargeTypeRepository $courierSurchargeTypes,
|
||
private readonly CourierSurchargeAuditLogRepository $courierSurchargeAudits,
|
||
private readonly WithdrawalManagementReport $withdrawalManagementReport,
|
||
private readonly ShippingOrderRepository $shippingOrders,
|
||
) {
|
||
$this->middleware('auth');
|
||
$this->middleware('company.identify');
|
||
}
|
||
|
||
/**
|
||
* 支持的营销账户平台列表
|
||
*/
|
||
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',
|
||
];
|
||
|
||
protected const ACCOUNT_BALANCE_SECTION = 'account-balance-data';
|
||
|
||
protected const SMALL_PAYMENT_SECTION = 'financial-operations';
|
||
|
||
protected const ACCOUNT_BALANCE_PAGES = [
|
||
'bank-balance',
|
||
'store-receiving-balance',
|
||
'marketing-account-balance',
|
||
'deposit-account-balance',
|
||
];
|
||
|
||
public function home()
|
||
{
|
||
$section = collect(SidebarPermissions::navigationFor(user()))->first();
|
||
|
||
if (empty($section)) {
|
||
return view('business.access-pending');
|
||
}
|
||
|
||
$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)
|
||
{
|
||
if ($section === 'refunds' && $page === 'small-payment-work-orders') {
|
||
$url = route('business.pages.show', [
|
||
'section' => self::SMALL_PAYMENT_SECTION,
|
||
'page' => $page,
|
||
]);
|
||
$query = request()->getQueryString();
|
||
|
||
return redirect()->to($url . ($query === null ? '' : '?' . $query));
|
||
}
|
||
|
||
if ($section === 'billing-transactions' && in_array($page, self::ACCOUNT_BALANCE_PAGES, true)) {
|
||
$url = route('business.pages.show', array_filter([
|
||
'section' => self::ACCOUNT_BALANCE_SECTION,
|
||
'page' => $page,
|
||
'tab' => $tab,
|
||
], static fn (mixed $value): bool => $value !== null));
|
||
$query = request()->getQueryString();
|
||
|
||
return redirect()->to($url . ($query === null ? '' : '?' . $query));
|
||
}
|
||
|
||
$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', $this->viewData($sectionConfig));
|
||
}
|
||
|
||
if ($page === null) {
|
||
if ($section === 'courier-payables' || in_array($section, self::PAYABLES_FINAL_SECTIONS, true)) {
|
||
return view('business.pages.show', $this->viewData($sectionConfig));
|
||
}
|
||
|
||
return $this->redirectToPage($sectionConfig, $pages->first());
|
||
}
|
||
|
||
$pageConfig = $pages
|
||
->flatMap(fn (array $configuredPage) => [
|
||
$configuredPage,
|
||
...($configuredPage['children'] ?? []),
|
||
])
|
||
->firstWhere('slug', $page);
|
||
|
||
abort_if(empty($pageConfig), 404);
|
||
|
||
if (! empty($pageConfig['children']) && empty($pageConfig['tabs'])) {
|
||
return $this->redirectToPage($sectionConfig, $pageConfig['children'][0]);
|
||
}
|
||
|
||
$tabs = collect($pageConfig['tabs'] ?? []);
|
||
|
||
if ($tabs->isEmpty()) {
|
||
// Old bookmarked tab URLs should land on the now-empty parent
|
||
// directory while the receiving-account pages are being rebuilt.
|
||
if ($tab !== null) {
|
||
return redirect()->route('business.pages.show', [
|
||
'section' => $sectionConfig['slug'],
|
||
'page' => $pageConfig['slug'],
|
||
]);
|
||
}
|
||
|
||
if ($section === 'courier-payables' && $page === 'freight-reconciliation' && request()->boolean('courier_summary')) {
|
||
try {
|
||
$summary = $this->courierFreightReconciliations->requestSummary($this->freightReconciliationFilters());
|
||
$response = response()->json(['data' => $summary], $summary === null ? 202 : 200, [
|
||
'Cache-Control' => 'private, no-store', 'X-Accel-Buffering' => 'no',
|
||
]);
|
||
// Explicit framing lets clients finish reading before the
|
||
// application termination callback computes the aggregate.
|
||
return $response->header('Content-Length', strlen($response->getContent()));
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
return response()->json(['message' => '完整汇总暂时无法计算,明细仍可正常查看。'], 503, ['Cache-Control' => 'private, no-store']);
|
||
}
|
||
}
|
||
|
||
return view('business.pages.show', $this->viewData(
|
||
$sectionConfig,
|
||
$pageConfig,
|
||
$tabs,
|
||
null,
|
||
$this->resolveContentForSection($section, $page, null),
|
||
));
|
||
}
|
||
|
||
if ($tab === null) {
|
||
return $this->redirectToPage($sectionConfig, $pageConfig);
|
||
}
|
||
|
||
$activeTab = $tabs->firstWhere('slug', $tab);
|
||
|
||
abort_if(empty($activeTab), 404);
|
||
|
||
return view('business.pages.show', $this->viewData(
|
||
$sectionConfig,
|
||
$pageConfig,
|
||
$tabs,
|
||
$activeTab,
|
||
$this->resolveContentForSection($section, $page, $tab),
|
||
));
|
||
}
|
||
|
||
public function installationDetails(string $guid)
|
||
{
|
||
$section = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', 'financial-operations');
|
||
abort_unless($section && collect($section['pages'] ?? [])->contains('slug', 'installation-details'), 403, '当前账号没有该页面权限。');
|
||
$payload = $this->installationDetails->details($guid);
|
||
if (! $payload['ready']) {
|
||
return response()->json(['message' => '安装明细镜像尚未完成首次同步'], 503);
|
||
}
|
||
|
||
return response()->json(['data' => $payload['data'], 'has_more' => $payload['has_more'] ?? false]);
|
||
}
|
||
|
||
public function exportInstallationDetails(Request $request): StreamedResponse
|
||
{
|
||
$section = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', 'financial-operations');
|
||
abort_unless($section && collect($section['pages'] ?? [])->contains('slug', 'installation-details'), 403, '当前账号没有该页面权限。');
|
||
|
||
$filters = InstallationDetailMirrorRepository::requestFilters();
|
||
$firstPage = $this->installationDetails->paginate(1, 100, $filters);
|
||
abort_if(! $firstPage['ready'], 503, '安装明细镜像尚未完成首次同步');
|
||
$labels = InstallationDetail::columnLabels();
|
||
|
||
return response()->streamDownload(function () use ($filters, $firstPage, $labels): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建安装明细导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, array_values($labels));
|
||
|
||
$page = 1;
|
||
$result = $firstPage;
|
||
do {
|
||
foreach ($result['data'] as $row) {
|
||
fputcsv($csv, array_map(
|
||
fn (string $name): string => $this->csvText($row[$name] ?? ''),
|
||
array_keys($labels),
|
||
));
|
||
}
|
||
|
||
$page++;
|
||
$hasMore = ($page - 1) * 100 < $result['total'];
|
||
if ($hasMore) {
|
||
$result = $this->installationDetails->paginate($page, 100, $filters);
|
||
}
|
||
} while ($hasMore);
|
||
|
||
fclose($csv);
|
||
}, '安装明细_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
public function exportBillOffset(Request $request): StreamedResponse
|
||
{
|
||
$section = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', 'order-data');
|
||
abort_unless($section && collect($section['pages'] ?? [])->contains('slug', 'bill-offset'), 403, '当前账号没有该页面权限。');
|
||
|
||
$repository = app(BillOffsetPageData::class);
|
||
$filters = $request->query();
|
||
$result = $repository->data($filters, 1, 100);
|
||
abort_if($result['billOffsetError'] !== null, 503, $result['billOffsetError']);
|
||
$filters = $result['billOffsetFilters'];
|
||
|
||
return response()->streamDownload(function () use ($repository, $filters, $result): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建账单冲减导出流。');
|
||
}
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
$labels = [
|
||
'internal_order_no' => '内部订单号', 'online_order_no' => '线上订单号',
|
||
'shop_name' => '店铺名称', 'shop_code' => '店铺编码', 'product_code' => '商品编码',
|
||
'style_code' => '款式编码(参考)', 'shop_product_code' => '店铺商品编码',
|
||
'shop_style_code' => '店铺款式编码', 'account_500112' => '500112冲减收入',
|
||
];
|
||
fputcsv($csv, array_values($labels));
|
||
$current = $result;
|
||
$page = 1;
|
||
do {
|
||
foreach ($current['billOffsetRows'] as $row) {
|
||
fputcsv($csv, array_map(
|
||
function (string $field) use ($row): string {
|
||
$value = data_get($row, $field);
|
||
|
||
if ($field === 'account_500112') {
|
||
return $value === null || $value === '' ? '' : number_format((float) $value, 2, '.', '');
|
||
}
|
||
|
||
return $field === 'shop_name'
|
||
? $this->csvText($value)
|
||
: $this->csvIdentifier($value);
|
||
},
|
||
array_keys($labels),
|
||
));
|
||
}
|
||
if (! $current['billOffsetRows']->hasMorePages()) break;
|
||
$current = $repository->dataForNormalizedFilters($filters, ++$page, 100);
|
||
if ($current['billOffsetError'] !== null) {
|
||
throw new \RuntimeException('账单冲减导出未完成,请重试。');
|
||
}
|
||
} while (true);
|
||
fclose($csv);
|
||
}, '账单冲减_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 商品月加权平均单价:读取已计算的月份数据,默认最新月份。
|
||
*/
|
||
protected function monthlyWeightedAveragePriceRows(): LengthAwarePaginator
|
||
{
|
||
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
|
||
$months = DB::connection($connection)->table('product_monthly_weighted_average_states')
|
||
->where('status', 'computed')
|
||
->orderByDesc('report_month')
|
||
->pluck('report_month')
|
||
->map(static fn ($month): string => substr((string) $month, 0, 10))
|
||
->all();
|
||
|
||
$requested = substr(trim((string) request('month', '')), 0, 10);
|
||
if (preg_match('/^\d{4}-\d{2}$/', $requested) === 1) {
|
||
// 筛选栏的月份选择器提交 Y-m,这里补成当月 1 号再匹配。
|
||
$requested .= '-01';
|
||
}
|
||
// 明确指定月份时严格按该月份取数(该月没算出来就显示空表),避免筛选条件与数据不一致。
|
||
$month = $requested !== '' ? $requested : ($months[0] ?? null);
|
||
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 50);
|
||
$page = max((int) request('page', 1), 1);
|
||
$empty = new LengthAwarePaginator([], 0, $limit, $page, [
|
||
'path' => request()->url(),
|
||
'query' => request()->query(),
|
||
]);
|
||
if ($month === null) {
|
||
return $empty;
|
||
}
|
||
|
||
$query = DB::connection($connection)->table('product_monthly_weighted_average_rows')
|
||
->where('report_month', $month);
|
||
$productCode = trim((string) request('product_code', ''));
|
||
if ($productCode !== '') {
|
||
// 与进销存总表一致:商品编码按精确查询。
|
||
$query->where('product_code', $productCode);
|
||
}
|
||
$provider = trim((string) request('warehouse_provider', ''));
|
||
if ($provider !== '') {
|
||
$query->where('warehouse_short_name', 'like', '%' . addcslashes($provider, '%_\\') . '%');
|
||
}
|
||
$category = trim((string) request('category', ''));
|
||
if ($category !== '') {
|
||
$query->where('product_name', 'like', '%' . addcslashes($category, '%_\\') . '%');
|
||
}
|
||
$total = (clone $query)->count();
|
||
if ($total === 0) {
|
||
return $empty;
|
||
}
|
||
|
||
$items = $query
|
||
->orderBy('warehouse_short_name')
|
||
->orderBy('product_code')
|
||
->forPage($page, $limit)
|
||
->get([
|
||
DB::raw('report_month as month'),
|
||
'barcode_warehouse',
|
||
'warehouse_short_name',
|
||
'supplier',
|
||
'product_code',
|
||
'product_name',
|
||
'opening_quantity',
|
||
'opening_unit_price',
|
||
'opening_original_amount',
|
||
DB::raw('adjusted_unit_price as opening_adjusted_price'),
|
||
'opening_amount',
|
||
'purchase_quantity',
|
||
'purchase_unit_price',
|
||
'purchase_amount',
|
||
'weighted_average_unit_price',
|
||
'pricing_status',
|
||
]);
|
||
|
||
return new LengthAwarePaginator($items, $total, $limit, $page, [
|
||
'path' => request()->url(),
|
||
'query' => request()->query(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 商品月加权平均单价导出:按当前月份与筛选条件导出 CSV,列序跟随页面显示列。
|
||
*/
|
||
public function exportMonthlyWeightedAveragePrice(Request $request): StreamedResponse
|
||
{
|
||
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
|
||
$map = [
|
||
'month' => ['report_month', '月份'],
|
||
'barcode_warehouse' => ['barcode_warehouse', '条码+仓库'],
|
||
'warehouse_short_name' => ['warehouse_short_name', '仓库简称'],
|
||
'supplier' => ['supplier', '供应商'],
|
||
'product_code' => ['product_code', '商品编码'],
|
||
'product_name' => ['product_name', '商品名称'],
|
||
'opening_quantity' => ['opening_quantity', '原始数量'],
|
||
'opening_unit_price' => ['opening_unit_price', '原始价格'],
|
||
'opening_original_amount' => ['opening_original_amount', '原始期初金额'],
|
||
'opening_adjusted_price' => ['adjusted_unit_price', '更改价格'],
|
||
'opening_amount' => ['opening_amount', '期初金额'],
|
||
'purchase_quantity' => ['purchase_quantity', '采购数量'],
|
||
'purchase_unit_price' => ['purchase_unit_price', '采购单价'],
|
||
'purchase_amount' => ['purchase_amount', '入库金额'],
|
||
'weighted_average_unit_price' => ['weighted_average_unit_price', '加权平均单价'],
|
||
'pricing_status' => ['pricing_status', '定价状态'],
|
||
];
|
||
|
||
$months = DB::connection($connection)->table('product_monthly_weighted_average_states')
|
||
->where('status', 'computed')->orderByDesc('report_month')->pluck('report_month')
|
||
->map(static fn ($month): string => substr((string) $month, 0, 10))->all();
|
||
$requested = substr(trim((string) $request->query('month', '')), 0, 10);
|
||
if (preg_match('/^\d{4}-\d{2}$/', $requested) === 1) {
|
||
// 导出时筛选栏提交的也是 Y-m,同样补成当月 1 号。
|
||
$requested .= '-01';
|
||
}
|
||
$month = $requested !== '' ? $requested : ($months[0] ?? null);
|
||
|
||
$query = DB::connection($connection)->table('product_monthly_weighted_average_rows');
|
||
if ($month !== null) {
|
||
$query->where('report_month', $month);
|
||
}
|
||
$code = trim((string) $request->query('product_code', ''));
|
||
if ($code !== '') {
|
||
$query->where('product_code', $code);
|
||
}
|
||
$provider = trim((string) $request->query('warehouse_provider', ''));
|
||
if ($provider !== '') {
|
||
$query->where('warehouse_short_name', 'like', '%' . addcslashes($provider, '%_\\') . '%');
|
||
}
|
||
$category = trim((string) $request->query('category', ''));
|
||
if ($category !== '') {
|
||
$query->where('product_name', 'like', '%' . addcslashes($category, '%_\\') . '%');
|
||
}
|
||
|
||
$selected = array_values(array_intersect(
|
||
array_filter(explode(',', (string) $request->query('columns', ''))),
|
||
array_keys($map),
|
||
));
|
||
if ($selected === []) {
|
||
$selected = array_keys($map);
|
||
}
|
||
|
||
$size = min(max((int) setting('default.list_limit', 25), 1), 50);
|
||
if ($request->query('scope') === 'page') {
|
||
$query->forPage(max((int) $request->query('page', 1), 1), $size);
|
||
}
|
||
|
||
$filename = '商品月加权平均单价_' . ($month ?? 'all') . '.csv';
|
||
|
||
return response()->streamDownload(function () use ($query, $selected, $map): void {
|
||
$out = fopen('php://output', 'w');
|
||
fwrite($out, "\xEF\xBB\xBF");
|
||
fputcsv($out, array_map(static fn (string $key): string => $map[$key][1], $selected));
|
||
$query->orderBy('warehouse_short_name')->orderBy('product_code')
|
||
->chunk(1000, function ($rows) use ($out, $selected, $map): void {
|
||
foreach ($rows as $row) {
|
||
$line = [];
|
||
foreach ($selected as $key) {
|
||
$value = $row->{$map[$key][0]} ?? '';
|
||
$line[] = is_string($value) ? $value : (string) $value;
|
||
}
|
||
fputcsv($out, $line);
|
||
}
|
||
});
|
||
fclose($out);
|
||
}, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']);
|
||
}
|
||
|
||
protected function viewData(
|
||
array $section,
|
||
?array $page = null,
|
||
$tabs = null,
|
||
?array $activeTab = null,
|
||
?array $content = null,
|
||
): array {
|
||
$descriptor = null;
|
||
$rows = collect();
|
||
$tabValue = null;
|
||
$tabColumn = null;
|
||
$upstreamError = null;
|
||
$inventoryWarehouseMappings = collect();
|
||
$agreementCourierNames = $this->agreementCourierNames($page['slug'] ?? null);
|
||
$agreementCourierAccounts = $this->agreementCourierAccounts($page['slug'] ?? null, $agreementCourierNames);
|
||
|
||
if (($section['slug'] ?? null) === 'product-inventory' && ($page['slug'] ?? null) === 'inventory-report') {
|
||
if (request()->boolean('mapping')) {
|
||
$rows = new LengthAwarePaginator([], 0, 1, 1, [
|
||
'path' => request()->url(),
|
||
'query' => request()->query(),
|
||
]);
|
||
} else {
|
||
[$rows, $upstreamError] = $this->inventoryReportRows();
|
||
}
|
||
$inventoryWarehouseMappings = $this->inventoryWarehouseMappingRows();
|
||
}
|
||
|
||
if (($section['slug'] ?? null) === 'product-inventory'
|
||
&& ($page['slug'] ?? null) === 'monthly-weighted-average-price') {
|
||
$rows = $this->monthlyWeightedAveragePriceRows();
|
||
}
|
||
|
||
if ($page && $content === null) {
|
||
$descriptor = TableRegistry::describe(
|
||
$section['slug'],
|
||
$page['slug'],
|
||
$activeTab['slug'] ?? null,
|
||
);
|
||
|
||
if ($descriptor) {
|
||
if ($descriptor->readOnly) {
|
||
try {
|
||
$rows = app(UpstreamBusinessInformation::class)->paginate(
|
||
$section['slug'],
|
||
$page['slug'],
|
||
50,
|
||
);
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
// 首份快照尚未生成或某个来源暂时不可用时,保留页面、
|
||
// 交代原因并允许后台刷新,而不是把整个业务页变成通用 503。
|
||
$rows = new LengthAwarePaginator([], 0, 50, 1, [
|
||
'path' => request()->url(),
|
||
'query' => request()->query(),
|
||
]);
|
||
$upstreamError = $exception->getMessage();
|
||
}
|
||
|
||
return [
|
||
'section' => $section,
|
||
'page' => $page,
|
||
'tabs' => $tabs ?? collect(),
|
||
'activeTab' => $activeTab,
|
||
'content' => $content,
|
||
'descriptor' => $descriptor,
|
||
'rows' => $rows,
|
||
'upstreamError' => $upstreamError,
|
||
'tabColumn' => null,
|
||
'tabValue' => null,
|
||
'edgeBanding' => $this->edgeBandingContent($page['slug']),
|
||
'inventoryData' => $this->inventoryContent($page['slug']),
|
||
'rawBoardData' => $this->rawBoardContent($page['slug']),
|
||
'agreementTasks' => $this->agreementTasks($page['slug']),
|
||
'agreementQuoteSignatures' => $this->agreementQuoteSignatures($page['slug']),
|
||
'agreementSubjects' => $this->agreementSubjects($page['slug']),
|
||
'agreementCourierNames' => $agreementCourierNames,
|
||
'agreementCourierAccounts' => $agreementCourierAccounts,
|
||
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug']),
|
||
'inventoryWarehouseMappings' => $inventoryWarehouseMappings,
|
||
];
|
||
}
|
||
|
||
if (($section['slug'] ?? null) === 'product-inventory'
|
||
&& ($page['slug'] ?? null) === 'inventory-report') {
|
||
// inventoryReportRows() 已从聚水潭共享镜像按全仓口径取数。
|
||
// 这里必须保留该结果,不能再被目录表的旧演示数据覆盖。
|
||
} elseif ($this->isStoreReceivingAccountPage($section['slug'], $page['slug'])) {
|
||
try {
|
||
app(\App\Business\StoreReceivingAccountSynchronizer::class)->sync((int) (company_id() ?: 1));
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$upstreamError = '店铺基础信息暂时无法同步,请稍后重试。';
|
||
}
|
||
$rows = $this->storeReceivingAccountRows($activeTab['slug'] ?? null);
|
||
$tabColumn = 'store_platform';
|
||
} else {
|
||
$query = ($descriptor->model)::query();
|
||
|
||
if ($descriptor->tabFilterColumn && $activeTab) {
|
||
$tabValue = TableRegistry::tabValue($section['slug'], $page['slug'], $activeTab['slug']);
|
||
|
||
if ($tabValue !== null) {
|
||
$query->where($descriptor->tabFilterColumn, $tabValue);
|
||
}
|
||
|
||
$tabColumn = $descriptor->tabFilterColumn;
|
||
}
|
||
|
||
foreach (TableRegistry::fixedFilters($section['slug'], $page['slug']) as $column => $value) {
|
||
$query->where($column, $value);
|
||
}
|
||
|
||
$rows = $query->orderByDesc('id')->paginate(50)->withQueryString();
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'section' => $section,
|
||
'page' => $page,
|
||
'tabs' => $tabs ?? collect(),
|
||
'activeTab' => $activeTab,
|
||
'content' => $content,
|
||
'descriptor' => $descriptor,
|
||
'rows' => $rows,
|
||
'upstreamError' => $upstreamError,
|
||
'tabColumn' => $tabColumn,
|
||
'tabValue' => $tabValue,
|
||
'edgeBanding' => $page ? $this->edgeBandingContent($page['slug']) : null,
|
||
'inventoryData' => $page ? $this->inventoryContent($page['slug']) : null,
|
||
'rawBoardData' => $page ? $this->rawBoardContent($page['slug']) : null,
|
||
'agreementTasks' => $this->agreementTasks($page['slug'] ?? null),
|
||
'agreementQuoteSignatures' => $this->agreementQuoteSignatures($page['slug'] ?? null),
|
||
'agreementSubjects' => $this->agreementSubjects($page['slug'] ?? null),
|
||
'agreementCourierNames' => $agreementCourierNames,
|
||
'agreementCourierAccounts' => $agreementCourierAccounts,
|
||
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug'] ?? null),
|
||
'inventoryWarehouseMappings' => $inventoryWarehouseMappings,
|
||
'payablesFinal' => $this->payablesFinalData($section, $page),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 「终版」目录的按表数据:独立存放,初始为空,由页面上的导入/导出/查询/编辑维护。
|
||
*
|
||
* @return array<string,mixed>|null
|
||
*/
|
||
protected function payablesFinalData(array $section, ?array $page): ?array
|
||
{
|
||
$sectionSlug = (string) ($section['slug'] ?? '');
|
||
if (empty($page) || ! in_array($sectionSlug, [
|
||
PayablesFinalRegistry::SECTION_SUPPLIER,
|
||
PayablesFinalRegistry::SECTION_COURIER,
|
||
], true)) {
|
||
return null;
|
||
}
|
||
|
||
$dataset = (string) ($page['slug'] ?? '');
|
||
$registry = app(PayablesFinalRegistry::class);
|
||
if (! $registry->supports($sectionSlug, $dataset)) {
|
||
return null;
|
||
}
|
||
|
||
$keyword = trim((string) request('keyword', ''));
|
||
$rows = $this->supplierPayableOverrides->listRows(
|
||
PayablesFinalActions::DATASET_PREFIX
|
||
. ($sectionSlug === PayablesFinalRegistry::SECTION_COURIER ? 'courier:' : 'supplier:')
|
||
. $dataset,
|
||
(int) (company_id() ?: 1),
|
||
);
|
||
|
||
if ($keyword !== '') {
|
||
$needle = mb_strtolower($keyword);
|
||
$rows = array_values(array_filter($rows, static function (array $row) use ($needle): bool {
|
||
foreach ($row as $key => $value) {
|
||
if ($key === '_action_key' || $value === null) {
|
||
continue;
|
||
}
|
||
if (str_contains(mb_strtolower((string) $value), $needle)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}));
|
||
}
|
||
|
||
$perPage = 50;
|
||
$currentPage = max((int) request('page', 1), 1);
|
||
|
||
return [
|
||
'dataset' => $dataset,
|
||
'fields' => collect($registry->fields($sectionSlug, $dataset))
|
||
->map(static fn (array $definition, string $name): array => ['name' => $name] + $definition)
|
||
->values()
|
||
->all(),
|
||
'rows' => new LengthAwarePaginator(
|
||
array_slice($rows, ($currentPage - 1) * $perPage, $perPage),
|
||
count($rows),
|
||
$perPage,
|
||
$currentPage,
|
||
['path' => request()->url(), 'query' => request()->query()],
|
||
),
|
||
'keyword' => $keyword,
|
||
'actions' => url('/' . (company_id() ?: 1) . '/workspace/' . $sectionSlug . '/actions/' . $dataset),
|
||
];
|
||
}
|
||
|
||
/** @return array{0:LengthAwarePaginator,1:?string} */
|
||
private function inventoryReportRows(): array
|
||
{
|
||
$page = max(1, request()->integer('page', 1));
|
||
$perPage = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$filters = $this->inventoryReportFilters(request());
|
||
$error = null;
|
||
|
||
try {
|
||
if (! $this->thirdPartyWarehouseStorage->available()) {
|
||
$error = '进销存共享镜像尚未建立,暂时无法读取。';
|
||
} elseif (! $this->thirdPartyWarehouseStorage->allWarehousesReady()) {
|
||
$error = '进销存全仓库共享镜像正在执行首次同步,完成前不会展示不完整的单仓数据。';
|
||
} else {
|
||
$result = $this->thirdPartyWarehouseStorage->paginateMonthlyInventoryReport(
|
||
$filters,
|
||
$page,
|
||
$perPage,
|
||
);
|
||
|
||
$shortNames = app(InventoryWarehouseShortNames::class);
|
||
$names = $shortNames->forCompany((int) (company_id() ?: 1));
|
||
|
||
return [new LengthAwarePaginator(
|
||
array_map(static fn (object $row): object => $shortNames->fill($row, $names), $result['data']),
|
||
$result['total'],
|
||
$perPage,
|
||
$page,
|
||
[
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
],
|
||
), null];
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$error = '进销存共享镜像暂时无法读取,请稍后重试。';
|
||
}
|
||
|
||
return [new LengthAwarePaginator([], 0, $perPage, $page, [
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
]), $error];
|
||
}
|
||
|
||
private function inventoryWarehouseMappingRows(): LengthAwarePaginator
|
||
{
|
||
$query = InventoryWarehouseNameMapping::query()->orderBy('warehouse_provider')->orderBy('warehouse');
|
||
$provider = mb_substr(trim((string) request('mapping_warehouse_provider', '')), 0, 191);
|
||
$warehouse = mb_substr(trim((string) request('mapping_warehouse', '')), 0, 191);
|
||
|
||
if ($provider !== '') {
|
||
$query->where('warehouse_provider', 'like', '%' . addcslashes($provider, '%_\\') . '%');
|
||
}
|
||
if ($warehouse !== '') {
|
||
$query->where('warehouse', 'like', '%' . addcslashes($warehouse, '%_\\') . '%');
|
||
}
|
||
|
||
return $query->paginate(50)->withQueryString();
|
||
}
|
||
|
||
public function storeInventoryWarehouseMapping(Request $request)
|
||
{
|
||
$data = $request->validate([
|
||
'warehouse_provider' => ['required', 'string', 'max:191'],
|
||
'warehouse' => ['required', 'string', 'max:191'],
|
||
'warehouse_short_name' => ['nullable', 'string', 'max:191'],
|
||
]);
|
||
$data['warehouse_provider'] = trim($data['warehouse_provider']);
|
||
$data['warehouse'] = trim($data['warehouse']);
|
||
$data['warehouse_short_name'] = trim((string) ($data['warehouse_short_name'] ?? ''))
|
||
?: InventoryWarehouseNameMappingWorkbookImporter::suggestion($data['warehouse_provider'], $data['warehouse']);
|
||
|
||
if (InventoryWarehouseNameMapping::query()
|
||
->where('warehouse_provider', $data['warehouse_provider'])
|
||
->where('warehouse', $data['warehouse'])
|
||
->exists()) {
|
||
return back()->withInput()->withErrors(['warehouse' => '相同“仓储方 + 仓库”已存在,请使用修改功能。']);
|
||
}
|
||
|
||
InventoryWarehouseNameMapping::create($data + [
|
||
'company_id' => company_id() ?: 1,
|
||
'created_by' => auth()->id(),
|
||
'updated_by' => auth()->id(),
|
||
]);
|
||
|
||
return $this->inventoryWarehouseMappingRedirect()->with('success', '仓库名称映射已新增。');
|
||
}
|
||
|
||
public function updateInventoryWarehouseMapping(Request $request, int $id)
|
||
{
|
||
$mapping = InventoryWarehouseNameMapping::query()->findOrFail($id);
|
||
$data = $request->validate([
|
||
'warehouse_provider' => ['required', 'string', 'max:191'],
|
||
'warehouse' => ['required', 'string', 'max:191'],
|
||
'warehouse_short_name' => ['required', 'string', 'max:191'],
|
||
]);
|
||
$data = array_map(static fn (string $value): string => trim($value), $data);
|
||
|
||
if (InventoryWarehouseNameMapping::query()
|
||
->where('id', '!=', $mapping->id)
|
||
->where('warehouse_provider', $data['warehouse_provider'])
|
||
->where('warehouse', $data['warehouse'])
|
||
->exists()) {
|
||
return back()->withInput()->withErrors(['warehouse' => '相同“仓储方 + 仓库”已存在,不能产生重复台账。']);
|
||
}
|
||
|
||
$mapping->update($data + ['updated_by' => auth()->id()]);
|
||
|
||
return $this->inventoryWarehouseMappingRedirect()->with('success', '仓库名称映射已修改。');
|
||
}
|
||
|
||
public function previewInventoryWarehouseMappingImport(
|
||
Request $request,
|
||
InventoryWarehouseNameMappingWorkbookImporter $importer,
|
||
) {
|
||
$request->validate([
|
||
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'],
|
||
]);
|
||
$preview = $importer->preview($request->file('import'));
|
||
$token = Str::random(40);
|
||
session()->put('inventory_warehouse_mapping_previews.' . $token, $preview);
|
||
|
||
return $this->inventoryWarehouseMappingRedirect()
|
||
->with('warehouse_mapping_preview', $preview + ['token' => $token]);
|
||
}
|
||
|
||
public function commitInventoryWarehouseMappingImport(
|
||
Request $request,
|
||
InventoryWarehouseNameMappingWorkbookImporter $importer,
|
||
) {
|
||
$token = (string) $request->input('preview_token');
|
||
$preview = session()->pull('inventory_warehouse_mapping_previews.' . $token);
|
||
abort_if($token === '' || ! is_array($preview), 422, '导入预览已过期,请重新上传文件。');
|
||
$result = $importer->commit($preview);
|
||
|
||
return $this->inventoryWarehouseMappingRedirect()->with(
|
||
'success',
|
||
"导入完成:新增 {$result['created']} 条,跳过 {$result['skipped']} 条重复映射。",
|
||
);
|
||
}
|
||
|
||
public function exportInventoryWarehouseMappings(Request $request): StreamedResponse
|
||
{
|
||
$query = InventoryWarehouseNameMapping::query()->orderBy('warehouse_provider')->orderBy('warehouse');
|
||
$provider = mb_substr(trim((string) $request->query('mapping_warehouse_provider', '')), 0, 191);
|
||
$warehouse = mb_substr(trim((string) $request->query('mapping_warehouse', '')), 0, 191);
|
||
if ($provider !== '') {
|
||
$query->where('warehouse_provider', 'like', '%' . addcslashes($provider, '%_\\') . '%');
|
||
}
|
||
if ($warehouse !== '') {
|
||
$query->where('warehouse', 'like', '%' . addcslashes($warehouse, '%_\\') . '%');
|
||
}
|
||
|
||
return response()->streamDownload(function () use ($query): void {
|
||
$spreadsheet = new Spreadsheet();
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$sheet->setTitle('仓库名称映射表');
|
||
$headers = ['仓储方', '仓库', '仓库简称'];
|
||
$sheet->fromArray($headers, null, 'A1');
|
||
$lastColumn = Coordinate::stringFromColumnIndex(count($headers));
|
||
$headerRange = "A1:{$lastColumn}1";
|
||
$sheet->getStyle($headerRange)->applyFromArray([
|
||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']],
|
||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||
]);
|
||
$sheet->getRowDimension(1)->setRowHeight(24);
|
||
$sheet->freezePane('A2');
|
||
$sheet->setAutoFilter($headerRange);
|
||
foreach ([20, 32, 20] as $index => $width) {
|
||
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
|
||
}
|
||
$rowNumber = 2;
|
||
foreach ($query->cursor() as $mapping) {
|
||
$sheet->fromArray([
|
||
$mapping->warehouse_provider,
|
||
$mapping->warehouse,
|
||
$mapping->warehouse_short_name,
|
||
], null, "A{$rowNumber}");
|
||
$rowNumber++;
|
||
}
|
||
(new Xlsx($spreadsheet))->save('php://output');
|
||
$spreadsheet->disconnectWorksheets();
|
||
}, '仓库名称映射表_' . now()->format('Ymd_His') . '.xlsx', [
|
||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'Cache-Control' => 'private, no-store, no-cache, must-revalidate',
|
||
]);
|
||
}
|
||
|
||
private function inventoryWarehouseMappingRedirect()
|
||
{
|
||
return redirect()->route('business.pages.show', [
|
||
'company_id' => company_id() ?: 1,
|
||
'section' => 'product-inventory',
|
||
'page' => 'inventory-report',
|
||
'mapping' => 1,
|
||
'mapping_warehouse_provider' => request('mapping_warehouse_provider', ''),
|
||
'mapping_warehouse' => request('mapping_warehouse', ''),
|
||
]);
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
private function inventoryReportFilters(Request $request): array
|
||
{
|
||
$month = trim((string) $request->query('month', now()->startOfMonth()->subMonth()->format('Y-m')));
|
||
if (preg_match('/^(\d{4})-(\d{2})$/', $month, $matches) !== 1
|
||
|| ! checkdate((int) $matches[2], 1, (int) $matches[1])) {
|
||
$month = now()->startOfMonth()->subMonth()->format('Y-m');
|
||
}
|
||
$minimumMonth = substr(ThirdPartyWarehouseStorageMirrorRepository::INVENTORY_REPORT_START_DATE, 0, 7);
|
||
if ($month < $minimumMonth) {
|
||
$month = $minimumMonth;
|
||
}
|
||
|
||
return [
|
||
'month' => $month,
|
||
'product_code' => mb_substr(trim((string) $request->query('product_code', '')), 0, 100),
|
||
'warehouse_provider' => mb_substr(trim((string) $request->query('warehouse_provider', '')), 0, 191),
|
||
'category' => mb_substr(trim((string) $request->query('category', '')), 0, 191),
|
||
];
|
||
}
|
||
|
||
private function isStoreReceivingAccountPage(string $section, string $page): bool
|
||
{
|
||
return $section === 'fund-management' && $page === 'store-receiving-accounts';
|
||
}
|
||
|
||
private function storeReceivingAccountRows(?string $tab): LengthAwarePaginator
|
||
{
|
||
$query = \App\Models\Business\StoreReceivingAccount::query();
|
||
$platforms = $tab === null
|
||
? ['淘宝', '阿里巴巴', '天猫', '淘宝分销', '唯品会', '京东', '京喜', '抖音', '拼多多']
|
||
: TableRegistry::tabValues('fund-management', 'store-receiving-accounts', $tab);
|
||
|
||
if ($platforms !== []) {
|
||
$query->whereIn('store_platform', $platforms);
|
||
}
|
||
|
||
$erpName = mb_substr(trim((string) request('erp_store_name', '')), 0, 191);
|
||
if ($erpName !== '') {
|
||
$query->where('store_name', 'like', '%' . addcslashes($erpName, '%_\\') . '%');
|
||
}
|
||
|
||
$rows = $query->orderByDesc('id')->get();
|
||
$shopName = mb_substr(trim((string) request('shop_name', '')), 0, 191);
|
||
$status = trim((string) request('store_status', ''));
|
||
|
||
if ($shopName !== '' || $status !== '') {
|
||
$rows = $rows->filter(function ($row) use ($shopName, $status): bool {
|
||
return ($shopName === '' || str_contains((string) $row->shop_name, $shopName))
|
||
&& ($status === '' || (string) $row->store_status === $status);
|
||
})->values();
|
||
}
|
||
|
||
$page = max((int) request('page', 1), 1);
|
||
$perPage = 50;
|
||
$total = $rows->count();
|
||
|
||
return new LengthAwarePaginator(
|
||
$rows->forPage($page, $perPage)->values(),
|
||
$total,
|
||
$perPage,
|
||
$page,
|
||
['path' => request()->url(), 'query' => request()->query()],
|
||
);
|
||
}
|
||
|
||
protected function agreementCourierNames(?string $page): Collection
|
||
{
|
||
if ($page !== 'agreement-signing') {
|
||
return collect();
|
||
}
|
||
|
||
try {
|
||
return collect($this->courierInformation->names());
|
||
} catch (Throwable $exception) {
|
||
// 快递镜像不可用时仍允许打开签署页,并明确展示暂无可选资料。
|
||
report($exception);
|
||
return collect();
|
||
}
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
protected function agreementCourierAccounts(?string $page, ?Collection $courierNames = null): array
|
||
{
|
||
if ($page !== 'agreement-signing') {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
return $this->courierInformation->signingAccountsByName(
|
||
($courierNames ?? $this->agreementCourierNames($page))->values()->all(),
|
||
);
|
||
} catch (Throwable $exception) {
|
||
// 快递镜像不可用时仍可手工填写 E 签宝账号,不能阻断签署页。
|
||
report($exception);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
protected function agreementTasks(?string $page): Collection|\Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||
{
|
||
if ($page !== 'agreement-signing') {
|
||
return collect();
|
||
}
|
||
|
||
try {
|
||
$type = $this->agreementFilterType();
|
||
if ($type === 'quote-confirmation') {
|
||
return collect();
|
||
}
|
||
|
||
$query = CourierAgreementSignature::query();
|
||
$search = $this->agreementFilterSearch();
|
||
$status = $this->agreementFilterStatus();
|
||
|
||
if ($search !== '') {
|
||
$like = '%' . addcslashes($search, '%_\\') . '%';
|
||
$query->where(function ($builder) use ($like): void {
|
||
$builder
|
||
->where('business_no', 'like', $like)
|
||
->orWhere('courier_name', 'like', $like)
|
||
->orWhere('organization_name', 'like', $like)
|
||
->orWhere('operator_name', 'like', $like)
|
||
->orWhere('company_full_name', 'like', $like);
|
||
});
|
||
}
|
||
|
||
if ($status !== null) {
|
||
$query->where('status', $status);
|
||
}
|
||
|
||
if ($type !== null) {
|
||
$query->where('document_type', $type);
|
||
}
|
||
|
||
return $query
|
||
->latest('id')
|
||
->paginate(50, ['*'], 'agreement_task_page')->withQueryString();
|
||
} catch (QueryException $exception) {
|
||
// 迁移尚未执行时仍允许打开页面;发送接口会明确提示数据库未就绪。
|
||
report($exception);
|
||
return collect();
|
||
}
|
||
}
|
||
|
||
protected function agreementFilterSearch(): string
|
||
{
|
||
return mb_substr(trim((string) request('agreement_search', '')), 0, 191);
|
||
}
|
||
|
||
protected function agreementFilterStatus(): ?string
|
||
{
|
||
$status = trim((string) request('agreement_status', ''));
|
||
$allowed = [
|
||
CourierAgreementSignature::STATUS_PREPARED,
|
||
CourierAgreementSignature::STATUS_SENDING,
|
||
CourierAgreementSignature::STATUS_SIGNING,
|
||
CourierAgreementSignature::STATUS_COMPLETED,
|
||
CourierAgreementSignature::STATUS_ARCHIVED,
|
||
CourierAgreementSignature::STATUS_REVOKED,
|
||
CourierAgreementSignature::STATUS_EXPIRED,
|
||
CourierAgreementSignature::STATUS_REJECTED,
|
||
CourierAgreementSignature::STATUS_FAILED,
|
||
CourierAgreementSignature::STATUS_UNKNOWN,
|
||
];
|
||
|
||
return in_array($status, $allowed, true) ? $status : null;
|
||
}
|
||
|
||
protected function agreementFilterType(): ?string
|
||
{
|
||
$type = trim((string) request('agreement_type', ''));
|
||
$allowed = array_keys(CourierAgreementSignature::DOCUMENTS);
|
||
|
||
return in_array($type, $allowed, true) ? $type : null;
|
||
}
|
||
|
||
protected function agreementSubjects(?string $page): Collection
|
||
{
|
||
if ($page !== 'agreement-signing') {
|
||
return collect();
|
||
}
|
||
|
||
try {
|
||
return CourierSigningSubject::query()
|
||
->orderByDesc('is_default')
|
||
->orderBy('name')
|
||
->get();
|
||
} catch (QueryException $exception) {
|
||
// 主体管理迁移尚未执行时仍允许打开签署页面,默认使用当前公司名称。
|
||
report($exception);
|
||
return collect();
|
||
}
|
||
}
|
||
|
||
protected function agreementQuoteSignatures(?string $page): Collection|\Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||
{
|
||
if ($page !== 'agreement-signing') {
|
||
return collect();
|
||
}
|
||
|
||
try {
|
||
$type = $this->agreementFilterType();
|
||
if ($type !== null && $type !== 'quote-confirmation') {
|
||
return collect();
|
||
}
|
||
|
||
$query = CourierQuoteSignature::query()->with('version');
|
||
$search = $this->agreementFilterSearch();
|
||
$status = $this->agreementFilterStatus();
|
||
|
||
if ($search !== '') {
|
||
$like = '%' . addcslashes($search, '%_\\') . '%';
|
||
$query->where(function ($builder) use ($like): void {
|
||
$builder
|
||
->where('organization_name', 'like', $like)
|
||
->orWhere('operator_name', 'like', $like)
|
||
->orWhereHas('version', function ($versionQuery) use ($like): void {
|
||
$versionQuery
|
||
->where('version_code', 'like', $like)
|
||
->orWhere('carrier_name', 'like', $like);
|
||
});
|
||
});
|
||
}
|
||
|
||
if ($status !== null) {
|
||
$query->where('status', $status);
|
||
}
|
||
|
||
return $query
|
||
->latest('id')
|
||
->paginate(50, ['*'], 'agreement_quote_page')->withQueryString();
|
||
} catch (QueryException $exception) {
|
||
// 报价签署迁移尚未执行时仍允许打开协议签署页。
|
||
report($exception);
|
||
return collect();
|
||
}
|
||
}
|
||
|
||
protected function agreementQuoteVersions(?string $page): Collection
|
||
{
|
||
if ($page !== 'agreement-signing') {
|
||
return collect();
|
||
}
|
||
|
||
try {
|
||
return CourierQuoteVersion::query()
|
||
->where('status', 'approved')
|
||
->orderByDesc('effective_date')
|
||
->orderByDesc('id')
|
||
->limit(100)
|
||
->get();
|
||
} catch (QueryException $exception) {
|
||
// 报价版本迁移尚未执行时仍允许打开协议签署页,报价确认单会提示暂无可选版本。
|
||
report($exception);
|
||
return collect();
|
||
}
|
||
}
|
||
|
||
protected function resolveContentForSection(string $section, string $page, ?string $tab): ?array
|
||
{
|
||
if (in_array($section, self::PAYABLES_FINAL_SECTIONS, true)) {
|
||
return null;
|
||
}
|
||
|
||
return $this->resolveContent($page, $tab);
|
||
}
|
||
|
||
protected function resolveContent(string $page, ?string $tab): ?array
|
||
{
|
||
return match ($page) {
|
||
'marketing-account-bills' => $this->contentForMarketingAccountBills($tab),
|
||
'marketing-account-balance' => $this->contentForMarketingAccountBalance($tab),
|
||
'jingxi-refund-data' => $this->contentForJingxiRefundData(),
|
||
'non-jingxi-refund-data' => $this->contentForNonJingxiRefundData(),
|
||
'small-payment-work-orders' => $this->contentForSmallPaymentWorkOrders(),
|
||
'zhongcai-reconciliation' => $this->contentForZhongcaiReconciliation(),
|
||
'normal-products' => $this->contentForNormalProducts(),
|
||
'product-prices' => $this->contentForProductPrices(),
|
||
'courier-information' => $this->contentForCourierInformation(),
|
||
'freight-reconciliation' => $this->contentForFreightReconciliation(),
|
||
'third-party-warehouse-storage' => $this->contentForThirdPartyWarehouseStorage(),
|
||
'board-deductions' => $this->contentForBoardDeductions(),
|
||
'supplier-payables-report' => $this->contentForSupplierPayablesReport(),
|
||
'payable-cloud-warehouse-vehicle-fee' => $this->contentForDingPayableWorkflows(),
|
||
'online-daifa-price' => $this->contentForOnlineDaifaPrice(),
|
||
'courier-surcharge' => $this->contentForCourierSurcharge(),
|
||
'freight-difference', 'other-surcharge-summary', 'return-fee' => $this->contentForCourierPayableSummary($page),
|
||
'shipping-data' => $this->contentForShippingData(),
|
||
'withdrawal-management' => $this->contentForWithdrawalManagement(),
|
||
'bill-offset' => app(\Modules\EcommerceFinance\View\BillOffsetPageData::class)->page(),
|
||
'account-mapping' => $this->contentForAccountMapping(),
|
||
'planting-details' => $this->contentForPlantingDetails(),
|
||
default => null,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 账单流水数据 › 种菜明细表。
|
||
*/
|
||
protected function contentForPlantingDetails(): array
|
||
{
|
||
$request = request();
|
||
$filters = $this->plantingDetailFilters($request);
|
||
$query = $this->plantingDetailQuery($filters);
|
||
|
||
$summaryRow = (clone $query)->selectRaw(
|
||
'COUNT(*) AS rows_count, COALESCE(SUM(pieces), 0) AS pieces_total, '
|
||
. 'COALESCE(SUM(finance_selling_price), 0) AS selling_price_total, '
|
||
. 'COALESCE(SUM(finance_payment_amount), 0) AS payment_amount_total, '
|
||
. 'COALESCE(SUM(deduction), 0) AS deduction_total, '
|
||
. 'COALESCE(SUM(jingxi_price_diff), 0) AS jingxi_diff_total'
|
||
)->first();
|
||
|
||
$limit = min(max((int) setting('default.list_limit', 25), 10), 100);
|
||
$records = (clone $query)
|
||
->orderByDesc('bill_date')
|
||
->orderByDesc('id')
|
||
->paginate($limit)
|
||
->withQueryString();
|
||
|
||
return [
|
||
'view' => 'business.pages.billing-transactions.planting-details',
|
||
'records' => $records,
|
||
'filters' => $filters,
|
||
'summary' => [
|
||
'rows' => (int) ($summaryRow->rows_count ?? 0),
|
||
'pieces' => (int) ($summaryRow->pieces_total ?? 0),
|
||
'selling_price' => (float) ($summaryRow->selling_price_total ?? 0),
|
||
'payment_amount' => (float) ($summaryRow->payment_amount_total ?? 0),
|
||
'deduction' => (float) ($summaryRow->deduction_total ?? 0),
|
||
'jingxi_diff' => (float) ($summaryRow->jingxi_diff_total ?? 0),
|
||
],
|
||
'filterOptions' => [
|
||
'stores' => \App\Models\Business\PlantingDetail::query()->where('company_id', company_id())
|
||
->whereNotNull('store')->distinct()->orderBy('store')->limit(500)->pluck('store')->all(),
|
||
'owners' => \App\Models\Business\PlantingDetail::query()->where('company_id', company_id())
|
||
->whereNotNull('owner')->distinct()->orderBy('owner')->limit(500)->pluck('owner')->all(),
|
||
'types' => \App\Models\Business\PlantingDetail::query()->where('company_id', company_id())
|
||
->whereNotNull('type')->distinct()->orderBy('type')->limit(200)->pluck('type')->all(),
|
||
'tags' => \App\Models\Business\PlantingDetail::query()->where('company_id', company_id())
|
||
->whereNotNull('tag')->distinct()->orderBy('tag')->limit(200)->pluck('tag')->all(),
|
||
],
|
||
];
|
||
}
|
||
|
||
/** @return array<string,string> 种菜明细表筛选条件 */
|
||
private function plantingDetailFilters(Request $request): array
|
||
{
|
||
return [
|
||
'date_from' => $this->dateFilter($request->query('date_from')),
|
||
'date_to' => $this->dateFilter($request->query('date_to')),
|
||
'store' => mb_substr(trim((string) $request->query('store')), 0, 191),
|
||
'owner' => mb_substr(trim((string) $request->query('owner')), 0, 191),
|
||
'type' => mb_substr(trim((string) $request->query('type')), 0, 191),
|
||
'tag' => mb_substr(trim((string) $request->query('tag')), 0, 191),
|
||
'keyword' => mb_substr(trim((string) $request->query('keyword')), 0, 191),
|
||
];
|
||
}
|
||
|
||
/** @param array<string,string> $filters */
|
||
private function plantingDetailQuery(array $filters): \Illuminate\Database\Eloquent\Builder
|
||
{
|
||
$query = \App\Models\Business\PlantingDetail::query()->where('company_id', company_id());
|
||
|
||
if ($filters['date_from'] !== '') {
|
||
$query->where('bill_date', '>=', $filters['date_from']);
|
||
}
|
||
if ($filters['date_to'] !== '') {
|
||
$query->where('bill_date', '<=', $filters['date_to']);
|
||
}
|
||
foreach (['store', 'owner', 'type', 'tag'] as $field) {
|
||
if ($filters[$field] !== '') {
|
||
$query->where($field, $filters[$field]);
|
||
}
|
||
}
|
||
if ($filters['keyword'] !== '') {
|
||
$like = '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $filters['keyword']) . '%';
|
||
$query->where(function ($sub) use ($like): void {
|
||
$sub->where('order_no', 'like', $like)
|
||
->orWhere('remark', 'like', $like)
|
||
->orWhere('account', 'like', $like)
|
||
->orWhere('external_id', 'like', $like)
|
||
->orWhere('creator', 'like', $like);
|
||
});
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
/** 账单流水数据 › 种菜明细表导出(按当前筛选,全量) */
|
||
public function exportPlantingDetails(Request $request): StreamedResponse
|
||
{
|
||
$query = $this->plantingDetailQuery($this->plantingDetailFilters($request));
|
||
|
||
return response()->streamDownload(function () use ($query): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建种菜明细导出流。');
|
||
}
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, [
|
||
'日期', '订单编号', '财务售价', '财务运费', '财务佣金', '京喜差价', '扣款',
|
||
'财务空包费', '财务打款金额', '笔数', '备注', '正确店铺', '负责人', '账户',
|
||
'类型', '创建人', '标签', '所属利润中心', '负责平台', 'ID',
|
||
]);
|
||
|
||
$query->orderBy('id')->chunkById(2000, function ($rows) use ($csv): void {
|
||
foreach ($rows as $row) {
|
||
fputcsv($csv, [
|
||
$row->bill_date?->toDateString(),
|
||
$row->order_no,
|
||
$row->finance_selling_price,
|
||
$row->finance_freight,
|
||
$row->finance_commission,
|
||
$row->jingxi_price_diff,
|
||
$row->deduction,
|
||
$row->finance_empty_package_fee,
|
||
$row->finance_payment_amount,
|
||
$row->pieces,
|
||
$row->remark,
|
||
$row->store,
|
||
$row->owner,
|
||
$row->account,
|
||
$row->type,
|
||
$row->creator,
|
||
$row->tag,
|
||
$row->profit_center,
|
||
$row->responsible_platform,
|
||
$row->external_id,
|
||
]);
|
||
}
|
||
});
|
||
fclose($csv);
|
||
}, '种菜明细表_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 财务规则 · 科目映射表
|
||
*
|
||
* 表里存的是「科目对 + 方向基准」,不存最终借贷方向;记账时按账单实际收支代入。
|
||
* 页面支持导入追加、单条修改、新增、删除,并实时给出账套科目校验结果。
|
||
*/
|
||
protected function contentForAccountMapping(): array
|
||
{
|
||
$store = app(AccountMappingStore::class);
|
||
$validator = app(AccountMappingValidator::class);
|
||
|
||
$rows = [];
|
||
$summary = [
|
||
'total' => 0, 'error_rows' => 0, 'warning_rows' => 0, 'ok_rows' => 0,
|
||
'error_count' => 0, 'warning_count' => 0, 'tip_count' => 0,
|
||
];
|
||
$options = ['platforms' => [], 'categories' => [], 'basis' => [], 'invoices' => []];
|
||
$error = null;
|
||
|
||
try {
|
||
$rules = $store->all();
|
||
$validated = $validator->validate($rules);
|
||
foreach ($rules as $rule) {
|
||
$rows[] = $rule + ['issues' => $validated['issues'][(int) $rule['id']] ?? []];
|
||
}
|
||
$summary = $validated['summary'];
|
||
$options = $store->options();
|
||
} catch (Throwable $exception) {
|
||
$error = '映射表存储暂时读不出来,请稍后重试。';
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.financial-rules.account-mapping',
|
||
'amRows' => $rows,
|
||
'amSummary' => $summary,
|
||
'amOptions' => $options,
|
||
'amEndpoint' => $this->accountMappingEndpoint(),
|
||
'amError' => $error,
|
||
];
|
||
}
|
||
|
||
/** 维护接口前缀(去掉末尾的 /rules) */
|
||
protected function accountMappingEndpoint(): string
|
||
{
|
||
try {
|
||
$url = route('business.financial-rules.account-mapping.actions.rules');
|
||
} catch (Throwable) {
|
||
$url = url((int) (company_id() ?: 1) . '/workspace/financial-rules/account-mapping/actions/rules');
|
||
}
|
||
|
||
return (string) preg_replace('#/rules$#', '', $url);
|
||
}
|
||
|
||
protected function contentForCourierPayableSummary(string $page): array
|
||
{
|
||
$store = app(CourierPayableSummaryStore::class);
|
||
$snapshot = null;
|
||
$error = null;
|
||
|
||
try {
|
||
$snapshot = $store->snapshot((int) (company_id() ?: 1), $page);
|
||
} catch (RuntimeException) {
|
||
$error = '汇总数据暂时无法读取,请重新导入后再试。';
|
||
}
|
||
|
||
$headers = $snapshot['headers'] ?? [];
|
||
$allRows = collect($snapshot['rows'] ?? []);
|
||
$filter = app(CourierPayableSummaryFilter::class);
|
||
$filters = $filter->fromRequest(request());
|
||
$filteredRows = $filter->apply($allRows, $headers, $filters);
|
||
$perPage = 50;
|
||
$lastPage = max((int) ceil($filteredRows->count() / $perPage), 1);
|
||
$currentPage = min(max((int) request()->query('page', 1), 1), $lastPage);
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.summary-table',
|
||
'courierSummaryPage' => $page,
|
||
'courierSummaryTitle' => $store->title($page),
|
||
'courierSummaryHeaders' => $headers,
|
||
'courierSummaryRows' => new LengthAwarePaginator(
|
||
$filteredRows->forPage($currentPage, $perPage)->values(),
|
||
$filteredRows->count(),
|
||
$perPage,
|
||
$currentPage,
|
||
['path' => request()->url(), 'query' => array_filter(
|
||
$filters,
|
||
static fn (string $value): bool => $value !== '',
|
||
)],
|
||
),
|
||
'courierSummaryFilters' => $filters,
|
||
'courierSummaryHasFilters' => $filter->hasFilters($filters),
|
||
'courierSummarySourceTotal' => $allRows->count(),
|
||
'courierSummarySourceName' => $snapshot['source_name'] ?? null,
|
||
'courierSummaryImportedAt' => $snapshot['imported_at'] ?? null,
|
||
'courierSummaryError' => $error,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Consolidated, non-destructive view of the withdrawal configuration kept
|
||
* with each store receiving account. Updates remain in the source page so
|
||
* a withdrawal action cannot delete an entire store-account record.
|
||
*/
|
||
protected function contentForWithdrawalManagement(): array
|
||
{
|
||
$report = $this->withdrawalManagementReport->results(request());
|
||
$withdrawalRows = $report['rows'];
|
||
|
||
$page = max((int) request()->query('page', 1), 1);
|
||
$perPage = 50;
|
||
$withdrawalRows = new LengthAwarePaginator(
|
||
$withdrawalRows->forPage($page, $perPage)->values(),
|
||
$withdrawalRows->count(),
|
||
$perPage,
|
||
$page,
|
||
['path' => request()->url(), 'query' => request()->except('page')],
|
||
);
|
||
return [
|
||
'view' => 'business.pages.fund-management.withdrawal-management',
|
||
'withdrawalRows' => $withdrawalRows,
|
||
'withdrawalFilters' => $report['filters'],
|
||
'withdrawalPlatforms' => $report['platforms'],
|
||
'withdrawalPaths' => $report['paths'],
|
||
'withdrawalStoreEntities' => $report['storeEntities'],
|
||
'withdrawalBalances' => $report['balances'],
|
||
'withdrawalReserveStandards' => $report['reserveStandards'],
|
||
'withdrawalAmounts' => $report['withdrawalAmounts'],
|
||
'withdrawalCompliance' => $report['compliance'],
|
||
'withdrawalBalanceSummary' => $report['summary'],
|
||
'withdrawalBalanceColumnLabel' => $report['balanceColumnLabel'],
|
||
'withdrawalBalanceDate' => $report['balanceDate'],
|
||
'withdrawalSort' => $report['sort'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 快递附加费报价维护。
|
||
*
|
||
* 报价维护与后续附加费结算分开:本页只负责渠道、类型、结算方式、
|
||
* 计价标准和生效时间的维护展示,结算页将来根据这些规则读取业务镜像。
|
||
*/
|
||
protected function contentForShippingData(): array
|
||
{
|
||
$request = request();
|
||
$filters = $this->shippingDataFilters($request);
|
||
$mode = $request->query('mode') === 'detail' ? 'detail' : 'merged';
|
||
$page = max((int) $request->query('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$rows = new LengthAwarePaginator([], 0, $limit, $page, [
|
||
'path' => $request->url(),
|
||
'query' => $request->except('page'),
|
||
]);
|
||
$sync = ['code' => 'unavailable', 'last_succeeded_at' => null];
|
||
$error = null;
|
||
|
||
try {
|
||
$result = $this->shippingOrders->paginate($filters, $page, $limit, $mode);
|
||
$rows = new LengthAwarePaginator(
|
||
Collection::make($result['data'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$limit,
|
||
$page,
|
||
['path' => $request->url(), 'query' => $request->except('page')],
|
||
);
|
||
$sync = $this->shippingOrders->syncStatus();
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$error = '发货数据暂时无法读取(可能尚未运行迁移或同步)。';
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.shipping-data',
|
||
'shippingRows' => $rows,
|
||
'shippingFilters' => $filters,
|
||
'shippingSync' => $sync,
|
||
'shippingError' => $error,
|
||
'shippingMode' => $mode,
|
||
'shippingExportUrl' => route('business.courier-payables.shipping-data.export'),
|
||
];
|
||
}
|
||
|
||
public function exportShippingData(Request $request): StreamedResponse
|
||
{
|
||
$filters = $this->shippingDataFilters($request);
|
||
$mode = $request->query('mode') === 'detail' ? 'detail' : 'merged';
|
||
$columns = $this->shippingDataColumns();
|
||
|
||
return response()->streamDownload(function () use ($filters, $columns, $mode): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建发货数据导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, array_column($columns, 'label'));
|
||
|
||
foreach ($this->shippingOrders->exportRows($filters, 1000, $mode) as $row) {
|
||
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
||
$value = data_get($row, $column['field']);
|
||
|
||
// 发货数据导出:标识列(快递单号/交易单号/商品ID/条形码)按原样文本导出,不加 ' 前缀
|
||
return match ($column['type']) {
|
||
'number' => $value === null || $value === '' ? '' : $value,
|
||
default => $this->csvText($value),
|
||
};
|
||
}, $columns));
|
||
}
|
||
|
||
fclose($csv);
|
||
}, '发货数据' . ($mode === 'detail' ? '明细' : '合并') . '_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
public function suggestShippingData(Request $request): \Illuminate\Http\JsonResponse
|
||
{
|
||
$allowed = ['supplier_name', 'shop_name', 'warehouse_name', 'express_company'];
|
||
$field = (string) $request->query('field', '');
|
||
if (! in_array($field, $allowed, true)) {
|
||
return response()->json(['items' => []]);
|
||
}
|
||
$keyword = mb_substr(trim((string) $request->query('q', '')), 0, 64);
|
||
$limit = min(max((int) $request->query('limit', 20), 1), 500);
|
||
|
||
try {
|
||
$items = $this->shippingOrders->distinctValues($field, $keyword, $limit);
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$items = [];
|
||
}
|
||
|
||
return response()->json(['items' => array_values($items)]);
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function shippingDataFilters(Request $request): array
|
||
{
|
||
$text = static fn (string $key, int $length = 128): string => mb_substr(
|
||
trim((string) $request->query($key, '')),
|
||
0,
|
||
$length,
|
||
);
|
||
// 多选字段:接收 name[] 数组(也兼容单个字符串),去重、限长、限个数。
|
||
$multi = static function (string $key, int $length = 191, int $max = 50) use ($request): array {
|
||
$raw = $request->query($key, []);
|
||
if (is_string($raw)) {
|
||
$raw = $raw === '' ? [] : [$raw];
|
||
}
|
||
if (! is_array($raw)) {
|
||
$raw = [];
|
||
}
|
||
$out = [];
|
||
foreach ($raw as $value) {
|
||
$value = mb_substr(trim((string) $value), 0, $length);
|
||
if ($value !== '' && ! in_array($value, $out, true)) {
|
||
$out[] = $value;
|
||
}
|
||
if (count($out) >= $max) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
};
|
||
$date = static function (mixed $value): string {
|
||
$value = trim((string) $value);
|
||
|
||
return preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $value) === 1 ? $value : '';
|
||
};
|
||
|
||
return [
|
||
'express_no' => $text('express_no', 80),
|
||
'sourceno' => $text('sourceno', 80),
|
||
'barcode' => $text('barcode'),
|
||
'supplier_name' => $multi('supplier_name'),
|
||
'shop_name' => $multi('shop_name', 255),
|
||
'warehouse_name' => $multi('warehouse_name'),
|
||
'express_company' => $multi('express_company'),
|
||
'ship_from' => $date($request->query('ship_from', '')),
|
||
'ship_to' => $date($request->query('ship_to', '')),
|
||
];
|
||
}
|
||
|
||
/** @return list<array{field:string,label:string,type:string}> */
|
||
private function shippingDataColumns(): array
|
||
{
|
||
return [
|
||
['field' => 'express_no', 'label' => '快递单号', 'type' => 'identifier'],
|
||
['field' => 'trade_no', 'label' => '交易单号', 'type' => 'identifier'],
|
||
['field' => 'item_id', 'label' => '商品ID', 'type' => 'identifier'],
|
||
['field' => 'barcode', 'label' => '条形码', 'type' => 'identifier'],
|
||
['field' => 'product_name', 'label' => '商品名称', 'type' => 'text'],
|
||
['field' => 'qty', 'label' => '数量', 'type' => 'number'],
|
||
['field' => 'pkg_count', 'label' => '件数', 'type' => 'text'],
|
||
['field' => 'receiver_state', 'label' => '省', 'type' => 'text'],
|
||
['field' => 'buyer_nick', 'label' => '买家昵称', 'type' => 'text'],
|
||
['field' => 'receiver_name', 'label' => '收货姓名', 'type' => 'text'],
|
||
['field' => 'shop_name', 'label' => '店铺名称', 'type' => 'text'],
|
||
['field' => 'express_name1', 'label' => '快递名称1', 'type' => 'text'],
|
||
['field' => 'warehouse_name', 'label' => '仓库', 'type' => 'text'],
|
||
['field' => 'ship_time', 'label' => '发货时间', 'type' => 'text'],
|
||
['field' => 'freight', 'label' => '运费', 'type' => 'number'],
|
||
['field' => 'online_weight', 'label' => '线上重量', 'type' => 'number'],
|
||
['field' => 'actual_weight', 'label' => '实称重量', 'type' => 'number'],
|
||
['field' => 'length_cm', 'label' => '长(cm)', 'type' => 'number'],
|
||
['field' => 'width_cm', 'label' => '宽(cm)', 'type' => 'number'],
|
||
['field' => 'height_cm', 'label' => '高(cm)', 'type' => 'number'],
|
||
['field' => 'volume_cm3', 'label' => '体积(cm³)', 'type' => 'number'],
|
||
['field' => 'receiver_city', 'label' => '市', 'type' => 'text'],
|
||
['field' => 'receiver_district', 'label' => '区', 'type' => 'text'],
|
||
['field' => 'supplier_name', 'label' => '供应商', 'type' => 'text'],
|
||
['field' => 'receiver_address', 'label' => '收货详细地址', 'type' => 'text'],
|
||
['field' => 'receiver_mobile', 'label' => '手机号码', 'type' => 'identifier'],
|
||
['field' => 'receiver_phone', 'label' => '座机号码', 'type' => 'identifier'],
|
||
['field' => 'express_name2', 'label' => '快递名称2', 'type' => 'text'],
|
||
['field' => 'platform', 'label' => '平台', 'type' => 'text'],
|
||
['field' => 'buyer_memo', 'label' => '买家留言', 'type' => 'text'],
|
||
['field' => 'seller_memo', 'label' => '卖家备注', 'type' => 'text'],
|
||
['field' => 'package_no', 'label' => '配货单号', 'type' => 'identifier'],
|
||
['field' => 'amount', 'label' => '金额', 'type' => 'number'],
|
||
['field' => 'product_code', 'label' => '货号', 'type' => 'identifier'],
|
||
['field' => 'download_time', 'label' => '下载时间', 'type' => 'text'],
|
||
['field' => 'pay_time', 'label' => '付款时间', 'type' => 'text'],
|
||
['field' => 'freight_rate', 'label' => '费率', 'type' => 'number'],
|
||
['field' => 'freight_formula', 'label' => '公式', 'type' => 'text'],
|
||
['field' => 'weight_kg', 'label' => '重量(kg)', 'type' => 'number'],
|
||
['field' => 'settle_weight_kg', 'label' => '结算重量(kg)', 'type' => 'number'],
|
||
['field' => 'settle_volume_cm3', 'label' => '结算体积(cm³)', 'type' => 'number'],
|
||
['field' => 'recon_weight_kg', 'label' => '对账重量(kg)', 'type' => 'number'],
|
||
['field' => 'recon_volume_cm3', 'label' => '对账体积(cm³)', 'type' => 'number'],
|
||
['field' => 'finished_barcode', 'label' => '成品条码', 'type' => 'identifier'],
|
||
['field' => 'finished_name', 'label' => '成品名称', 'type' => 'text'],
|
||
['field' => 'finished_supplier', 'label' => '成品供应商', 'type' => 'text'],
|
||
['field' => 'settle_freight', 'label' => '结算运费', 'type' => 'number'],
|
||
['field' => 'front_no', 'label' => '换单前单号', 'type' => 'identifier'],
|
||
];
|
||
}
|
||
|
||
protected function contentForCourierSurcharge(): array
|
||
{
|
||
$clean = static fn (mixed $value, int $length = 60): string => mb_substr(trim((string) $value), 0, $length);
|
||
$filters = [
|
||
'keyword' => $clean(request('keyword', ''), 40),
|
||
'courier' => $clean(request('courier', ''), 40),
|
||
'type' => $clean(request('type', ''), 40),
|
||
'mode' => $clean(request('mode', ''), 40),
|
||
'scope' => $clean(request('scope', ''), 20),
|
||
'reconciler' => $clean(request('reconciler', ''), 40),
|
||
'supplier' => $clean(request('supplier', ''), 191),
|
||
'status' => $clean(request('status', ''), 20),
|
||
];
|
||
$data = $this->courierSurchargeQuotes->pageData($filters);
|
||
$auditReady = $this->courierSurchargeAudits->ready();
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.courier-surcharge',
|
||
'courierSurchargeRows' => $data['rows'],
|
||
'courierSurchargeFilters' => $filters,
|
||
'courierSurchargeSummary' => $data['summary'],
|
||
'courierSurchargeOptions' => $data['options'],
|
||
'courierSurchargeReady' => $data['ready'] && $auditReady,
|
||
'courierSurchargeDataReady' => $data['ready'],
|
||
'courierSurchargeAuditReady' => $auditReady,
|
||
'courierSurchargeTypeUsage' => $this->courierSurchargeTypes->usageCounts($data['options']['type_records'] ?? collect()),
|
||
'courierSurchargeAuditLogs' => $this->courierSurchargeAudits->recent(),
|
||
];
|
||
}
|
||
|
||
private function freightReconciliationFilters(): array
|
||
{
|
||
$request = request();
|
||
$date = static function (mixed $value): string {
|
||
$value = trim((string) $value);
|
||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1 ? $value : '';
|
||
};
|
||
return [
|
||
'express_no' => mb_substr(trim((string) $request->query('express_no', '')), 0, 80),
|
||
'express_name' => mb_substr(trim((string) $request->query('express_name', '')), 0, 80),
|
||
'month' => mb_substr(trim((string) $request->query('month', '')), 0, 20),
|
||
'status' => mb_substr(trim((string) $request->query('status', '')), 0, 40),
|
||
'modified_from' => $date($request->query('modified_from', '')),
|
||
'modified_to' => $date($request->query('modified_to', '')),
|
||
];
|
||
}
|
||
|
||
protected function contentForFreightReconciliation(): array
|
||
{
|
||
$request = request();
|
||
$filters = $this->freightReconciliationFilters();
|
||
$page = max((int) $request->query('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$rows = new Paginator([], $limit, $page, [
|
||
'path' => $request->url(),
|
||
'query' => $request->except('page'),
|
||
]);
|
||
$state = null;
|
||
$sync = ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null];
|
||
$summary = ['count' => null, 'freight' => null];
|
||
$error = null;
|
||
$pending = false;
|
||
|
||
try {
|
||
if (! $this->courierFreightReconciliations->available()) {
|
||
$error = '共享业务镜像尚未建立,运费对账暂时无法读取。';
|
||
} elseif (! $this->courierFreightReconciliations->ready()) {
|
||
$state = $this->courierFreightReconciliations->state();
|
||
$sync = $this->courierFreightReconciliations->syncStatus($state);
|
||
$pending = true;
|
||
$error = '运费对账镜像正在完成首次同步,完成前不会把未同步数据误判为无数据。';
|
||
} else {
|
||
$result = $this->courierFreightReconciliations->paginate($filters, $page, $limit);
|
||
$rows = new Paginator(
|
||
Collection::make($result['data'] ?? []),
|
||
$limit,
|
||
$page,
|
||
['path' => $request->url(), 'query' => $request->except('page')],
|
||
);
|
||
$rows->hasMorePagesWhen((bool) ($result['has_more'] ?? false));
|
||
$state = $result['state'] ?? null;
|
||
$sync = $result['sync'] ?? $sync;
|
||
$summary = $result['summary'] ?? $summary;
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$error = '运费对账数据暂时无法读取,请稍后重试。';
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.freight-reconciliation',
|
||
'freightReconciliations' => $rows,
|
||
'freightReconciliationFilters' => $filters,
|
||
'freightReconciliationSummary' => $summary,
|
||
'freightReconciliationState' => $state,
|
||
'freightReconciliationSync' => $sync,
|
||
'freightReconciliationError' => $error,
|
||
'freightReconciliationPending' => $pending,
|
||
'freightReconciliationSummaryUrl' => $error === null ? $request->fullUrlWithQuery(['courier_summary' => 1, 'page' => null]) : null,
|
||
];
|
||
}
|
||
|
||
protected function contentForThirdPartyWarehouseStorage(): array
|
||
{
|
||
$request = request();
|
||
$filters = $this->thirdPartyWarehouseStorageFilters($request);
|
||
$page = max((int) $request->query('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$rows = new LengthAwarePaginator([], 0, $limit, $page, [
|
||
'path' => $request->url(),
|
||
'query' => $request->except('page'),
|
||
]);
|
||
$state = null;
|
||
$sync = ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null];
|
||
$summary = [];
|
||
$error = null;
|
||
$pending = false;
|
||
|
||
try {
|
||
if (! $this->thirdPartyWarehouseStorage->available()) {
|
||
$error = '共享业务镜像尚未建立,三方仓进销存暂时无法读取。';
|
||
} elseif (! $this->thirdPartyWarehouseStorage->ready()) {
|
||
$state = $this->thirdPartyWarehouseStorage->state();
|
||
$sync = $this->thirdPartyWarehouseStorage->syncStatus($state);
|
||
$pending = true;
|
||
$error = ($sync['code'] ?? null) === 'dependency_pending'
|
||
? '普通商品资料镜像正在执行首次全量同步,完成前不会展示口径不完整的商品属性。'
|
||
: '三方仓进销存正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||
} else {
|
||
$result = $this->thirdPartyWarehouseStorage->paginate($filters, $page, $limit);
|
||
$rows = new LengthAwarePaginator(
|
||
Collection::make($result['data'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$limit,
|
||
$page,
|
||
[
|
||
'path' => $request->url(),
|
||
'query' => $request->except('page'),
|
||
],
|
||
);
|
||
$state = $result['state'] ?? null;
|
||
$sync = $result['sync'] ?? $sync;
|
||
$summary = $result['summary'] ?? [];
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$error = '三方仓进销存暂时无法读取,请稍后重试。';
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.third-party-warehouse-storage',
|
||
'thirdPartyWarehouseRows' => $rows,
|
||
'thirdPartyWarehouseFilters' => $filters,
|
||
'thirdPartyWarehouseSummary' => $summary,
|
||
'thirdPartyWarehouseState' => $state,
|
||
'thirdPartyWarehouseSync' => $sync,
|
||
'thirdPartyWarehouseError' => $error,
|
||
'thirdPartyWarehousePending' => $pending,
|
||
'thirdPartyWarehouseExportUrl' => route('business.courier-payables.third-party-warehouse-storage.export'),
|
||
];
|
||
}
|
||
|
||
public function exportThirdPartyWarehouseStorage(Request $request): StreamedResponse
|
||
{
|
||
abort_unless(
|
||
$this->thirdPartyWarehouseStorage->available() && $this->thirdPartyWarehouseStorage->ready(),
|
||
503,
|
||
);
|
||
|
||
$filters = $this->thirdPartyWarehouseStorageFilters($request);
|
||
$columns = $this->filterExportColumns(
|
||
$this->thirdPartyWarehouseStorageExportColumns(),
|
||
$request->query('fields'),
|
||
);
|
||
$period = str_replace('-', '', $filters['period']);
|
||
|
||
return response()->streamDownload(function () use ($filters, $columns): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建三方仓进销存导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, array_column($columns, 'label'));
|
||
|
||
foreach ($this->thirdPartyWarehouseStorage->exportRows($filters) as $row) {
|
||
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
||
$value = data_get($row, $column['field']);
|
||
|
||
return match ($column['type'] ?? 'text') {
|
||
'number', 'money' => $value === null || $value === '' ? '' : $value,
|
||
'code' => $this->csvIdentifier($value),
|
||
default => $this->csvText($value),
|
||
};
|
||
}, $columns));
|
||
}
|
||
|
||
fclose($csv);
|
||
}, '三方仓进销存_' . $period . '_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
public function exportInventoryReport(Request $request): Response
|
||
{
|
||
abort_unless(
|
||
$this->thirdPartyWarehouseStorage->available()
|
||
&& $this->thirdPartyWarehouseStorage->allWarehousesReady(),
|
||
503,
|
||
);
|
||
|
||
$filters = $this->inventoryReportFilters($request);
|
||
$productInventory = collect(config('business_navigation'))->firstWhere('slug', 'product-inventory');
|
||
$inventoryPage = collect($productInventory['pages'] ?? [])->firstWhere('slug', 'inventory-report');
|
||
$availableColumns = collect($inventoryPage['directory']['fields'] ?? [])
|
||
->reject(static fn (array $column): bool => in_array($column['kind'] ?? '', ['secret', 'password'], true))
|
||
->keyBy('name');
|
||
$requestedColumnsParam = $request->query('columns', '');
|
||
abort_unless(is_string($requestedColumnsParam), 422);
|
||
$requestedColumns = trim($requestedColumnsParam);
|
||
$columns = $availableColumns->values()->all();
|
||
if ($requestedColumns !== '') {
|
||
$names = explode(',', $requestedColumns);
|
||
abort_if(count($names) > $availableColumns->count() || count($names) !== count(array_unique($names)), 422);
|
||
$columns = [];
|
||
foreach ($names as $name) {
|
||
abort_unless($availableColumns->has($name), 422);
|
||
$columns[] = $availableColumns->get($name);
|
||
}
|
||
}
|
||
$scope = $request->query('scope', 'all');
|
||
abort_unless(in_array($scope, ['all', 'page'], true), 422);
|
||
$page = max(1, $request->integer('page', 1));
|
||
$perPage = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
|
||
if ($scope === 'all' && should_queue()) {
|
||
$fileName = Str::filename('进销存报表') . '-' . now()->format('Ymd_His') . '-' . Str::random(8) . '.csv';
|
||
dispatch(new InventoryReportExport(user(), $filters, $columns, $fileName, (int) (company_id() ?: 1)))->onQueue('exports');
|
||
flash('进销存报表已提交后台生成,完成后可从通知中心下载。')->success();
|
||
|
||
return back();
|
||
}
|
||
|
||
if ($scope === 'all') {
|
||
// The local environment intentionally uses the sync queue and has
|
||
// no worker process. Defer the same export job until the response
|
||
// has been sent so a large CSV never depends on a live browser
|
||
// download connection. The job stores the completed file and
|
||
// raises the existing notification when it is ready.
|
||
$fileName = Str::filename('进销存报表') . '-' . now()->format('Ymd_His') . '-' . Str::random(8) . '.csv';
|
||
dispatch(new InventoryReportExport(user(), $filters, $columns, $fileName, (int) (company_id() ?: 1)))->afterResponse();
|
||
flash('进销存报表已提交后台生成,完成后可从通知中心下载。')->success();
|
||
|
||
return back();
|
||
}
|
||
|
||
// Build the complete CSV before opening the HTTP response. Streaming
|
||
// rows directly from the database keeps the browser request open for
|
||
// the whole aggregation and makes a slow client look like a failed
|
||
// export. A disk-backed file keeps memory bounded and lets the web
|
||
// server send a framed response with a known length.
|
||
set_time_limit(0);
|
||
$filePath = tempnam(sys_get_temp_dir(), 'inventory-report-');
|
||
if ($filePath === false) {
|
||
throw new RuntimeException('无法创建进销存报表导出临时文件。');
|
||
}
|
||
|
||
try {
|
||
$csv = fopen($filePath, 'wb');
|
||
if ($csv === false) {
|
||
throw new RuntimeException('无法写入进销存报表导出临时文件。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, array_column($columns, 'label'));
|
||
$rows = $scope === 'page'
|
||
? $this->thirdPartyWarehouseStorage->paginateMonthlyInventoryReport($filters, $page, $perPage, false)['data']
|
||
: $this->thirdPartyWarehouseStorage->exportMonthlyInventoryReportRows($filters);
|
||
$shortNames = app(InventoryWarehouseShortNames::class);
|
||
$names = $shortNames->forCompany((int) (company_id() ?: 1));
|
||
$written = 0;
|
||
foreach ($rows as $row) {
|
||
$row = $shortNames->fill($row, $names);
|
||
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
||
$value = data_get($row, $column['name']);
|
||
|
||
return $column['name'] === 'product_code'
|
||
? $this->csvIdentifier($value)
|
||
: (($column['kind'] ?? null) === 'integer'
|
||
? ($value === null || $value === '' ? '' : $value)
|
||
: $this->csvText($value));
|
||
}, $columns));
|
||
$written++;
|
||
if ($written % 5000 === 0) {
|
||
fflush($csv);
|
||
gc_collect_cycles();
|
||
}
|
||
}
|
||
fclose($csv);
|
||
clearstatcache(true, $filePath);
|
||
$fileSize = filesize($filePath);
|
||
if ($fileSize === false) {
|
||
throw new RuntimeException('无法读取进销存报表导出文件大小。');
|
||
}
|
||
} catch (Throwable $exception) {
|
||
if (isset($csv) && is_resource($csv)) {
|
||
fclose($csv);
|
||
}
|
||
@unlink($filePath);
|
||
throw $exception;
|
||
}
|
||
|
||
$fileName = '进销存报表_' . str_replace('-', '', $filters['month']) . '_' . now()->format('Ymd_His') . '.csv';
|
||
|
||
return response()->streamDownload(function () use ($filePath): void {
|
||
try {
|
||
set_time_limit(0);
|
||
readfile($filePath);
|
||
} finally {
|
||
@unlink($filePath);
|
||
}
|
||
}, $fileName, [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
'X-Accel-Buffering' => 'no',
|
||
'Content-Length' => (string) $fileSize,
|
||
]);
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
private function thirdPartyWarehouseStorageFilters(Request $request): array
|
||
{
|
||
$dimension = in_array((string) $request->query('dimension'), ['month', 'day'], true)
|
||
? (string) $request->query('dimension')
|
||
: 'month';
|
||
$period = trim((string) $request->query(
|
||
'period',
|
||
$dimension === 'day' ? $request->query('date', '') : $request->query('month', ''),
|
||
));
|
||
|
||
if ($dimension === 'day') {
|
||
if ($this->dateFilter($period) === '') {
|
||
$period = now()->subDay()->toDateString();
|
||
}
|
||
if ($period < ThirdPartyWarehouseStorageMirrorRepository::START_DATE) {
|
||
$period = ThirdPartyWarehouseStorageMirrorRepository::START_DATE;
|
||
}
|
||
} elseif (preg_match('/^(\d{4})-(\d{2})$/', $period, $matches) !== 1
|
||
|| ! checkdate((int) $matches[2], 1, (int) $matches[1])) {
|
||
$period = now()->startOfMonth()->subMonth()->format('Y-m');
|
||
}
|
||
if ($dimension === 'month'
|
||
&& $period < substr(ThirdPartyWarehouseStorageMirrorRepository::START_DATE, 0, 7)) {
|
||
$period = substr(ThirdPartyWarehouseStorageMirrorRepository::START_DATE, 0, 7);
|
||
}
|
||
|
||
$clean = static fn (mixed $value, int $length): string => mb_substr(trim((string) $value), 0, $length);
|
||
|
||
return [
|
||
'dimension' => $dimension,
|
||
'period' => $period,
|
||
'sku_code' => $clean($request->query('sku_code', ''), 80),
|
||
'product_name' => $clean($request->query('product_name', ''), 120),
|
||
'supplier' => $clean($request->query('supplier', ''), 120),
|
||
'category' => $clean($request->query('category', ''), 120),
|
||
];
|
||
}
|
||
|
||
/** @return array<int,array{label:string,field:string,type?:string}> */
|
||
private function thirdPartyWarehouseStorageExportColumns(): array
|
||
{
|
||
return [
|
||
['field' => 'owner', 'label' => '货主'],
|
||
['field' => 'date', 'label' => '日期'],
|
||
['field' => 'sku_code', 'label' => '商品编码', 'type' => 'code'],
|
||
['field' => 'image_url', 'label' => '图片'],
|
||
['field' => 'product_name', 'label' => '商品名称'],
|
||
['field' => 'short_name', 'label' => '商品简称'],
|
||
['field' => 'supplier', 'label' => '供应商'],
|
||
['field' => 'category', 'label' => '分类'],
|
||
['field' => 'virtual_category', 'label' => '虚拟分类'],
|
||
['field' => 'supplier_style_no', 'label' => '供应商款号', 'type' => 'code'],
|
||
['field' => 'color_spec', 'label' => '颜色规格'],
|
||
['field' => 'brand', 'label' => '品牌'],
|
||
['field' => 'unit', 'label' => '单位'],
|
||
['field' => 'opening_qty', 'label' => '期初数量', 'type' => 'number'],
|
||
['field' => 'closing_qty', 'label' => '期末数量', 'type' => 'number'],
|
||
['field' => 'processing_in', 'label' => '加工进仓', 'type' => 'number'],
|
||
['field' => 'processing_out', 'label' => '加工出仓', 'type' => 'number'],
|
||
['field' => 'purchase_in', 'label' => '采购进仓', 'type' => 'number'],
|
||
['field' => 'purchase_return', 'label' => '采购退货', 'type' => 'number'],
|
||
['field' => 'other_in', 'label' => '其它进仓', 'type' => 'number'],
|
||
['field' => 'other_return', 'label' => '其它退货', 'type' => 'number'],
|
||
['field' => 'sales_out', 'label' => '销售出仓', 'type' => 'number'],
|
||
['field' => 'sales_return', 'label' => '销售退货', 'type' => 'number'],
|
||
['field' => 'stocktake', 'label' => '盘点', 'type' => 'number'],
|
||
['field' => 'transfer_out', 'label' => '调拨出', 'type' => 'number'],
|
||
['field' => 'transfer_in', 'label' => '调拨入', 'type' => 'number'],
|
||
['field' => 'opening_aux', 'label' => '期初', 'type' => 'number'],
|
||
['field' => 'other_out', 'label' => '其它出库', 'type' => 'number'],
|
||
];
|
||
}
|
||
|
||
protected function contentForDingPayableWorkflows(): array
|
||
{
|
||
$normalizeDate = static function (mixed $value): string {
|
||
$date = trim((string) $value);
|
||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches) !== 1
|
||
|| ! checkdate((int) $matches[2], (int) $matches[3], (int) $matches[1])) {
|
||
return '';
|
||
}
|
||
|
||
return $date;
|
||
};
|
||
$status = strtoupper(mb_substr(trim((string) request('status', '')), 0, 30));
|
||
|
||
$filters = [
|
||
'process_instance_id' => mb_substr(trim((string) request('process_instance_id', '')), 0, 100),
|
||
'created_from' => $normalizeDate(request('created_from', '')),
|
||
'finished_to' => $normalizeDate(request('finished_to', '')),
|
||
'status' => preg_match('/^[A-Z_]+$/', $status) === 1 ? $status : '',
|
||
];
|
||
$page = max((int) request('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
|
||
try {
|
||
$result = $this->dingPayableWorkflows->paginate($filters, $page, $limit);
|
||
$rows = new LengthAwarePaginator(
|
||
Collection::make($result['data'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$limit,
|
||
$page,
|
||
['path' => request()->url(), 'query' => request()->except('page')],
|
||
);
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.payable-cloud-warehouse-vehicle-fee',
|
||
'dingPayableWorkflowRows' => $rows,
|
||
'dingPayableWorkflowFilters' => $filters,
|
||
'dingPayableWorkflowStatus' => $result['status'] ?? 'unknown',
|
||
'dingPayableWorkflowSyncedAt' => $result['last_synced_at'] ?? null,
|
||
'dingPayableWorkflowError' => null,
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.payable-cloud-warehouse-vehicle-fee',
|
||
'dingPayableWorkflowRows' => new LengthAwarePaginator([], 0, $limit, $page, [
|
||
'path' => request()->url(), 'query' => request()->except('page'),
|
||
]),
|
||
'dingPayableWorkflowFilters' => $filters,
|
||
'dingPayableWorkflowStatus' => 'unavailable',
|
||
'dingPayableWorkflowSyncedAt' => null,
|
||
'dingPayableWorkflowError' => '钉钉应付流程镜像尚未完成同步,请稍后重试。',
|
||
];
|
||
}
|
||
}
|
||
|
||
protected function contentForOnlineDaifaPrice(): array
|
||
{
|
||
$filters = $this->onlineDaifaPriceFilters(request());
|
||
$page = max((int) request('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
|
||
try {
|
||
$result = $this->onlineDaifaPrices->paginate($filters, $page, $limit);
|
||
$rows = new LengthAwarePaginator(
|
||
Collection::make($result['data'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$limit,
|
||
$page,
|
||
['path' => request()->url(), 'query' => request()->except('page')],
|
||
);
|
||
|
||
return [
|
||
'view' => 'business.pages.product-inventory.online-daifa-price',
|
||
'onlineDaifaRows' => $rows,
|
||
'onlineDaifaFilters' => $filters,
|
||
'onlineDaifaStatus' => $result['status'] ?? 'unknown',
|
||
'onlineDaifaSyncedAt' => $result['last_synced_at'] ?? null,
|
||
'onlineDaifaSummary' => $result['summary'] ?? [],
|
||
'onlineDaifaSources' => $result['source_statuses'] ?? [],
|
||
'onlineDaifaError' => null,
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return [
|
||
'view' => 'business.pages.product-inventory.online-daifa-price',
|
||
'onlineDaifaRows' => new LengthAwarePaginator([], 0, $limit, $page, [
|
||
'path' => request()->url(), 'query' => request()->except('page'),
|
||
]),
|
||
'onlineDaifaFilters' => $filters,
|
||
'onlineDaifaStatus' => 'unavailable',
|
||
'onlineDaifaSyncedAt' => null,
|
||
'onlineDaifaSummary' => [],
|
||
'onlineDaifaSources' => [],
|
||
'onlineDaifaError' => '线上代发价格镜像尚未完成同步,请稍后重试。',
|
||
];
|
||
}
|
||
}
|
||
|
||
public function exportOnlineDaifaPrice(Request $request): StreamedResponse
|
||
{
|
||
abort_unless($this->onlineDaifaPrices->ready(), 503);
|
||
|
||
$filters = $this->onlineDaifaPriceFilters($request);
|
||
|
||
return response()->streamDownload(function () use ($filters): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建线上代发价格导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, [
|
||
'审批编号', '标题', '状态', '申请人', '收款人全称', '标签', '商品ID', '商品名称(匹配)', '店铺名称',
|
||
'代发交易单号', '店铺交易单号', '发货数量', '订单实付金额', '代发订单金额', '差价',
|
||
'供应商名称', '商品编码', '创建时间',
|
||
]);
|
||
|
||
foreach ($this->onlineDaifaPrices->exportRows($filters) as $row) {
|
||
$workflow = (array) ($row['workflow'] ?? []);
|
||
$approvalNumber = trim((string) ($workflow['businessId'] ?? '')) ?: (string) ($row['process_instance_id'] ?? '');
|
||
$originator = trim((string) ($workflow['originatorUserName'] ?? ''))
|
||
?: trim((string) ($workflow['originatorUserId'] ?? ''));
|
||
$matchedProduct = (array) ($row['matched_product'] ?? []);
|
||
$productName = ($row['source_type'] ?? 'ding') === 'purchase'
|
||
? ''
|
||
: (($matchedProduct['matched'] ?? false)
|
||
? (string) ($matchedProduct['name'] ?? '')
|
||
: '(未匹配)');
|
||
|
||
fputcsv($csv, [
|
||
$this->csvIdentifier($approvalNumber),
|
||
$this->csvText($workflow['title'] ?? ''),
|
||
$this->csvText($row['status_label'] ?? ''),
|
||
$this->csvText($originator),
|
||
$this->csvText($row['payee_name'] ?? ''),
|
||
$this->csvText($row['tag'] ?? ''),
|
||
$this->csvIdentifier($row['product_id'] ?? ''),
|
||
$this->csvText($productName),
|
||
$this->csvText($row['shop_name'] ?? ''),
|
||
$this->csvIdentifier($row['daifa_trade_no'] ?? ''),
|
||
$this->csvIdentifier($row['shop_trade_no'] ?? ''),
|
||
$row['ship_qty'] ?? null,
|
||
$row['paid_amount'] ?? null,
|
||
$row['daifa_amount'] ?? null,
|
||
$row['price_diff'] ?? null,
|
||
$this->csvText($row['supplier_name'] ?? ''),
|
||
$this->csvIdentifier($row['material_no'] ?? ''),
|
||
$this->csvText($row['workflow_created_at'] ?? ''),
|
||
]);
|
||
}
|
||
|
||
fclose($csv);
|
||
}, '线上代发价格_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
private function onlineDaifaPriceFilters(Request $request): array
|
||
{
|
||
$clean = static fn (mixed $value, int $length = 80): string => mb_substr(trim((string) $value), 0, $length);
|
||
$date = static function (mixed $value): string {
|
||
$value = trim((string) $value);
|
||
|
||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1 ? $value : '';
|
||
};
|
||
$status = strtoupper(mb_substr(trim((string) $request->query('status', '')), 0, 30));
|
||
|
||
return [
|
||
'keyword' => $clean($request->query('keyword', ''), 80),
|
||
'product_id' => $clean($request->query('product_id', ''), 64),
|
||
'shop_name' => $clean($request->query('shop_name', ''), 120),
|
||
'supplier_name' => $clean($request->query('supplier_name', ''), 120),
|
||
'material_no' => $clean($request->query('material_no', ''), 120),
|
||
'status' => preg_match('/^[A-Z_]+$/', $status) === 1 ? $status : '',
|
||
'created_from' => $date($request->query('created_from', '')),
|
||
'created_to' => $date($request->query('created_to', '')),
|
||
];
|
||
}
|
||
|
||
protected function contentForJingxiRefundData(): array
|
||
{
|
||
return [
|
||
'view' => 'business.pages.refunds.jingxi-refund-data',
|
||
];
|
||
}
|
||
|
||
protected function contentForNonJingxiRefundData(): array
|
||
{
|
||
$request = request();
|
||
$defaultTo = now()->toDateString();
|
||
$defaultFrom = now()->subDays(6)->toDateString();
|
||
$filters = $this->nonJingxiRefundFilters($request, $defaultFrom, $defaultTo);
|
||
$page = max((int) $request->query('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$rows = new LengthAwarePaginator([], 0, $limit, 1, [
|
||
'path' => $request->url(),
|
||
'query' => $request->query(),
|
||
]);
|
||
$syncStatus = ['code' => 'unavailable', 'last_succeeded_at' => null];
|
||
$error = null;
|
||
$pending = false;
|
||
$indexPending = false;
|
||
|
||
try {
|
||
if (! $this->nonJingxiRefunds->available()) {
|
||
$error = '共享业务镜像尚未建立,非京喜退款数据暂时无法读取。';
|
||
} else {
|
||
$states = $this->nonJingxiRefunds->states();
|
||
$syncStatus = $this->nonJingxiRefunds->syncStatus($states);
|
||
if (! $this->nonJingxiRefunds->ready()) {
|
||
$pending = true;
|
||
} elseif (! $this->nonJingxiRefunds->queryReady()) {
|
||
$indexPending = true;
|
||
} else {
|
||
$result = $this->nonJingxiRefunds->paginate($filters, $page, $limit);
|
||
$rows = new LengthAwarePaginator(
|
||
Collection::make($result['data'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$limit,
|
||
$page,
|
||
['path' => $request->url(), 'query' => $request->query()],
|
||
);
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$error = '非京喜退款数据暂时无法读取,请稍后重试。';
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.refunds.non-jingxi-refund-data',
|
||
'filters' => $filters,
|
||
'rows' => $rows,
|
||
'refundSyncStatus' => $syncStatus,
|
||
'refundError' => $error,
|
||
'refundPending' => $pending,
|
||
'refundIndexPending' => $indexPending,
|
||
];
|
||
}
|
||
|
||
public function exportNonJingxiRefunds(Request $request)
|
||
{
|
||
$filters = $this->nonJingxiRefundFilters($request);
|
||
$redirectParameters = array_filter([
|
||
'section' => 'refunds',
|
||
'page' => 'non-jingxi-refund-data',
|
||
...$filters,
|
||
], static fn (mixed $value): bool => $value !== '' && $value !== null);
|
||
|
||
try {
|
||
if (! $this->nonJingxiRefunds->available()) {
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '共享业务镜像尚未建立,售后(退货退款)暂时不能导出。');
|
||
}
|
||
|
||
if (! $this->nonJingxiRefunds->ready()) {
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '业务镜像首次同步尚未完成,售后(退货退款)暂时不能导出。');
|
||
}
|
||
|
||
if (! $this->nonJingxiRefunds->queryReady()) {
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '页面查询索引尚未就绪,售后(退货退款)暂时不能导出。');
|
||
}
|
||
|
||
return $this->streamNonJingxiRefundCsv(
|
||
$this->nonJingxiRefunds->exportRows($filters),
|
||
$filters,
|
||
$redirectParameters,
|
||
$request->query('columns'),
|
||
);
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '售后(退货退款)导出暂时无法完成,请稍后重试。');
|
||
}
|
||
}
|
||
|
||
/** @return array{from:string,to:string,order_no:string,shop_name:string,sku_id:string} */
|
||
private function nonJingxiRefundFilters(
|
||
Request $request,
|
||
?string $defaultFrom = null,
|
||
?string $defaultTo = null,
|
||
): array {
|
||
$defaultTo ??= now()->toDateString();
|
||
$defaultFrom ??= now()->subDays(6)->toDateString();
|
||
$from = $this->dateFilter($request->query('from', $defaultFrom)) ?: $defaultFrom;
|
||
$to = $this->dateFilter($request->query('to', $defaultTo)) ?: $defaultTo;
|
||
if ($from > $to) {
|
||
[$from, $to] = [$to, $from];
|
||
}
|
||
|
||
return [
|
||
'from' => $from,
|
||
'to' => $to,
|
||
'order_no' => mb_substr(trim((string) $request->query('order_no', '')), 0, 100),
|
||
'shop_name' => mb_substr(trim((string) $request->query('shop_name', '')), 0, 191),
|
||
'sku_id' => mb_substr(trim((string) $request->query('sku_id', '')), 0, 100),
|
||
];
|
||
}
|
||
|
||
private function streamNonJingxiRefundCsv(iterable $rows, array $filters, array $redirectParameters, mixed $requestedFields = null)
|
||
{
|
||
$columns = [
|
||
['label' => '序号/主键ID', 'field' => 'Id', 'identifier' => true],
|
||
['label' => '退款日期', 'field' => 'refundDate'],
|
||
['label' => '售后单ID', 'field' => 'as_id', 'identifier' => true],
|
||
['label' => '外部售后单号', 'field' => 'outer_as_id', 'identifier' => true],
|
||
['label' => '订单ID', 'field' => 'o_id', 'identifier' => true],
|
||
['label' => '', 'field' => ''],
|
||
['label' => '子订单ID', 'field' => 'so_id', 'identifier' => true],
|
||
['label' => '类型', 'field' => 'type'],
|
||
['label' => '节点', 'field' => 'node'],
|
||
['label' => '是否自营', 'field' => 'isZC'],
|
||
['label' => '店铺名称', 'field' => 'shop_name'],
|
||
['label' => '平台', 'field' => 'platform'],
|
||
['label' => '责任部门', 'field' => 'responsibleDepartment'],
|
||
['label' => '退款总金额', 'field' => 'totalRefundMoney', 'number' => true],
|
||
['label' => '原始邮编', 'field' => 'originPostcode'],
|
||
['label' => '店铺商品ID', 'field' => 'shop_i_id', 'identifier' => true],
|
||
['label' => '店铺SKU编码', 'field' => 'shop_sku_id', 'identifier' => true],
|
||
['label' => '商品ID', 'field' => 'i_id', 'identifier' => true],
|
||
['label' => 'SKU编码', 'field' => 'sku_id', 'identifier' => true],
|
||
['label' => '买家实付金额', 'field' => 'buyer_paid_amount', 'number' => true],
|
||
['label' => '数量', 'field' => 'qty', 'number' => true],
|
||
['label' => '商品单位成本', 'field' => 'unitCostGoods', 'number' => true],
|
||
['label' => '明细退款金额', 'field' => 'detailRefundMoney', 'number' => true],
|
||
['label' => '退款类型', 'field' => 'refundType'],
|
||
['label' => '退款数量', 'field' => 'refundQty', 'number' => true],
|
||
['label' => '退款商品成本', 'field' => 'refundCostGoods', 'number' => true],
|
||
['label' => '退款回款金额', 'field' => 'RefundMoneyIncome', 'number' => true],
|
||
['label' => '售后金额', 'field' => 'AfSalesMoney', 'number' => true],
|
||
['label' => '创建时间', 'field' => 'createTime'],
|
||
['label' => '状态', 'field' => 'status'],
|
||
['label' => '店铺状态', 'field' => 'shop_status'],
|
||
['label' => '创建记录/创建时间', 'field' => 'created'],
|
||
['label' => '确认日期', 'field' => 'confirm_date'],
|
||
];
|
||
$columns = $this->filterExportColumns($columns, $requestedFields);
|
||
|
||
$csv = null;
|
||
|
||
try {
|
||
$csv = fopen('php://temp/maxmemory:5242880', 'w+b');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建售后退款导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, array_column($columns, 'label'));
|
||
foreach ($rows as $row) {
|
||
fputcsv($csv, array_map(
|
||
fn (array $column): mixed => $column['field'] === ''
|
||
? ''
|
||
: (($column['number'] ?? false)
|
||
? data_get($row, $column['field'])
|
||
: (($column['identifier'] ?? false)
|
||
? $this->csvIdentifier(data_get($row, $column['field']))
|
||
: $this->csvText(data_get($row, $column['field'])))),
|
||
$columns,
|
||
));
|
||
}
|
||
rewind($csv);
|
||
} catch (Throwable $exception) {
|
||
if (is_resource($csv)) {
|
||
fclose($csv);
|
||
}
|
||
report($exception);
|
||
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '售后(退货退款)导出暂时无法完成,请稍后重试。');
|
||
}
|
||
|
||
return response()->streamDownload(function () use ($csv): void {
|
||
fpassthru($csv);
|
||
fclose($csv);
|
||
}, 'after_sales_refunds_' . $filters['from'] . '_' . $filters['to'] . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
protected function contentForSmallPaymentWorkOrders(): array
|
||
{
|
||
$request = request();
|
||
$filters = $this->smallPaymentWorkOrderFilters($request);
|
||
$page = max((int) $request->query('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$rows = new LengthAwarePaginator([], 0, $limit, 1, [
|
||
'path' => $request->url(),
|
||
'query' => $request->query(),
|
||
]);
|
||
$syncStatus = ['code' => 'unavailable', 'last_succeeded_at' => null];
|
||
$pagePaymentAmount = '0.00';
|
||
$filteredPaymentAmount = '0.00';
|
||
$shopNames = [];
|
||
$paymentReasons = [];
|
||
$error = null;
|
||
$pending = false;
|
||
$indexPending = false;
|
||
|
||
try {
|
||
if (! $this->smallPaymentWorkOrders->available()) {
|
||
$error = '共享业务镜像尚未建立,小额打款工单暂时无法读取。';
|
||
} else {
|
||
$states = $this->smallPaymentWorkOrders->states();
|
||
$syncStatus = $this->smallPaymentWorkOrders->syncStatus($states);
|
||
if (! $this->smallPaymentWorkOrders->ready()) {
|
||
$pending = true;
|
||
} elseif (! $this->smallPaymentWorkOrders->queryReady()) {
|
||
$indexPending = true;
|
||
} else {
|
||
$result = $this->smallPaymentWorkOrders->paginate($filters, $page, $limit);
|
||
$rows = new LengthAwarePaginator(
|
||
Collection::make($result['data'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$limit,
|
||
$page,
|
||
['path' => $request->url(), 'query' => $request->query()],
|
||
);
|
||
$pagePaymentAmount = (string) ($result['page_payment_amount'] ?? '0.00');
|
||
$filteredPaymentAmount = (string) ($result['filtered_payment_amount'] ?? '0.00');
|
||
$filterOptions = $this->smallPaymentWorkOrders->filterOptions();
|
||
$shopNames = $filterOptions['shops'];
|
||
$paymentReasons = $filterOptions['payment_reasons'];
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$error = '小额打款工单暂时无法读取,请稍后重试。';
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.refunds.small-payment-work-orders',
|
||
'filters' => $filters,
|
||
'rows' => $rows,
|
||
'smallPaymentSyncStatus' => $syncStatus,
|
||
'smallPaymentPageAmount' => $pagePaymentAmount,
|
||
'smallPaymentFilteredAmount' => $filteredPaymentAmount,
|
||
'smallPaymentShops' => $shopNames,
|
||
'smallPaymentReasons' => $paymentReasons,
|
||
'smallPaymentStatuses' => [
|
||
'待审核', '审核驳回', '待确认', '确认中', '已完成', '已作废',
|
||
'支付宝已打款', '银行卡已打款', '已打款',
|
||
],
|
||
'smallPaymentError' => $error,
|
||
'smallPaymentPending' => $pending,
|
||
'smallPaymentIndexPending' => $indexPending,
|
||
];
|
||
}
|
||
|
||
public function exportSmallPaymentWorkOrders(Request $request)
|
||
{
|
||
$filters = $this->smallPaymentWorkOrderFilters($request);
|
||
$redirectParameters = array_filter([
|
||
'section' => self::SMALL_PAYMENT_SECTION,
|
||
'page' => 'small-payment-work-orders',
|
||
...$filters,
|
||
], static fn (mixed $value): bool => $value !== '' && $value !== null);
|
||
|
||
try {
|
||
if (! $this->smallPaymentWorkOrders->available()) {
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '共享业务镜像尚未建立,小额打款工单暂时不能导出。');
|
||
}
|
||
if (! $this->smallPaymentWorkOrders->ready()) {
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '业务镜像首次同步尚未完成,小额打款工单暂时不能导出。');
|
||
}
|
||
if (! $this->smallPaymentWorkOrders->queryReady()) {
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '页面查询索引尚未就绪,小额打款工单暂时不能导出。');
|
||
}
|
||
|
||
return $this->streamSmallPaymentWorkOrderCsv(
|
||
$this->smallPaymentWorkOrders->exportRows($filters),
|
||
$filters,
|
||
$redirectParameters,
|
||
$request->query('columns'),
|
||
);
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '小额打款工单导出暂时无法完成,请稍后重试。');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @return array{from:string,to:string,work_order_no:string,shop_name:string,transaction_no:string,online_order_no:string,status:string,payment_reason:string}
|
||
*/
|
||
private function smallPaymentWorkOrderFilters(Request $request): array
|
||
{
|
||
$defaultTo = now()->format('Y-m-d') . 'T23:59:59';
|
||
$defaultFrom = now()->subDays(29)->format('Y-m-d') . 'T00:00:00';
|
||
$rawFrom = $request->query('from', $defaultFrom);
|
||
$rawTo = $request->query('to', $defaultTo);
|
||
$from = $this->smallPaymentDateTimeFilter($rawFrom, false) ?: $defaultFrom;
|
||
$to = $this->smallPaymentDateTimeFilter($rawTo, true) ?: $defaultTo;
|
||
if ($from > $to) {
|
||
$from = $this->smallPaymentDateTimeFilter($rawTo, false) ?: $defaultFrom;
|
||
$to = $this->smallPaymentDateTimeFilter($rawFrom, true) ?: $defaultTo;
|
||
}
|
||
|
||
$text = static fn (mixed $value, int $length = 100): string => mb_substr(trim((string) $value), 0, $length);
|
||
|
||
return [
|
||
'from' => $from,
|
||
'to' => $to,
|
||
'work_order_no' => $text($request->query('work_order_no', '')),
|
||
'shop_name' => $text($request->query('shop_name', ''), 191),
|
||
'transaction_no' => $text($request->query('transaction_no', '')),
|
||
'online_order_no' => $text($request->query('online_order_no', '')),
|
||
'status' => $text($request->query('status', ''), 60),
|
||
'payment_reason' => $text($request->query('payment_reason', ''), 191),
|
||
];
|
||
}
|
||
|
||
private function smallPaymentDateTimeFilter(mixed $value, bool $upperBound): string
|
||
{
|
||
$value = trim((string) $value);
|
||
if (preg_match('/^(\d{4}-\d{2}-\d{2})$/', $value, $dateMatch) === 1) {
|
||
if ($this->dateFilter($dateMatch[1]) === '') {
|
||
return '';
|
||
}
|
||
|
||
return $dateMatch[1] . ($upperBound ? 'T23:59:59' : 'T00:00:00');
|
||
}
|
||
|
||
if (preg_match(
|
||
'/^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/',
|
||
$value,
|
||
$matches,
|
||
) !== 1 || $this->dateFilter($matches[1]) === '') {
|
||
return '';
|
||
}
|
||
|
||
$hour = (int) $matches[2];
|
||
$minute = (int) $matches[3];
|
||
$second = isset($matches[4]) ? (int) $matches[4] : ($upperBound ? 59 : 0);
|
||
if ($hour > 23 || $minute > 59 || $second > 59) {
|
||
return '';
|
||
}
|
||
|
||
return sprintf('%sT%02d:%02d:%02d', $matches[1], $hour, $minute, $second);
|
||
}
|
||
|
||
private function streamSmallPaymentWorkOrderCsv(iterable $rows, array $filters, array $redirectParameters, mixed $requestedFields = null)
|
||
{
|
||
$columns = [
|
||
['label' => '工单编号', 'field' => 'work_order_no', 'identifier' => true],
|
||
['label' => '支付宝批次号', 'field' => 'alipay_batch_no', 'identifier' => true],
|
||
['label' => '工单状态', 'field' => 'work_order_status'],
|
||
['label' => '打款状态', 'field' => 'payment_status'],
|
||
['label' => '工单标题', 'field' => 'work_order_title'],
|
||
['label' => '工单内容', 'field' => 'work_order_content'],
|
||
['label' => '工单类型', 'field' => 'work_order_type'],
|
||
['label' => '优先级', 'field' => 'priority'],
|
||
['label' => '打款金额', 'field' => 'payment_amount', 'number' => true],
|
||
['label' => '催处理次数', 'field' => 'urging_count', 'number' => true],
|
||
['label' => '店铺名称', 'field' => 'shop_name'],
|
||
['label' => '线上订单号', 'field' => 'online_order_no', 'identifier' => true],
|
||
['label' => '买家账号', 'field' => 'buyer_account'],
|
||
['label' => '支付宝账号', 'field' => 'alipay_account'],
|
||
['label' => '支付宝姓名', 'field' => 'alipay_name'],
|
||
['label' => '商品编码', 'field' => 'product_code', 'identifier' => true],
|
||
['label' => 'SKU编码', 'field' => 'sku_code', 'identifier' => true],
|
||
['label' => '平台', 'field' => 'platform'],
|
||
['label' => '实际已付金额', 'field' => 'actual_paid_amount', 'number' => true],
|
||
['label' => '支付时间', 'field' => 'payment_time'],
|
||
['label' => '打款原因', 'field' => 'payment_reason'],
|
||
['label' => '创建人', 'field' => 'creator'],
|
||
['label' => '创建时间', 'field' => 'work_order_created_at'],
|
||
['label' => '完成时间', 'field' => 'completed_at'],
|
||
];
|
||
$columns = $this->filterExportColumns($columns, $requestedFields);
|
||
$csv = null;
|
||
|
||
try {
|
||
$csv = fopen('php://temp/maxmemory:5242880', 'w+b');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建小额打款工单导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, array_column($columns, 'label'));
|
||
foreach ($rows as $row) {
|
||
fputcsv($csv, array_map(
|
||
fn (array $column): mixed => ($column['number'] ?? false)
|
||
? data_get($row, $column['field'])
|
||
: (($column['identifier'] ?? false)
|
||
? $this->csvIdentifier(data_get($row, $column['field']))
|
||
: $this->csvText(data_get($row, $column['field']))),
|
||
$columns,
|
||
));
|
||
}
|
||
rewind($csv);
|
||
} catch (Throwable $exception) {
|
||
if (is_resource($csv)) {
|
||
fclose($csv);
|
||
}
|
||
report($exception);
|
||
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '小额打款工单导出暂时无法完成,请稍后重试。');
|
||
}
|
||
|
||
$filenameFrom = str_replace(['T', ':'], ['_', '-'], $filters['from']);
|
||
$filenameTo = str_replace(['T', ':'], ['_', '-'], $filters['to']);
|
||
|
||
return response()->streamDownload(function () use ($csv): void {
|
||
fpassthru($csv);
|
||
fclose($csv);
|
||
}, 'small_payment_work_orders_' . $filenameFrom . '_' . $filenameTo . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
protected function contentForMarketingAccountBalance(?string $tab): array
|
||
{
|
||
$content = [
|
||
'view' => 'banking.marketing_account_balances.partials.embedded-table',
|
||
'platform' => $tab,
|
||
'balances' => collect(),
|
||
'error' => null,
|
||
'pending' => false,
|
||
];
|
||
|
||
if (! in_array($tab, ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet'], true)) {
|
||
return $content;
|
||
}
|
||
|
||
$request = request();
|
||
$page = max(1, (int) $request->query('page', 1));
|
||
$pageSize = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$storeName = mb_substr(trim((string) $request->query('store_name', '')), 0, 191);
|
||
$month = trim((string) $request->query('month', ''));
|
||
$month = preg_match('/^\d{4}-\d{2}$/', $month) === 1 ? $month : '';
|
||
$filters = [
|
||
'store_name' => $storeName !== '' ? $storeName : null,
|
||
'month' => $month !== '' ? $month : null,
|
||
];
|
||
$isTaobao = $tab === 'taobao-alipay';
|
||
$isAlibaba = $tab === 'taobao-wechat';
|
||
$isJd = $tab === 'jd-wallet';
|
||
$repository = match ($tab) {
|
||
'taobao-alipay' => app(TmallTaobaoSpreadBillRepository::class),
|
||
'taobao-wechat' => app(AlibabaAccountBalanceRepository::class),
|
||
'jd-wallet' => app(JdPromotionAccountBalanceRepository::class),
|
||
default => app(PinduoduoSpreadBillRepository::class),
|
||
};
|
||
$scope = $isTaobao ? 'taobao-alipay-bills.list' : 'pinduoduo-bills.list';
|
||
$cache = ($isAlibaba || $isJd) ? null : $this->upstreamCaches->forSource('jiansun');
|
||
$usingMirror = $repository->usesBusinessMirror();
|
||
|
||
try {
|
||
if ($isJd && ! $usingMirror) {
|
||
throw new ServiceUnavailableHttpException(null, '京东账户余额共享业务镜像尚未完成同步,请稍后重试。');
|
||
}
|
||
$resolver = fn () => $repository->paginateMonthlyBalances(
|
||
$filters['store_name'],
|
||
$filters['month'],
|
||
$page,
|
||
$pageSize,
|
||
);
|
||
$result = ($usingMirror || $isAlibaba || $isJd)
|
||
? $resolver()
|
||
: $cache->remember(
|
||
$scope,
|
||
['view' => 'monthly-balances', 'filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||
$resolver,
|
||
);
|
||
} catch (UpstreamSnapshotPendingException $exception) {
|
||
$content['pending'] = true;
|
||
$result = ['rows' => [], 'total' => 0];
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
$content['error'] = $exception->getMessage();
|
||
$result = ['rows' => [], 'total' => 0];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$platformName = $isTaobao ? '淘系' : ($isAlibaba ? '阿里巴巴' : ($isJd ? '京东' : '拼多多'));
|
||
$content['error'] = $platformName . '账户余额数据暂时无法读取,请稍后重试。';
|
||
$result = ['rows' => [], 'total' => 0];
|
||
}
|
||
|
||
try {
|
||
$resolver = fn () => $repository->storeOptions();
|
||
$storeOptions = ($usingMirror || $isAlibaba || $isJd)
|
||
? $resolver()
|
||
: $cache->remember(
|
||
$isTaobao ? 'taobao-alipay-bills.options' : 'pinduoduo-bills.options',
|
||
[],
|
||
$resolver,
|
||
);
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$storeOptions = [];
|
||
}
|
||
|
||
$content['balances'] = new LengthAwarePaginator(
|
||
Collection::make($result['rows'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$pageSize,
|
||
$page,
|
||
['path' => $request->url(), 'query' => $request->query()],
|
||
);
|
||
$content['filters'] = ['store_name' => $storeName, 'month' => $month];
|
||
$content['storeOptions'] = $storeOptions;
|
||
|
||
return $content;
|
||
}
|
||
|
||
public function export(Request $request, string $section, string $page, string $tab)
|
||
{
|
||
abort_unless(in_array($section, ['billing-transactions', self::ACCOUNT_BALANCE_SECTION], true), 404);
|
||
|
||
$sectionConfig = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', $section);
|
||
abort_if(empty($sectionConfig), 403, '当前账号没有该页面权限。');
|
||
$pageConfig = collect($sectionConfig['pages'] ?? [])->firstWhere('slug', $page);
|
||
abort_if(empty($pageConfig), 403, '当前账号没有该页面权限。');
|
||
abort_unless(collect($pageConfig['tabs'] ?? [])->pluck('slug')->contains($tab), 404);
|
||
|
||
if ($page === 'marketing-account-bills') {
|
||
abort_unless($section === 'billing-transactions', 404);
|
||
|
||
if (in_array($tab, self::PROMOTION_MARKETING_TABS, true)) {
|
||
return $this->exportPlatformPromotionAccounts($request, $section, $page, $tab);
|
||
}
|
||
|
||
return $this->exportMarketingAccountBills($request, $section, $page, $tab);
|
||
}
|
||
|
||
abort_unless($section === self::ACCOUNT_BALANCE_SECTION && $page === 'marketing-account-balance', 404);
|
||
abort_unless(in_array($tab, ['taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet'], true), 404);
|
||
|
||
$storeName = mb_substr(trim((string) $request->query('store_name', '')), 0, 191);
|
||
$month = trim((string) $request->query('month', ''));
|
||
$month = preg_match('/^\d{4}-\d{2}$/', $month) === 1 ? $month : '';
|
||
$filters = [
|
||
'store_name' => $storeName !== '' ? $storeName : null,
|
||
'month' => $month !== '' ? $month : null,
|
||
];
|
||
$isTaobao = $tab === 'taobao-alipay';
|
||
$isAlibaba = $tab === 'taobao-wechat';
|
||
$isJd = $tab === 'jd-wallet';
|
||
$repository = match ($tab) {
|
||
'taobao-alipay' => app(TmallTaobaoSpreadBillRepository::class),
|
||
'taobao-wechat' => app(AlibabaAccountBalanceRepository::class),
|
||
'jd-wallet' => app(JdPromotionAccountBalanceRepository::class),
|
||
default => app(PinduoduoSpreadBillRepository::class),
|
||
};
|
||
$scope = $isTaobao ? 'taobao-alipay-bills.list' : 'pinduoduo-bills.list';
|
||
$redirectParameters = array_filter([
|
||
'section' => $section,
|
||
'page' => $page,
|
||
'tab' => $tab,
|
||
'store_name' => $storeName,
|
||
'month' => $month,
|
||
], static fn (mixed $value): bool => $value !== '');
|
||
|
||
try {
|
||
$usingMirror = $repository->usesBusinessMirror();
|
||
if ($isJd && ! $usingMirror) {
|
||
throw new ServiceUnavailableHttpException(null, '京东账户余额共享业务镜像尚未完成同步。');
|
||
}
|
||
$resolver = fn () => $repository->paginateMonthlyBalances($filters['store_name'], $filters['month'], 1, 5000);
|
||
$result = ($usingMirror || $isAlibaba || $isJd)
|
||
? $resolver()
|
||
: $this->upstreamCaches->forSource('jiansun')->remember(
|
||
$scope,
|
||
['view' => 'monthly-balances', 'filters' => $filters, 'page' => 1, 'limit' => 5000, 'export' => true],
|
||
$resolver,
|
||
);
|
||
} catch (ServiceUnavailableHttpException) {
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', $isJd
|
||
? '京东账户余额共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'
|
||
: '首份共享快照正在准备,系统已登记刷新请求;请稍后刷新页面后再导出。');
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', '余额数据导出暂时无法完成,请稍后重试。');
|
||
}
|
||
$rows = $result['rows'] ?? [];
|
||
|
||
$filePrefix = $isTaobao ? 'taobao' : ($isAlibaba ? 'alibaba' : ($isJd ? 'jd' : 'pinduoduo'));
|
||
|
||
return response()->streamDownload(function () use ($rows): void {
|
||
$csv = fopen('php://output', 'w');
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, [
|
||
'日期', '店铺名称', '期初余额', '本期收入', '本期支出', '期末余额',
|
||
'系统期末余额', '余额差异', '差异原因', '核对状态', '备注',
|
||
]);
|
||
|
||
foreach ($rows as $row) {
|
||
fputcsv($csv, [
|
||
$row['date'], $row['store_name'], $row['opening_balance'], $row['income_total'],
|
||
$row['expense_total'], $row['closing_balance'], $row['system_closing_balance'],
|
||
$row['balance_difference'], $row['difference_reason'], $row['verification_status'], $row['notes'],
|
||
]);
|
||
}
|
||
|
||
fclose($csv);
|
||
}, $filePrefix . '_account_balances_' . ($month !== '' ? $month : 'all') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
]);
|
||
}
|
||
|
||
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 = [
|
||
'month' => $month,
|
||
'shop_name' => mb_substr(trim((string) $request->query('shop_name', '')), 0, 100),
|
||
'account' => mb_substr(trim((string) $request->query('account', '')), 0, 100),
|
||
];
|
||
if ($platform === 'vip') {
|
||
$filters['advertiser_id'] = mb_substr(trim((string) $request->query('advertiser_id', '')), 0, 100);
|
||
}
|
||
|
||
try {
|
||
$repository = app(\Modules\EcommerceFinance\Repositories\PlatformPromotionAccountRepository::class);
|
||
$filters['month'] = $month ?: ($repository->latestMonth() ?: now()->format('Y-m'));
|
||
$data = $repository->dashboard($filters, 5000, $platform);
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
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, $platform): void {
|
||
$csv = fopen('php://output', 'w');
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
|
||
if ($platform === 'vip') {
|
||
fputcsv($csv, [
|
||
'平台', '营销账户', '账户名称', '广告主 ID', '广告主名称',
|
||
'报表日期', '花费(元)', '曝光量', '更新时间',
|
||
]);
|
||
foreach ($data['vipMarketingDaily'] ?? [] as $row) {
|
||
fputcsv($csv, [
|
||
'唯品会推广流水',
|
||
$row['account'] ?? '',
|
||
$row['accountName'] ?? '',
|
||
$row['advertiserId'] ?? '',
|
||
$row['advertiserTitle'] ?? '',
|
||
$row['reportDate'] ?? '',
|
||
$row['costYuan'] ?? '',
|
||
$row['impressionCount'] ?? '',
|
||
$row['updatedAt'] ?? '',
|
||
]);
|
||
}
|
||
fclose($csv);
|
||
|
||
return;
|
||
}
|
||
|
||
if ($platform === 'jd') {
|
||
fputcsv($csv, [
|
||
'平台', '店铺', '账户', '日期', '记账时间', '收支类型', '交易类型',
|
||
'金额(元)', '余额(元)', '备注',
|
||
]);
|
||
foreach ($data['jd'] ?? [] as $row) {
|
||
fputcsv($csv, [
|
||
'京东京准通',
|
||
$row['shop_name'] ?? '',
|
||
$row['account'] ?? '',
|
||
$row['show_date'] ?? '',
|
||
$row['settle_datetime'] ?? '',
|
||
$row['in_out_type_name'] ?? '',
|
||
$row['trade_type_name'] ?? '',
|
||
$row['amount'] ?? '',
|
||
$row['balance_amount'] ?? '',
|
||
$row['remark'] ?? '',
|
||
]);
|
||
}
|
||
fclose($csv);
|
||
|
||
return;
|
||
}
|
||
|
||
fputcsv($csv, ['平台', '店铺/账户', '日期', '类型', '金额(元)', '余额(元)', '备注']);
|
||
$emit = static function (string $platform, array $rows, array $map) use ($csv): void {
|
||
foreach ($rows as $row) {
|
||
fputcsv($csv, [
|
||
$platform,
|
||
$map['name']($row),
|
||
$map['date']($row),
|
||
$map['type']($row),
|
||
$map['amount']($row),
|
||
$map['balance']($row),
|
||
$map['remark']($row),
|
||
]);
|
||
}
|
||
};
|
||
if ($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'] ?? '']);
|
||
}
|
||
fclose($csv);
|
||
}, '店铺营销账户账单_' . $platform . '_' . $filters['month'] . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
]);
|
||
}
|
||
|
||
private function exportMarketingAccountBills(
|
||
Request $request,
|
||
string $section,
|
||
string $page,
|
||
string $tab,
|
||
) {
|
||
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')
|
||
: '';
|
||
$redirectParameters = $this->marketingBillRedirectParameters($section, $page, $tab, [
|
||
'store_name' => $storeName,
|
||
'from' => $fromDate,
|
||
'to' => $toDate,
|
||
'flow_type' => $flowType,
|
||
]);
|
||
$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 {
|
||
$csv = fopen('php://temp/maxmemory:5242880', 'w+b');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建账单导出临时文件。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, $headers);
|
||
|
||
foreach ($rows as $row) {
|
||
fputcsv($csv, $rowValues($row));
|
||
}
|
||
|
||
rewind($csv);
|
||
} catch (Throwable $exception) {
|
||
if (is_resource($csv)) {
|
||
fclose($csv);
|
||
}
|
||
report($exception);
|
||
|
||
return redirect()->route('business.pages.show', $redirectParameters)
|
||
->with('error', $errorMessage);
|
||
}
|
||
|
||
return response()->streamDownload(function () use ($csv): void {
|
||
fpassthru($csv);
|
||
fclose($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 = [
|
||
'stock_in_date' => $this->dateFilter(request('stock_in_date')),
|
||
'stock_out_date' => $this->dateFilter(request('stock_out_date')),
|
||
'furniture_factory' => mb_substr(trim((string) request('furniture_factory', '')), 0, 191),
|
||
'lamination_factory' => mb_substr(trim((string) request('lamination_factory', '')), 0, 191),
|
||
'deduction_type' => mb_substr(trim((string) request('deduction_type', '')), 0, 191),
|
||
'material_name' => mb_substr(trim((string) request('material_name', '')), 0, 191),
|
||
'claim_number' => mb_substr(trim((string) request('claim_number', '')), 0, 191),
|
||
];
|
||
$page = max((int) request('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||
|
||
try {
|
||
$result = $cache->remember(
|
||
'supplier-payables.board-deductions.list',
|
||
['filters' => $filters, 'page' => $page, 'limit' => $limit],
|
||
fn () => $this->supplierPayableReports->boardDeductions($filters, $page, $limit),
|
||
);
|
||
$rows = $this->supplierPayableOverrides->decorate(
|
||
'board-deductions',
|
||
(array) ($result['rows'] ?? []),
|
||
(int) (company_id() ?: 1),
|
||
);
|
||
|
||
return [
|
||
'boardDeductions' => $this->supplierPayablePaginator($rows, $limit, $page),
|
||
'boardDeductionFilters' => $filters,
|
||
'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('board-deductions'),
|
||
'boardDeductionStatus' => $cache->responseHeaders(),
|
||
'boardDeductionError' => null,
|
||
];
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
return [
|
||
'boardDeductions' => $this->supplierPayablePaginator([], $limit, $page),
|
||
'boardDeductionFilters' => $filters,
|
||
'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('board-deductions'),
|
||
'boardDeductionStatus' => [],
|
||
'boardDeductionError' => $exception->getMessage(),
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return [
|
||
'boardDeductions' => $this->supplierPayablePaginator([], $limit, $page),
|
||
'boardDeductionFilters' => $filters,
|
||
'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('board-deductions'),
|
||
'boardDeductionStatus' => [],
|
||
'boardDeductionError' => '板材扣款数据暂时无法读取,请稍后重试。',
|
||
];
|
||
}
|
||
}
|
||
|
||
protected function contentForSupplierPayablesReport(): array
|
||
{
|
||
$month = trim((string) request('month', ''));
|
||
$filters = [
|
||
'month' => preg_match('/^\d{4}-\d{2}$/', $month) === 1 ? $month : '',
|
||
'supplier' => mb_substr(trim((string) request('supplier', '')), 0, 191),
|
||
'checker' => mb_substr(trim((string) request('checker', '')), 0, 191),
|
||
'settlement_method' => mb_substr(trim((string) request('settlement_method', '')), 0, 191),
|
||
];
|
||
$page = max((int) request('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||
|
||
try {
|
||
$result = $cache->remember(
|
||
'supplier-payables.report.list',
|
||
['filters' => $filters, 'page' => $page, 'limit' => $limit],
|
||
fn () => $this->supplierPayableReports->supplierPayables($filters, $page, $limit),
|
||
);
|
||
$rows = $this->supplierPayableOverrides->decorate(
|
||
'supplier-payables-report',
|
||
(array) ($result['rows'] ?? []),
|
||
(int) (company_id() ?: 1),
|
||
);
|
||
|
||
return [
|
||
'supplierPayables' => $this->supplierPayablePaginator($rows, $limit, $page),
|
||
'supplierPayableFilters' => $filters,
|
||
'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('supplier-payables-report'),
|
||
'supplierPayableStatus' => $cache->responseHeaders(),
|
||
'supplierPayableError' => null,
|
||
];
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
return [
|
||
'supplierPayables' => $this->supplierPayablePaginator([], $limit, $page),
|
||
'supplierPayableFilters' => $filters,
|
||
'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('supplier-payables-report'),
|
||
'supplierPayableStatus' => [],
|
||
'supplierPayableError' => $exception->getMessage(),
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return [
|
||
'supplierPayables' => $this->supplierPayablePaginator([], $limit, $page),
|
||
'supplierPayableFilters' => $filters,
|
||
'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('supplier-payables-report'),
|
||
'supplierPayableStatus' => [],
|
||
'supplierPayableError' => '供应商应付数据暂时无法读取,请稍后重试。',
|
||
];
|
||
}
|
||
}
|
||
|
||
private function supplierPayablePaginator(array $rows, int $limit, int $page): Paginator
|
||
{
|
||
return new Paginator(
|
||
Collection::make($rows)->map(static fn (array $row): object => (object) $row),
|
||
$limit,
|
||
$page,
|
||
[
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
],
|
||
);
|
||
}
|
||
|
||
protected function contentForNormalProducts(): array
|
||
{
|
||
$filters = $this->normalProductFilters(request());
|
||
|
||
$page = max((int) request('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 50);
|
||
|
||
try {
|
||
if (! $this->productCatalog->available()) {
|
||
return $this->emptyProductCatalogContent(
|
||
$filters,
|
||
$limit,
|
||
$page,
|
||
'共享业务镜像尚未建立,普通商品资料暂时无法读取。',
|
||
null,
|
||
['code' => 'unavailable', 'lag_minutes' => null],
|
||
);
|
||
}
|
||
|
||
$state = $this->productCatalog->state();
|
||
$syncStatus = $this->productCatalog->syncStatus($state);
|
||
if (! $this->productCatalog->ready()) {
|
||
return $this->emptyProductCatalogContent(
|
||
$filters,
|
||
$limit,
|
||
$page,
|
||
'普通商品资料正在执行首次全量同步,完成前不会把未同步数据误判为无数据。',
|
||
$state,
|
||
$syncStatus,
|
||
);
|
||
}
|
||
|
||
$result = $this->productCatalog->paginate($filters, $page, $limit);
|
||
$paginator = new LengthAwarePaginator(
|
||
$result['data'],
|
||
$result['total'],
|
||
$limit,
|
||
$page,
|
||
[
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
],
|
||
);
|
||
|
||
return [
|
||
'productCatalogRows' => $paginator,
|
||
'productCatalogFilters' => $filters,
|
||
'productCatalogError' => null,
|
||
'productCatalogState' => $state,
|
||
'productCatalogSyncStatus' => $syncStatus,
|
||
'productCatalogPurchasePriceSyncStatus' => $result['purchase_price_sync_status'] ?? [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
'productCatalogWorkPlatformPriceSyncStatus' => $result['work_platform_price_sync_status'] ?? [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
'productCatalogPriceChangeSyncStatus' => $result['price_change_sync_status'] ?? [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
'productCatalogJushuitanCostPriceHistorySyncStatus' => $result['jushuitan_cost_price_history_sync_status'] ?? [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return $this->emptyProductCatalogContent(
|
||
$filters,
|
||
$limit,
|
||
$page,
|
||
'共享业务数据暂时无法读取,请稍后重试。',
|
||
null,
|
||
['code' => 'unavailable', 'lag_minutes' => null],
|
||
);
|
||
}
|
||
}
|
||
|
||
public function exportNormalProducts(Request $request): StreamedResponse
|
||
{
|
||
abort_unless($this->productCatalog->available() && $this->productCatalog->ready(), 503);
|
||
|
||
$filters = $this->normalProductFilters($request);
|
||
|
||
return response()->streamDownload(function () use ($filters): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建普通商品资料导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, [
|
||
'ID', '款式编码', '商品编码', '商品名称', '商品简称', '规格 / 颜色',
|
||
'品牌', '类目', '供应商', '销售价', '成本价', '普通商品采购价',
|
||
'工作平台价格', '工作平台价格生效日期', '定价状态', '变价前价格', '变价后价格', '差额', '变价时间',
|
||
'数据来源', '市场价', '商品状态', '库存同步', '业务更新时间',
|
||
]);
|
||
|
||
foreach ($this->productCatalog->exportRows($filters) as $row) {
|
||
$specification = collect([$row->properties_value, $row->color])
|
||
->filter(static fn (mixed $value): bool => $value !== null && $value !== '')
|
||
->unique()
|
||
->implode(' / ');
|
||
$enabledLabel = match ((int) $row->enabled) {
|
||
1 => '已启用',
|
||
-1 => '已删除',
|
||
default => '已停用',
|
||
};
|
||
|
||
fputcsv($csv, [
|
||
$row->autoid,
|
||
$this->csvText($row->i_id),
|
||
$this->csvText($row->sku_id),
|
||
$this->csvText($row->name),
|
||
$this->csvText($row->short_name),
|
||
$this->csvText($specification),
|
||
$this->csvText($row->brand),
|
||
$this->csvText($row->category),
|
||
$this->csvText($row->supplier_name),
|
||
$row->sale_price,
|
||
$row->cost_price,
|
||
$row->purchase_price,
|
||
$row->work_platform_price ?? null,
|
||
$this->workPlatformPriceDate($row->work_platform_price_effect_time ?? null),
|
||
$row->pricing_status ?? '未定价',
|
||
$row->price_change_before_price ?? null,
|
||
$row->price_change_after_price ?? null,
|
||
$row->price_change_diff_amount ?? null,
|
||
$this->workPlatformPriceDate($row->price_change_effect_time ?? null),
|
||
implode(';', $row->data_sources),
|
||
$row->market_price,
|
||
$enabledLabel,
|
||
(int) $row->stock_disabled === 0 ? '正常同步' : '停止同步',
|
||
$row->modified,
|
||
]);
|
||
}
|
||
|
||
fclose($csv);
|
||
}, '普通商品资料_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
private function workPlatformPriceDate(mixed $value): string
|
||
{
|
||
if (! is_string($value) || trim($value) === '') {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
return Carbon::parse($value)->format('Y-m-d');
|
||
} catch (Throwable) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
public function exportCourierInformation(Request $request): StreamedResponse
|
||
{
|
||
abort_unless($this->courierInformation->available() && $this->courierInformation->ready(), 503);
|
||
|
||
$filters = $this->courierInformationFilters($request);
|
||
|
||
return response()->streamDownload(function () use ($filters): void {
|
||
$csv = fopen('php://output', 'w');
|
||
if ($csv === false) {
|
||
throw new \RuntimeException('无法创建快递资料导出流。');
|
||
}
|
||
|
||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||
fputcsv($csv, [
|
||
'ID', '快递公司代码', '快递公司名称', '外部编码', '杉橙快递代码', '运费打款方式',
|
||
'是否启用', '可达服务', '是否快运', '自动寻号', '配货单号寻号', '数量拆单', '重量0寻号',
|
||
'停运时间', '合同开始时间', '合同结束时间', '联系人', '联系电话', '自定义1', '自定义2', '自定义3',
|
||
'体积0寻号', '手工单号', '解密', '电子签章经办人', '最后修改',
|
||
]);
|
||
|
||
$bool = static fn (mixed $value): string => in_array(strtolower((string) $value), ['1', 'true', 'yes', 'y'], true) || $value === true ? '是' : '否';
|
||
$date = static fn (mixed $value): string => $value === null || $value === '' ? '' : substr((string) $value, 0, 10);
|
||
|
||
foreach ($this->courierInformation->exportRows($filters) as $row) {
|
||
fputcsv($csv, [
|
||
$row->Id,
|
||
$this->csvText($row->expcode),
|
||
$this->csvText($row->expname),
|
||
$this->csvText($row->outcode),
|
||
$this->csvText($row->scexpcode),
|
||
$this->csvText($row->paymethod),
|
||
$bool($row->status),
|
||
$bool($row->addvalidsgn),
|
||
$bool($row->kuaiyunsgn),
|
||
$bool($row->autonosgn),
|
||
$bool($row->packnosgn),
|
||
$bool($row->skucountsplitsgn),
|
||
$bool($row->weight0sgn),
|
||
$date($row->stopdate),
|
||
$date($row->tradebtime),
|
||
$date($row->tradeetime),
|
||
$this->csvText($row->linker),
|
||
$this->csvText($row->linkmobile),
|
||
$this->csvText($row->undefined1),
|
||
$this->csvText($row->undefined2),
|
||
$this->csvText($row->undefined3),
|
||
$bool($row->volume0sgn),
|
||
$bool($row->handnosgn),
|
||
$bool($row->decryptsgn),
|
||
$this->csvText($row->dzqzjbr),
|
||
$date($row->modified),
|
||
]);
|
||
}
|
||
|
||
fclose($csv);
|
||
}, '快递资料_' . now()->format('Ymd_His') . '.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function normalProductFilters(Request $request): array
|
||
{
|
||
$filters = [
|
||
'i_id' => mb_substr(trim((string) $request->query('i_id', '')), 0, 50),
|
||
'sku_id' => mb_substr(trim((string) $request->query('sku_id', '')), 0, 50),
|
||
'keyword' => mb_substr(trim((string) $request->query('keyword', '')), 0, 100),
|
||
'brand' => mb_substr(trim((string) $request->query('brand', '')), 0, 100),
|
||
'category' => mb_substr(trim((string) $request->query('category', '')), 0, 100),
|
||
'supplier_name' => mb_substr(trim((string) $request->query('supplier_name', '')), 0, 191),
|
||
'enabled' => in_array((string) $request->query('enabled', ''), ['-1', '0', '1'], true)
|
||
? (string) $request->query('enabled')
|
||
: '',
|
||
'stock_disabled' => in_array((string) $request->query('stock_disabled', ''), ['0', '1'], true)
|
||
? (string) $request->query('stock_disabled')
|
||
: '',
|
||
'sale_price_min' => $this->decimalFilter($request->query('sale_price_min')),
|
||
'sale_price_max' => $this->decimalFilter($request->query('sale_price_max')),
|
||
];
|
||
|
||
if ($filters['sale_price_min'] !== null
|
||
&& $filters['sale_price_max'] !== null
|
||
&& (float) $filters['sale_price_min'] > (float) $filters['sale_price_max']) {
|
||
[$filters['sale_price_min'], $filters['sale_price_max']] = [
|
||
$filters['sale_price_max'],
|
||
$filters['sale_price_min'],
|
||
];
|
||
}
|
||
|
||
return $filters;
|
||
}
|
||
|
||
private function csvText(mixed $value): string
|
||
{
|
||
$text = trim((string) ($value ?? ''));
|
||
|
||
return preg_match('/^[=+\-@]/u', $text) === 1 ? "'" . $text : $text;
|
||
}
|
||
|
||
/**
|
||
* Keep client-selected exports within the fields declared by the page.
|
||
* An omitted or invalid selection falls back to the complete safe schema.
|
||
*
|
||
* @param array<int,array{label:string,field:string}> $columns
|
||
* @return array<int,array{label:string,field:string}>
|
||
*/
|
||
private function filterExportColumns(array $columns, mixed $requestedFields): array
|
||
{
|
||
if (! is_string($requestedFields) || trim($requestedFields) === '') {
|
||
return $columns;
|
||
}
|
||
|
||
$available = [];
|
||
foreach ($columns as $column) {
|
||
if ($column['field'] !== '') {
|
||
$available[$column['field']] = $column;
|
||
}
|
||
}
|
||
|
||
$selected = [];
|
||
foreach (explode(',', $requestedFields) as $field) {
|
||
$field = trim($field);
|
||
if ($field !== '' && isset($available[$field])) {
|
||
$selected[$field] = $available[$field];
|
||
}
|
||
}
|
||
|
||
return $selected !== [] ? array_values($selected) : $columns;
|
||
}
|
||
|
||
/**
|
||
* Export identifiers as text so Excel does not round or truncate long IDs.
|
||
* Keep the formula-injection guard from csvText for values beginning with a
|
||
* formula marker.
|
||
*/
|
||
private function csvIdentifier(mixed $value): string
|
||
{
|
||
$text = $this->csvText($value);
|
||
|
||
if ($text === '' || str_starts_with($text, "'")) {
|
||
return $text;
|
||
}
|
||
|
||
return "'" . $text;
|
||
}
|
||
|
||
private function emptyProductCatalogContent(
|
||
array $filters,
|
||
int $limit,
|
||
int $page,
|
||
string $error,
|
||
?array $state,
|
||
array $syncStatus,
|
||
): array {
|
||
return [
|
||
'productCatalogRows' => new LengthAwarePaginator(
|
||
[],
|
||
0,
|
||
$limit,
|
||
$page,
|
||
[
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
],
|
||
),
|
||
'productCatalogFilters' => $filters,
|
||
'productCatalogError' => $error,
|
||
'productCatalogState' => $state,
|
||
'productCatalogSyncStatus' => $syncStatus,
|
||
'productCatalogPurchasePriceSyncStatus' => [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
'productCatalogWorkPlatformPriceSyncStatus' => [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
'productCatalogPriceChangeSyncStatus' => [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
'productCatalogJushuitanCostPriceHistorySyncStatus' => [
|
||
'code' => 'unavailable',
|
||
'initialized' => false,
|
||
'last_succeeded_at' => null,
|
||
],
|
||
];
|
||
}
|
||
|
||
protected function contentForCourierInformation(): array
|
||
{
|
||
$filters = $this->courierInformationFilters(request());
|
||
$tab = (string) request()->query('courier_tab', 'express');
|
||
$tab = in_array($tab, ['express', 'networks', 'accounts', 'logs'], true) ? $tab : 'express';
|
||
$page = max((int) request()->query('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$rows = new LengthAwarePaginator([], 0, $limit, $page, [
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
]);
|
||
$error = null;
|
||
$pending = false;
|
||
$state = null;
|
||
$syncStatus = ['code' => 'unavailable', 'lag_minutes' => null];
|
||
$relatedAvailability = $this->courierInformation->relatedAvailability();
|
||
|
||
try {
|
||
if ($tab === 'express') {
|
||
if (! $this->courierInformation->available()) {
|
||
$error = '共享业务镜像尚未建立,快递资料暂时无法读取。';
|
||
} else {
|
||
$state = $this->courierInformation->state();
|
||
$syncStatus = $this->courierInformation->syncStatus($state);
|
||
if (! $this->courierInformation->ready()) {
|
||
$pending = true;
|
||
$error = '快递资料正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||
} else {
|
||
$result = $this->courierInformation->paginate($filters, $page, $limit);
|
||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||
}
|
||
}
|
||
} elseif ($tab === 'networks') {
|
||
if (! ($relatedAvailability['networks'] ?? false)) {
|
||
$error = '快递网点镜像尚未建立,请先完成 wp_express_expNet 的首次同步。';
|
||
} elseif (! ($relatedAvailability['network_ready'] ?? false)) {
|
||
$pending = true;
|
||
$error = '快递网点正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||
} else {
|
||
$result = $this->courierInformation->paginateNetworks($filters['expcode'] ?: null, $page, $limit);
|
||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||
}
|
||
} elseif ($tab === 'accounts') {
|
||
if (! ($relatedAvailability['accounts'] ?? false)) {
|
||
$error = '打款账户镜像尚未建立,请先完成 wp_express_account 的首次同步。';
|
||
} elseif (! ($relatedAvailability['account_ready'] ?? false)) {
|
||
$pending = true;
|
||
$error = '打款账户正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||
} else {
|
||
$result = $this->courierInformation->paginateAccounts($filters['expcode'] ?: null, $page, $limit);
|
||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||
}
|
||
} else {
|
||
if (! ($relatedAvailability['logs'] ?? false)) {
|
||
$error = '操作日志镜像尚未建立,请先完成 wp_express_extension 的首次同步。';
|
||
} elseif (! ($relatedAvailability['log_ready'] ?? false)) {
|
||
$pending = true;
|
||
$error = '操作日志正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||
} else {
|
||
$result = $this->courierInformation->paginateLogs($filters['expcode'] ?: null, $page, $limit);
|
||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$error = '快递资料共享镜像暂时无法读取,请稍后重试。';
|
||
}
|
||
|
||
return [
|
||
'view' => 'business.pages.courier-payables.courier-information',
|
||
'courierTab' => $tab,
|
||
'courierFilters' => $filters,
|
||
'courierRows' => $rows,
|
||
'courierState' => $state,
|
||
'courierSyncStatus' => $syncStatus,
|
||
'courierError' => $error,
|
||
'courierPending' => $pending,
|
||
'courierRelatedAvailability' => $relatedAvailability,
|
||
];
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
private function courierInformationFilters(Request $request): array
|
||
{
|
||
$filters = [
|
||
'expcode' => mb_substr(trim((string) $request->query('expcode', '')), 0, 64),
|
||
'expname' => mb_substr(trim((string) $request->query('expname', '')), 0, 100),
|
||
'undefined1' => mb_substr(trim((string) $request->query('undefined1', '')), 0, 100),
|
||
'undefined2' => mb_substr(trim((string) $request->query('undefined2', '')), 0, 100),
|
||
'undefined3' => mb_substr(trim((string) $request->query('undefined3', '')), 0, 100),
|
||
'tradebtime_from' => $this->dateFilter($request->query('tradebtime_from')),
|
||
'tradebtime_to' => $this->dateFilter($request->query('tradebtime_to')),
|
||
'status' => in_array((string) $request->query('status', ''), ['0', '1'], true)
|
||
? (string) $request->query('status')
|
||
: '',
|
||
];
|
||
|
||
if ($filters['tradebtime_from'] !== ''
|
||
&& $filters['tradebtime_to'] !== ''
|
||
&& $filters['tradebtime_from'] > $filters['tradebtime_to']) {
|
||
[$filters['tradebtime_from'], $filters['tradebtime_to']] = [
|
||
$filters['tradebtime_to'],
|
||
$filters['tradebtime_from'],
|
||
];
|
||
}
|
||
|
||
return $filters;
|
||
}
|
||
|
||
private function courierPaginator(array $rows, int $total, int $limit, int $page): LengthAwarePaginator
|
||
{
|
||
return new LengthAwarePaginator(
|
||
Collection::make($rows),
|
||
$total,
|
||
$limit,
|
||
$page,
|
||
[
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
],
|
||
);
|
||
}
|
||
|
||
protected function contentForProductPrices(): array
|
||
{
|
||
$filters = [
|
||
'barcode' => mb_substr(trim((string) request('barcode', '')), 0, 191),
|
||
'completed_from' => $this->dateFilter(request('completed_from')),
|
||
'completed_to' => $this->dateFilter(request('completed_to')),
|
||
];
|
||
$page = max((int) request('page', 1), 1);
|
||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
try {
|
||
if (! $this->productPrices->ready()) {
|
||
return $this->emptyProductPriceContent(
|
||
$filters,
|
||
$limit,
|
||
$page,
|
||
'共享业务库尚未完成建库,请等待负责人完成初始化。'
|
||
);
|
||
}
|
||
|
||
if (! $this->productPrices->initialized()) {
|
||
return $this->emptyProductPriceContent(
|
||
$filters,
|
||
$limit,
|
||
$page,
|
||
'商品价格正在执行首次全量同步,完成前不会把未同步数据误判为无数据。'
|
||
);
|
||
}
|
||
|
||
$paginator = $this->productPrices->paginate($filters, $page, $limit);
|
||
$paginator->appends(request()->except('page'));
|
||
|
||
return [
|
||
'productPrices' => $paginator,
|
||
'productPriceFilters' => $filters,
|
||
'productPriceError' => null,
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return $this->emptyProductPriceContent(
|
||
$filters,
|
||
$limit,
|
||
$page,
|
||
'共享业务数据暂时无法读取,请稍后重试。'
|
||
);
|
||
}
|
||
}
|
||
|
||
private function emptyProductPriceContent(array $filters, int $limit, int $page, string $error): array
|
||
{
|
||
return [
|
||
'productPrices' => new LengthAwarePaginator(
|
||
[],
|
||
0,
|
||
$limit,
|
||
$page,
|
||
[
|
||
'path' => request()->url(),
|
||
'query' => request()->except('page'),
|
||
],
|
||
),
|
||
'productPriceFilters' => $filters,
|
||
'productPriceError' => $error,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 封边条页面只读取上游快照;未启用或无快照时将错误交给页面显示,
|
||
* 避免工作台因上游暂不可用直接返回 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;
|
||
}
|
||
|
||
/**
|
||
* 素板价格与对账页面使用 JianSun 只读查询,并保留共享快照链路。
|
||
*/
|
||
protected function rawBoardContent(string $page): ?array
|
||
{
|
||
$scope = match ($page) {
|
||
'board-prices' => 'raw-board.prices',
|
||
'board-reconciliation' => 'raw-board.reconciliations',
|
||
default => null,
|
||
};
|
||
|
||
if ($scope === null) {
|
||
return null;
|
||
}
|
||
|
||
$filters = $scope === 'raw-board.prices'
|
||
? $this->rawBoardFilters(['raw_name', 'raw_code', 'production_code', 'supplier'])
|
||
: $this->rawBoardReconciliationFilters();
|
||
$pageNumber = max((int) request('data_page', 1), 1);
|
||
$limit = min(max((int) request('data_limit', 25), 1), 100);
|
||
|
||
try {
|
||
$cache = $this->upstreamCaches->forSource(self::RAW_BOARD_SOURCE);
|
||
$result = $cache->remember(
|
||
$scope,
|
||
['filters' => $filters, 'page' => $pageNumber, 'limit' => $limit],
|
||
fn () => $scope === 'raw-board.prices'
|
||
? $this->rawBoardPayables->prices($filters, $pageNumber, $limit)
|
||
: $this->rawBoardPayables->reconciliations($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 $this->emptyRawBoardContent($filters, $pageNumber, $limit, $exception->getMessage());
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return $this->emptyRawBoardContent(
|
||
$filters,
|
||
$pageNumber,
|
||
$limit,
|
||
'素板应付数据暂时无法读取,请稍后重试。',
|
||
);
|
||
}
|
||
}
|
||
|
||
private function rawBoardFilters(array $fields): array
|
||
{
|
||
$filters = [];
|
||
|
||
foreach ($fields as $field) {
|
||
$value = mb_substr(trim((string) request($field, '')), 0, 100);
|
||
|
||
if ($value !== '') {
|
||
$filters[$field] = $value;
|
||
}
|
||
}
|
||
|
||
return $filters;
|
||
}
|
||
|
||
private function rawBoardReconciliationFilters(): array
|
||
{
|
||
$filters = $this->rawBoardFilters([
|
||
'production_code', 'raw_name', 'outbound_supplier',
|
||
'arrival_supplier', 'outbound_no',
|
||
]);
|
||
|
||
foreach (['date_from', 'date_to'] as $field) {
|
||
$value = $this->dateFilter(request($field));
|
||
|
||
if ($value !== '') {
|
||
$filters[$field] = $value;
|
||
}
|
||
}
|
||
|
||
$returnFlag = trim((string) request('return_flag', ''));
|
||
|
||
if (in_array($returnFlag, ['normal', 'return'], true)) {
|
||
$filters['return_flag'] = $returnFlag;
|
||
}
|
||
|
||
return $filters;
|
||
}
|
||
|
||
private function emptyRawBoardContent(array $filters, int $page, int $limit, string $error): array
|
||
{
|
||
return [
|
||
'filters' => $filters,
|
||
'result' => ['data' => [], 'total' => 0, 'totals' => []],
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'last_page' => 1,
|
||
'status' => [],
|
||
'error' => $error,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 板材库存页面使用 JianSun 最新素板快照,并保留生产共享快照链路。
|
||
*/
|
||
protected function inventoryContent(string $page): ?array
|
||
{
|
||
if ($page !== 'board-inventory-details') {
|
||
return null;
|
||
}
|
||
|
||
$filters = $this->inventoryFilters();
|
||
$pageNumber = max((int) request('data_page', 1), 1);
|
||
$limit = min(max((int) request('data_limit', 25), 1), 100);
|
||
|
||
try {
|
||
$cache = $this->upstreamCaches->forSource(self::INVENTORY_SOURCE);
|
||
$result = $cache->remember(
|
||
'inventory.board-details',
|
||
['filters' => $filters, 'page' => $pageNumber, 'limit' => $limit],
|
||
fn () => $this->inventory->boardInventoryDetails($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 $this->emptyInventoryContent($filters, $pageNumber, $limit, $exception->getMessage());
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return $this->emptyInventoryContent(
|
||
$filters,
|
||
$pageNumber,
|
||
$limit,
|
||
'板材库存数据暂时无法读取,请稍后重试。',
|
||
);
|
||
}
|
||
}
|
||
|
||
private function inventoryFilters(): array
|
||
{
|
||
$filters = [];
|
||
|
||
foreach (['manufacturer', 'specification', 'matching_value'] as $field) {
|
||
$value = mb_substr(trim((string) request($field, '')), 0, 100);
|
||
|
||
if ($value !== '') {
|
||
$filters[$field] = $value;
|
||
}
|
||
}
|
||
|
||
foreach (['date_from', 'date_to'] as $field) {
|
||
$value = $this->dateFilter(request($field));
|
||
|
||
if ($value !== '') {
|
||
$filters[$field] = $value;
|
||
}
|
||
}
|
||
|
||
return $filters;
|
||
}
|
||
|
||
private function emptyInventoryContent(array $filters, int $page, int $limit, string $error): array
|
||
{
|
||
return [
|
||
'filters' => $filters,
|
||
'result' => ['data' => [], 'total' => 0, 'totals' => []],
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
'last_page' => 1,
|
||
'status' => [],
|
||
'error' => $error,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 种菜应收对账 - 加载对账数据(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;
|
||
}
|
||
|
||
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();
|
||
$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'));
|
||
$page = max(1, (int) $request->query('page', 1));
|
||
$pageSize = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$filters = [
|
||
'shop_name' => $shopName !== '' ? $shopName : null,
|
||
'product_type' => $productType !== '' ? $productType : null,
|
||
'from' => $fromDate !== '' ? $fromDate : null,
|
||
'to' => $toDate !== '' ? $toDate : null,
|
||
];
|
||
$repository = app(AlibabaBillRepository::class);
|
||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||
$usingMirror = $repository->usesBusinessMirror();
|
||
|
||
try {
|
||
$listResolver = fn () => $repository->paginate(
|
||
$filters['shop_name'], $filters['product_type'],
|
||
$filters['from'], $filters['to'], $page, $pageSize,
|
||
);
|
||
$summaryResolver = fn () => $repository->summary(
|
||
$filters['shop_name'], $filters['product_type'], $filters['from'], $filters['to'],
|
||
);
|
||
$optionsResolver = fn () => $repository->filterOptions();
|
||
$result = $usingMirror
|
||
? $listResolver()
|
||
: $cache->remember(
|
||
'marketing-account-bills.alibaba.list',
|
||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||
$listResolver,
|
||
);
|
||
$summary = $usingMirror
|
||
? $summaryResolver()
|
||
: $cache->remember(
|
||
'marketing-account-bills.alibaba.summary',
|
||
['filters' => $filters],
|
||
$summaryResolver,
|
||
);
|
||
$options = $usingMirror
|
||
? $optionsResolver()
|
||
: $cache->remember(
|
||
'marketing-account-bills.alibaba.options',
|
||
[],
|
||
$optionsResolver,
|
||
);
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
return [
|
||
'view' => 'banking.marketing_alibaba_bills.partials.embedded-table',
|
||
'bills' => new LengthAwarePaginator([], 0, $pageSize, $page, [
|
||
'path' => $request->url(), 'query' => $request->query(),
|
||
]),
|
||
'filters' => $filters,
|
||
'summary' => ['income_total' => 0, 'expense_total' => 0, 'net' => 0, 'count' => 0],
|
||
'options' => ['shops' => [], 'product_types' => []],
|
||
'error' => $exception->getMessage(),
|
||
];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
|
||
return [
|
||
'view' => 'banking.marketing_alibaba_bills.partials.embedded-table',
|
||
'bills' => new LengthAwarePaginator([], 0, $pageSize, $page, [
|
||
'path' => $request->url(), 'query' => $request->query(),
|
||
]),
|
||
'filters' => $filters,
|
||
'summary' => ['income_total' => 0, 'expense_total' => 0, 'net' => 0, 'count' => 0],
|
||
'options' => ['shops' => [], 'product_types' => []],
|
||
'error' => 'Alibaba 账单数据暂时无法读取,请稍后重试。',
|
||
];
|
||
}
|
||
|
||
$bills = new LengthAwarePaginator(
|
||
Collection::make($result['rows'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$pageSize,
|
||
$page,
|
||
['path' => $request->url(), 'query' => $request->query()],
|
||
);
|
||
|
||
return [
|
||
'view' => 'banking.marketing_alibaba_bills.partials.embedded-table',
|
||
'bills' => $bills,
|
||
'filters' => $filters,
|
||
'summary' => $summary,
|
||
'options' => $options,
|
||
'error' => null,
|
||
];
|
||
}
|
||
|
||
// 淘系支付宝 - 从上游 JianSun.tmall_spread_bills 读取并按表1字段展示(9列)
|
||
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'));
|
||
$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(TmallTaobaoSpreadBillRepository::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,
|
||
];
|
||
|
||
$listResolver = fn () => $repository->paginate(
|
||
$filters['store_name'],
|
||
$filters['from'],
|
||
$filters['to'],
|
||
$filters['flow_type'],
|
||
$page,
|
||
$pageSize
|
||
);
|
||
$summaryResolver = fn () => $repository->summary(
|
||
$filters['store_name'],
|
||
$filters['from'],
|
||
$filters['to'],
|
||
$filters['flow_type']
|
||
);
|
||
$optionsResolver = fn () => $repository->storeOptions();
|
||
$usingMirror = $repository->usesBusinessMirror();
|
||
$result = $usingMirror
|
||
? $listResolver()
|
||
: $cache->remember(
|
||
'taobao-alipay-bills.list',
|
||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||
$listResolver,
|
||
);
|
||
$summary = $usingMirror
|
||
? $summaryResolver()
|
||
: $cache->remember(
|
||
'taobao-alipay-bills.summary',
|
||
['filters' => $filters],
|
||
$summaryResolver,
|
||
);
|
||
$storeOptions = $usingMirror
|
||
? $optionsResolver()
|
||
: $cache->remember(
|
||
'taobao-alipay-bills.options',
|
||
[],
|
||
$optionsResolver,
|
||
);
|
||
|
||
$bills = new LengthAwarePaginator(
|
||
Collection::make($result['rows']),
|
||
$result['total'],
|
||
$pageSize,
|
||
$page,
|
||
[
|
||
'path' => $request->url(),
|
||
'query' => $request->query(),
|
||
]
|
||
);
|
||
|
||
return [
|
||
'view' => 'banking.marketing_taobao_alipay_bills.partials.embedded-table',
|
||
'bills' => $bills,
|
||
'filters' => [
|
||
'store_name' => $storeName,
|
||
'from' => $fromDate,
|
||
'to' => $toDate,
|
||
'flow_type' => $flowType,
|
||
'store_options' => $storeOptions,
|
||
],
|
||
'summary' => $summary,
|
||
];
|
||
}
|
||
|
||
// 拼多多 - 从上游 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,
|
||
];
|
||
|
||
// 页面只读取共享查询快照;冷查询由统一限流和熔断链路访问上游。
|
||
$listResolver = fn () => $repository->paginate(
|
||
$filters['store_name'],
|
||
$filters['from'],
|
||
$filters['to'],
|
||
$filters['flow_type'],
|
||
$page,
|
||
$pageSize
|
||
);
|
||
$summaryResolver = fn () => $repository->summary(
|
||
$filters['store_name'],
|
||
$filters['from'],
|
||
$filters['to'],
|
||
$filters['flow_type']
|
||
);
|
||
$optionsResolver = fn () => $repository->storeOptions();
|
||
$usingMirror = $repository->usesBusinessMirror();
|
||
$result = $usingMirror
|
||
? $listResolver()
|
||
: $cache->remember(
|
||
'pinduoduo-bills.list',
|
||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||
$listResolver,
|
||
);
|
||
$summary = $usingMirror
|
||
? $summaryResolver()
|
||
: $cache->remember(
|
||
'pinduoduo-bills.summary',
|
||
['filters' => $filters],
|
||
$summaryResolver,
|
||
);
|
||
$storeOptions = $usingMirror
|
||
? $optionsResolver()
|
||
: $cache->remember(
|
||
'pinduoduo-bills.options',
|
||
[],
|
||
$optionsResolver,
|
||
);
|
||
|
||
// 包装成 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,
|
||
];
|
||
}
|
||
|
||
// 京东钱包 - 按附件表1分别展示充值账单和消耗账单,数据来自 JianSun 上游只读表。
|
||
if ($tab === 'jd-wallet') {
|
||
$request = request();
|
||
$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'));
|
||
$page = max(1, (int) $request->query('page', 1));
|
||
$pageSize = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||
$filters = [
|
||
'store_name' => $storeName !== '' ? $storeName : null,
|
||
'from' => $fromDate !== '' ? $fromDate : null,
|
||
'to' => $toDate !== '' ? $toDate : null,
|
||
];
|
||
$repository = app(JdWalletBillRepository::class);
|
||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||
$scopePrefix = 'jd-wallet-bills.' . $ledger;
|
||
$errors = [];
|
||
$usingMirror = $repository->usesBusinessMirror($ledger);
|
||
$remember = function (string $scope, array $parameters, callable $resolver, mixed $fallback) use ($cache, $usingMirror, &$errors): mixed {
|
||
try {
|
||
return $usingMirror ? $resolver() : $cache->remember($scope, $parameters, $resolver);
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
$errors[] = $exception->getMessage();
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$errors[] = '京东账单数据暂时无法读取,请稍后重试。';
|
||
}
|
||
|
||
return $fallback;
|
||
};
|
||
|
||
// 三项分别读取,确保生产冷启动时一次请求就能登记列表、汇总和选项,
|
||
// 同时保留已经命中的共享快照,不因另一项暂缺而清空整页。
|
||
$result = $remember(
|
||
$scopePrefix . '.list',
|
||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||
fn () => $ledger === 'consumption'
|
||
? $repository->paginateConsumption($filters['store_name'], $filters['from'], $filters['to'], $page, $pageSize)
|
||
: $repository->paginateRecharge($filters['store_name'], $filters['from'], $filters['to'], $page, $pageSize),
|
||
['rows' => [], 'total' => 0],
|
||
);
|
||
$summary = $remember(
|
||
$scopePrefix . '.summary',
|
||
['filters' => $filters],
|
||
fn () => $ledger === 'consumption'
|
||
? $repository->summaryConsumption($filters['store_name'], $filters['from'], $filters['to'])
|
||
: $repository->summaryRecharge($filters['store_name'], $filters['from'], $filters['to']),
|
||
['amount_total' => 0, 'balance_total' => 0, 'count' => 0],
|
||
);
|
||
try {
|
||
$optionsResolver = fn () => $repository->shopOptions();
|
||
$shopOptions = $repository->usesBusinessMirrorForOptions()
|
||
? $optionsResolver()
|
||
: $cache->remember('jd-wallet-bills.options', [], $optionsResolver);
|
||
} catch (ServiceUnavailableHttpException $exception) {
|
||
$errors[] = $exception->getMessage();
|
||
$shopOptions = [];
|
||
} catch (Throwable $exception) {
|
||
report($exception);
|
||
$errors[] = '京东账单数据暂时无法读取,请稍后重试。';
|
||
$shopOptions = [];
|
||
}
|
||
|
||
return [
|
||
'view' => 'banking.marketing_jd_wallet_bills.partials.embedded-table',
|
||
'ledger' => $ledger,
|
||
'bills' => new LengthAwarePaginator(
|
||
Collection::make($result['rows'] ?? []),
|
||
(int) ($result['total'] ?? 0),
|
||
$pageSize,
|
||
$page,
|
||
['path' => $request->url(), 'query' => $request->query()],
|
||
),
|
||
'filters' => $filters,
|
||
'summary' => $summary,
|
||
'shopOptions' => $shopOptions,
|
||
'error' => $errors === []
|
||
? null
|
||
: implode(' ', array_values(array_unique(array_filter($errors)))),
|
||
];
|
||
}
|
||
|
||
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)),
|
||
];
|
||
}
|
||
|
||
public function store(Request $request, string $section, string $page, ?string $tab = null)
|
||
{
|
||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||
abort_if(! $descriptor, 404);
|
||
|
||
$data = $request->validate($this->validationRules($descriptor));
|
||
|
||
if ($this->isStoreReceivingAccountPage($section, $page)) {
|
||
abort_unless(in_array($tab, ['taobao', 'jd', 'douyin', 'pinduoduo'], true), 404);
|
||
$allowedPlatforms = TableRegistry::tabValues($section, $page, $tab);
|
||
if (in_array($tab, ['douyin', 'pinduoduo'], true)) {
|
||
$data['store_platform'] = $allowedPlatforms[0];
|
||
} else {
|
||
abort_unless(in_array($data['store_platform'] ?? null, $allowedPlatforms, true), 422, '当前页签不支持该平台类型。');
|
||
}
|
||
} elseif ($descriptor->tabFilterColumn && $tab && ($tabValue = TableRegistry::tabValue($section, $page, $tab))) {
|
||
$data[$descriptor->tabFilterColumn] = $tabValue;
|
||
}
|
||
|
||
foreach (TableRegistry::fixedFilters($section, $page) as $column => $value) {
|
||
$data[$column] = $value;
|
||
}
|
||
|
||
($descriptor->model)::create($data);
|
||
|
||
return $this->redirectBack($section, $page, $tab);
|
||
}
|
||
|
||
public function import(
|
||
Request $request,
|
||
BusinessTableImporter $importer,
|
||
string $section,
|
||
string $page,
|
||
?string $tab = null,
|
||
) {
|
||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||
abort_if(! $descriptor, 404);
|
||
|
||
$request->validate([
|
||
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'],
|
||
]);
|
||
|
||
if ($this->isStoreReceivingAccountPage($section, $page)) {
|
||
abort_unless(in_array($tab, ['taobao', 'jd', 'douyin', 'pinduoduo'], true), 404);
|
||
$result = app(StoreReceivingAccountWorkbookImporter::class)->import(
|
||
$request->file('import'),
|
||
$tab,
|
||
(int) (company_id() ?: 1),
|
||
);
|
||
} else {
|
||
$result = $importer->import($request->file('import'), $descriptor, $section, $page, $tab);
|
||
}
|
||
|
||
$message = "导入完成:新增 {$result['created']} 条,更新 {$result['updated']} 条。";
|
||
if (($result['skipped'] ?? 0) > 0) {
|
||
$message .= " 跳过 {$result['skipped']} 条空 ERP 店铺名称示例行。";
|
||
}
|
||
|
||
return $this->redirectBack($section, $page, $tab)->with('success', $message);
|
||
}
|
||
|
||
public function storeReceivingAccountTemplate(string $section, string $page, string $tab)
|
||
{
|
||
abort_unless($this->isStoreReceivingAccountPage($section, $page), 404);
|
||
abort_unless(in_array($tab, ['taobao', 'jd', 'douyin', 'pinduoduo'], true), 404);
|
||
|
||
return app(StoreReceivingAccountTemplate::class)->download($tab);
|
||
}
|
||
|
||
public function fundForecastTemplate(FundForecastTemplate $template): StreamedResponse
|
||
{
|
||
return $template->download();
|
||
}
|
||
|
||
public function update(Request $request, string $section, string $page, $id, ?string $tab = null)
|
||
{
|
||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||
abort_if(! $descriptor, 404);
|
||
|
||
$row = ($descriptor->model)::findOrFail($id);
|
||
$row->update($request->validate($this->validationRules($descriptor)));
|
||
|
||
return $this->redirectBack($section, $page, $tab);
|
||
}
|
||
|
||
public function destroy(string $section, string $page, $id, ?string $tab = null)
|
||
{
|
||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||
abort_if(! $descriptor, 404);
|
||
|
||
($descriptor->model)::findOrFail($id)->delete();
|
||
|
||
return $this->redirectBack($section, $page, $tab);
|
||
}
|
||
|
||
protected function validationRules($descriptor): array
|
||
{
|
||
$rules = [];
|
||
|
||
foreach ($descriptor->fields as $field) {
|
||
$rules[$field['name']] = match ($field['type']) {
|
||
'number', 'currency' => ['nullable', 'numeric'],
|
||
'boolean' => ['nullable', 'boolean'],
|
||
'date' => ['nullable', 'date'],
|
||
'select' => ['nullable', 'string', 'max:64'],
|
||
default => ['nullable', 'string', 'max:255'],
|
||
};
|
||
}
|
||
|
||
return $rules;
|
||
}
|
||
|
||
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 : '';
|
||
}
|
||
|
||
private function decimalFilter(mixed $value): ?string
|
||
{
|
||
$value = trim((string) $value);
|
||
|
||
if ($value === '' || ! is_numeric($value)) {
|
||
return null;
|
||
}
|
||
|
||
return (string) (float) $value;
|
||
}
|
||
|
||
protected function redirectBack(string $section, string $page, ?string $tab)
|
||
{
|
||
$parameters = [
|
||
'company_id' => company_id() ?: 1,
|
||
'section' => $section,
|
||
'page' => $page,
|
||
];
|
||
|
||
if ($tab) {
|
||
$parameters['tab'] = $tab;
|
||
}
|
||
|
||
return redirect()->route('business.pages.show', $parameters)->with('success', true);
|
||
}
|
||
}
|