新增供应商对账目录:财务端实时上传情况与行级差异(条码+单价匹配,四类口径),厂家端严格按模板上传(表头不一致/必填缺失/非数字/串厂一律失败);复用终版存储与流式读取。
259 lines
9.6 KiB
PHP
259 lines
9.6 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Closure;
|
||
use Illuminate\Database\ConnectionInterface;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class SupplierPayableOverrideStore
|
||
{
|
||
public function key(string $dataset, array $row): string
|
||
{
|
||
if (isset($row['sourceId']) && $row['sourceId'] !== '') {
|
||
return 'id:' . (string) ($row['recordType'] ?? '') . ':' . (string) $row['sourceId'];
|
||
}
|
||
if (isset($row['id']) && $row['id'] !== '') {
|
||
return 'id:' . (string) $row['id'];
|
||
}
|
||
|
||
$immutable = $row;
|
||
unset($immutable['_action_key'], $immutable['_overridden']);
|
||
|
||
return 'hash:' . hash('sha256', json_encode($immutable, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||
}
|
||
|
||
/** @param array<int,array<string,mixed>> $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<string,array<string,mixed>> $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<string,array<string,mixed>> $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<string> 某个前缀下已有的数据集名 */
|
||
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<int,array<string,mixed>>
|
||
*/
|
||
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'));
|
||
}
|
||
}
|