接入 1688 营销账户账单 (#12)

根据 Alibaba.xlsx 字段接入 JianSun.dbo.alibaba_bill,提供筛选、汇总、分页、快照刷新和错误状态。
This commit is contained in:
chenxi 2026-08-06 16:07:31 +08:00
parent 87c1ea80bf
commit a9fd2cfb37
6 changed files with 477 additions and 1 deletions

View File

@ -13,6 +13,7 @@ use Illuminate\Routing\Controller;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
use Modules\EcommerceFinance\Repositories\InventoryRepository;
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
@ -705,6 +706,89 @@ class Pages extends Controller
return null;
}
// 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');
try {
$result = $cache->remember(
'marketing-account-bills.alibaba.list',
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
fn () => $repository->paginate(
$filters['shop_name'], $filters['product_type'],
$filters['from'], $filters['to'], $page, $pageSize,
),
);
$summary = $cache->remember(
'marketing-account-bills.alibaba.summary',
['filters' => $filters],
fn () => $repository->summary(
$filters['shop_name'], $filters['product_type'], $filters['from'], $filters['to'],
),
);
$options = $cache->remember(
'marketing-account-bills.alibaba.options',
[],
fn () => $repository->filterOptions(),
);
} 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();

View File

@ -0,0 +1,138 @@
<?php
namespace Modules\EcommerceFinance\Repositories;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
/**
* Alibaba/1688 营销账户账单。
*
* 数据来源:JianSun.dbo.alibaba_bill。金额字段由上游按元存储。
*/
class AlibabaBillRepository
{
private const TABLE = '[dbo].[alibaba_bill]';
private const SOURCE = 'jiansun';
private UpstreamReadOnlyQuery $query;
public function __construct(UpstreamReadOnlyQueryFactory $factory)
{
$this->query = $factory->forSource(self::SOURCE);
}
/** @return array{shops: string[], product_types: string[]} */
public function filterOptions(): array
{
$shops = $this->query->select(
'SELECT DISTINCT TOP (500) shopName FROM ' . self::TABLE
. ' WHERE shopName IS NOT NULL AND LTRIM(RTRIM(shopName)) <> \'\' ORDER BY shopName'
);
$types = $this->query->select(
'SELECT DISTINCT TOP (500) tradeTypeName FROM ' . self::TABLE
. ' WHERE tradeTypeName IS NOT NULL AND LTRIM(RTRIM(tradeTypeName)) <> \'\' ORDER BY tradeTypeName'
);
return [
'shops' => array_values(array_map(static fn (array $row): string => (string) ($row['shopName'] ?? ''), $shops)),
'product_types' => array_values(array_map(static fn (array $row): string => (string) ($row['tradeTypeName'] ?? ''), $types)),
];
}
/** @return array{rows: array<int,array<string,mixed>>, total: int} */
public function paginate(
?string $shopName = null,
?string $productType = null,
?string $fromDate = null,
?string $toDate = null,
int $page = 1,
int $pageSize = 25,
): array {
$page = max(1, $page);
$pageSize = min(max(1, $pageSize), 100);
[$where, $bindings] = $this->where($shopName, $productType, $fromDate, $toDate);
$total = (int) $this->query->scalar('SELECT COUNT(*) FROM ' . self::TABLE . ' src' . $where, $bindings);
if ($total === 0) {
return ['rows' => [], 'total' => 0];
}
$start = (($page - 1) * $pageSize) + 1;
$end = $page * $pageSize;
$sql = 'SELECT * FROM ('
. 'SELECT src.id, src.shopName, src.tradeDay, src.tradeTypeName, '
. 'src.tradeAMTIn, src.tradeAMTOut, '
. 'ROW_NUMBER() OVER (ORDER BY src.tradeDay DESC, src.id DESC) AS rn '
. 'FROM ' . self::TABLE . ' src' . $where
. ') AS t WHERE t.rn BETWEEN ? AND ? ORDER BY t.rn';
$rows = $this->query->select($sql, array_merge($bindings, [$start, $end]));
return [
'rows' => array_map(fn (array $row): array => $this->normalize($row), $rows),
'total' => $total,
];
}
/** @return array{income_total: float, expense_total: float, net: float, count: int} */
public function summary(
?string $shopName = null,
?string $productType = null,
?string $fromDate = null,
?string $toDate = null,
): array {
[$where, $bindings] = $this->where($shopName, $productType, $fromDate, $toDate);
$row = $this->query->first(
'SELECT COALESCE(SUM(tradeAMTIn), 0) AS income_total, '
. 'COALESCE(SUM(tradeAMTOut), 0) AS expense_total, COUNT(*) AS cnt '
. 'FROM ' . self::TABLE . ' src' . $where,
$bindings,
) ?? [];
$income = round((float) ($row['income_total'] ?? 0), 2);
$expense = round((float) ($row['expense_total'] ?? 0), 2);
return [
'income_total' => $income,
'expense_total' => $expense,
'net' => round($income - $expense, 2),
'count' => (int) ($row['cnt'] ?? 0),
];
}
/** @return array{0: string, 1: array<int,mixed>} */
private function where(?string $shopName, ?string $productType, ?string $fromDate, ?string $toDate): array
{
$conditions = [];
$bindings = [];
if ($shopName !== null && $shopName !== '') {
$conditions[] = 'src.shopName = ?';
$bindings[] = $shopName;
}
if ($productType !== null && $productType !== '') {
$conditions[] = 'src.tradeTypeName = ?';
$bindings[] = $productType;
}
if ($fromDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) === 1) {
$conditions[] = 'CAST(src.tradeDay AS date) >= CAST(? AS date)';
$bindings[] = $fromDate;
}
if ($toDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) === 1) {
$conditions[] = 'CAST(src.tradeDay AS date) <= CAST(? AS date)';
$bindings[] = $toDate;
}
return [$conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions), $bindings];
}
private function normalize(array $row): array
{
return [
'shop_name' => (string) ($row['shopName'] ?? ''),
'trade_day' => (string) ($row['tradeDay'] ?? ''),
'product_type' => (string) ($row['tradeTypeName'] ?? ''),
'income_amount' => round((float) ($row['tradeAMTIn'] ?? 0), 2),
'expense_amount' => round((float) ($row['tradeAMTOut'] ?? 0), 2),
];
}
}

