CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerFlowSummaryEditor.php
shanshi 78f3bbb2a1 feat: 支持账户资金汇总新增行和列 (#1064)
账户资金汇总支持新增账户行和自定义列;自定义列可逐账户填写、自动保存、左右插入和删除,并使用独立镜像表持久化。
2026-11-10 22:00:41 +08:00

275 lines
11 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use App\Utilities\FinanceLedgerIndependentAccounts;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class FinanceLedgerFlowSummaryEditor
{
private const MAX_COLUMNS = 30;
private readonly ConnectionInterface $connection;
public function __construct(DatabaseManager $databases)
{
$this->connection = $databases->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
}
/**
* @param list<string> $fieldNames
* @return list<array<string,mixed>>
*/
public function createColumns(
array $fieldNames,
?int $anchorFieldId = null,
string $position = 'end',
): array {
$position = in_array($position, ['left', 'right'], true) ? $position : 'end';
$fieldNames = array_values(array_map(function (mixed $value): string {
$text = trim((string) $value);
return preg_replace('/\s+/u', ' ', $text) ?? $text;
}, $fieldNames));
if ($fieldNames === [] || count($fieldNames) > 20) {
throw ValidationException::withMessages([
'field_names' => '一次可新增 1 至 20 个自定义列。',
]);
}
foreach ($fieldNames as $index => $fieldName) {
if ($fieldName === '') {
throw ValidationException::withMessages([
'field_names.' . $index => '请填写列名。',
]);
}
if (mb_strlen($fieldName) > 50) {
throw ValidationException::withMessages([
'field_names.' . $index => '列名不能超过 50 个字符。',
]);
}
}
$normalizedNames = array_map(static fn (string $name): string => mb_strtolower($name), $fieldNames);
if (count(array_unique($normalizedNames)) !== count($normalizedNames)) {
throw ValidationException::withMessages([
'field_names' => '一次新增的列名不能重复。',
]);
}
return $this->connection->transaction(function () use (
$fieldNames,
$normalizedNames,
$anchorFieldId,
$position,
): array {
$existingFields = $this->connection->table('fin_flow_summary_custom_fields')
->orderBy('sort_order')
->orderBy('id')
->lockForUpdate()
->get();
if ($existingFields->count() + count($fieldNames) > self::MAX_COLUMNS) {
throw ValidationException::withMessages([
'field_names' => '账户资金汇总最多可设置 ' . self::MAX_COLUMNS . ' 个自定义列。',
]);
}
$existingNames = $existingFields
->pluck('field_name')
->map(static fn (mixed $name): string => mb_strtolower(trim((string) $name)))
->all();
if (array_intersect($normalizedNames, $existingNames) !== []) {
throw ValidationException::withMessages([
'field_names' => '账户资金汇总已存在同名列。',
]);
}
$anchor = null;
if ($anchorFieldId !== null) {
$anchor = $existingFields->firstWhere('id', $anchorFieldId);
if ($anchor === null) {
throw ValidationException::withMessages([
'anchor_field_id' => '插入位置已变更,请刷新页面后重试。',
]);
}
}
$insertIndex = $existingFields->count();
if ($anchor !== null) {
$anchorIndex = $existingFields->search(
static fn (object $field): bool => (int) $field->id === $anchorFieldId,
);
$insertIndex = $position === 'left' ? (int) $anchorIndex : (int) $anchorIndex + 1;
}
$now = now();
$ordered = $existingFields->values()->all();
$placeholders = array_map(
static fn (string $name): array => ['new_name' => $name],
$fieldNames,
);
array_splice($ordered, $insertIndex, 0, $placeholders);
$created = [];
foreach ($ordered as $index => $field) {
$sortOrder = ($index + 1) * 10;
if (is_array($field) && isset($field['new_name'])) {
$values = [
'field_key' => (string) Str::uuid(),
'field_name' => (string) $field['new_name'],
'sort_order' => $sortOrder,
'created_by' => (string) (user()?->name ?? ''),
'updated_by' => (string) (user()?->name ?? ''),
'created_at' => $now,
'updated_at' => $now,
];
$id = (int) $this->connection->table('fin_flow_summary_custom_fields')
->insertGetId($values);
$created[] = ['id' => $id] + $values;
continue;
}
if ((int) $field->sort_order !== $sortOrder) {
$this->connection->table('fin_flow_summary_custom_fields')
->where('id', (int) $field->id)
->update([
'sort_order' => $sortOrder,
'updated_by' => (string) (user()?->name ?? ''),
'updated_at' => $now,
]);
}
}
$this->audit(
'flow_summary_col_add',
(string) Str::uuid(),
[
'position' => $position,
'anchor_field_id' => $anchorFieldId,
'after' => $created,
],
);
return $created;
});
}
/**
* @return array<string,mixed>
*/
public function deleteColumn(int $id): array
{
return $this->connection->transaction(function () use ($id): array {
$field = $this->connection->table('fin_flow_summary_custom_fields')
->where('id', $id)
->lockForUpdate()
->first();
if ($field === null) {
throw new NotFoundHttpException('未找到可删除的账户资金汇总自定义列。');
}
$this->connection->table('fin_flow_summary_custom_values')
->where('custom_field_id', $id)
->delete();
$this->connection->table('fin_flow_summary_custom_fields')->where('id', $id)->delete();
$this->audit(
'flow_summary_col_del',
(string) $field->field_key,
['before' => (array) $field, 'after' => null],
);
return (array) $field;
});
}
/**
* @return array<string,mixed>
*/
public function updateValue(int $fieldId, string $accountShort, mixed $value): array
{
$accountShort = preg_replace('/\s+/u', ' ', trim($accountShort)) ?? trim($accountShort);
$text = trim((string) ($value ?? ''));
$text = preg_replace('/\s+/u', ' ', $text) ?? $text;
return $this->connection->transaction(function () use ($fieldId, $accountShort, $text): array {
$field = $this->connection->table('fin_flow_summary_custom_fields')
->where('id', $fieldId)
->lockForUpdate()
->first();
if ($field === null) {
throw new NotFoundHttpException('自定义列已删除,请刷新页面后重试。');
}
if ($accountShort === '' || FinanceLedgerIndependentAccounts::contains($accountShort)) {
throw ValidationException::withMessages([
'account_short' => '请选择账户资金汇总中的账户。',
]);
}
if (! $this->connection->table('fin_accounts')->where('account_short', $accountShort)->exists()) {
throw ValidationException::withMessages([
'account_short' => '账户简称不存在,请刷新页面后重试。',
]);
}
$query = $this->connection->table('fin_flow_summary_custom_values')
->where('custom_field_id', $fieldId)
->where('account_short', $accountShort);
$existing = $query->first();
$before = $existing?->value;
$nextValue = $text !== '' ? mb_substr($text, 0, 4000) : null;
if ($nextValue === null) {
if ($existing !== null) {
$query->delete();
}
} elseif ($existing === null) {
$this->connection->table('fin_flow_summary_custom_values')->insert([
'custom_field_id' => $fieldId,
'account_short' => $accountShort,
'value' => $nextValue,
'created_at' => now(),
'updated_at' => now(),
]);
} else {
$this->connection->table('fin_flow_summary_custom_values')
->where('id', $existing->id)
->update([
'value' => $nextValue,
'updated_at' => now(),
]);
}
$this->audit(
'flow_summary_val_set',
(string) $field->field_key . '|' . $accountShort,
[
'before' => $before,
'after' => $nextValue,
],
);
return [
'field_id' => $fieldId,
'field_key' => (string) $field->field_key,
'account_short' => $accountShort,
'value' => $nextValue,
];
});
}
/**
* @param array<string,mixed> $changes
*/
private function audit(string $entityType, string $entityKey, array $changes): void
{
$this->connection->table('fin_ledger_edit_events')->insert([
'entity_type' => $entityType,
'entity_key' => mb_substr('flow_summary|' . $entityKey, 0, 200),
'changes' => json_encode($changes, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'user_id' => user()?->id,
'user_name' => (string) (user()?->name ?? ''),
'created_at' => now(),
]);
}
}