CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerAccountEditor.php
92451 040d0d08ae feat: 接入公司支付宝账单与台账手动编辑 (#797)
接入公司非店铺支付宝账单、受限批处理导入,并为账户总表及每日资金流水增加管理员手动编辑与审计。
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-30 20:03:04 +08:00

206 lines
9.0 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use App\Utilities\FinanceLedgerAccountStatus;
use App\Utilities\FinanceLedgerAccountType;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
class FinanceLedgerAccountEditor
{
private readonly ConnectionInterface $connection;
public function __construct(
DatabaseManager $databases,
private readonly FinanceLedgerCalculator $calculator,
) {
$this->connection = $databases->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
}
/**
* @param array<string,mixed> $data
* @return array<string,mixed>
*/
public function update(int $id, array $data): array
{
return $this->connection->transaction(function () use ($id, $data): array {
$account = $this->connection->table('fin_accounts')->where('id', $id)->lockForUpdate()->first();
if ($account === null) {
abort(404);
}
$oldShort = trim((string) $account->account_short);
$accountShort = $this->text($data['account_short'] ?? '');
$companyName = $this->text($data['company_name'] ?? '');
if ($accountShort === '' || $companyName === '') {
throw ValidationException::withMessages([
'account_short' => '账户简称和公司名称不能为空。',
]);
}
$duplicate = $this->connection->table('fin_accounts')
->where('account_short', $accountShort)
->where('id', '<>', $id)
->exists();
if ($duplicate) {
throw ValidationException::withMessages([
'account_short' => '账户简称已存在,请改用其他简称。',
]);
}
$accountType = $this->nullableText($data['account_type'] ?? null);
$accountNo = $this->nullableText($data['account_no'] ?? null);
$status = FinanceLedgerAccountStatus::normalize((string) ($data['status'] ?? ''));
$accountLast4 = FinanceLedgerAccountType::last4(
(string) $accountType,
(string) $accountNo,
(string) ($account->account_last4 ?? ''),
);
$values = [
'company_name' => $companyName,
'account_short' => $accountShort,
'region' => $this->nullableText($data['region'] ?? null),
'account_no' => $accountNo,
'bank_name' => $this->nullableText($data['bank_name'] ?? null),
'bank_city_branch' => $this->nullableText($data['bank_city_branch'] ?? null),
'account_type' => $accountType,
'company_plan' => $this->nullableText($data['company_plan'] ?? null),
'account_should_keep' => $this->nullableText($data['account_should_keep'] ?? null),
'account_last4' => $accountLast4,
'single_limit' => $this->nullableText($data['single_limit'] ?? null),
'daily_limit' => $this->nullableText($data['daily_limit'] ?? null),
'monthly_limit' => $this->nullableText($data['monthly_limit'] ?? null),
'yearly_limit' => $this->nullableText($data['yearly_limit'] ?? null),
'clearing_days' => $this->nullableNumber($data['clearing_days'] ?? null),
'raise_target' => $this->nullableText($data['raise_target'] ?? null),
'raise_status' => $this->nullableText($data['raise_status'] ?? null),
'raise_actions' => $this->nullableText($data['raise_actions'] ?? null),
'done_actions' => $this->nullableText($data['done_actions'] ?? null),
'next_action' => $this->nullableText($data['next_action'] ?? null),
'remark' => $this->nullableText($data['remark'] ?? null),
'owner' => $this->nullableText($data['owner'] ?? null),
'updated_at' => now(),
];
$before = (array) $account;
if ($oldShort !== $accountShort) {
$this->renameReferences($oldShort, $accountShort);
}
$this->connection->table('fin_accounts')->where('id', $id)->update($values);
$this->upsertStatus($accountShort, $status);
$this->connection->table('fin_account_type_overrides')
->where('account_short', $accountShort)
->delete();
$this->connection->table('fin_daily_flows')
->where('account_short', $accountShort)
->update([
'company_name' => $companyName,
'updated_at' => now(),
]);
$this->calculator->refreshAccountAmounts($accountShort);
$after = (array) $this->connection->table('fin_accounts')->where('id', $id)->first();
$this->connection->table('fin_ledger_edit_events')->insert([
'entity_type' => 'account',
'entity_key' => (string) $id,
'changes' => json_encode([
'before' => $before,
'after' => $after,
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'user_id' => user()?->id,
'user_name' => (string) (user()?->name ?? ''),
'created_at' => now(),
]);
return $after;
});
}
private function upsertStatus(string $accountShort, string $status): void
{
$values = [
'status' => $status,
'updated_by' => (string) (user()?->name ?? ''),
'updated_at' => now(),
];
if ($this->connection->table('fin_account_statuses')->where('account_short', $accountShort)->exists()) {
$this->connection->table('fin_account_statuses')->where('account_short', $accountShort)->update($values);
return;
}
$this->connection->table('fin_account_statuses')->insert($values + [
'account_short' => $accountShort,
'created_at' => now(),
]);
}
private function renameReferences(string $oldShort, string $newShort): void
{
$now = now();
foreach ([
'fin_account_opening' => ['account_short' => [], 'updated_at' => $now],
'fin_daily_flows' => ['account_short' => [], 'updated_at' => $now],
'fin_transactions' => ['account_short' => [], 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => $now],
'fin_account_map' => ['account_short' => [], 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => $now],
'fin_account_statuses' => ['account_short' => [], 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => $now],
'fin_account_type_overrides' => ['account_short' => [], 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => $now],
'fin_account_todos' => ['account_short' => [], 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => $now],
'fin_account_keep_todos' => ['account_short' => [], 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => $now],
'fin_shops' => ['account_short' => [], 'updated_at' => $now],
] as $table => $extra) {
if (! Schema::connection($this->connection->getName())->hasTable($table)
|| ! Schema::connection($this->connection->getName())->hasColumn($table, 'account_short')) {
continue;
}
$this->connection->table($table)->where('account_short', $oldShort)->update([
'account_short' => $newShort,
...$extra,
]);
}
if (Schema::connection($this->connection->getName())->hasTable('fin_daily_flow_overrides')) {
$this->connection->table('fin_daily_flow_overrides')
->where('account_short', $oldShort)
->update([
'account_short' => $newShort,
'updated_at' => $now,
]);
}
$this->connection->table('fin_account_map_events')
->where('old_account_short', $oldShort)
->update(['old_account_short' => $newShort]);
$this->connection->table('fin_account_map_events')
->where('new_account_short', $oldShort)
->update(['new_account_short' => $newShort]);
}
private function text(mixed $value): string
{
return preg_replace('/\s+/u', ' ', trim((string) $value)) ?? trim((string) $value);
}
private function nullableText(mixed $value): ?string
{
$text = $this->text($value);
return $text !== '' ? $text : null;
}
private function nullableNumber(mixed $value): ?float
{
if ($value === null || trim((string) $value) === '') {
return null;
}
return round((float) $value, 2);
}
}