View File

@ -4,6 +4,7 @@ namespace Modules\EcommerceFinance\Services;
use InvalidArgumentException;
use Modules\EcommerceFinance\Contracts\UpstreamSnapshotRefresher;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
use Modules\EcommerceFinance\Repositories\InventoryRepository;
@ -24,6 +25,7 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
private readonly EdgeBandingRepository $edgeBanding,
private readonly GoodsReconciliationRepository $goods,
private readonly InventoryRepository $inventory,
private readonly AlibabaBillRepository $alibaba,
private readonly PinduoduoSpreadBillRepository $pinduoduo,
private readonly ProductPriceRepository $productPrices,
private readonly RawBoardPayablesRepository $rawBoardPayables,
@ -55,11 +57,14 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
$filters = (array) ($parameters['filters'] ?? []);
$page = max((int) ($parameters['page'] ?? 1), 1);
$limit = min(max((int) ($parameters['limit'] ?? 25), 1), 100);
$storeName = isset($filters['store_name']) ? (string) $filters['store_name'] : null;
$storeName = isset($filters['shop_name'])
? (string) $filters['shop_name']
: (isset($filters['store_name']) ? (string) $filters['store_name'] : null);
$fromDate = isset($filters['from']) ? (string) $filters['from'] : null;
$toDate = isset($filters['to']) ? (string) $filters['to'] : null;
$flowType = isset($filters['flow_type']) ? (string) $filters['flow_type'] : null;
$flowType = in_array($flowType, ['income', 'expense'], true) ? $flowType : null;
$productType = isset($filters['product_type']) ? (string) $filters['product_type'] : null;
return match ($scope) {
'edge-banding.prices' => $this->edgeBanding->prices($filters, $page, $limit),
@ -72,6 +77,22 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
'inventory.board-details' => $this->inventory->boardInventoryDetails($filters, $page, $limit),
'marketing-account-bills.alibaba.list' => $this->alibaba->paginate(
$storeName,
$productType,
$fromDate,
$toDate,
$page,
$limit,
),
'marketing-account-bills.alibaba.summary' => $this->alibaba->summary(
$storeName,
$productType,
$fromDate,
$toDate,
),
'marketing-account-bills.alibaba.options' => $this->alibaba->filterOptions(),
'raw-board.prices' => $this->rawBoardPayables->prices($filters, $page, $limit),
'raw-board.reconciliations' => $this->rawBoardPayables->reconciliations($filters, $page, $limit),

View File

@ -0,0 +1,109 @@
{{-- Alibaba/1688 营销账户账单:店铺名称 | 交易时间 | 产品类型 | 入账金额 | 出帐金额 --}}
<style>
.alibaba-bill-table-wrapper { overflow-x: auto; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; }
.alibaba-bill-table { min-width: 760px; width: 100%; white-space: nowrap; font-size: .78125rem; }
.alibaba-bill-table th { background: var(--vip-content-accent-soft, #F7F0DE); color: var(--vip-content-muted, #595959); padding: .65rem .75rem; border-bottom: 1px solid var(--vip-border, #D8D2C7); font-size: .75rem; font-weight: 600; text-align: left; }
.alibaba-bill-table td { padding: .65rem .75rem; border-bottom: 1px solid var(--vip-border, #D8D2C7); }
.alibaba-bill-table tbody tr:hover { background: var(--vip-content-accent-soft, #F7F0DE); }
.alibaba-bill-amount { text-align: right; font-variant-numeric: tabular-nums; }
.alibaba-bill-income { color: #53793E; font-weight: 600; }
.alibaba-bill-expense { color: #B80000; font-weight: 600; }
.alibaba-bill-control { height: 34px; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; background: #fff; padding: 0 10px; color: #171717; font-size: .8125rem; }
.alibaba-bill-control:focus { border-color: var(--vip-content-accent-dark, #856632); outline: none; box-shadow: 0 0 0 3px rgba(133, 102, 50, .2); }
.alibaba-bill-primary, .alibaba-bill-secondary { display: inline-flex; height: 34px; align-items: center; gap: 8px; border-radius: 8px; padding: 0 14px; font-size: .8125rem; font-weight: 500; }
.alibaba-bill-primary { background: var(--vip-content-accent-dark, #856632); color: #fff; }
.alibaba-bill-secondary { border: 1px solid var(--vip-border, #D8D2C7); background: #fff; color: #171717; }
</style>
@php
$summary = $summary ?? ['income_total' => 0, 'expense_total' => 0, 'net' => 0, 'count' => 0];
$filters = $filters ?? ['shop_name' => '', 'product_type' => '', 'from' => '', 'to' => ''];
$options = $options ?? ['shops' => [], 'product_types' => []];
$shops = is_array($options['shops'] ?? null) ? $options['shops'] : [];
$productTypes = is_array($options['product_types'] ?? null) ? $options['product_types'] : [];
@endphp
<div class="w-full">
@if (!empty($error))
<div class="mb-4 border-l-4 border-red-500 bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">
{{ $error }}
</div>
@endif
<div class="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
@foreach ([
['label' => '入账合计(元)', 'value' => (float) ($summary['income_total'] ?? 0), 'class' => 'alibaba-bill-income', 'prefix' => '+ '],
['label' => '出帐合计(元)', 'value' => (float) ($summary['expense_total'] ?? 0), 'class' => 'alibaba-bill-expense', 'prefix' => '- '],
['label' => '净额(入账 - 出帐,元)', 'value' => (float) ($summary['net'] ?? 0), 'class' => ((float) ($summary['net'] ?? 0) >= 0 ? 'text-gray-900' : 'alibaba-bill-expense'), 'prefix' => ((float) ($summary['net'] ?? 0) >= 0 ? '+ ' : '')],
['label' => '流水笔数', 'value' => (int) ($summary['count'] ?? 0), 'class' => 'text-gray-900', 'prefix' => ''],
] as $metric)
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3">
<div class="text-xs font-medium text-gray-500">{{ $metric['label'] }}</div>
<div class="mt-1 text-lg font-semibold tabular-nums {{ $metric['class'] }}">
{{ $metric['prefix'] }}{{ number_format($metric['value'], $metric['label'] === '流水笔数' ? 0 : 2) }}
</div>
</div>
@endforeach
</div>
<form method="GET" action="{{ request()->url() }}" class="mb-4 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3">
<div class="flex flex-wrap items-end gap-4">
<div>
<label for="alibaba-filter-shop" class="mb-1 block text-xs font-medium text-gray-600">店铺名称</label>
<select id="alibaba-filter-shop" name="shop_name" class="alibaba-bill-control min-w-[200px]">
<option value="">全部店铺</option>
@foreach ($shops as $shop)
<option value="{{ $shop }}" @selected(($filters['shop_name'] ?? '') === $shop)>{{ $shop }}</option>
@endforeach
</select>
</div>
<div>
<label for="alibaba-filter-type" class="mb-1 block text-xs font-medium text-gray-600">产品类型</label>
<select id="alibaba-filter-type" name="product_type" class="alibaba-bill-control min-w-[220px]">
<option value="">全部产品类型</option>
@foreach ($productTypes as $type)
<option value="{{ $type }}" @selected(($filters['product_type'] ?? '') === $type)>{{ $type }}</option>
@endforeach
</select>
</div>
<div>
<label for="alibaba-filter-from" class="mb-1 block text-xs font-medium text-gray-600">交易时间 起</label>
<input id="alibaba-filter-from" type="date" name="from" value="{{ $filters['from'] ?? '' }}" class="alibaba-bill-control" />
</div>
<div>
<label for="alibaba-filter-to" class="mb-1 block text-xs font-medium text-gray-600">交易时间 止</label>
<input id="alibaba-filter-to" type="date" name="to" value="{{ $filters['to'] ?? '' }}" class="alibaba-bill-control" />
</div>
<div class="flex items-end gap-2">
<button type="submit" class="alibaba-bill-primary"><span class="material-icons text-base" aria-hidden="true">search</span>查询</button>
<a href="{{ request()->url() }}" class="alibaba-bill-secondary"><span class="material-icons text-base" aria-hidden="true">clear</span>清除</a>
</div>
@if (($filters['shop_name'] ?? '') !== '' || ($filters['product_type'] ?? '') !== '' || ($filters['from'] ?? '') !== '' || ($filters['to'] ?? '') !== '')
<div class="ml-auto self-center text-xs text-gray-500">共匹配 {{ $bills->total() }} 条</div>
@endif
</div>
</form>
<div class="alibaba-bill-table-wrapper">
<table class="alibaba-bill-table">
<thead><tr><th>店铺名称</th><th>交易时间</th><th>产品类型</th><th class="text-right">入账金额(元)</th><th class="text-right">出帐金额(元)</th></tr></thead>
<tbody class="divide-y divide-gray-100 bg-white">
@forelse ($bills as $bill)
<tr>
<td>{{ $bill['shop_name'] ?? '' }}</td>
<td>{{ $bill['trade_day'] ?? '' }}</td>
<td>{{ $bill['product_type'] ?? '' }}</td>
<td class="alibaba-bill-amount {{ ((float) ($bill['income_amount'] ?? 0)) > 0 ? 'alibaba-bill-income' : '' }}">{{ number_format((float) ($bill['income_amount'] ?? 0), 2) }}</td>
<td class="alibaba-bill-amount {{ ((float) ($bill['expense_amount'] ?? 0)) > 0 ? 'alibaba-bill-expense' : '' }}">{{ number_format((float) ($bill['expense_amount'] ?? 0), 2) }}</td>
</tr>
@empty
<tr><td colspan="5" class="px-5 py-10 text-center text-gray-500">当前筛选条件没有账单数据,请调整店铺、产品类型或交易时间。</td></tr>
@endforelse
</tbody>
</table>
</div>
@unless ($bills->isEmpty())
<div class="mt-5"><x-pagination :items="$bills" /></div>
@endunless
</div>

View File

@ -0,0 +1,76 @@
<?php
namespace Tests\Unit\EcommerceFinance;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
use PHPUnit\Framework\TestCase;
class AlibabaBillRepositoryTest extends TestCase
{
public function test_it_maps_template_columns_and_keeps_yuan_amounts(): void
{
$query = new class('jiansun') extends UpstreamReadOnlyQuery {
public array $calls = [];
public function __construct()
{
}
public function scalar(string $sql, array $bindings = []): mixed
{
$this->calls[] = ['sql' => $sql, 'bindings' => $bindings];
return 1;
}
public function select(string $sql, array $bindings = []): array
{
$this->calls[] = ['sql' => $sql, 'bindings' => $bindings];
return [[
'id' => 9,
'shopName' => '测试店铺',
'tradeDay' => '2026-06-30 12:00:00',
'tradeTypeName' => '全站投放',
'tradeAMTIn' => 12.345,
'tradeAMTOut' => 67.891,
]];
}
public function first(string $sql, array $bindings = []): ?array
{
$this->calls[] = ['sql' => $sql, 'bindings' => $bindings];
return ['income_total' => 12.345, 'expense_total' => 67.891, 'cnt' => 1];
}
};
$factory = new class($query) extends UpstreamReadOnlyQueryFactory {
public function __construct(private readonly UpstreamReadOnlyQuery $query)
{
}
public function forSource(string $source): UpstreamReadOnlyQuery
{
TestCase::assertSame('jiansun', $source);
return $this->query;
}
};
$repository = new AlibabaBillRepository($factory);
$result = $repository->paginate('测试店铺', '全站投放', '2026-06-30', '2026-06-30', 2, 250);
$summary = $repository->summary('测试店铺', '全站投放', '2026-06-30', '2026-06-30');
$this->assertSame(1, $result['total']);
$this->assertSame('测试店铺', $result['rows'][0]['shop_name']);
$this->assertSame(12.35, $result['rows'][0]['income_amount']);
$this->assertSame(67.89, $result['rows'][0]['expense_amount']);
$this->assertSame(-55.54, $summary['net']);
$this->assertStringContainsString('tradeTypeName = ?', $query->calls[0]['sql']);
$this->assertStringContainsString('CAST(src.tradeDay AS date)', $query->calls[0]['sql']);
$this->assertSame(101, $query->calls[1]['bindings'][4]);
$this->assertSame(200, $query->calls[1]['bindings'][5]);
}
}

View File

@ -3,6 +3,7 @@
namespace Tests\Unit\EcommerceFinance;
use Closure;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
use Modules\EcommerceFinance\Repositories\InventoryRepository;
@ -19,6 +20,51 @@ use ReflectionClass;
class JianSunSnapshotRefresherTest extends TestCase
{
public function test_alibaba_filters_are_preserved_during_background_refresh(): void
{
$alibaba = new class extends AlibabaBillRepository {
public array $arguments = [];
public function __construct()
{
}
public function paginate(
?string $shopName = null,
?string $productType = null,
?string $fromDate = null,
?string $toDate = null,
int $page = 1,
int $pageSize = 25,
): array {
$this->arguments = func_get_args();
return ['rows' => [], 'total' => 0];
}
};
$refresher = $this->refresher(alibaba: $alibaba);
$status = $refresher->refresh([
'scope' => 'marketing-account-bills.alibaba.list',
'parameters' => [
'filters' => [
'shop_name' => '测试店铺',
'product_type' => '全站投放',
'from' => '2026-06-30',
'to' => '2026-07-01',
],
'page' => 3,
'limit' => 50,
],
]);
$this->assertSame('fresh', $status);
$this->assertSame(
['测试店铺', '全站投放', '2026-06-30', '2026-07-01', 3, 50],
$alibaba->arguments,
);
}
public function test_pinduoduo_flow_filter_is_preserved_during_background_refresh(): void
{
$pinduoduo = new class extends PinduoduoSpreadBillRepository {
@ -173,6 +219,7 @@ class JianSunSnapshotRefresherTest extends TestCase
private function refresher(
?InventoryRepository $inventory = null,
?AlibabaBillRepository $alibaba = null,
?PinduoduoSpreadBillRepository $pinduoduo = null,
?ProductPriceRepository $productPrices = null,
?RawBoardPayablesRepository $rawBoardPayables = null,
@ -197,6 +244,7 @@ class JianSunSnapshotRefresherTest extends TestCase
$this->placeholder(EdgeBandingRepository::class),
$this->placeholder(GoodsReconciliationRepository::class),
$inventory ?? $this->placeholder(InventoryRepository::class),
$alibaba ?? $this->placeholder(AlibabaBillRepository::class),
$pinduoduo ?? $this->placeholder(PinduoduoSpreadBillRepository::class),
$productPrices ?? $this->placeholder(ProductPriceRepository::class),
$rawBoardPayables ?? $this->placeholder(RawBoardPayablesRepository::class),