CaiWuXiTong/modules/EcommerceFinance/Services/StoreReceivingBillBalanceSnapshotSynchronizer.php
92451 4cd3a9e571 fix: 隔离店铺台账余额快照来源
避免店铺台账专用快照影响提现管理余额匹配。
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-27 18:21:55 +08:00

219 lines
8.3 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Carbon\CarbonImmutable;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use InvalidArgumentException;
use Modules\EcommerceFinance\Repositories\FinanceBillBalanceRepository;
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
use Throwable;
/**
* 生成店铺台账专用日余额快照。
*
* 店铺收款账户账单中具备真实累计余额的来源优先(淘系支付宝、京东钱包);
* 其他平台暂时沿用聚水潭店铺余额。结果单独写入 store-ledger 来源范围,
* 不改变提现管理现有余额口径。
*/
class StoreReceivingBillBalanceSnapshotSynchronizer
{
public const SOURCE_SCOPE = JushuitanShopBalanceRepository::SOURCE_SCOPE_STORE_LEDGER;
public const DATA_SOURCE_RECEIVING_BILL = 'store_receiving_bill';
private readonly ConnectionInterface $connection;
public function __construct(
DatabaseManager $databases,
private readonly FinanceBillBalanceRepository $billBalances,
) {
$this->connection = $databases->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
}
/**
* @return array{
* date:string,total:int,receiving_bill:int,jushuitan:int,
* replaced:int,source_errors:list<string>
* }
*/
public function sync(string $balanceDate): array
{
$date = CarbonImmutable::createFromFormat('!Y-m-d', $balanceDate);
if (! $date instanceof CarbonImmutable || $date->format('Y-m-d') !== $balanceDate) {
throw new InvalidArgumentException('店铺余额日期必须使用 YYYY-MM-DD 格式。');
}
$rows = [];
$sourceErrors = [];
$receivingBillCount = 0;
foreach ([FinanceBillBalanceRepository::SCOPE_TAOBAO, FinanceBillBalanceRepository::SCOPE_JD_WALLET] as $scope) {
try {
foreach ($this->billBalances->latestBalances($scope, $balanceDate) as $source) {
$key = $this->key(
(string) ($source['source_platform'] ?? ''),
(string) ($source['store_name'] ?? ''),
);
if ($key === '') {
continue;
}
$rows[$key] = $this->snapshotRow(
$balanceDate,
self::DATA_SOURCE_RECEIVING_BILL,
$key,
$source,
);
$receivingBillCount++;
}
} catch (Throwable $exception) {
report($exception);
$sourceErrors[] = $scope . ':' . $exception->getMessage();
}
}
$legacy = $this->connection->table(JushuitanShopBalanceRepository::TABLE)
->where('balance_date', $balanceDate)
->where('source_scope', '<>', self::SOURCE_SCOPE)
->orderByRaw("CASE WHEN data_source = 'jushuitan' THEN 0 ELSE 1 END")
->orderBy('id')
->get();
$jushuitanCount = 0;
foreach ($legacy as $source) {
$key = $this->key(
(string) ($source->source_platform ?? ''),
(string) ($source->store_name ?? ''),
);
if ($key === '' || isset($rows[$key])) {
continue;
}
$dataSource = (string) ($source->data_source ?? JushuitanShopBalanceRepository::DATA_SOURCE_JUSHUITAN);
$rows[$key] = $this->snapshotRow($balanceDate, $dataSource, $key, (array) $source);
if ($dataSource === JushuitanShopBalanceRepository::DATA_SOURCE_JUSHUITAN) {
$jushuitanCount++;
}
}
$rows = array_values($rows);
$existing = $this->connection->table(JushuitanShopBalanceRepository::TABLE)
->where('balance_date', $balanceDate)
->where('source_scope', self::SOURCE_SCOPE)
->count();
if ($rows === []) {
return [
'date' => $balanceDate,
'total' => 0,
'receiving_bill' => 0,
'jushuitan' => 0,
'replaced' => $existing,
'source_errors' => $sourceErrors,
];
}
$this->connection->transaction(function () use ($balanceDate, $rows): void {
$this->connection->table(JushuitanShopBalanceRepository::TABLE)
->where('balance_date', $balanceDate)
->where('source_scope', self::SOURCE_SCOPE)
->delete();
foreach (array_chunk($rows, 500) as $chunk) {
$this->connection->table(JushuitanShopBalanceRepository::TABLE)->insert($chunk);
}
});
return [
'date' => $balanceDate,
'total' => count($rows),
'receiving_bill' => $receivingBillCount,
'jushuitan' => $jushuitanCount,
'replaced' => $existing,
'source_errors' => $sourceErrors,
];
}
/**
* @param array<string,mixed> $source
* @return array<string,mixed>
*/
private function snapshotRow(string $balanceDate, string $dataSource, string $key, array $source): array
{
$now = now();
$storeName = trim((string) ($source['store_name'] ?? ''));
$normalized = trim((string) ($source['normalized_store_name'] ?? ''));
if ($normalized === '') {
$normalized = JushuitanShopBalanceRepository::normalizeStoreName($storeName);
}
$sourceFileName = trim((string) ($source['source_file_name'] ?? ''));
if ($sourceFileName === '') {
$sourceFileName = match ($dataSource) {
self::DATA_SOURCE_RECEIVING_BILL => '财务系统/店铺收款账户账单',
JushuitanShopBalanceRepository::DATA_SOURCE_FINANCE => '财务系统账单补录',
default => '聚水潭店铺余额',
};
}
return [
'balance_date' => $balanceDate,
'source_scope' => self::SOURCE_SCOPE,
'data_source' => $dataSource,
'source_platform' => trim((string) ($source['source_platform'] ?? '')) ?: null,
'store_name' => $storeName,
'normalized_store_name' => $normalized,
'platform_account_id' => trim((string) ($source['platform_account_id'] ?? '')) ?: null,
'balance' => round((float) ($source['balance'] ?? 0), 6),
'source_row_key' => hash('sha256', implode("\0", [
self::SOURCE_SCOPE,
$dataSource,
$key,
])),
'source_file_name' => mb_substr($sourceFileName, 0, 255),
'source_sha256' => hash('sha256', implode("\0", [
$balanceDate,
$dataSource,
$key,
(string) ($source['occurred_at'] ?? ''),
number_format((float) ($source['balance'] ?? 0), 6, '.', ''),
])),
'imported_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
}
private function key(string $sourcePlatform, string $storeName): string
{
$storeName = trim($storeName);
if ($storeName === '') {
return '';
}
$platform = JushuitanShopBalanceRepository::platformKey($sourcePlatform)
?? mb_strtolower(trim($sourcePlatform));
$normalized = JushuitanShopBalanceRepository::normalizeStoreName($storeName);
return $normalized === '' ? '' : $platform . "\0" . $normalized;
}
public static function sourceLabel(?string $dataSource, ?string $sourceFileName = null): string
{
return match ($dataSource) {
self::DATA_SOURCE_RECEIVING_BILL => str_replace(
'财务系统/店铺收款账户账单/',
'店铺收款账户账单 · ',
trim((string) $sourceFileName) ?: '财务系统/店铺收款账户账单',
),
JushuitanShopBalanceRepository::DATA_SOURCE_FINANCE => '财务系统账单补录',
JushuitanShopBalanceRepository::DATA_SOURCE_JUSHUITAN => '聚水潭店铺余额',
default => trim((string) $sourceFileName) ?: '未标明来源',
};
}
}