## 任务占用与边界 - 任务名称:全项目上游业务镜像目录与通用同步框架 - 实际开发者:WuYongJie - 目标结果:登记当前代码实际使用的上游原表,提供低压、可断点的首次全量、增量重叠和全量校准框架。 - 实际修改:13 个文件;新增通用数据集目录、动态镜像建表、同步状态、同步命令、生产低压轮询、测试和团队清单。 - 将写入的公共契约:`business_mirror_table_states`、`business-mirror:sync-tables`、`BusinessMirrorDatasetRegistry`、`BusinessMirrorSchemaManager`。 - 仅复用、明确不修改:`UpstreamReadOnlyQueryFactory`、`upstream_schema_columns`、既有 `product_price_mirror_rows`。 - 明确不包含:本 PR 不切换其余业务页面 Repository,不执行生产迁移或首次全量,不提交真实业务数据或凭据。 ## 合并前自审 - [x] 已重新拉取最新 `origin/main`,分支仅领先 1 个提交 - [x] `git merge-tree --write-tree origin/main HEAD` 无冲突 - [x] 完整 diff 共 13 个文件,未发现越界文件或敏感内容 - [x] 生产共享数据字典核对 38 个数据集,缺失对象/字段为 0 - [x] 临时 MariaDB 空库迁移实测成功,验证库已删除 - [x] 上游异常空结果不会清空已有镜像 - [x] 定时任务只轮转已完成首次全量的数据集,不会自动扫描未初始化大表 ## 测试 - `BusinessMirrorSynchronizerTest` 等相关测试:14 tests / 65 assertions 全通过 - 新同步器专项:5 tests / 25 assertions 全通过 - 新增 PHP 文件语法检查全部通过 - 两份生产激活脚本 `bash -n` 通过 - `git diff --check` 通过 ## 风险与回滚 - 包含新增迁移,但发布脚本遇到迁移仍会要求独立高风险授权;合并本身不会执行生产迁移。 - 首次全量必须逐数据集显式执行并做数量/翻页/筛选验收,特别是约 280 万行的大表不得随发布自动全扫。 - 回滚代码可回退本合并提交;生产迁移执行前必须备份 `caiwu_system_config` 与 `caiwu_business_mirror`。 Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/53
108 lines
3.8 KiB
PHP
108 lines
3.8 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
|
||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||
use Modules\EcommerceFinance\Services\BusinessMirrorSynchronizer;
|
||
use Throwable;
|
||
|
||
class SyncBusinessMirrors extends Command
|
||
{
|
||
protected $signature = 'business-mirror:sync-tables
|
||
{dataset? : 只同步指定数据集}
|
||
{--full : 执行或继续首次全量/校准同步}
|
||
{--batch= : 每批最多读取行数}
|
||
{--max-batches= : 每个数据集本轮最多批次数,0 表示直到完成}
|
||
{--max-datasets= : 本轮最多处理的数据集数量}';
|
||
|
||
protected $description = '将项目实际使用的上游表按稳定游标同步到共享业务镜像库';
|
||
|
||
public function handle(
|
||
BusinessMirrorDatasetRegistry $datasets,
|
||
BusinessMirrorSynchronizer $synchronizer,
|
||
GenericBusinessMirrorRepository $mirror,
|
||
): int
|
||
{
|
||
if (! (bool) config('upstream_mirrors.refresh_enabled', false) && ! $this->option('full')) {
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$requested = $this->argument('dataset');
|
||
$keys = $requested === null
|
||
? $this->scheduledKeys($datasets, $mirror)
|
||
: [(string) $requested];
|
||
$maxDatasets = max((int) ($this->option('max-datasets')
|
||
?? config('upstream_mirrors.table_sync.max_datasets_per_run', 1)), 1);
|
||
$processed = 0;
|
||
$failed = false;
|
||
|
||
foreach ($keys as $key) {
|
||
if ($processed >= $maxDatasets) {
|
||
break;
|
||
}
|
||
|
||
try {
|
||
$result = $synchronizer->sync(
|
||
$key,
|
||
(bool) $this->option('full'),
|
||
$this->option('batch') === null ? null : (int) $this->option('batch'),
|
||
$this->option('max-batches') === null ? null : (int) $this->option('max-batches'),
|
||
);
|
||
} catch (Throwable $exception) {
|
||
$failed = true;
|
||
Log::error('共享业务镜像表同步失败。', ['dataset' => $key, 'exception' => $exception::class]);
|
||
$this->error($key . ':同步失败,已保留上一版共享数据。');
|
||
$processed++;
|
||
continue;
|
||
}
|
||
|
||
if ($result['status'] === 'requires_full_sync') {
|
||
if ($requested !== null) {
|
||
$this->warn($key . ':尚未完成首次全量同步,请显式使用 --full。');
|
||
}
|
||
continue;
|
||
}
|
||
|
||
$processed++;
|
||
$this->info(sprintf(
|
||
'%s:%s,本轮 %d 批,累计处理 %d 行。',
|
||
$key,
|
||
$result['status'] === 'succeeded' ? '同步完成' : '同步进行中',
|
||
$result['batches'],
|
||
$result['rows'],
|
||
));
|
||
}
|
||
|
||
return $failed ? self::FAILURE : self::SUCCESS;
|
||
}
|
||
|
||
/** @return list<string> */
|
||
private function scheduledKeys(
|
||
BusinessMirrorDatasetRegistry $datasets,
|
||
GenericBusinessMirrorRepository $mirror,
|
||
): array {
|
||
$scheduled = [];
|
||
|
||
foreach ($datasets->all() as $key => $dataset) {
|
||
$state = $mirror->state($dataset);
|
||
if (! is_array($state) || ($state['initial_completed_at'] ?? null) === null) {
|
||
continue;
|
||
}
|
||
|
||
$scheduled[] = [
|
||
'key' => $key,
|
||
'last_attempted_at' => (string) ($state['last_attempted_at'] ?? ''),
|
||
];
|
||
}
|
||
|
||
usort($scheduled, fn (array $left, array $right): int =>
|
||
[$left['last_attempted_at'], $left['key']] <=> [$right['last_attempted_at'], $right['key']]
|
||
);
|
||
|
||
return array_column($scheduled, 'key');
|
||
}
|
||
}
|