142 lines
4.7 KiB
PHP
142 lines
4.7 KiB
PHP
<?php
|
||
|
||
namespace App\Business;
|
||
|
||
use App\Models\Business\LaborExpenseSnapshot;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Validation\ValidationException;
|
||
|
||
class LaborExpenseDataStore
|
||
{
|
||
public const MAX_ROWS = 2000;
|
||
|
||
/**
|
||
* @return array{rows: array<int, array<string, mixed>>, row_count: int, source_name: ?string, saved_by: ?int, saved_at: ?string}
|
||
*/
|
||
public function load(): array
|
||
{
|
||
$snapshot = LaborExpenseSnapshot::query()
|
||
->where('company_id', company_id())
|
||
->first();
|
||
|
||
return [
|
||
'rows' => is_array($snapshot?->rows) ? array_values($snapshot->rows) : [],
|
||
'row_count' => (int) ($snapshot?->row_count ?? 0),
|
||
'source_name' => $snapshot?->source_name,
|
||
'saved_by' => $snapshot?->saved_by,
|
||
'saved_at' => $snapshot?->saved_at?->toIso8601String(),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param array<int, mixed> $rows
|
||
* @return array{rows: array<int, array<string, mixed>>, row_count: int, source_name: ?string, saved_by: ?int, saved_at: string}
|
||
*/
|
||
public function save(array $rows, ?string $sourceName, ?int $savedBy): array
|
||
{
|
||
if (count($rows) > self::MAX_ROWS) {
|
||
throw ValidationException::withMessages([
|
||
'rows' => '单次最多保存 ' . number_format(self::MAX_ROWS) . ' 行,请拆分后再保存。',
|
||
]);
|
||
}
|
||
|
||
$fields = collect(config('labor_expense_table.fields', []))
|
||
->filter(static fn (array $field): bool => ($field['kind'] ?? null) !== 'sequence'
|
||
&& blank($field['formula'] ?? null))
|
||
->values();
|
||
|
||
if ($fields->isEmpty()) {
|
||
throw ValidationException::withMessages([
|
||
'rows' => '人工费用表没有可保存的录入列。',
|
||
]);
|
||
}
|
||
|
||
$companyId = (int) company_id();
|
||
if ($companyId < 1) {
|
||
throw ValidationException::withMessages([
|
||
'rows' => '当前没有可保存的公司。',
|
||
]);
|
||
}
|
||
|
||
$normalizedRows = [];
|
||
foreach (array_values($rows) as $index => $row) {
|
||
if (! is_array($row)) {
|
||
throw ValidationException::withMessages([
|
||
'rows' => '第 ' . ($index + 1) . ' 行数据格式无效。',
|
||
]);
|
||
}
|
||
|
||
$record = [];
|
||
$hasValue = false;
|
||
foreach ($fields as $field) {
|
||
$value = $this->normalizeCell($row[$field['key']] ?? null, $field, $index + 1);
|
||
$record[$field['key']] = $value;
|
||
$hasValue = $hasValue || $value !== '';
|
||
}
|
||
|
||
if ($hasValue) {
|
||
$normalizedRows[] = $record;
|
||
}
|
||
}
|
||
|
||
$sourceName = trim((string) $sourceName);
|
||
$sourceName = $sourceName === '' ? null : mb_substr($sourceName, 0, 191);
|
||
$savedAt = now();
|
||
|
||
DB::transaction(function () use ($companyId, $sourceName, $savedBy, $savedAt, $normalizedRows): void {
|
||
LaborExpenseSnapshot::query()->updateOrCreate(
|
||
['company_id' => $companyId],
|
||
[
|
||
'source_name' => $sourceName,
|
||
'rows' => $normalizedRows,
|
||
'row_count' => count($normalizedRows),
|
||
'saved_by' => $savedBy,
|
||
'saved_at' => $savedAt,
|
||
],
|
||
);
|
||
});
|
||
|
||
return [
|
||
'rows' => $normalizedRows,
|
||
'row_count' => count($normalizedRows),
|
||
'source_name' => $sourceName,
|
||
'saved_by' => $savedBy,
|
||
'saved_at' => $savedAt->toIso8601String(),
|
||
];
|
||
}
|
||
|
||
private function normalizeCell(mixed $value, array $field, int $line): mixed
|
||
{
|
||
if ($value === null) {
|
||
return '';
|
||
}
|
||
|
||
$text = trim((string) $value);
|
||
if ($text === '') {
|
||
return '';
|
||
}
|
||
|
||
if (! in_array($field['kind'] ?? 'text', ['number', 'percent'], true)) {
|
||
if (mb_strlen($text) > 120) {
|
||
throw ValidationException::withMessages([
|
||
'rows' => "第 {$line} 行“{$field['label']}”不能超过 120 个字符。",
|
||
]);
|
||
}
|
||
|
||
return $text;
|
||
}
|
||
|
||
$isPercentText = ($field['kind'] ?? null) === 'percent' && str_ends_with($text, '%');
|
||
$number = str_replace([',', ',', '¥', '¥', '%'], '', $text);
|
||
if (! is_numeric($number)) {
|
||
throw ValidationException::withMessages([
|
||
'rows' => "第 {$line} 行“{$field['label']}”不是有效数字。",
|
||
]);
|
||
}
|
||
|
||
$normalized = (float) $number;
|
||
|
||
return $isPercentText ? $normalized / 100 : $normalized;
|
||
}
|
||
}
|