feat: read marketing bills from business mirror
首次全量完成后,营销账户账单、余额、汇总、选项及分页优先读取 caiwu_business_mirror;未就绪时保留旧快照链路。
This commit is contained in:
parent
bb9a18eddb
commit
43445fde4b
@ -272,18 +272,22 @@ class Pages extends Controller
|
||||
: app(PinduoduoSpreadBillRepository::class);
|
||||
$scope = $isTaobao ? 'taobao-alipay-bills.list' : 'pinduoduo-bills.list';
|
||||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||||
$usingMirror = $repository->usesBusinessMirror();
|
||||
|
||||
try {
|
||||
$result = $cache->remember(
|
||||
$scope,
|
||||
['view' => 'monthly-balances', 'filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||||
fn () => $repository->paginateMonthlyBalances(
|
||||
$filters['store_name'],
|
||||
$filters['month'],
|
||||
$page,
|
||||
$pageSize,
|
||||
),
|
||||
$resolver = fn () => $repository->paginateMonthlyBalances(
|
||||
$filters['store_name'],
|
||||
$filters['month'],
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
$result = $usingMirror
|
||||
? $resolver()
|
||||
: $cache->remember(
|
||||
$scope,
|
||||
['view' => 'monthly-balances', 'filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||||
$resolver,
|
||||
);
|
||||
} catch (UpstreamSnapshotPendingException $exception) {
|
||||
$content['pending'] = true;
|
||||
$result = ['rows' => [], 'total' => 0];
|
||||
@ -297,11 +301,14 @@ class Pages extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$storeOptions = $cache->remember(
|
||||
$isTaobao ? 'taobao-alipay-bills.options' : 'pinduoduo-bills.options',
|
||||
[],
|
||||
fn () => $repository->storeOptions(),
|
||||
);
|
||||
$resolver = fn () => $repository->storeOptions();
|
||||
$storeOptions = $usingMirror
|
||||
? $resolver()
|
||||
: $cache->remember(
|
||||
$isTaobao ? 'taobao-alipay-bills.options' : 'pinduoduo-bills.options',
|
||||
[],
|
||||
$resolver,
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$storeOptions = [];
|
||||
@ -352,11 +359,14 @@ class Pages extends Controller
|
||||
], static fn (mixed $value): bool => $value !== '');
|
||||
|
||||
try {
|
||||
$result = $this->upstreamCaches->forSource('jiansun')->remember(
|
||||
$scope,
|
||||
['view' => 'monthly-balances', 'filters' => $filters, 'page' => 1, 'limit' => 5000, 'export' => true],
|
||||
fn () => $repository->paginateMonthlyBalances($filters['store_name'], $filters['month'], 1, 5000),
|
||||
);
|
||||
$resolver = fn () => $repository->paginateMonthlyBalances($filters['store_name'], $filters['month'], 1, 5000);
|
||||
$result = $repository->usesBusinessMirror()
|
||||
? $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', '首份共享快照正在准备,系统已登记刷新请求;请稍后刷新页面后再导出。');
|
||||
@ -880,28 +890,38 @@ class Pages extends Controller
|
||||
];
|
||||
$repository = app(AlibabaBillRepository::class);
|
||||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||||
$usingMirror = $repository->usesBusinessMirror();
|
||||
|
||||
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,
|
||||
),
|
||||
$listResolver = 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(),
|
||||
$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',
|
||||
@ -966,33 +986,43 @@ class Pages extends Controller
|
||||
'flow_type' => $flowType !== '' ? $flowType : null,
|
||||
];
|
||||
|
||||
$result = $cache->remember(
|
||||
'taobao-alipay-bills.list',
|
||||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||||
fn () => $repository->paginate(
|
||||
$filters['store_name'],
|
||||
$filters['from'],
|
||||
$filters['to'],
|
||||
$filters['flow_type'],
|
||||
$page,
|
||||
$pageSize
|
||||
)
|
||||
$listResolver = fn () => $repository->paginate(
|
||||
$filters['store_name'],
|
||||
$filters['from'],
|
||||
$filters['to'],
|
||||
$filters['flow_type'],
|
||||
$page,
|
||||
$pageSize
|
||||
);
|
||||
$summary = $cache->remember(
|
||||
'taobao-alipay-bills.summary',
|
||||
['filters' => $filters],
|
||||
fn () => $repository->summary(
|
||||
$filters['store_name'],
|
||||
$filters['from'],
|
||||
$filters['to'],
|
||||
$filters['flow_type']
|
||||
)
|
||||
);
|
||||
$storeOptions = $cache->remember(
|
||||
'taobao-alipay-bills.options',
|
||||
[],
|
||||
fn () => $repository->storeOptions()
|
||||
$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']),
|
||||
@ -1041,34 +1071,43 @@ class Pages extends Controller
|
||||
];
|
||||
|
||||
// 页面只读取共享查询快照;冷查询由统一限流和熔断链路访问上游。
|
||||
$result = $cache->remember(
|
||||
'pinduoduo-bills.list',
|
||||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||||
fn () => $repository->paginate(
|
||||
$filters['store_name'],
|
||||
$filters['from'],
|
||||
$filters['to'],
|
||||
$filters['flow_type'],
|
||||
$page,
|
||||
$pageSize
|
||||
)
|
||||
$listResolver = fn () => $repository->paginate(
|
||||
$filters['store_name'],
|
||||
$filters['from'],
|
||||
$filters['to'],
|
||||
$filters['flow_type'],
|
||||
$page,
|
||||
$pageSize
|
||||
);
|
||||
$summary = $cache->remember(
|
||||
'pinduoduo-bills.summary',
|
||||
['filters' => $filters],
|
||||
fn () => $repository->summary(
|
||||
$filters['store_name'],
|
||||
$filters['from'],
|
||||
$filters['to'],
|
||||
$filters['flow_type']
|
||||
)
|
||||
);
|
||||
|
||||
$storeOptions = $cache->remember(
|
||||
'pinduoduo-bills.options',
|
||||
[],
|
||||
fn () => $repository->storeOptions()
|
||||
$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(
|
||||
@ -1114,9 +1153,10 @@ class Pages extends Controller
|
||||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||||
$scopePrefix = 'jd-wallet-bills.' . $ledger;
|
||||
$errors = [];
|
||||
$remember = function (string $scope, array $parameters, callable $resolver, mixed $fallback) use ($cache, &$errors): mixed {
|
||||
$usingMirror = $repository->usesBusinessMirror($ledger);
|
||||
$remember = function (string $scope, array $parameters, callable $resolver, mixed $fallback) use ($cache, $usingMirror, &$errors): mixed {
|
||||
try {
|
||||
return $cache->remember($scope, $parameters, $resolver);
|
||||
return $usingMirror ? $resolver() : $cache->remember($scope, $parameters, $resolver);
|
||||
} catch (ServiceUnavailableHttpException $exception) {
|
||||
$errors[] = $exception->getMessage();
|
||||
} catch (Throwable $exception) {
|
||||
@ -1145,12 +1185,19 @@ class Pages extends Controller
|
||||
: $repository->summaryRecharge($filters['store_name'], $filters['from'], $filters['to']),
|
||||
['amount_total' => 0, 'balance_total' => 0, 'count' => 0],
|
||||
);
|
||||
$shopOptions = $remember(
|
||||
'jd-wallet-bills.options',
|
||||
[],
|
||||
fn () => $repository->shopOptions(),
|
||||
[],
|
||||
);
|
||||
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',
|
||||
|
||||
@ -17,14 +17,25 @@ class AlibabaBillRepository
|
||||
|
||||
private UpstreamReadOnlyQuery $query;
|
||||
|
||||
public function __construct(UpstreamReadOnlyQueryFactory $factory)
|
||||
{
|
||||
public function __construct(
|
||||
UpstreamReadOnlyQueryFactory $factory,
|
||||
private readonly ?MarketingAccountBillMirrorRepository $mirror = null,
|
||||
) {
|
||||
$this->query = $factory->forSource(self::SOURCE);
|
||||
}
|
||||
|
||||
public function usesBusinessMirror(): bool
|
||||
{
|
||||
return $this->mirror?->ready(MarketingAccountBillMirrorRepository::ALIBABA) ?? false;
|
||||
}
|
||||
|
||||
/** @return array{shops: string[], product_types: string[]} */
|
||||
public function filterOptions(): array
|
||||
{
|
||||
if ($this->usesBusinessMirror()) {
|
||||
return $this->mirror->alibabaFilterOptions();
|
||||
}
|
||||
|
||||
$shops = $this->query->select(
|
||||
'SELECT DISTINCT TOP (500) shopName FROM ' . self::TABLE
|
||||
. ' WHERE shopName IS NOT NULL AND LTRIM(RTRIM(shopName)) <> \'\' ORDER BY shopName'
|
||||
@ -51,6 +62,23 @@ class AlibabaBillRepository
|
||||
): array {
|
||||
$page = max(1, $page);
|
||||
$pageSize = min(max(1, $pageSize), 100);
|
||||
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$result = $this->mirror->paginateAlibaba(
|
||||
$shopName,
|
||||
$productType,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
|
||||
return [
|
||||
'rows' => array_map(fn (array $row): array => $this->normalize($row), $result['rows']),
|
||||
'total' => $result['total'],
|
||||
];
|
||||
}
|
||||
|
||||
[$where, $bindings] = $this->where($shopName, $productType, $fromDate, $toDate);
|
||||
$total = (int) $this->query->scalar('SELECT COUNT(*) FROM ' . self::TABLE . ' src' . $where, $bindings);
|
||||
|
||||
@ -82,13 +110,17 @@ class AlibabaBillRepository
|
||||
?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,
|
||||
) ?? [];
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$row = $this->mirror->summarizeAlibaba($shopName, $productType, $fromDate, $toDate);
|
||||
} else {
|
||||
[$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);
|
||||
|
||||
|
||||
@ -19,14 +19,34 @@ class JdWalletBillRepository
|
||||
|
||||
private UpstreamReadOnlyQuery $query;
|
||||
|
||||
public function __construct(UpstreamReadOnlyQueryFactory $factory)
|
||||
{
|
||||
public function __construct(
|
||||
UpstreamReadOnlyQueryFactory $factory,
|
||||
private readonly ?MarketingAccountBillMirrorRepository $mirror = null,
|
||||
) {
|
||||
$this->query = $factory->forSource(self::SOURCE);
|
||||
}
|
||||
|
||||
public function usesBusinessMirror(string $ledger): bool
|
||||
{
|
||||
$dataset = $ledger === 'consumption'
|
||||
? MarketingAccountBillMirrorRepository::JD_CONSUMPTION
|
||||
: MarketingAccountBillMirrorRepository::JD_RECHARGE;
|
||||
|
||||
return $this->mirror?->ready($dataset) ?? false;
|
||||
}
|
||||
|
||||
public function usesBusinessMirrorForOptions(): bool
|
||||
{
|
||||
return $this->usesBusinessMirror('recharge') && $this->usesBusinessMirror('consumption');
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function shopOptions(): array
|
||||
{
|
||||
if ($this->usesBusinessMirrorForOptions()) {
|
||||
return $this->mirror->jdShopOptions();
|
||||
}
|
||||
|
||||
$rows = $this->query->select(
|
||||
"SELECT TOP (500) shopName FROM (SELECT shopName FROM " . self::RECHARGE_TABLE
|
||||
. " UNION SELECT shopName FROM " . self::CONSUMPTION_TABLE
|
||||
@ -47,6 +67,21 @@ class JdWalletBillRepository
|
||||
int $page = 1,
|
||||
int $pageSize = 25,
|
||||
): array {
|
||||
if ($this->usesBusinessMirror('recharge')) {
|
||||
$result = $this->mirror->paginateJdRecharge(
|
||||
$shopName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
|
||||
return [
|
||||
'rows' => array_map(fn (array $row): array => $this->normalizeRecharge($row), $result['rows']),
|
||||
'total' => $result['total'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->paginate(self::RECHARGE_TABLE, $this->rechargeWhere($shopName, $fromDate, $toDate), $page, $pageSize, [
|
||||
'id', 'shopName', 'amount', 'balanceAmount', 'orderTypeName', 'pk',
|
||||
'createDate', 'updateTime', 'statusName', 'remark',
|
||||
@ -61,6 +96,21 @@ class JdWalletBillRepository
|
||||
int $page = 1,
|
||||
int $pageSize = 25,
|
||||
): array {
|
||||
if ($this->usesBusinessMirror('consumption')) {
|
||||
$result = $this->mirror->paginateJdConsumption(
|
||||
$shopName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
|
||||
return [
|
||||
'rows' => array_map(fn (array $row): array => $this->normalizeConsumption($row), $result['rows']),
|
||||
'total' => $result['total'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->paginate(self::CONSUMPTION_TABLE, $this->consumptionWhere($shopName, $fromDate, $toDate), $page, $pageSize, [
|
||||
'id', 'shopName', 'amount', 'balanceAmount', 'orderTypeName', 'creatTime', 'settleTime', 'pk',
|
||||
'invoiceApplyTypeName',
|
||||
@ -70,12 +120,32 @@ class JdWalletBillRepository
|
||||
/** @return array{amount_total: float, balance_total: float, count: int} */
|
||||
public function summaryRecharge(?string $shopName = null, ?string $fromDate = null, ?string $toDate = null): array
|
||||
{
|
||||
if ($this->usesBusinessMirror('recharge')) {
|
||||
return $this->normalizeSummary($this->mirror->summarizeJd(
|
||||
MarketingAccountBillMirrorRepository::JD_RECHARGE,
|
||||
'createDate',
|
||||
$shopName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
));
|
||||
}
|
||||
|
||||
return $this->summary(self::RECHARGE_TABLE, $this->rechargeWhere($shopName, $fromDate, $toDate));
|
||||
}
|
||||
|
||||
/** @return array{amount_total: float, balance_total: float, count: int} */
|
||||
public function summaryConsumption(?string $shopName = null, ?string $fromDate = null, ?string $toDate = null): array
|
||||
{
|
||||
if ($this->usesBusinessMirror('consumption')) {
|
||||
return $this->normalizeSummary($this->mirror->summarizeJd(
|
||||
MarketingAccountBillMirrorRepository::JD_CONSUMPTION,
|
||||
'creatTime',
|
||||
$shopName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
));
|
||||
}
|
||||
|
||||
return $this->summary(self::CONSUMPTION_TABLE, $this->consumptionWhere($shopName, $fromDate, $toDate));
|
||||
}
|
||||
|
||||
@ -111,6 +181,12 @@ class JdWalletBillRepository
|
||||
$where['bindings'],
|
||||
) ?? [];
|
||||
|
||||
return $this->normalizeSummary($row);
|
||||
}
|
||||
|
||||
/** @return array{amount_total: float, balance_total: float, count: int} */
|
||||
private function normalizeSummary(array $row): array
|
||||
{
|
||||
return [
|
||||
'amount_total' => round((float) ($row['amount_total'] ?? 0), 2),
|
||||
'balance_total' => round((float) ($row['balance_total'] ?? 0), 2),
|
||||
|
||||
@ -0,0 +1,559 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 营销账户账单的完整业务镜像读取层。
|
||||
*
|
||||
* 这里只返回与旧 SQL Server Repository 相同的原始字段;金额换算和业务名称
|
||||
* 仍由原 Repository 负责,确保镜像切换不会改变页面口径。
|
||||
*/
|
||||
class MarketingAccountBillMirrorRepository
|
||||
{
|
||||
public const TAOBAO = 'jiansun.dbo.tmall_spread_bills';
|
||||
public const PINDUODUO = 'jiansun.dbo.pdd_spread_bills';
|
||||
public const ALIBABA = 'jiansun.dbo.alibaba_bill';
|
||||
public const JD_RECHARGE = 'jiansun.dbo.jd_spreadBills';
|
||||
public const JD_CONSUMPTION = 'jiansun.dbo.jd_costBills';
|
||||
|
||||
private readonly ConnectionInterface $connection;
|
||||
|
||||
/** @var array<string,bool> */
|
||||
private array $readiness = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly BusinessMirrorDatasetRegistry $datasets,
|
||||
private readonly GenericBusinessMirrorRepository $mirror,
|
||||
) {
|
||||
$this->connection = $mirror->connection();
|
||||
}
|
||||
|
||||
public function ready(string $datasetKey): bool
|
||||
{
|
||||
if (array_key_exists($datasetKey, $this->readiness)) {
|
||||
return $this->readiness[$datasetKey];
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->readiness[$datasetKey] = $this->mirror->ready($this->dataset($datasetKey));
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return $this->readiness[$datasetKey] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function taobaoStoreOptions(): array
|
||||
{
|
||||
return $this->stringOptions(self::TAOBAO, 'shopName');
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginateTaobao(
|
||||
?string $storeName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
?string $flowType,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
$query = $this->table(self::TAOBAO, 'src');
|
||||
$this->applyTaobaoFilters($query, $storeName, $fromDate, $toDate, $flowType);
|
||||
|
||||
return $this->paginate($query, [
|
||||
'src.id', 'src.shopName', 'src.uniTransTime', 'src.transTime', 'src.amount',
|
||||
'src.balance', 'src.comment', 'src.finType', 'src.bizType', 'src.tradeType',
|
||||
], ['src.uniTransTime', 'src.id'], $page, min(max($pageSize, 1), 200));
|
||||
}
|
||||
|
||||
/** @return array{income_total_raw:float,expense_total_raw:float,cnt:int} */
|
||||
public function summarizeTaobao(
|
||||
?string $storeName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
?string $flowType,
|
||||
): array {
|
||||
$query = $this->table(self::TAOBAO, 'src');
|
||||
$this->applyTaobaoFilters($query, $storeName, $fromDate, $toDate, $flowType);
|
||||
$row = $query->selectRaw(
|
||||
"COALESCE(SUM(CASE WHEN src.finType = 'DEBIT' THEN src.amount ELSE 0 END), 0) AS income_total_raw, "
|
||||
. "COALESCE(SUM(CASE WHEN src.finType = 'CREDIT' THEN src.amount ELSE 0 END), 0) AS expense_total_raw, "
|
||||
. 'COUNT(*) AS cnt'
|
||||
)->first();
|
||||
|
||||
return [
|
||||
'income_total_raw' => (float) ($row->income_total_raw ?? 0),
|
||||
'expense_total_raw' => (float) ($row->expense_total_raw ?? 0),
|
||||
'cnt' => (int) ($row->cnt ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginateTaobaoMonthlyBalances(
|
||||
?string $storeName,
|
||||
?string $month,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
return $this->monthlyBalances(
|
||||
self::TAOBAO,
|
||||
'transTime',
|
||||
"CASE WHEN src.finType = 'DEBIT' THEN src.amount ELSE 0 END",
|
||||
"CASE WHEN src.finType = 'CREDIT' THEN src.amount ELSE 0 END",
|
||||
$storeName,
|
||||
$month,
|
||||
$page,
|
||||
min(max($pageSize, 1), 5000),
|
||||
'opening_balance_raw',
|
||||
'income_total_raw',
|
||||
'expense_total_raw',
|
||||
'system_closing_balance_raw',
|
||||
);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function pinduoduoStoreOptions(): array
|
||||
{
|
||||
return $this->stringOptions(self::PINDUODUO, 'shopName');
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginatePinduoduo(
|
||||
?string $storeName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
?string $flowType,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
$query = $this->table(self::PINDUODUO, 'src');
|
||||
$this->applyPinduoduoFilters($query, $storeName, $fromDate, $toDate, $flowType);
|
||||
|
||||
return $this->paginate($query, [
|
||||
'src.id', 'src.shopName', 'src.amount', 'src.balance', 'src.summary',
|
||||
'src.flowType', 'src.toAdvertiserFundType', 'src.createTime',
|
||||
], ['src.createTime', 'src.id'], $page, min(max($pageSize, 1), 200));
|
||||
}
|
||||
|
||||
/** @return array{income_total:float,expense_total:float,cnt:int} */
|
||||
public function summarizePinduoduo(
|
||||
?string $storeName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
?string $flowType,
|
||||
): array {
|
||||
$query = $this->table(self::PINDUODUO, 'src');
|
||||
$this->applyPinduoduoFilters($query, $storeName, $fromDate, $toDate, $flowType);
|
||||
$row = $query->selectRaw(
|
||||
'COALESCE(SUM(CASE WHEN src.flowType = 1 THEN src.amount ELSE 0 END), 0) AS income_total, '
|
||||
. 'COALESCE(SUM(CASE WHEN src.flowType = 2 THEN src.amount ELSE 0 END), 0) AS expense_total, '
|
||||
. 'COUNT(*) AS cnt'
|
||||
)->first();
|
||||
|
||||
return [
|
||||
'income_total' => (float) ($row->income_total ?? 0),
|
||||
'expense_total' => (float) ($row->expense_total ?? 0),
|
||||
'cnt' => (int) ($row->cnt ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginatePinduoduoMonthlyBalances(
|
||||
?string $storeName,
|
||||
?string $month,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
return $this->monthlyBalances(
|
||||
self::PINDUODUO,
|
||||
'createTime',
|
||||
'CASE WHEN src.flowType = 1 THEN src.amount ELSE 0 END',
|
||||
'CASE WHEN src.flowType = 2 THEN src.amount ELSE 0 END',
|
||||
$storeName,
|
||||
$month,
|
||||
$page,
|
||||
min(max($pageSize, 1), 5000),
|
||||
'opening_balance',
|
||||
'income_total',
|
||||
'expense_total',
|
||||
'system_closing_balance',
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array{shops:string[],product_types:string[]} */
|
||||
public function alibabaFilterOptions(): array
|
||||
{
|
||||
return [
|
||||
'shops' => $this->stringOptions(self::ALIBABA, 'shopName', 500),
|
||||
'product_types' => $this->stringOptions(self::ALIBABA, 'tradeTypeName', 500),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginateAlibaba(
|
||||
?string $shopName,
|
||||
?string $productType,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
$query = $this->table(self::ALIBABA, 'src');
|
||||
$this->applyAlibabaFilters($query, $shopName, $productType, $fromDate, $toDate);
|
||||
|
||||
return $this->paginate($query, [
|
||||
'src.id', 'src.shopName', 'src.tradeDay', 'src.tradeTypeName',
|
||||
'src.tradeAMTIn', 'src.tradeAMTOut',
|
||||
], ['src.tradeDay', 'src.id'], $page, min(max($pageSize, 1), 100));
|
||||
}
|
||||
|
||||
/** @return array{income_total:float,expense_total:float,cnt:int} */
|
||||
public function summarizeAlibaba(
|
||||
?string $shopName,
|
||||
?string $productType,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
): array {
|
||||
$query = $this->table(self::ALIBABA, 'src');
|
||||
$this->applyAlibabaFilters($query, $shopName, $productType, $fromDate, $toDate);
|
||||
$row = $query->selectRaw(
|
||||
'COALESCE(SUM(src.tradeAMTIn), 0) AS income_total, '
|
||||
. 'COALESCE(SUM(src.tradeAMTOut), 0) AS expense_total, COUNT(*) AS cnt'
|
||||
)->first();
|
||||
|
||||
return [
|
||||
'income_total' => (float) ($row->income_total ?? 0),
|
||||
'expense_total' => (float) ($row->expense_total ?? 0),
|
||||
'cnt' => (int) ($row->cnt ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function jdShopOptions(): array
|
||||
{
|
||||
$shops = array_merge(
|
||||
$this->stringOptions(self::JD_RECHARGE, 'shopName', 500),
|
||||
$this->stringOptions(self::JD_CONSUMPTION, 'shopName', 500),
|
||||
);
|
||||
$shops = array_values(array_unique($shops));
|
||||
sort($shops, SORT_NATURAL);
|
||||
|
||||
return array_slice($shops, 0, 500);
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginateJdRecharge(
|
||||
?string $shopName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
return $this->paginateJd(
|
||||
self::JD_RECHARGE,
|
||||
'createDate',
|
||||
['src.id', 'src.shopName', 'src.amount', 'src.balanceAmount', 'src.orderTypeName',
|
||||
'src.pk', 'src.createDate', 'src.updateTime', 'src.statusName', 'src.remark'],
|
||||
$shopName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
public function paginateJdConsumption(
|
||||
?string $shopName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
return $this->paginateJd(
|
||||
self::JD_CONSUMPTION,
|
||||
'creatTime',
|
||||
['src.id', 'src.shopName', 'src.amount', 'src.balanceAmount', 'src.orderTypeName',
|
||||
'src.creatTime', 'src.settleTime', 'src.pk', 'src.invoiceApplyTypeName'],
|
||||
$shopName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
/** @return array{amount_total:float,balance_total:float,cnt:int} */
|
||||
public function summarizeJd(
|
||||
string $datasetKey,
|
||||
string $dateField,
|
||||
?string $shopName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
): array {
|
||||
$query = $this->table($datasetKey, 'src');
|
||||
$this->applyJdFilters($query, $dateField, $shopName, $fromDate, $toDate);
|
||||
$row = $query->selectRaw(
|
||||
'COALESCE(SUM(ABS(src.amount)), 0) AS amount_total, '
|
||||
. 'COALESCE(SUM(src.balanceAmount), 0) AS balance_total, COUNT(*) AS cnt'
|
||||
)->first();
|
||||
|
||||
return [
|
||||
'amount_total' => (float) ($row->amount_total ?? 0),
|
||||
'balance_total' => (float) ($row->balance_total ?? 0),
|
||||
'cnt' => (int) ($row->cnt ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
private function paginateJd(
|
||||
string $datasetKey,
|
||||
string $dateField,
|
||||
array $fields,
|
||||
?string $shopName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
): array {
|
||||
$query = $this->table($datasetKey, 'src');
|
||||
$this->applyJdFilters($query, $dateField, $shopName, $fromDate, $toDate);
|
||||
|
||||
return $this->paginate(
|
||||
$query,
|
||||
$fields,
|
||||
['src.' . $dateField, 'src.id'],
|
||||
$page,
|
||||
min(max($pageSize, 1), 100),
|
||||
);
|
||||
}
|
||||
|
||||
private function applyTaobaoFilters(
|
||||
Builder $query,
|
||||
?string $storeName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
?string $flowType,
|
||||
): void {
|
||||
$this->whereValue($query, 'src.shopName', $storeName);
|
||||
$this->whereFromDate($query, 'src.transTime', $fromDate);
|
||||
$this->whereToDate($query, 'src.transTime', $toDate);
|
||||
if ($flowType === 'income') {
|
||||
$query->where('src.finType', 'DEBIT');
|
||||
} elseif ($flowType === 'expense') {
|
||||
$query->where('src.finType', 'CREDIT');
|
||||
}
|
||||
}
|
||||
|
||||
private function applyPinduoduoFilters(
|
||||
Builder $query,
|
||||
?string $storeName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
?string $flowType,
|
||||
): void {
|
||||
$this->whereValue($query, 'src.shopName', $storeName);
|
||||
$this->whereFromDate($query, 'src.createTime', $fromDate, true);
|
||||
$this->whereToDate($query, 'src.createTime', $toDate);
|
||||
if ($flowType === 'income') {
|
||||
$query->where('src.flowType', 1);
|
||||
} elseif ($flowType === 'expense') {
|
||||
$query->where('src.flowType', 2);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyAlibabaFilters(
|
||||
Builder $query,
|
||||
?string $shopName,
|
||||
?string $productType,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
): void {
|
||||
$this->whereValue($query, 'src.shopName', $shopName);
|
||||
$this->whereValue($query, 'src.tradeTypeName', $productType);
|
||||
if ($this->validDate($fromDate)) {
|
||||
$query->whereDate('src.tradeDay', '>=', $fromDate);
|
||||
}
|
||||
if ($this->validDate($toDate)) {
|
||||
$query->whereDate('src.tradeDay', '<=', $toDate);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyJdFilters(
|
||||
Builder $query,
|
||||
string $dateField,
|
||||
?string $shopName,
|
||||
?string $fromDate,
|
||||
?string $toDate,
|
||||
): void {
|
||||
$this->whereValue($query, 'src.shopName', $shopName);
|
||||
$this->whereFromDate($query, 'src.' . $dateField, $fromDate, true);
|
||||
$this->whereToDate($query, 'src.' . $dateField, $toDate);
|
||||
}
|
||||
|
||||
private function whereValue(Builder $query, string $column, ?string $value): void
|
||||
{
|
||||
if ($value !== null && $value !== '') {
|
||||
$query->where($column, $value);
|
||||
}
|
||||
}
|
||||
|
||||
private function whereFromDate(Builder $query, string $column, ?string $date, bool $midnight = false): void
|
||||
{
|
||||
if ($this->validDate($date)) {
|
||||
$query->where($column, '>=', $date . ($midnight ? ' 00:00:00' : ''));
|
||||
}
|
||||
}
|
||||
|
||||
private function whereToDate(Builder $query, string $column, ?string $date): void
|
||||
{
|
||||
if ($this->validDate($date)) {
|
||||
$query->where($column, '<', date('Y-m-d', strtotime($date . ' +1 day')) . ' 00:00:00');
|
||||
}
|
||||
}
|
||||
|
||||
private function validDate(?string $date): bool
|
||||
{
|
||||
return $date !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
private function stringOptions(string $datasetKey, string $column, ?int $limit = null): array
|
||||
{
|
||||
$query = $this->table($datasetKey)
|
||||
->whereNotNull($column)
|
||||
->where($column, '<>', '')
|
||||
->distinct()
|
||||
->orderBy($column);
|
||||
if ($limit !== null) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
return $query->pluck($column)->map(static fn (mixed $value): string => (string) $value)->all();
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
private function paginate(Builder $query, array $fields, array $orderColumns, int $page, int $pageSize): array
|
||||
{
|
||||
$page = max($page, 1);
|
||||
$total = (clone $query)->count();
|
||||
if ($total === 0) {
|
||||
return ['rows' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
foreach ($orderColumns as $column) {
|
||||
$query->orderByDesc($column);
|
||||
}
|
||||
$rows = $query->select($fields)
|
||||
->forPage($page, $pageSize)
|
||||
->get()
|
||||
->map(static fn (object $row): array => (array) $row)
|
||||
->all();
|
||||
|
||||
return ['rows' => $rows, 'total' => $total];
|
||||
}
|
||||
|
||||
/** @return array{rows:array<int,array<string,mixed>>,total:int} */
|
||||
private function monthlyBalances(
|
||||
string $datasetKey,
|
||||
string $dateField,
|
||||
string $incomeExpression,
|
||||
string $expenseExpression,
|
||||
?string $storeName,
|
||||
?string $month,
|
||||
int $page,
|
||||
int $pageSize,
|
||||
string $openingAlias,
|
||||
string $incomeAlias,
|
||||
string $expenseAlias,
|
||||
string $closingAlias,
|
||||
): array {
|
||||
$page = max($page, 1);
|
||||
$driver = $this->connection->getDriverName();
|
||||
$monthStart = $driver === 'sqlite'
|
||||
? "date(src.$dateField, 'start of month')"
|
||||
: "DATE_FORMAT(src.$dateField, '%Y-%m-01')";
|
||||
$currentMonth = $driver === 'sqlite'
|
||||
? "date('now', 'start of month')"
|
||||
: "DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')";
|
||||
$nextMonth = $driver === 'sqlite'
|
||||
? "date(m.month_start, '+1 month')"
|
||||
: 'DATE_ADD(m.month_start, INTERVAL 1 MONTH)';
|
||||
$lastDay = $driver === 'sqlite'
|
||||
? "date(m.month_start, '+1 month', '-1 day')"
|
||||
: "DATE_FORMAT(LAST_DAY(m.month_start), '%Y-%m-%d')";
|
||||
$table = $this->dataset($datasetKey)->targetTable();
|
||||
|
||||
$groups = $this->connection->table($table . ' AS src')
|
||||
->whereNotNull('src.shopName')
|
||||
->whereNotNull('src.' . $dateField)
|
||||
->whereRaw("src.$dateField < $currentMonth")
|
||||
->when($storeName !== null && $storeName !== '', fn (Builder $query) => $query->where('src.shopName', $storeName))
|
||||
->when($month !== null && preg_match('/^\d{4}-\d{2}$/', $month) === 1, function (Builder $query) use ($dateField, $month): void {
|
||||
$start = $month . '-01';
|
||||
$end = date('Y-m-d', strtotime($start . ' +1 month'));
|
||||
$query->where('src.' . $dateField, '>=', $start)->where('src.' . $dateField, '<', $end);
|
||||
})
|
||||
->selectRaw("src.shopName, $monthStart AS month_start, COALESCE(SUM($incomeExpression), 0) AS $incomeAlias, COALESCE(SUM($expenseExpression), 0) AS $expenseAlias")
|
||||
->groupBy('src.shopName')
|
||||
->groupByRaw($monthStart);
|
||||
|
||||
$total = $this->connection->query()->fromSub(clone $groups, 'monthly_count')->count();
|
||||
if ($total === 0) {
|
||||
return ['rows' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
$query = $this->connection->query()->fromSub($groups, 'm')
|
||||
->selectRaw("m.shopName, $lastDay AS balance_date, m.$incomeAlias, m.$expenseAlias")
|
||||
->selectSub(function (Builder $query) use ($table, $dateField): void {
|
||||
$query->from($table . ' AS opening_entry')
|
||||
->select('opening_entry.balance')
|
||||
->whereColumn('opening_entry.shopName', 'm.shopName')
|
||||
->whereColumn('opening_entry.' . $dateField, '<', 'm.month_start')
|
||||
->orderByDesc('opening_entry.' . $dateField)
|
||||
->orderByDesc('opening_entry.id')
|
||||
->limit(1);
|
||||
}, $openingAlias)
|
||||
->selectSub(function (Builder $query) use ($table, $dateField, $nextMonth): void {
|
||||
$query->from($table . ' AS closing_entry')
|
||||
->select('closing_entry.balance')
|
||||
->whereColumn('closing_entry.shopName', 'm.shopName')
|
||||
->whereRaw("closing_entry.$dateField < $nextMonth")
|
||||
->orderByDesc('closing_entry.' . $dateField)
|
||||
->orderByDesc('closing_entry.id')
|
||||
->limit(1);
|
||||
}, $closingAlias)
|
||||
->orderByDesc('m.month_start')
|
||||
->orderBy('m.shopName')
|
||||
->forPage($page, $pageSize);
|
||||
|
||||
return [
|
||||
'rows' => $query->get()->map(static fn (object $row): array => (array) $row)->all(),
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
private function table(string $datasetKey, ?string $alias = null): Builder
|
||||
{
|
||||
if (! $this->ready($datasetKey)) {
|
||||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||||
}
|
||||
|
||||
$table = $this->dataset($datasetKey)->targetTable();
|
||||
|
||||
return $this->connection->table($alias === null ? $table : $table . ' AS ' . $alias);
|
||||
}
|
||||
|
||||
private function dataset(string $datasetKey): BusinessMirrorDataset
|
||||
{
|
||||
return $this->datasets->get($datasetKey);
|
||||
}
|
||||
}
|
||||
@ -41,11 +41,18 @@ class PinduoduoSpreadBillRepository
|
||||
|
||||
private UpstreamReadOnlyQuery $query;
|
||||
|
||||
public function __construct(UpstreamReadOnlyQueryFactory $factory)
|
||||
{
|
||||
public function __construct(
|
||||
UpstreamReadOnlyQueryFactory $factory,
|
||||
private readonly ?MarketingAccountBillMirrorRepository $mirror = null,
|
||||
) {
|
||||
$this->query = $factory->forSource(self::SOURCE);
|
||||
}
|
||||
|
||||
public function usesBusinessMirror(): bool
|
||||
{
|
||||
return $this->mirror?->ready(MarketingAccountBillMirrorRepository::PINDUODUO) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺下拉选项(全部去重)
|
||||
*
|
||||
@ -53,6 +60,10 @@ class PinduoduoSpreadBillRepository
|
||||
*/
|
||||
public function storeOptions(): array
|
||||
{
|
||||
if ($this->usesBusinessMirror()) {
|
||||
return $this->mirror->pinduoduoStoreOptions();
|
||||
}
|
||||
|
||||
$rows = $this->query->select(
|
||||
'SELECT DISTINCT shopName FROM ' . self::TABLE . ' WHERE shopName IS NOT NULL ORDER BY shopName'
|
||||
);
|
||||
@ -79,6 +90,22 @@ class PinduoduoSpreadBillRepository
|
||||
$page = max(1, $page);
|
||||
$pageSize = max(1, min(200, $pageSize));
|
||||
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$result = $this->mirror->paginatePinduoduo(
|
||||
$storeName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$flowType,
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
|
||||
return [
|
||||
'rows' => array_map(fn (array $row): array => $this->normalize($row), $result['rows']),
|
||||
'total' => $result['total'],
|
||||
];
|
||||
}
|
||||
|
||||
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
|
||||
|
||||
// 总数
|
||||
@ -125,16 +152,20 @@ class PinduoduoSpreadBillRepository
|
||||
?string $toDate = null,
|
||||
?string $flowType = null
|
||||
): array {
|
||||
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$row = $this->mirror->summarizePinduoduo($storeName, $fromDate, $toDate, $flowType);
|
||||
} else {
|
||||
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
|
||||
|
||||
$row = $this->query->first(
|
||||
'SELECT '
|
||||
. 'COALESCE(SUM(CASE WHEN flowType = 1 THEN amount ELSE 0 END), 0) AS income_total, '
|
||||
. 'COALESCE(SUM(CASE WHEN flowType = 2 THEN amount ELSE 0 END), 0) AS expense_total, '
|
||||
. 'COUNT(*) AS cnt '
|
||||
. 'FROM ' . self::TABLE . $whereSql,
|
||||
$params
|
||||
);
|
||||
$row = $this->query->first(
|
||||
'SELECT '
|
||||
. 'COALESCE(SUM(CASE WHEN flowType = 1 THEN amount ELSE 0 END), 0) AS income_total, '
|
||||
. 'COALESCE(SUM(CASE WHEN flowType = 2 THEN amount ELSE 0 END), 0) AS expense_total, '
|
||||
. 'COUNT(*) AS cnt '
|
||||
. 'FROM ' . self::TABLE . $whereSql,
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
$income = (float) ($row['income_total'] ?? 0);
|
||||
$expense = (float) ($row['expense_total'] ?? 0);
|
||||
@ -156,6 +187,16 @@ class PinduoduoSpreadBillRepository
|
||||
): array {
|
||||
$page = max(1, $page);
|
||||
$pageSize = max(1, min(5000, $pageSize));
|
||||
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$result = $this->mirror->paginatePinduoduoMonthlyBalances($storeName, $month, $page, $pageSize);
|
||||
|
||||
return [
|
||||
'rows' => array_map(fn (array $row): array => $this->normalizeMonthlyBalance($row), $result['rows']),
|
||||
'total' => $result['total'],
|
||||
];
|
||||
}
|
||||
|
||||
$monthExpression = 'DATEADD(MONTH, DATEDIFF(MONTH, 0, createTime), 0)';
|
||||
[$whereSql, $params] = $this->buildMonthlyWhere($storeName, $month);
|
||||
|
||||
|
||||
@ -58,11 +58,18 @@ class TmallTaobaoSpreadBillRepository
|
||||
|
||||
private UpstreamReadOnlyQuery $query;
|
||||
|
||||
public function __construct(UpstreamReadOnlyQueryFactory $factory)
|
||||
{
|
||||
public function __construct(
|
||||
UpstreamReadOnlyQueryFactory $factory,
|
||||
private readonly ?MarketingAccountBillMirrorRepository $mirror = null,
|
||||
) {
|
||||
$this->query = $factory->forSource(self::SOURCE);
|
||||
}
|
||||
|
||||
public function usesBusinessMirror(): bool
|
||||
{
|
||||
return $this->mirror?->ready(MarketingAccountBillMirrorRepository::TAOBAO) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺下拉选项(全部去重)
|
||||
*
|
||||
@ -70,6 +77,10 @@ class TmallTaobaoSpreadBillRepository
|
||||
*/
|
||||
public function storeOptions(): array
|
||||
{
|
||||
if ($this->usesBusinessMirror()) {
|
||||
return $this->mirror->taobaoStoreOptions();
|
||||
}
|
||||
|
||||
$rows = $this->query->select(
|
||||
'SELECT DISTINCT shopName FROM ' . self::TABLE . ' WHERE shopName IS NOT NULL ORDER BY shopName'
|
||||
);
|
||||
@ -96,6 +107,22 @@ class TmallTaobaoSpreadBillRepository
|
||||
$page = max(1, $page);
|
||||
$pageSize = max(1, min(200, $pageSize));
|
||||
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$result = $this->mirror->paginateTaobao(
|
||||
$storeName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$flowType,
|
||||
$page,
|
||||
$pageSize,
|
||||
);
|
||||
|
||||
return [
|
||||
'rows' => array_map(fn (array $row): array => $this->normalize($row), $result['rows']),
|
||||
'total' => $result['total'],
|
||||
];
|
||||
}
|
||||
|
||||
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
|
||||
|
||||
// 总数
|
||||
@ -143,17 +170,21 @@ class TmallTaobaoSpreadBillRepository
|
||||
?string $toDate = null,
|
||||
?string $flowType = null
|
||||
): array {
|
||||
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$row = $this->mirror->summarizeTaobao($storeName, $fromDate, $toDate, $flowType);
|
||||
} else {
|
||||
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
|
||||
|
||||
// amount 单位为「分」,汇总时除以 100 转为「元」
|
||||
$row = $this->query->first(
|
||||
'SELECT '
|
||||
. 'COALESCE(SUM(CASE WHEN finType = \'DEBIT\' THEN amount ELSE 0 END), 0) AS income_total_raw, '
|
||||
. 'COALESCE(SUM(CASE WHEN finType = \'CREDIT\' THEN amount ELSE 0 END), 0) AS expense_total_raw, '
|
||||
. 'COUNT(*) AS cnt '
|
||||
. 'FROM ' . self::TABLE . $whereSql,
|
||||
$params
|
||||
);
|
||||
// amount 单位为「分」,汇总时除以 100 转为「元」
|
||||
$row = $this->query->first(
|
||||
'SELECT '
|
||||
. 'COALESCE(SUM(CASE WHEN finType = \'DEBIT\' THEN amount ELSE 0 END), 0) AS income_total_raw, '
|
||||
. 'COALESCE(SUM(CASE WHEN finType = \'CREDIT\' THEN amount ELSE 0 END), 0) AS expense_total_raw, '
|
||||
. 'COUNT(*) AS cnt '
|
||||
. 'FROM ' . self::TABLE . $whereSql,
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
// 分 -> 元
|
||||
$income = ((float) ($row['income_total_raw'] ?? 0)) / 100;
|
||||
@ -176,6 +207,16 @@ class TmallTaobaoSpreadBillRepository
|
||||
): array {
|
||||
$page = max(1, $page);
|
||||
$pageSize = max(1, min(5000, $pageSize));
|
||||
|
||||
if ($this->usesBusinessMirror()) {
|
||||
$result = $this->mirror->paginateTaobaoMonthlyBalances($storeName, $month, $page, $pageSize);
|
||||
|
||||
return [
|
||||
'rows' => array_map(fn (array $row): array => $this->normalizeMonthlyBalance($row), $result['rows']),
|
||||
'total' => $result['total'],
|
||||
];
|
||||
}
|
||||
|
||||
$monthExpression = 'DATEADD(MONTH, DATEDIFF(MONTH, 0, transTime), 0)';
|
||||
[$where, $params] = $this->buildMonthlyWhere($storeName, $month);
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\MarketingAccountBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
@ -15,10 +16,20 @@ use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$mirror = Mockery::mock(MarketingAccountBillMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->byDefault()->andReturnFalse();
|
||||
$this->app->instance(MarketingAccountBillMirrorRepository::class, $mirror);
|
||||
}
|
||||
|
||||
public function testMarketingAccountBalanceRedirectsToTaobaoAndShowsRestoredControls(): void
|
||||
{
|
||||
$this->allowMarketingAccountBalancePage();
|
||||
$repository = Mockery::mock(PinduoduoSpreadBillRepository::class);
|
||||
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
|
||||
$this->app->instance(PinduoduoSpreadBillRepository::class, $repository);
|
||||
$cache = Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldReceive('remember')->twice()->andReturnUsing(
|
||||
@ -141,6 +152,10 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
$this->app->instance(JdWalletBillRepository::class, Mockery::mock(JdWalletBillRepository::class));
|
||||
$this->app->make(JdWalletBillRepository::class)
|
||||
->shouldReceive('usesBusinessMirror')->once()->with('recharge')->andReturnFalse();
|
||||
$this->app->make(JdWalletBillRepository::class)
|
||||
->shouldReceive('usesBusinessMirrorForOptions')->once()->andReturnFalse();
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
@ -177,7 +192,9 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
->andReturn($cache);
|
||||
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, Mockery::mock(TmallTaobaoSpreadBillRepository::class));
|
||||
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
|
||||
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.export', [
|
||||
@ -214,7 +231,9 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
|
||||
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, Mockery::mock(TmallTaobaoSpreadBillRepository::class));
|
||||
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
|
||||
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
@ -239,7 +258,9 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
|
||||
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, Mockery::mock(TmallTaobaoSpreadBillRepository::class));
|
||||
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
|
||||
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.export', [
|
||||
@ -255,6 +276,88 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
->assertSessionHas('error', '余额数据导出暂时无法完成,请稍后重试。');
|
||||
}
|
||||
|
||||
public function testMarketingBillSecondPageReadsReadyMirrorWithoutRegisteringAColdSnapshot(): void
|
||||
{
|
||||
$this->allowMarketingAccountBillsPage();
|
||||
|
||||
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
|
||||
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('paginate')
|
||||
->once()
|
||||
->with(null, null, null, null, 2, 25)
|
||||
->andReturn(['rows' => [['store_name' => '镜像店铺']], 'total' => 26]);
|
||||
$repository->shouldReceive('summary')->once()->andReturn([
|
||||
'income_total' => 0,
|
||||
'expense_total' => 0,
|
||||
'balance' => 0,
|
||||
'count' => 26,
|
||||
]);
|
||||
$repository->shouldReceive('storeOptions')->once()->andReturn(['镜像店铺']);
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
|
||||
|
||||
$cache = Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldNotReceive('remember');
|
||||
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
|
||||
$this->loginAs();
|
||||
$url = route('business.pages.show', [
|
||||
'section' => 'billing-transactions',
|
||||
'page' => 'marketing-account-bills',
|
||||
'tab' => 'taobao-alipay',
|
||||
]) . '?page=2';
|
||||
|
||||
$this->get($url)
|
||||
->assertOk()
|
||||
->assertSeeText('镜像店铺')
|
||||
->assertDontSeeText('首份共享快照正在准备');
|
||||
}
|
||||
|
||||
public function testMarketingBalanceSecondPageReadsReadyMirrorWithoutRegisteringAColdSnapshot(): void
|
||||
{
|
||||
$this->allowMarketingAccountBalancePage();
|
||||
|
||||
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
|
||||
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('paginateMonthlyBalances')
|
||||
->once()
|
||||
->with(null, null, 2, 25)
|
||||
->andReturn(['rows' => [[
|
||||
'date' => '2026-08',
|
||||
'store_name' => '镜像余额店铺',
|
||||
'opening_balance' => 100,
|
||||
'income_total' => 20,
|
||||
'expense_total' => 5,
|
||||
'closing_balance' => 115,
|
||||
'system_closing_balance' => 115,
|
||||
'balance_difference' => 0,
|
||||
'difference_reason' => '',
|
||||
'verification_status' => '一致',
|
||||
'notes' => '',
|
||||
]], 'total' => 26]);
|
||||
$repository->shouldReceive('storeOptions')->once()->andReturn(['镜像余额店铺']);
|
||||
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
|
||||
|
||||
$cache = Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldNotReceive('remember');
|
||||
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
|
||||
$this->loginAs();
|
||||
$url = route('business.pages.show', [
|
||||
'section' => 'billing-transactions',
|
||||
'page' => 'marketing-account-balance',
|
||||
'tab' => 'taobao-alipay',
|
||||
]) . '?page=2';
|
||||
|
||||
$this->get($url)
|
||||
->assertOk()
|
||||
->assertSeeText('镜像余额店铺')
|
||||
->assertDontSeeText('后台正在准备首份共享数据');
|
||||
}
|
||||
|
||||
private function allowMarketingAccountBillsPage(): void
|
||||
{
|
||||
$this->user->update(['locale' => 'zh-CN']);
|
||||
|
||||
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\MarketingAccountBillMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MarketingAccountBillMirrorRepositoryTest extends TestCase
|
||||
{
|
||||
private MarketingAccountBillMirrorRepository $mirror;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.connections.business_mirror' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
],
|
||||
'upstream_mirrors.connection' => 'business_mirror',
|
||||
'upstream_mirrors.datasets' => [
|
||||
MarketingAccountBillMirrorRepository::TAOBAO => $this->definition('tmall_spread_bills', 'js_tmall_spread_bills'),
|
||||
MarketingAccountBillMirrorRepository::PINDUODUO => $this->definition('pdd_spread_bills', 'js_pdd_spread_bills'),
|
||||
MarketingAccountBillMirrorRepository::ALIBABA => $this->definition('alibaba_bill', 'js_alibaba_bill'),
|
||||
MarketingAccountBillMirrorRepository::JD_RECHARGE => $this->definition('jd_spreadBills', 'js_jd_spread_bills'),
|
||||
MarketingAccountBillMirrorRepository::JD_CONSUMPTION => $this->definition('jd_costBills', 'js_jd_cost_bills'),
|
||||
],
|
||||
]);
|
||||
DB::purge('business_mirror');
|
||||
$this->createTables();
|
||||
|
||||
$generic = new GenericBusinessMirrorRepository(app('db'));
|
||||
$this->mirror = new MarketingAccountBillMirrorRepository(
|
||||
new BusinessMirrorDatasetRegistry(),
|
||||
$generic,
|
||||
);
|
||||
}
|
||||
|
||||
public function test_ready_mirrors_drive_lists_summaries_options_monthly_balances_and_second_pages(): void
|
||||
{
|
||||
$db = DB::connection('business_mirror');
|
||||
$db->table('js_tmall_spread_bills')->insert([
|
||||
$this->taobao(1, '2026-05-31 23:00:00', 10000, 10000, 'DEBIT'),
|
||||
$this->taobao(2, '2026-06-01 09:00:00', 5000, 15000, 'DEBIT'),
|
||||
$this->taobao(3, '2026-06-02 09:00:00', 2000, 13000, 'CREDIT'),
|
||||
]);
|
||||
$db->table('js_pdd_spread_bills')->insert([
|
||||
$this->pinduoduo(1, '2026-05-31 23:00:00', 100, 100, 1),
|
||||
$this->pinduoduo(2, '2026-06-01 09:00:00', 50, 150, 1),
|
||||
$this->pinduoduo(3, '2026-06-02 09:00:00', 20, 130, 2),
|
||||
]);
|
||||
$db->table('js_alibaba_bill')->insert([
|
||||
['id' => 1, 'shopName' => '阿里店', 'tradeDay' => '2026-06-01 09:00:00', 'tradeTypeName' => '全站', 'tradeAMTIn' => 10, 'tradeAMTOut' => 1],
|
||||
['id' => 2, 'shopName' => '阿里店', 'tradeDay' => '2026-06-02 09:00:00', 'tradeTypeName' => '全站', 'tradeAMTIn' => 20, 'tradeAMTOut' => 2],
|
||||
]);
|
||||
$db->table('js_jd_spread_bills')->insert([
|
||||
['id' => 1, 'shopName' => '京东店', 'amount' => 100, 'balanceAmount' => 100, 'orderTypeName' => '充值', 'pk' => 'R1', 'createDate' => '2026-06-01 10:00:00', 'updateTime' => '2026-06-01 10:01:00', 'statusName' => '成功', 'remark' => ''],
|
||||
['id' => 2, 'shopName' => '京东店', 'amount' => 200, 'balanceAmount' => 300, 'orderTypeName' => '充值', 'pk' => 'R2', 'createDate' => '2026-06-02 10:00:00', 'updateTime' => '2026-06-02 10:01:00', 'statusName' => '成功', 'remark' => ''],
|
||||
]);
|
||||
$db->table('js_jd_cost_bills')->insert([
|
||||
['id' => 1, 'shopName' => '京东店', 'amount' => -30, 'balanceAmount' => 270, 'orderTypeName' => '消耗', 'creatTime' => '2026-06-03 10:00:00', 'settleTime' => '2026-06-04 00:00:00', 'pk' => 'C1', 'invoiceApplyTypeName' => '推广费'],
|
||||
]);
|
||||
$this->markReady();
|
||||
|
||||
$factory = $this->throwingFactory();
|
||||
$taobao = new TmallTaobaoSpreadBillRepository($factory, $this->mirror);
|
||||
$pinduoduo = new PinduoduoSpreadBillRepository($factory, $this->mirror);
|
||||
$alibaba = new AlibabaBillRepository($factory, $this->mirror);
|
||||
$jd = new JdWalletBillRepository($factory, $this->mirror);
|
||||
|
||||
$taobaoPage2 = $taobao->paginate('镜像店铺', '2026-06-01', '2026-06-30', null, 2, 1);
|
||||
$taobaoSummary = $taobao->summary('镜像店铺', '2026-06-01', '2026-06-30');
|
||||
$taobaoMonthly = $taobao->paginateMonthlyBalances('镜像店铺', '2026-06', 1, 25);
|
||||
$pddPage2 = $pinduoduo->paginate('镜像店铺', '2026-06-01', '2026-06-30', null, 2, 1);
|
||||
$pddSummary = $pinduoduo->summary('镜像店铺', '2026-06-01', '2026-06-30');
|
||||
$pddMonthly = $pinduoduo->paginateMonthlyBalances('镜像店铺', '2026-06', 1, 25);
|
||||
$alibabaPage2 = $alibaba->paginate('阿里店', '全站', '2026-06-01', '2026-06-30', 2, 1);
|
||||
$jdRechargePage2 = $jd->paginateRecharge('京东店', '2026-06-01', '2026-06-30', 2, 1);
|
||||
|
||||
$this->assertSame(2, $taobaoPage2['total']);
|
||||
$this->assertSame(50.0, $taobaoPage2['rows'][0]['amount']);
|
||||
$this->assertSame(['镜像店铺'], $taobao->storeOptions());
|
||||
$this->assertSame(30.0, $taobaoSummary['net']);
|
||||
$this->assertSame(130.0, $taobaoMonthly['rows'][0]['closing_balance']);
|
||||
$this->assertSame(0.0, $taobaoMonthly['rows'][0]['balance_difference']);
|
||||
|
||||
$this->assertSame(2, $pddPage2['total']);
|
||||
$this->assertSame(50.0, $pddPage2['rows'][0]['amount']);
|
||||
$this->assertSame(30.0, $pddSummary['net']);
|
||||
$this->assertSame(130.0, $pddMonthly['rows'][0]['closing_balance']);
|
||||
$this->assertSame(0.0, $pddMonthly['rows'][0]['balance_difference']);
|
||||
|
||||
$this->assertSame(2, $alibabaPage2['total']);
|
||||
$this->assertSame(10.0, $alibabaPage2['rows'][0]['income_amount']);
|
||||
$this->assertSame(['shops' => ['阿里店'], 'product_types' => ['全站']], $alibaba->filterOptions());
|
||||
$this->assertSame(27.0, $alibaba->summary('阿里店', '全站')['net']);
|
||||
|
||||
$this->assertSame(2, $jdRechargePage2['total']);
|
||||
$this->assertSame('R1', $jdRechargePage2['rows'][0]['order_number']);
|
||||
$this->assertSame(['京东店'], $jd->shopOptions());
|
||||
$this->assertSame(300.0, $jd->summaryRecharge('京东店')['amount_total']);
|
||||
$this->assertSame(30.0, $jd->summaryConsumption('京东店')['amount_total']);
|
||||
}
|
||||
|
||||
public function test_unready_dataset_keeps_the_existing_upstream_repository_path(): void
|
||||
{
|
||||
$query = new class('jiansun') extends UpstreamReadOnlyQuery {
|
||||
public function scalar(string $sql, array $bindings = []): mixed { return 1; }
|
||||
public function select(string $sql, array $bindings = []): array
|
||||
{
|
||||
return [['shopName' => '上游回退', 'amount' => 8, 'balance' => 9, 'summary' => '', 'flowType' => 1, 'toAdvertiserFundType' => 1, 'createTime' => '2026-06-01']];
|
||||
}
|
||||
};
|
||||
$factory = new class($query) extends UpstreamReadOnlyQueryFactory {
|
||||
public function __construct(private readonly UpstreamReadOnlyQuery $query) {}
|
||||
public function forSource(string $source): UpstreamReadOnlyQuery { return $this->query; }
|
||||
};
|
||||
|
||||
$result = (new PinduoduoSpreadBillRepository($factory, $this->mirror))->paginate();
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('上游回退', $result['rows'][0]['store_name']);
|
||||
}
|
||||
|
||||
private function throwingFactory(): UpstreamReadOnlyQueryFactory
|
||||
{
|
||||
$query = new class('jiansun') extends UpstreamReadOnlyQuery {
|
||||
public function scalar(string $sql, array $bindings = []): mixed { throw new RuntimeException('upstream_should_not_be_called'); }
|
||||
public function select(string $sql, array $bindings = []): array { throw new RuntimeException('upstream_should_not_be_called'); }
|
||||
public function first(string $sql, array $bindings = []): ?array { throw new RuntimeException('upstream_should_not_be_called'); }
|
||||
};
|
||||
|
||||
return new class($query) extends UpstreamReadOnlyQueryFactory {
|
||||
public function __construct(private readonly UpstreamReadOnlyQuery $query) {}
|
||||
public function forSource(string $source): UpstreamReadOnlyQuery { return $this->query; }
|
||||
};
|
||||
}
|
||||
|
||||
private function createTables(): void
|
||||
{
|
||||
$schema = Schema::connection('business_mirror');
|
||||
$schema->create('business_mirror_table_states', function (Blueprint $table): void {
|
||||
$table->string('dataset_key')->primary();
|
||||
$table->dateTime('initial_completed_at')->nullable();
|
||||
});
|
||||
$schema->create('js_tmall_spread_bills', function (Blueprint $table): void {
|
||||
$table->integer('id'); $table->string('shopName')->nullable(); $table->dateTime('uniTransTime')->nullable(); $table->dateTime('transTime')->nullable();
|
||||
$table->decimal('amount', 18, 2)->nullable(); $table->decimal('balance', 18, 2)->nullable(); $table->string('comment')->nullable();
|
||||
$table->string('finType')->nullable(); $table->string('bizType')->nullable(); $table->string('tradeType')->nullable();
|
||||
});
|
||||
$schema->create('js_pdd_spread_bills', function (Blueprint $table): void {
|
||||
$table->integer('id'); $table->string('shopName')->nullable(); $table->decimal('amount', 18, 2)->nullable(); $table->decimal('balance', 18, 2)->nullable();
|
||||
$table->string('summary')->nullable(); $table->integer('flowType')->nullable(); $table->integer('toAdvertiserFundType')->nullable(); $table->dateTime('createTime')->nullable();
|
||||
});
|
||||
$schema->create('js_alibaba_bill', function (Blueprint $table): void {
|
||||
$table->integer('id'); $table->string('shopName')->nullable(); $table->dateTime('tradeDay')->nullable(); $table->string('tradeTypeName')->nullable();
|
||||
$table->decimal('tradeAMTIn', 18, 2)->nullable(); $table->decimal('tradeAMTOut', 18, 2)->nullable();
|
||||
});
|
||||
$schema->create('js_jd_spread_bills', function (Blueprint $table): void {
|
||||
$table->integer('id'); $table->string('shopName')->nullable(); $table->decimal('amount', 18, 2)->nullable(); $table->decimal('balanceAmount', 18, 2)->nullable();
|
||||
$table->string('orderTypeName')->nullable(); $table->string('pk')->nullable(); $table->dateTime('createDate')->nullable(); $table->dateTime('updateTime')->nullable();
|
||||
$table->string('statusName')->nullable(); $table->string('remark')->nullable();
|
||||
});
|
||||
$schema->create('js_jd_cost_bills', function (Blueprint $table): void {
|
||||
$table->integer('id'); $table->string('shopName')->nullable(); $table->decimal('amount', 18, 2)->nullable(); $table->decimal('balanceAmount', 18, 2)->nullable();
|
||||
$table->string('orderTypeName')->nullable(); $table->dateTime('creatTime')->nullable(); $table->dateTime('settleTime')->nullable();
|
||||
$table->string('pk')->nullable(); $table->string('invoiceApplyTypeName')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
private function markReady(): void
|
||||
{
|
||||
foreach (array_keys((array) config('upstream_mirrors.datasets')) as $key) {
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
|
||||
'dataset_key' => $key,
|
||||
'initial_completed_at' => '2026-08-14 12:00:00',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function definition(string $object, string $target): array
|
||||
{
|
||||
return ['source_key' => 'jiansun', 'schema' => 'dbo', 'object' => $object, 'target' => $target, 'keys' => ['id']];
|
||||
}
|
||||
|
||||
private function taobao(int $id, string $time, float $amount, float $balance, string $finType): array
|
||||
{
|
||||
return ['id' => $id, 'shopName' => '镜像店铺', 'uniTransTime' => $time, 'transTime' => $time, 'amount' => $amount, 'balance' => $balance, 'comment' => '', 'finType' => $finType, 'bizType' => '', 'tradeType' => ''];
|
||||
}
|
||||
|
||||
private function pinduoduo(int $id, string $time, float $amount, float $balance, int $flowType): array
|
||||
{
|
||||
return ['id' => $id, 'shopName' => '镜像店铺', 'amount' => $amount, 'balance' => $balance, 'summary' => '', 'flowType' => $flowType, 'toAdvertiserFundType' => 1, 'createTime' => $time];
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user