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

301 lines
12 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;
/**
* 京东钱包充值和消耗账单的只读上游适配器。
*
* 数据来源:JianSun.dbo.jd_spreadBills、JianSun.dbo.jd_costBills。
* 金额字段已按元存储,查询只返回京东表格需要的字段。
*/
class JdWalletBillRepository
{
private const RECHARGE_TABLE = '[dbo].[jd_spreadBills]';
private const CONSUMPTION_TABLE = '[dbo].[jd_costBills]';
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(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
. ") shops WHERE shopName IS NOT NULL AND LTRIM(RTRIM(shopName)) <> '' ORDER BY shopName"
);
return array_values(array_map(
static fn (array $row): string => (string) ($row['shopName'] ?? ''),
$rows,
));
}
/** @return array{rows: array<int,array<string,mixed>>, total: int} */
public function paginateRecharge(
?string $shopName = null,
?string $fromDate = null,
?string $toDate = null,
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',
], 'createDate', 'id', fn (array $row): array => $this->normalizeRecharge($row));
}
/** @return array{rows: array<int,array<string,mixed>>, total: int} */
public function paginateConsumption(
?string $shopName = null,
?string $fromDate = null,
?string $toDate = null,
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',
], 'creatTime', 'id', fn (array $row): array => $this->normalizeConsumption($row));
}
/** @return \Generator<int,array<string,mixed>> */
public function exportRows(
string $ledger,
?string $shopName = null,
?string $fromDate = null,
?string $toDate = null,
int $batchSize = 100,
): \Generator {
$ledger = $ledger === 'consumption' ? 'consumption' : 'recharge';
if (! $this->usesBusinessMirror($ledger)) {
throw new \RuntimeException('京东账单共享业务镜像尚未完成同步。');
}
$batchSize = max(1, min(100, $batchSize));
$page = 1;
$exported = 0;
do {
$result = $ledger === 'consumption'
? $this->mirror->paginateJdConsumption($shopName, $fromDate, $toDate, $page, $batchSize)
: $this->mirror->paginateJdRecharge($shopName, $fromDate, $toDate, $page, $batchSize);
$rows = is_array($result['rows'] ?? null) ? $result['rows'] : [];
$total = max(0, (int) ($result['total'] ?? 0));
foreach ($rows as $row) {
yield $ledger === 'consumption'
? $this->normalizeConsumption($row)
: $this->normalizeRecharge($row);
}
$count = count($rows);
$exported += $count;
$page++;
} while ($count > 0 && $exported < $total);
}
/** @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));
}
/** @param array{sql:string,bindings:array<int,mixed>} $where */
private function paginate(string $table, array $where, int $page, int $pageSize, array $fields, string $dateField, string $idField, callable $normalize): array
{
$page = max(1, $page);
$pageSize = min(max(1, $pageSize), 100);
$total = (int) $this->query->scalar('SELECT COUNT(*) FROM ' . $table . ' src' . $where['sql'], $where['bindings']);
if ($total === 0) {
return ['rows' => [], 'total' => 0];
}
$start = (($page - 1) * $pageSize) + 1;
$end = $page * $pageSize;
$select = implode(', ', array_map(static fn (string $field): string => 'src.' . $field, $fields));
$sql = 'SELECT * FROM ('
. 'SELECT ' . $select . ', ROW_NUMBER() OVER (ORDER BY src.' . $dateField . ' DESC, src.' . $idField . ' DESC) AS rn '
. 'FROM ' . $table . ' src' . $where['sql']
. ') AS t WHERE t.rn BETWEEN ? AND ? ORDER BY t.rn';
$rows = $this->query->select($sql, array_merge($where['bindings'], [$start, $end]));
return ['rows' => array_map($normalize, $rows), 'total' => $total];
}
/** @param array{sql:string,bindings:array<int,mixed>} $where */
private function summary(string $table, array $where): array
{
$row = $this->query->first(
'SELECT COALESCE(SUM(ABS(amount)), 0) AS amount_total, COALESCE(SUM(balanceAmount), 0) AS balance_total, COUNT(*) AS cnt FROM '
. $table . ' src' . $where['sql'],
$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),
'count' => (int) ($row['cnt'] ?? 0),
];
}
/** @return array{sql:string,bindings:array<int,mixed>} */
private function rechargeWhere(?string $shopName, ?string $fromDate, ?string $toDate): array
{
return $this->dateWhere('src.createDate', $shopName, $fromDate, $toDate);
}
/** @return array{sql:string,bindings:array<int,mixed>} */
private function consumptionWhere(?string $shopName, ?string $fromDate, ?string $toDate): array
{
return $this->dateWhere('src.creatTime', $shopName, $fromDate, $toDate);
}
/** @return array{sql:string,bindings:array<int,mixed>} */
private function dateWhere(string $dateField, ?string $shopName, ?string $fromDate, ?string $toDate): array
{
$conditions = [];
$bindings = [];
if ($shopName !== null && $shopName !== '') {
$conditions[] = 'src.shopName = ?';
$bindings[] = $shopName;
}
if ($fromDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) === 1) {
$conditions[] = $dateField . ' >= ?';
$bindings[] = $fromDate . ' 00:00:00';
}
if ($toDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) === 1) {
$conditions[] = $dateField . ' < DATEADD(day, 1, CAST(? AS date))';
$bindings[] = $toDate;
}
return ['sql' => $conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions), 'bindings' => $bindings];
}
private function normalizeRecharge(array $row): array
{
return [
'id' => (string) ($row['id'] ?? $row['pk'] ?? ''),
'store_name' => (string) ($row['shopName'] ?? ''),
'amount' => round((float) ($row['amount'] ?? 0), 2),
'balance' => round((float) ($row['balanceAmount'] ?? 0), 2),
'transaction_type' => (string) ($row['orderTypeName'] ?? ''),
'order_number' => (string) ($row['pk'] ?? ''),
'recharge_time' => (string) ($row['createDate'] ?? ''),
'arrival_time' => (string) ($row['updateTime'] ?? ''),
'serial_number' => (string) ($row['id'] ?? $row['pk'] ?? ''),
'status' => (string) ($row['statusName'] ?? ''),
'notes' => (string) ($row['remark'] ?? ''),
];
}
private function normalizeConsumption(array $row): array
{
$amount = abs((float) ($row['amount'] ?? 0));
return [
'id' => (string) ($row['id'] ?? $row['pk'] ?? ''),
'store_name' => (string) ($row['shopName'] ?? ''),
'amount' => round($amount, 2),
'balance' => round((float) ($row['balanceAmount'] ?? 0), 2),
'transaction_type' => (string) ($row['orderTypeName'] ?? ''),
'investment_date' => (string) ($row['creatTime'] ?? ''),
'settlement_date' => (string) ($row['settleTime'] ?? ''),
'serial_number' => (string) ($row['pk'] ?? $row['id'] ?? ''),
'notes' => (string) ($row['invoiceApplyTypeName'] ?? ''),
];
}
}