避免店铺台账专用快照影响提现管理余额匹配。 Co-authored-by: 92451 <924518135@qq.com> Co-committed-by: 92451 <924518135@qq.com>
219 lines
8.3 KiB
PHP
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) ?: '未标明来源',
|
|
};
|
|
}
|
|
}
|