接入 1688 营销账户账单 (#12)
根据 Alibaba.xlsx 字段接入 JianSun.dbo.alibaba_bill,提供筛选、汇总、分页、快照刷新和错误状态。
This commit is contained in:
parent
87c1ea80bf
commit
a9fd2cfb37
@ -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();
|
||||
|
||||
138
modules/EcommerceFinance/Repositories/AlibabaBillRepository.php
Normal file
138
modules/EcommerceFinance/Repositories/AlibabaBillRepository.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
|
||||
@ -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>
|
||||
76
tests/Unit/EcommerceFinance/AlibabaBillRepositoryTest.php
Normal file
76
tests/Unit/EcommerceFinance/AlibabaBillRepositoryTest.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user