135 lines
4.9 KiB
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;
|
|
}
|
|
}
|