CaiWuXiTong/modules/EcommerceFinance/Repositories/AlibabaBillRepository.php
chenxi 8bfd82aa29 店铺营销账户账单各平台导出 (#77)
为淘系、阿里巴巴和京东专用账单补齐按当前筛选条件导出,并保留拼多多及通用平台既有导出能力。导出统一优先读取共享业务镜像。
2026-08-24 14:29:46 +08:00

209 lines
7.4 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 \Generator<int,array<string,mixed>> */
public function exportRows(
?string $shopName = null,
?string $productType = null,
?string $fromDate = null,
?string $toDate = null,
int $batchSize = 100,
): \Generator {
if (! $this->usesBusinessMirror()) {
throw new \RuntimeException('阿里巴巴账单共享业务镜像尚未完成同步。');
}
$batchSize = max(1, min(100, $batchSize));
$page = 1;
$exported = 0;
do {
$result = $this->mirror->paginateAlibaba(
$shopName,
$productType,
$fromDate,
$toDate,
$page,
$batchSize,
);
$rows = is_array($result['rows'] ?? null) ? $result['rows'] : [];
$total = max(0, (int) ($result['total'] ?? 0));
foreach ($rows as $row) {
yield $this->normalize($row);
}
$count = count($rows);
$exported += $count;
$page++;
} while ($count > 0 && $exported < $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),
];
}
}