342 lines
12 KiB
PHP
342 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Services;
|
|
|
|
use App\Business\XlsxSheetReader;
|
|
use App\Utilities\FinanceLedgerAccountShouldKeep;
|
|
use App\Utilities\FinanceLedgerAccountType;
|
|
use Generator;
|
|
use Illuminate\Database\ConnectionInterface;
|
|
use Illuminate\Database\DatabaseManager;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* 合并导入网商银行基本信息。
|
|
*
|
|
* 文件中的账户会按网商账号、其次按公司名称匹配现有记录。匹配后只更新本表提供的
|
|
* 字段;表中没有的现有账户会保留,避免基础资料调整误删历史流水关联。
|
|
*/
|
|
class FinanceLedgerWangshangBasicImporter
|
|
{
|
|
private const MAX_ROWS = 1000;
|
|
|
|
private const REQUIRED_HEADERS = [
|
|
'公司名称',
|
|
'账户简称',
|
|
'法人',
|
|
'账户状态',
|
|
'网商账号',
|
|
'开户行',
|
|
'绑定支付宝',
|
|
'是否保留',
|
|
'后4位',
|
|
'备注',
|
|
];
|
|
|
|
private readonly ConnectionInterface $connection;
|
|
|
|
public function __construct(
|
|
DatabaseManager $databases,
|
|
private readonly XlsxSheetReader $reader,
|
|
) {
|
|
$this->connection = $databases->connection(
|
|
(string) config('upstream_mirrors.connection', 'business_mirror'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array{total:int,inserted:int,updated:int,unchanged:int,preserved:int}
|
|
*/
|
|
public function import(string $path): array
|
|
{
|
|
if (! is_file($path)) {
|
|
throw new RuntimeException('导入文件不存在。');
|
|
}
|
|
|
|
return $this->connection->transaction(function () use ($path): array {
|
|
$current = $this->connection->table('fin_accounts')
|
|
->where(function ($scope): void {
|
|
$scope->where('account_short', 'like', '%网商%')
|
|
->orWhere('account_type', 'like', '%网商%');
|
|
})
|
|
->orderBy('id')
|
|
->lockForUpdate()
|
|
->get()
|
|
->all();
|
|
|
|
$byAccountNo = [];
|
|
$byCompany = [];
|
|
foreach ($current as $account) {
|
|
$accountNo = $this->normalizeAccountNo($account->account_no ?? null);
|
|
if ($accountNo !== '') {
|
|
$byAccountNo[$accountNo] = $account;
|
|
}
|
|
$byCompany[(string) $account->company_name][] = $account;
|
|
}
|
|
|
|
$used = [];
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
$unchanged = 0;
|
|
$now = now();
|
|
|
|
foreach ($this->rows($path) as $row) {
|
|
$account = $byAccountNo[$row['account_no']] ?? null;
|
|
if ($account !== null && isset($used[(int) $account->id])) {
|
|
$account = null;
|
|
}
|
|
if ($account === null) {
|
|
foreach ($byCompany[$row['company_name']] ?? [] as $candidate) {
|
|
if (! isset($used[(int) $candidate->id])) {
|
|
$account = $candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($account !== null) {
|
|
$used[(int) $account->id] = true;
|
|
$values = $this->updateValues($row, $account);
|
|
if ($values === []) {
|
|
$unchanged++;
|
|
continue;
|
|
}
|
|
$this->connection->table('fin_accounts')->where('id', $account->id)->update([
|
|
...$values,
|
|
'updated_at' => $now,
|
|
]);
|
|
$updated++;
|
|
continue;
|
|
}
|
|
|
|
$accountShort = $row['account_short'] !== ''
|
|
? $this->uniqueAccountShort($row['account_short'])
|
|
: $this->uniqueAccountShort($this->deriveAccountShort($row['company_name']));
|
|
$this->ensureCompany($row['company_name'], $now);
|
|
$this->connection->table('fin_accounts')->insert([
|
|
'company_name' => $row['company_name'],
|
|
'account_short' => $accountShort,
|
|
'legal_representative' => $row['legal_representative'],
|
|
'region' => null,
|
|
'account_no' => $row['account_no'],
|
|
'bank_name' => $row['bank_name'],
|
|
'bound_alipay' => $row['bound_alipay'],
|
|
'bank_city_branch' => null,
|
|
'account_type' => '网商银行',
|
|
'company_plan' => $row['company_plan'],
|
|
'account_should_keep' => $row['account_should_keep'],
|
|
'account_last4' => $row['account_last4'],
|
|
'current_balance' => 0,
|
|
'blocked_amount' => 0,
|
|
'daily_available' => 0,
|
|
'remark' => $row['remark'],
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
$inserted++;
|
|
}
|
|
|
|
if ($inserted + $updated + $unchanged === 0) {
|
|
throw new RuntimeException('表格中没有可导入的网商银行账户。');
|
|
}
|
|
|
|
return [
|
|
'total' => $inserted + $updated + $unchanged,
|
|
'inserted' => $inserted,
|
|
'updated' => $updated,
|
|
'unchanged' => $unchanged,
|
|
'preserved' => count(array_filter(
|
|
$current,
|
|
static fn (object $account): bool => ! isset($used[(int) $account->id]),
|
|
)),
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return Generator<int,array{
|
|
* company_name:string,account_short:string,legal_representative:?string,
|
|
* account_no:string,bank_name:?string,bound_alipay:?string,
|
|
* company_plan:?string,account_should_keep:?string,
|
|
* account_last4:?string,remark:?string
|
|
* }>
|
|
*/
|
|
private function rows(string $path): Generator
|
|
{
|
|
$seenAccountNos = [];
|
|
$line = 1;
|
|
foreach ($this->reader->rows($path, self::REQUIRED_HEADERS) as $row) {
|
|
$line++;
|
|
if ($line - 1 > self::MAX_ROWS) {
|
|
throw new RuntimeException('单次最多导入 ' . self::MAX_ROWS . ' 条网商银行账户。');
|
|
}
|
|
$value = static fn (string $header): mixed => $row[$header] ?? null;
|
|
$company = $this->text($value('公司名称'));
|
|
$accountNo = $this->normalizeAccountNo($value('网商账号'));
|
|
if ($company === '' && $accountNo === '') {
|
|
continue;
|
|
}
|
|
if ($company === '') {
|
|
throw new RuntimeException("第 {$line} 行缺少公司名称。");
|
|
}
|
|
if ($accountNo === '') {
|
|
throw new RuntimeException("第 {$line} 行缺少网商账号。");
|
|
}
|
|
if (isset($seenAccountNos[$accountNo])) {
|
|
throw new RuntimeException("第 {$line} 行网商账号与第 {$seenAccountNos[$accountNo]} 行重复。");
|
|
}
|
|
$seenAccountNos[$accountNo] = $line;
|
|
|
|
$accountType = '网商银行';
|
|
yield [
|
|
'company_name' => $company,
|
|
'account_short' => $this->text($value('账户简称')),
|
|
'legal_representative' => $this->nullableText($value('法人')),
|
|
'account_no' => $accountNo,
|
|
'bank_name' => $this->nullableText($value('开户行')),
|
|
'bound_alipay' => $this->nullableText($value('绑定支付宝')),
|
|
'company_plan' => $this->companyPlan($value('账户状态')),
|
|
'account_should_keep' => $this->shouldKeep($value('是否保留')),
|
|
'account_last4' => FinanceLedgerAccountType::last4(
|
|
$accountType,
|
|
$accountNo,
|
|
$this->text($value('后4位')),
|
|
) ?: null,
|
|
'remark' => $this->nullableText($value('备注')),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $row
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function updateValues(array $row, object $account): array
|
|
{
|
|
$values = [
|
|
'company_name' => $row['company_name'],
|
|
'account_no' => $row['account_no'],
|
|
'account_type' => trim((string) ($account->account_type ?? '')) !== ''
|
|
? (string) $account->account_type
|
|
: '网商银行',
|
|
'legal_representative' => $row['legal_representative'] ?? $account->legal_representative,
|
|
'bank_name' => $row['bank_name'] ?? $account->bank_name,
|
|
'bound_alipay' => $row['bound_alipay'] ?? $account->bound_alipay,
|
|
'company_plan' => $row['company_plan'] ?? $account->company_plan,
|
|
'account_should_keep' => $row['account_should_keep'] ?? $account->account_should_keep,
|
|
'account_last4' => $row['account_last4'] ?? $account->account_last4,
|
|
'remark' => $row['remark'] ?? $account->remark,
|
|
];
|
|
|
|
$changed = [];
|
|
foreach ($values as $field => $value) {
|
|
$current = $account->{$field} ?? null;
|
|
if ((string) ($value ?? '') !== (string) ($current ?? '')) {
|
|
$changed[$field] = $value;
|
|
}
|
|
}
|
|
|
|
return $changed;
|
|
}
|
|
|
|
private function ensureCompany(string $companyName, mixed $now): void
|
|
{
|
|
if ($this->connection->table('fin_companies')->where('name', $companyName)->exists()) {
|
|
return;
|
|
}
|
|
|
|
$this->connection->table('fin_companies')->insert([
|
|
'name' => $companyName,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
private function uniqueAccountShort(string $base): string
|
|
{
|
|
$base = mb_substr($this->text($base), 0, 90);
|
|
$candidate = $base;
|
|
$suffix = 1;
|
|
while ($this->connection->table('fin_accounts')->where('account_short', $candidate)->exists()) {
|
|
$suffix++;
|
|
$candidate = $base . $suffix;
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
private function deriveAccountShort(string $companyName): string
|
|
{
|
|
$name = preg_replace(
|
|
'/^(?:常州市?|天津市?|上海市?|深圳市?|杭州市?|义乌市?|南召县?|佛山市?|苏州市?|南京市?|青岛市?|株洲市?|广州市?|永康市?|武进区)/u',
|
|
'',
|
|
$companyName,
|
|
) ?? $companyName;
|
|
$name = preg_replace(
|
|
'/(?:有限责任公司|股份有限公司|有限公司|个人独资|商贸行|家具有限公司|家具店|公司)$/u',
|
|
'',
|
|
$name,
|
|
) ?? $name;
|
|
$name = mb_substr(trim($name) !== '' ? trim($name) : $companyName, 0, 18);
|
|
|
|
return $name . '网商';
|
|
}
|
|
|
|
private function companyPlan(mixed $value): ?string
|
|
{
|
|
$text = $this->text($value);
|
|
if ($text === '') {
|
|
return null;
|
|
}
|
|
if (str_contains($text, '注销') || str_contains($text, '不保留')) {
|
|
return '待注销';
|
|
}
|
|
if (str_contains($text, '保留')) {
|
|
return '保留';
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
|
|
private function shouldKeep(mixed $value): ?string
|
|
{
|
|
$text = $this->text($value);
|
|
if ($text === '') {
|
|
return null;
|
|
}
|
|
if (in_array($text, ['注销', '不保留', '不需要', '否'], true)) {
|
|
return FinanceLedgerAccountShouldKeep::NO;
|
|
}
|
|
$normalized = FinanceLedgerAccountShouldKeep::normalize($text);
|
|
|
|
return in_array($normalized, FinanceLedgerAccountShouldKeep::OPTIONS, true)
|
|
? $normalized
|
|
: null;
|
|
}
|
|
|
|
private function normalizeAccountNo(mixed $value): string
|
|
{
|
|
return preg_replace('/[\s-]+/u', '', $this->text($value)) ?? '';
|
|
}
|
|
|
|
private function text(mixed $value): string
|
|
{
|
|
if ($value === null) {
|
|
return '';
|
|
}
|
|
$text = trim((string) $value);
|
|
if (in_array($text, ['#N/A', '#DIV/0!', '#VALUE!', '#REF!', 'None'], true)) {
|
|
return '';
|
|
}
|
|
|
|
return preg_replace('/\s+/u', ' ', $text) ?? $text;
|
|
}
|
|
|
|
private function nullableText(mixed $value): ?string
|
|
{
|
|
$text = $this->text($value);
|
|
|
|
return $text !== '' ? $text : null;
|
|
}
|
|
}
|