CaiWuXiTong/modules/EcommerceFinance/Services/SupplierPayableOverrideStore.php
renshen 87eaf48c2c 供应商对账:应付款项下新增对账页面(财务端数据 / 厂家端数据) (#364)
新增供应商对账目录:财务端实时上传情况与行级差异(条码+单价匹配,四类口径),厂家端严格按模板上传(表头不一致/必填缺失/非数字/串厂一律失败);复用终版存储与流式读取。
2026-10-17 08:49:17 +08:00

259 lines
9.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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'));
}
}