> $rows */ public function decorate(string $dataset, array $rows, int $companyId): array { if ($rows === []) { return []; } $keys = []; foreach ($rows as $row) { $keys[] = $this->key($dataset, $row); } $overrides = $this->connection()->table('supplier_payable_overrides') ->where('company_id', $companyId) ->where('dataset', $dataset) ->whereIn('record_key', $keys) ->get() ->keyBy('record_key'); foreach ($rows as $index => &$row) { $key = $keys[$index]; $row['_action_key'] = $key; $override = $overrides->get($key); if ($override) { $values = json_decode((string) $override->values, true) ?: []; $row = array_merge($row, $values); $row['_overridden'] = true; } else { $row['_overridden'] = false; } } unset($row); return $rows; } /** @return array{created:int,updated:int} */ public function save(string $dataset, string $recordKey, array $values, int $companyId, ?int $userId, string $source = 'manual'): array { $connection = $this->connection(); $existing = $connection->table('supplier_payable_overrides') ->where(['company_id' => $companyId, 'dataset' => $dataset, 'record_key' => $recordKey]) ->first(); $before = $existing ? (json_decode((string) $existing->values, true) ?: []) : null; $now = now(); $connection->transaction(function () use ($connection, $existing, $dataset, $recordKey, $values, $companyId, $userId, $now, $source, $before): void { $connection->table('supplier_payable_overrides')->updateOrInsert( ['company_id' => $companyId, 'dataset' => $dataset, 'record_key' => $recordKey], [ 'values' => json_encode($values, JSON_UNESCAPED_UNICODE), 'updated_by' => $userId, 'created_by' => $existing->created_by ?? $userId, 'created_at' => $existing->created_at ?? $now, 'updated_at' => $now, ], ); $connection->table('supplier_payable_override_events')->insert([ 'company_id' => $companyId, 'dataset' => $dataset, 'record_key' => $recordKey, 'source' => $source, 'before_values' => $before === null ? null : json_encode($before, JSON_UNESCAPED_UNICODE), 'after_values' => json_encode($values, JSON_UNESCAPED_UNICODE), 'user_id' => $userId, 'created_at' => $now, ]); }); return ['created' => $existing ? 0 : 1, 'updated' => $existing ? 1 : 0]; } /** * 批量保存(大文件导入用):按记录键 upsert,并批量写审计事件。 * * @param array> $records 记录键 => 字段值 * @return array{created:int,updated:int} */ public function saveBatch(string $dataset, array $records, int $companyId, ?int $userId, string $source = 'import'): array { if ($records === []) { return ['created' => 0, 'updated' => 0]; } $connection = $this->connection(); $now = now(); $created = 0; $updated = 0; $connection->transaction(function () use ($connection, $dataset, $records, $companyId, $userId, $source, $now, &$created, &$updated): void { $existing = []; foreach (array_chunk(array_keys($records), 1000) as $keys) { foreach ($connection->table('supplier_payable_overrides') ->where('company_id', $companyId) ->where('dataset', $dataset) ->whereIn('record_key', $keys) ->get() as $row) { $existing[(string) $row->record_key] = $row; } } $upserts = []; $events = []; foreach ($records as $key => $values) { $before = $existing[$key] ?? null; $before === null ? $created++ : $updated++; $encoded = $this->encodeValues($values); $upserts[] = [ 'company_id' => $companyId, 'dataset' => $dataset, 'record_key' => $key, 'values' => $encoded, 'created_by' => $before->created_by ?? $userId, 'updated_by' => $userId, 'created_at' => $before->created_at ?? $now, 'updated_at' => $now, ]; $events[] = [ 'company_id' => $companyId, 'dataset' => $dataset, 'record_key' => $key, 'source' => $source, 'before_values' => $before === null ? null : (string) $before->values, 'after_values' => $encoded, 'user_id' => $userId, 'created_at' => $now, ]; } foreach (array_chunk($upserts, 500) as $chunk) { $connection->table('supplier_payable_overrides')->upsert( $chunk, ['company_id', 'dataset', 'record_key'], ['values', 'updated_by', 'updated_at'], ); } foreach (array_chunk($events, 500) as $chunk) { $connection->table('supplier_payable_override_events')->insert($chunk); } }); return ['created' => $created, 'updated' => $updated]; } public function transaction(Closure $callback): mixed { return $this->connection()->transaction($callback); } /** * 整份替换某个数据集(厂家对账单:以最新一版为准),不写逐行审计事件。 * * @param array> $records */ public function replaceAll(string $dataset, array $records, int $companyId, ?int $userId): int { $connection = $this->connection(); $now = now(); $rows = []; foreach ($records as $key => $values) { $rows[] = [ 'company_id' => $companyId, 'dataset' => $dataset, 'record_key' => (string) $key, 'values' => $this->encodeValues($values), 'created_by' => $userId, 'updated_by' => $userId, 'created_at' => $now, 'updated_at' => $now, ]; } $connection->transaction(function () use ($connection, $dataset, $companyId, $rows): void { $connection->table('supplier_payable_overrides') ->where('company_id', $companyId) ->where('dataset', $dataset) ->delete(); foreach (array_chunk($rows, 500) as $chunk) { $connection->table('supplier_payable_overrides')->insert($chunk); } }); return count($rows); } /** @return list 某个前缀下已有的数据集名 */ public function datasetsWithPrefix(string $prefix, int $companyId): array { return $this->connection()->table('supplier_payable_overrides') ->where('company_id', $companyId) ->where('dataset', 'like', addcslashes($prefix, '%_\\') . '%') ->distinct() ->orderBy('dataset') ->pluck('dataset') ->map(static fn (mixed $value): string => (string) $value) ->all(); } /** 值编码为 JSON;脏字节(非法 UTF-8)用替代字符,避免整批写入失败。 */ private function encodeValues(array $values): string { return (string) json_encode($values, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE); } /** * 列出某个数据集已经保存的全部记录(「终版」目录的独立数据就是这些行)。 * * @return array> */ public function listRows(string $dataset, int $companyId): array { return $this->connection()->table('supplier_payable_overrides') ->where('company_id', $companyId) ->where('dataset', $dataset) ->orderBy('id') ->get() ->map(static function (object $record): array { $values = json_decode((string) $record->values, true) ?: []; $values['_action_key'] = (string) $record->record_key; return $values; }) ->all(); } private function connection(): ConnectionInterface { return DB::connection(config('database.supplier_payable_maintenance_connection')); } }