CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerShopDirectory.php
92451 be3b95e70e 接入店铺台账负责人字段 (#663)
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-26 22:34:20 +08:00

335 lines
13 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 Modules\EcommerceFinance\Repositories\StoreAccountMirrorRepository;
use Throwable;
/**
* 财务台账店铺台账的店铺目录。
*
* 平台和 ERP 店铺名称来自店铺维度报表的店铺资料;店铺主体只取当前
* 失效时间为「存续中」的主体期间。财务台账自己的账户和余额字段按 ERP
* 店铺名称合并,避免把报表销量或其他财务字段混入店铺台账。
*/
class FinanceLedgerShopDirectory
{
public function __construct(
private readonly BusinessInformationMirrorRepository $storeInformation,
private readonly StoreReportPeriodBuilder $periods,
private readonly StoreAccountMirrorRepository $storeAccounts,
private readonly DatabaseManager $databases,
) {
}
/**
* 返回 null 表示店铺维度报表镜像尚未就绪,调用方应回退到财务台账原表。
*
* @return list<array{
* id:int,
* platform:?string,
* shop_name:string,
* company_name:?string,
* account_short:?string,
* status:string,
* store_manager:?string,
* department_manager:?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();
$ownership = $this->storeOwnership();
$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);
$account = $this->matchAccount(
$storePayoutAccount ?? $financePayoutAccount,
$this->nullableString($financeShop->account_short ?? null),
$accountMaster,
);
$storeManager = $this->nullableString($store['store_manager'] ?? null);
$departmentManager = $this->nullableString($store['department_manager'] ?? null);
$ownershipMatch = $ownership[StoreAccountMirrorRepository::storeNameKey($erpStoreName)] ?? null;
if ($ownershipMatch !== null) {
if (trim((string) ($ownershipMatch['operation_owner'] ?? '')) !== '') {
$storeManager = trim((string) $ownershipMatch['operation_owner']);
}
if (trim((string) ($ownershipMatch['operation_department_head'] ?? '')) !== '') {
$departmentManager = trim((string) $ownershipMatch['operation_department_head']);
}
}
$rows[] = [
'id' => (int) ($store['id'] ?? 0),
'account_match_key' => $this->normalizeAccountValue($storePayoutAccount ?? $financePayoutAccount),
'platform' => $this->nullableString($store['platform'] ?? null),
'shop_name' => $erpStoreName,
'company_name' => $companyName,
'account_short' => $account['account_short'] ?? null,
'status' => '在营',
'store_manager' => $storeManager,
'department_manager' => $departmentManager,
'payout_account' => $this->nullableString($account['account_no'] ?? null),
'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->propagateAccountLinks($rows);
}
/**
* 与店铺维度报表一致:店铺账号镜像有值时覆盖店铺资料中的负责人字段。
*
* @return array<string,array{operation_owner:string,operation_department_head:string,operation_director:string,profit_center:string}>
*/
private function storeOwnership(): array
{
try {
return $this->storeAccounts->ownershipByStoreName();
} catch (Throwable) {
return [];
}
}
/** @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,array{account_short:string,account_no:?string}>,
* by_short:array<string,array{account_short:string,account_no:?string}>
* }
*/
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) {
$identities = $group
->map(fn (object $account): string => $this->accountIdentity($account))
->unique();
if ($key !== '' && $identities->count() === 1) {
$byAccountNo[$key] = $this->accountFields($group->first());
}
}
$byShort = [];
foreach ($accounts->groupBy(
static fn (object $account): string => trim((string) ($account->account_short ?? '')),
) as $short => $group) {
if ($short !== '' && $group->count() === 1) {
$byShort[$short] = $this->accountFields($group->first());
}
}
return [
'by_account_no' => $byAccountNo,
'by_short' => $byShort,
];
}
/**
* @param array{
* by_account_no:array<string,array{account_short:string,account_no:?string}>,
* by_short:array<string,array{account_short:string,account_no:?string}>
* } $accountMaster
* @return array{account_short:string,account_no:?string}|null
*/
private function matchAccount(
?string $payoutAccount,
?string $existingShort,
array $accountMaster,
): ?array {
$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])
? $accountMaster['by_short'][$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 propagateAccountLinks(array $rows): array
{
$knownGroups = [];
foreach ($rows as $row) {
$payoutKey = (string) ($row['account_match_key'] ?? '');
$accountShort = trim((string) ($row['account_short'] ?? ''));
if ($payoutKey !== '' && $accountShort !== '') {
$knownGroups[$payoutKey][] = [
'account_short' => $accountShort,
'account_no' => $row['payout_account'] ?? null,
];
}
}
$knownAccounts = [];
foreach ($knownGroups as $payoutKey => $accounts) {
$uniqueAccounts = [];
foreach ($accounts as $account) {
$identity = $this->normalizeAccountValue($account['account_no'] ?? null)
. "\0" . trim((string) ($account['account_short'] ?? ''));
$uniqueAccounts[$identity] = $account;
}
if (count($uniqueAccounts) === 1) {
$knownAccounts[$payoutKey] = reset($uniqueAccounts);
}
}
foreach ($rows as &$row) {
if (trim((string) ($row['account_short'] ?? '')) !== '') {
continue;
}
$payoutKey = (string) ($row['account_match_key'] ?? '');
if ($payoutKey !== '' && isset($knownAccounts[$payoutKey])) {
$row['account_short'] = $knownAccounts[$payoutKey]['account_short'];
$row['payout_account'] = $knownAccounts[$payoutKey]['account_no'];
}
}
unset($row);
foreach ($rows as &$row) {
unset($row['account_match_key']);
}
unset($row);
return $rows;
}
/** @return array{account_short:string,account_no:?string} */
private function accountFields(object $account): array
{
return [
'account_short' => trim((string) ($account->account_short ?? '')),
'account_no' => $this->nullableString($account->account_no ?? null),
];
}
private function accountIdentity(object $account): string
{
return $this->normalizeAccountValue($account->account_no ?? null)
. "\0" . trim((string) ($account->account_short ?? ''));
}
private function nullableString(mixed $value): ?string
{
$value = trim((string) $value);
return $value === '' ? null : $value;
}
}