聚水潭缺失店铺按财务账单补录;页面和导出标记数据来源。 PR #552 Co-authored-by: 92451 <924518135@qq.com> Co-committed-by: 92451 <924518135@qq.com>
281 lines
10 KiB
PHP
281 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Services;
|
|
|
|
use App\Models\Business\StoreReceivingAccount;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Database\ConnectionInterface;
|
|
use Illuminate\Database\DatabaseManager;
|
|
use InvalidArgumentException;
|
|
use Modules\EcommerceFinance\Repositories\FinanceBillBalanceRepository;
|
|
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
|
|
|
|
/**
|
|
* 聚水潭余额缺失时,用财务系统店铺收款账户账单中的累计余额补位。
|
|
*/
|
|
class FinanceBillShopBalanceFallback
|
|
{
|
|
private const SCOPE_PLATFORM_KEYS = [
|
|
FinanceBillBalanceRepository::SCOPE_TAOBAO => ['tmall', 'taobao', '1688'],
|
|
FinanceBillBalanceRepository::SCOPE_JD_WALLET => ['jd', 'jingxi'],
|
|
'pinduoduo' => ['pinduoduo'],
|
|
'douyin' => ['douyin'],
|
|
];
|
|
|
|
private readonly ConnectionInterface $connection;
|
|
|
|
public function __construct(
|
|
DatabaseManager $database,
|
|
private readonly JushuitanShopBalanceRepository $jushuitanBalances,
|
|
private readonly FinanceBillBalanceRepository $financeBalances,
|
|
) {
|
|
$this->connection = $database->connection(
|
|
(string) config('upstream_mirrors.connection', 'business_mirror'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* source_scope:string,supported:bool,reason:?string,missing:int,resolved:int,
|
|
* unresolved:int,filled:int,dry_run:bool
|
|
* }
|
|
*/
|
|
public function fillMissing(string $balanceDate, string $sourceScope, bool $dryRun = false): array
|
|
{
|
|
$date = CarbonImmutable::createFromFormat('!Y-m-d', $balanceDate);
|
|
if (! $date instanceof CarbonImmutable || $date->format('Y-m-d') !== $balanceDate) {
|
|
throw new InvalidArgumentException('余额日期必须使用 YYYY-MM-DD 格式。');
|
|
}
|
|
|
|
$sourceScope = trim($sourceScope);
|
|
$platformKeys = $this->platformKeysForScope($sourceScope);
|
|
if ($platformKeys === []) {
|
|
return [
|
|
'source_scope' => $sourceScope,
|
|
'supported' => false,
|
|
'reason' => 'unknown_balance_scope',
|
|
'missing' => 0,
|
|
'resolved' => 0,
|
|
'unresolved' => 0,
|
|
'filled' => 0,
|
|
'dry_run' => $dryRun,
|
|
];
|
|
}
|
|
|
|
$accounts = StoreReceivingAccount::withoutGlobalScopes()
|
|
->whereNull('deleted_at')
|
|
->get()
|
|
->filter(static function (StoreReceivingAccount $account) use ($platformKeys): bool {
|
|
$platformKey = JushuitanShopBalanceRepository::platformKey(
|
|
(string) $account->store_platform,
|
|
);
|
|
|
|
return $platformKey !== null && in_array($platformKey, $platformKeys, true);
|
|
})
|
|
->values();
|
|
|
|
if ($accounts->isEmpty()) {
|
|
return $this->result($sourceScope, true, null, 0, 0, $dryRun);
|
|
}
|
|
|
|
$matches = $this->jushuitanBalances->matchesForStoresOnDates(
|
|
$accounts,
|
|
[$balanceDate],
|
|
JushuitanShopBalanceRepository::DATA_SOURCE_JUSHUITAN,
|
|
);
|
|
$missingAccounts = $accounts->filter(
|
|
static function (StoreReceivingAccount $account) use ($matches, $balanceDate): bool {
|
|
$status = $matches[(int) $account->id][$balanceDate]['status'] ?? 'not_ready';
|
|
|
|
return in_array($status, ['unmatched', 'not_ready'], true);
|
|
},
|
|
)->values();
|
|
|
|
$providerScopes = $this->providerScopes($sourceScope);
|
|
if ($missingAccounts->isEmpty()) {
|
|
if (! $dryRun && $providerScopes !== []) {
|
|
$this->replaceFinanceRows($balanceDate, $sourceScope, []);
|
|
}
|
|
|
|
return $this->result(
|
|
$sourceScope,
|
|
$providerScopes !== [],
|
|
$providerScopes === [] ? 'finance_bill_has_no_account_balance' : null,
|
|
0,
|
|
0,
|
|
$dryRun,
|
|
);
|
|
}
|
|
if ($providerScopes === []) {
|
|
return $this->result(
|
|
$sourceScope,
|
|
false,
|
|
'finance_bill_has_no_account_balance',
|
|
$missingAccounts->count(),
|
|
0,
|
|
$dryRun,
|
|
);
|
|
}
|
|
|
|
$storeNames = $missingAccounts
|
|
->flatMap(static fn (StoreReceivingAccount $account): array => [
|
|
(string) ($account->shop_name ?? ''),
|
|
(string) ($account->erp_store_name ?? ''),
|
|
])
|
|
->map(static fn (string $storeName): string => trim($storeName))
|
|
->filter()
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$financeRows = [];
|
|
foreach ($providerScopes as $providerScope) {
|
|
$financeRows = [
|
|
...$financeRows,
|
|
...$this->financeBalances->latestBalances($providerScope, $balanceDate, $storeNames),
|
|
];
|
|
}
|
|
|
|
$financeByStore = [];
|
|
foreach ($financeRows as $row) {
|
|
$platformKey = JushuitanShopBalanceRepository::platformKey(
|
|
(string) ($row['source_platform'] ?? ''),
|
|
);
|
|
if ($platformKey === null) {
|
|
continue;
|
|
}
|
|
$financeByStore[$platformKey . "\0" . $row['normalized_store_name']] = $row;
|
|
}
|
|
|
|
$now = now();
|
|
$rows = [];
|
|
$resolved = 0;
|
|
foreach ($missingAccounts as $account) {
|
|
$platformKey = JushuitanShopBalanceRepository::platformKey(
|
|
(string) $account->store_platform,
|
|
);
|
|
if ($platformKey === null) {
|
|
continue;
|
|
}
|
|
|
|
$names = array_values(array_unique(array_filter([
|
|
JushuitanShopBalanceRepository::normalizeStoreName((string) ($account->shop_name ?? '')),
|
|
JushuitanShopBalanceRepository::normalizeStoreName((string) ($account->erp_store_name ?? '')),
|
|
])));
|
|
$candidates = [];
|
|
foreach ($names as $normalizedStoreName) {
|
|
$key = $platformKey . "\0" . $normalizedStoreName;
|
|
if (isset($financeByStore[$key])) {
|
|
$candidates[$key] = $financeByStore[$key];
|
|
}
|
|
}
|
|
if (count($candidates) !== 1) {
|
|
continue;
|
|
}
|
|
|
|
$resolved++;
|
|
$candidateKey = array_key_first($candidates);
|
|
if (isset($rows[$candidateKey])) {
|
|
continue;
|
|
}
|
|
$candidate = $candidates[$candidateKey];
|
|
$rows[$candidateKey] = [
|
|
'balance_date' => $balanceDate,
|
|
'source_scope' => $sourceScope,
|
|
'data_source' => JushuitanShopBalanceRepository::DATA_SOURCE_FINANCE,
|
|
'source_platform' => $candidate['source_platform'],
|
|
'store_name' => $candidate['store_name'],
|
|
'normalized_store_name' => $candidate['normalized_store_name'],
|
|
'platform_account_id' => $candidate['platform_account_id'],
|
|
'balance' => round((float) $candidate['balance'], 6),
|
|
'source_row_key' => hash('sha256', 'finance' . "\0" . $sourceScope . "\0" . $candidateKey),
|
|
'source_file_name' => mb_substr((string) $candidate['source_file_name'], 0, 255),
|
|
'source_sha256' => hash(
|
|
'sha256',
|
|
implode("\0", [$balanceDate, $sourceScope, $candidateKey, (string) $candidate['occurred_at']]),
|
|
),
|
|
'imported_at' => $now,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
];
|
|
}
|
|
|
|
if (! $dryRun) {
|
|
$rows = array_values($rows);
|
|
$this->replaceFinanceRows($balanceDate, $sourceScope, $rows);
|
|
}
|
|
|
|
return $this->result(
|
|
$sourceScope,
|
|
true,
|
|
null,
|
|
$missingAccounts->count(),
|
|
$resolved,
|
|
$dryRun,
|
|
count($rows),
|
|
);
|
|
}
|
|
|
|
/** @return list<string> */
|
|
private function platformKeysForScope(string $sourceScope): array
|
|
{
|
|
if ($sourceScope === 'all') {
|
|
return array_values(array_unique(array_merge(...array_values(self::SCOPE_PLATFORM_KEYS))));
|
|
}
|
|
|
|
return self::SCOPE_PLATFORM_KEYS[$sourceScope] ?? [];
|
|
}
|
|
|
|
/** @return list<string> */
|
|
private function providerScopes(string $sourceScope): array
|
|
{
|
|
return match ($sourceScope) {
|
|
'all' => [
|
|
FinanceBillBalanceRepository::SCOPE_TAOBAO,
|
|
FinanceBillBalanceRepository::SCOPE_JD_WALLET,
|
|
],
|
|
FinanceBillBalanceRepository::SCOPE_TAOBAO,
|
|
FinanceBillBalanceRepository::SCOPE_JD_WALLET => [$sourceScope],
|
|
default => [],
|
|
};
|
|
}
|
|
|
|
/** @param list<array<string,mixed>> $rows */
|
|
private function replaceFinanceRows(string $balanceDate, string $sourceScope, array $rows): void
|
|
{
|
|
$this->connection->transaction(function () use ($balanceDate, $sourceScope, $rows): void {
|
|
$this->connection->table(JushuitanShopBalanceRepository::TABLE)
|
|
->where('balance_date', $balanceDate)
|
|
->where('source_scope', $sourceScope)
|
|
->where('data_source', JushuitanShopBalanceRepository::DATA_SOURCE_FINANCE)
|
|
->delete();
|
|
|
|
foreach (array_chunk($rows, 500) as $chunk) {
|
|
$this->connection->table(JushuitanShopBalanceRepository::TABLE)->insert($chunk);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** @return array{source_scope:string,supported:bool,reason:?string,missing:int,resolved:int,unresolved:int,filled:int,dry_run:bool} */
|
|
private function result(
|
|
string $sourceScope,
|
|
bool $supported,
|
|
?string $reason,
|
|
int $missing,
|
|
int $resolved,
|
|
bool $dryRun,
|
|
?int $filled = null,
|
|
): array {
|
|
return [
|
|
'source_scope' => $sourceScope,
|
|
'supported' => $supported,
|
|
'reason' => $reason,
|
|
'missing' => $missing,
|
|
'resolved' => $resolved,
|
|
'unresolved' => max(0, $missing - $resolved),
|
|
'filled' => $filled ?? 0,
|
|
'dry_run' => $dryRun,
|
|
];
|
|
}
|
|
}
|