CaiWuXiTong/modules/EcommerceFinance/Services/UpstreamSchemaCatalogSynchronizer.php

135 lines
4.9 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
/**
* 将已支持类型的上游表、视图和字段结构同步到共享系统数据库。
*
* 这里只读取 INFORMATION_SCHEMA 元数据,不读取业务行,也不保存连接资料。
* 获取完整结果成功后才替换单个来源的旧目录,避免网络故障清空现有字典。
*/
class UpstreamSchemaCatalogSynchronizer
{
public function __construct(
private readonly UpstreamSourceRegistry $sources,
private readonly UpstreamReadOnlyQueryFactory $queries,
) {
}
public function sync(string $source): array
{
$definition = $this->sources->definition($source);
if (! $this->sources->enabled($source)) {
throw new RuntimeException('该上游数据库尚未启用。');
}
$driver = $this->sources->driver($source);
$rows = $this->queries->forSource($source)->select((new UpstreamSqlDialect($driver))->catalogSql());
if ($driver === 'mysql') {
foreach ($rows as &$row) {
// SQL Server timestamp is binary; MySQL timestamp is temporal.
if (strtolower($row['data_type'] ?? '') === 'timestamp') $row['data_type'] = 'datetime';
}
unset($row);
}
if ($rows === []) {
throw new RuntimeException('上游数据库没有返回可用的表字段目录。');
}
$now = now();
$normalized = $this->normalizeAndDeduplicate($definition['key'], $rows, $now);
$objectCount = collect($normalized)
->map(fn (array $row): string => $row['schema_name'] . '.' . $row['object_name'])
->unique()
->count();
DB::transaction(function () use ($definition, $normalized): void {
DB::table('upstream_schema_columns')
->where('source_key', $definition['key'])
->delete();
foreach (array_chunk($normalized, 500) as $chunk) {
DB::table('upstream_schema_columns')->insert($chunk);
}
});
return [
'source' => $definition['key'],
'objects' => $objectCount,
'columns' => count($normalized),
'synchronized_at' => $now,
];
}
public function ready(): bool
{
return Schema::hasTable('upstream_schema_columns');
}
private function normalize(string $source, array $row, $now): array
{
$row = array_change_key_case($row, CASE_LOWER);
$schema = trim((string) ($row['schema_name'] ?? ''));
$object = trim((string) ($row['object_name'] ?? ''));
$column = trim((string) ($row['column_name'] ?? ''));
$type = strtolower(trim((string) ($row['object_type'] ?? 'table')));
$dataType = trim((string) ($row['data_type'] ?? ''));
if ($schema === '' || $object === '' || $column === '' || $dataType === '') {
throw new RuntimeException('上游数据库返回了不完整的字段目录。');
}
return [
'source_key' => $source,
'schema_name' => $schema,
'object_name' => $object,
'object_type' => $type === 'view' ? 'view' : 'table',
'column_name' => $column,
'ordinal_position' => max((int) ($row['ordinal_position'] ?? 0), 1),
'data_type' => $dataType,
'max_length' => $this->nullableInteger($row['max_length'] ?? null),
'numeric_precision' => $this->nullableInteger($row['numeric_precision'] ?? null),
'numeric_scale' => $this->nullableInteger($row['numeric_scale'] ?? null),
'is_nullable' => strtoupper(trim((string) ($row['is_nullable'] ?? 'YES'))) === 'YES',
'synchronized_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
}
/**
* 部分旧 SQL Server 库的 INFORMATION_SCHEMA 会返回重复字段。
* 共享库的目录键不允许重复,因此写入前按数据库常见的不区分大小写规则去重,
* 避免少量脏元数据让整个来源的目录同步回滚。
*/
private function normalizeAndDeduplicate(string $source, array $rows, $now): array
{
$unique = [];
foreach ($rows as $row) {
$normalized = $this->normalize($source, $row, $now);
$identity = implode("\0", [
mb_strtolower($normalized['source_key'], 'UTF-8'),
mb_strtolower($normalized['schema_name'], 'UTF-8'),
mb_strtolower($normalized['object_name'], 'UTF-8'),
mb_strtolower($normalized['column_name'], 'UTF-8'),
]);
$unique[$identity] ??= $normalized;
}
return array_values($unique);
}
private function nullableInteger(mixed $value): ?int
{
return $value === null || $value === '' ? null : (int) $value;
}
}