CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerShopDirectory.php
92451 f593cd5bb0 复用同回款账户的确认简称 (#660)
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-26 21:53:26 +08:00

267 lines
9.7 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository;
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
use Throwable;
/**
* 财务台账店铺台账的店铺目录。
*
* 平台和 ERP 店铺名称来自店铺维度报表的店铺资料;店铺主体只取当前
* 失效时间为「存续中」的主体期间。财务台账自己的账户和余额字段按 ERP
* 店铺名称合并,避免把报表销量或其他财务字段混入店铺台账。
*/
class FinanceLedgerShopDirectory
{
public function __construct(
private readonly BusinessInformationMirrorRepository $storeInformation,
private readonly StoreReportPeriodBuilder $periods,
private readonly DatabaseManager $databases,
) {
}
/**
* 返回 null 表示店铺维度报表镜像尚未就绪,调用方应回退到财务台账原表。
*
* @return list<array{
* id:int,
* platform:?string,
* shop_name:string,
* company_name:?string,
* account_short:?string,
* status:string,
* payout_account:?string,
* shop_balance:float,
* shop_balance_date:?string,
* remark:?string
* }>|null
*/
public function rows(): ?array
{
try {
$stores = $this->storeInformation->allStores();
} catch (Throwable $exception) {
report($exception);
return null;
}
$openStores = array_values(array_filter(
$stores,
static fn (array $store): bool => in_array(
(string) ($store['store_status'] ?? ''),
['0', '正常'],
true,
) && trim((string) ($store['erp_store_name'] ?? '')) !== '',
));
if ($openStores === []) {
return [];
}
try {
$periods = $this->periods->build($openStores);
} catch (Throwable $exception) {
report($exception);
return null;
}
$financeShops = $this->financeShops();
$accountMaster = $this->accountMaster();
$rows = [];
foreach ($openStores as $store) {
$erpStoreName = trim((string) ($store['erp_store_name'] ?? ''));
$companyName = null;
foreach ($periods[$erpStoreName] ?? [] as $period) {
// 报表把失效时间为空的最后一个期间显示为「存续中」。
if (($period['expires_at'] ?? null) === null) {
$companyName = $this->nullableString($period['company_name'] ?? null);
}
}
$financeShop = $financeShops['exact'][$erpStoreName] ?? null;
if ($financeShop === null) {
$normalized = JushuitanShopBalanceRepository::normalizeStoreName($erpStoreName);
$financeShop = $financeShops['normalized'][$normalized] ?? null;
}
$storePayoutAccount = $this->nullableString($store['withdrawal_account'] ?? null);
$financePayoutAccount = $this->nullableString($financeShop->payout_account ?? null);
$accountShort = $this->matchAccountShort(
$storePayoutAccount ?? $financePayoutAccount,
$this->nullableString($financeShop->account_short ?? null),
$accountMaster,
);
$rows[] = [
'id' => (int) ($store['id'] ?? 0),
'platform' => $this->nullableString($store['platform'] ?? null),
'shop_name' => $erpStoreName,
'company_name' => $companyName,
'account_short' => $accountShort,
'status' => '在营',
'payout_account' => $this->nullableString(
$financePayoutAccount ?? $storePayoutAccount,
),
'shop_balance' => round((float) ($financeShop->shop_balance ?? 0), 2),
'shop_balance_date' => $this->nullableString($financeShop->shop_balance_date ?? null),
'remark' => $this->nullableString($financeShop->remark ?? null),
];
}
return $this->propagateAccountShorts($rows);
}
/** @return array{exact:array<string,object>,normalized:array<string,object>} */
private function financeShops(): array
{
$connection = $this->databases->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
if (! Schema::connection($connection->getName())->hasTable('fin_shops')) {
return ['exact' => [], 'normalized' => []];
}
$rows = $connection->table('fin_shops')->get();
$exact = [];
$normalizedGroups = [];
foreach ($rows as $row) {
$name = trim((string) ($row->shop_name ?? ''));
if ($name === '') {
continue;
}
$exact[$name] = $row;
$normalizedGroups[JushuitanShopBalanceRepository::normalizeStoreName($name)][] = $row;
}
return [
'exact' => $exact,
// 规范化名称重复时置空,避免跨平台误匹配。
'normalized' => array_map(
static fn (array $group): ?object => count($group) === 1 ? $group[0] : null,
$normalizedGroups,
),
];
}
/**
* @return array{by_account_no:array<string,string>,by_short:array<string,true>}
*/
private function accountMaster(): array
{
$connection = $this->databases->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
if (! Schema::connection($connection->getName())->hasTable('fin_accounts')) {
return ['by_account_no' => [], 'by_short' => []];
}
$accounts = $connection->table('fin_accounts')
->get(['account_short', 'account_no', 'bank_name', 'account_type'])
->filter(function (object $account): bool {
$label = (string) ($account->account_type ?? '') . ' ' . (string) ($account->bank_name ?? '');
return mb_stripos($label, '网商银行') === false
&& mb_stripos($label, '支付宝') === false
&& trim((string) ($account->account_short ?? '')) !== '';
});
$byAccountNo = [];
foreach ($accounts->groupBy(
fn (object $account): string => $this->normalizeAccountValue($account->account_no ?? null),
) as $key => $group) {
$shorts = $group
->pluck('account_short')
->map(static fn (mixed $value): string => trim((string) $value))
->filter()
->unique();
if ($key !== '' && $shorts->count() === 1) {
$byAccountNo[$key] = $shorts->first();
}
}
return [
'by_account_no' => $byAccountNo,
'by_short' => $accounts
->pluck('account_short')
->map(static fn (mixed $value): string => trim((string) $value))
->filter()
->unique()
->mapWithKeys(static fn (string $short): array => [$short => true])
->all(),
];
}
/**
* @param array{by_account_no:array<string,string>,by_short:array<string,true>} $accountMaster
*/
private function matchAccountShort(
?string $payoutAccount,
?string $existingShort,
array $accountMaster,
): ?string {
$accountNo = $this->normalizeAccountValue($payoutAccount);
if ($accountNo !== '' && isset($accountMaster['by_account_no'][$accountNo])) {
return $accountMaster['by_account_no'][$accountNo];
}
$existingShort = trim((string) $existingShort);
return $existingShort !== '' && isset($accountMaster['by_short'][$existingShort])
? $existingShort
: null;
}
private function normalizeAccountValue(mixed $value): string
{
return preg_replace('/[^a-z0-9]+/i', '', mb_strtolower(trim((string) $value))) ?? '';
}
/**
* 同一回款账号已在其他店铺匹配到唯一简称时,可安全复用到同账号店铺。
* 同账号出现多个简称时视为歧义,不传播。
*
* @param list<array<string,mixed>> $rows
* @return list<array<string,mixed>>
*/
private function propagateAccountShorts(array $rows): array
{
$knownGroups = [];
foreach ($rows as $row) {
$payoutKey = $this->normalizeAccountValue($row['payout_account'] ?? null);
$accountShort = trim((string) ($row['account_short'] ?? ''));
if ($payoutKey !== '' && $accountShort !== '') {
$knownGroups[$payoutKey][] = $accountShort;
}
}
$knownShorts = [];
foreach ($knownGroups as $payoutKey => $shorts) {
$uniqueShorts = array_values(array_unique($shorts));
if (count($uniqueShorts) === 1) {
$knownShorts[$payoutKey] = $uniqueShorts[0];
}
}
foreach ($rows as &$row) {
if (trim((string) ($row['account_short'] ?? '')) !== '') {
continue;
}
$payoutKey = $this->normalizeAccountValue($row['payout_account'] ?? null);
if ($payoutKey !== '' && isset($knownShorts[$payoutKey])) {
$row['account_short'] = $knownShorts[$payoutKey];
}
}
unset($row);
return $rows;
}
private function nullableString(mixed $value): ?string
{
$value = trim((string) $value);
return $value === '' ? null : $value;
}
}