CaiWuXiTong/modules/EcommerceFinance/Repositories/AlibabaBillRepository.php
WuYongJie 43445fde4b feat: read marketing bills from business mirror
首次全量完成后,营销账户账单、余额、汇总、选项及分页优先读取 caiwu_business_mirror;未就绪时保留旧快照链路。
2026-08-14 17:48:23 +08:00

171 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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,
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'
);
$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);
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);
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 {
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);
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),
];
}
}