>, 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 $rows * @return array{rows: array>, 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; } }