335 lines
13 KiB
PHP
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;
|
|
}
|
|
}
|