供应商管理增加每天08:15完整同步 #302
49
app/Console/Commands/SyncSupplierManagementMirror.php
Normal file
49
app/Console/Commands/SyncSupplierManagementMirror.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorSynchronizer;
|
||||
use Throwable;
|
||||
|
||||
class SyncSupplierManagementMirror extends Command
|
||||
{
|
||||
protected $signature = 'business-mirror:sync-supplier-management';
|
||||
|
||||
protected $description = '完整校准供应商管理的档案、合同、联系人和打款资料共享镜像';
|
||||
|
||||
public function handle(BusinessMirrorDatasetRegistry $datasets, BusinessMirrorSynchronizer $synchronizer): int
|
||||
{
|
||||
if (! (bool) config('upstream_mirrors.refresh_enabled', false)) {
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$failed = false;
|
||||
foreach ([SupplierManagementMirrorRepository::DATASET, ...array_values(SupplierManagementMirrorRepository::RELATED)] as $key) {
|
||||
if (! $datasets->scheduled($key)) {
|
||||
$this->warn($key . ':未启用后台同步,已跳过。');
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 四个已接入的小数据集顺序校准,每批沿用现有大小,直到完整结束。
|
||||
$result = $synchronizer->sync($key, true, null, 0);
|
||||
if ($result['status'] !== 'succeeded') {
|
||||
$failed = true;
|
||||
$this->error($key . ':同步未完成,请检查同步状态。');
|
||||
continue;
|
||||
}
|
||||
$this->info(sprintf('%s:同步完成,本轮 %d 批,累计处理 %d 行。', $key, $result['batches'], $result['rows']));
|
||||
} catch (Throwable $exception) {
|
||||
$failed = true;
|
||||
Log::error('供应商管理每日镜像同步失败。', ['dataset' => $key, 'exception' => $exception::class]);
|
||||
$this->error($key . ':同步失败,请检查同步状态。');
|
||||
}
|
||||
}
|
||||
|
||||
return $failed ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -53,6 +53,12 @@ class Kernel extends ConsoleKernel
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground();
|
||||
|
||||
$schedule->command('business-mirror:sync-supplier-management')
|
||||
->dailyAt('08:15')
|
||||
->timezone('Asia/Shanghai')
|
||||
->withoutOverlapping(60)
|
||||
->runInBackground();
|
||||
|
||||
// 店铺违规信息预警:每天同步副本库抓取结果到统一表(本地补充字段不覆盖)。
|
||||
$schedule->command('store-violations:sync')
|
||||
->dailyAt('05:30')
|
||||
|
||||
@ -197,6 +197,8 @@ CRON_TZ=Asia/Shanghai
|
||||
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-third-party-warehouse-storage --max-batches=0 --quiet && /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:warm-report-caches --quiet' >/dev/null 2>&1
|
||||
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --quiet >/dev/null 2>&1
|
||||
0 5 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-online-daifa-price --quiet' >/dev/null 2>&1
|
||||
# 供应商管理每天北京时间 08:15 完整校准档案、合同、联系人与打款资料。
|
||||
15 8 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-supplier-management --quiet >/dev/null 2>&1
|
||||
*/2 * * * * root /usr/bin/flock -n /run/caiwu-esign-polling.lock /usr/bin/docker exec -u www-data caiwu_app php artisan esign:poll-signatures --quiet >/dev/null 2>&1
|
||||
28 1 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan jushuitan:sync-shipping --days=3 >/dev/null 2>&1
|
||||
# 头条放心购(店铺收款账户账单 › 抖音账户)资金流水:每天 09:30 定点把该渠道所用
|
||||
|
||||
@ -201,6 +201,8 @@ CRON_TZ=Asia/Shanghai
|
||||
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-third-party-warehouse-storage --max-batches=0 --quiet && /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:warm-report-caches --quiet' >/dev/null 2>&1
|
||||
0 0 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-jushuitan-cost-price-history --quiet >/dev/null 2>&1
|
||||
0 5 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-purchase-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-ding-online-daifa-price --quiet' >/dev/null 2>&1
|
||||
# 供应商管理每天北京时间 08:15 完整校准档案、合同、联系人与打款资料。
|
||||
15 8 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-supplier-management --quiet >/dev/null 2>&1
|
||||
*/2 * * * * root /usr/bin/flock -n /run/caiwu-esign-polling.lock /usr/bin/docker exec -u www-data caiwu_app php artisan esign:poll-signatures --quiet >/dev/null 2>&1
|
||||
28 1 * * * root /usr/bin/flock -w 1800 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan jushuitan:sync-shipping --days=3 >/dev/null 2>&1
|
||||
# 头条放心购(店铺收款账户账单 › 抖音账户)资金流水:每天 09:30 定点把该渠道所用
|
||||
|
||||
@ -91,6 +91,8 @@
|
||||
|
||||
## 同步规则
|
||||
|
||||
供应商管理保留现有后台轮转刷新,同时每天北京时间 08:15 通过 `business-mirror:sync-supplier-management` 顺序完整校准供应商档案(含易签宝账号)、合同、联系人和打款资料。生产任务使用现有上游同步锁和 `www-data`,有其他同步占用时排队执行;每批沿用现有行数限制,直到四个数据集分别完成,失败记录在原镜像同步状态中。固定任务随正式发布安装,页面继续显示最近同步时间。
|
||||
|
||||
1. 首次全量只能通过 `business-mirror:sync-tables <dataset> --full` 显式启动;失败或中断后使用同一命令继续。
|
||||
2. 日常任务只处理已经完成首次全量的数据集。存在可靠更新时间的表使用 48 小时重叠窗口;没有可靠更新时间的表按稳定主键追加。`rpt.jst_sku` 由普通商品专用任务每分钟优先处理,每轮默认最多 5 批、每批 1000 行,不再参加通用数据集轮转。
|
||||
3. 全量校准使用新的同步代次,只有全部完成后才删除镜像中本轮未出现的旧行,因此中途失败不会清空当前可用数据。
|
||||
|
||||
@ -34,6 +34,7 @@ class BusinessMirrorScheduleTest extends TestCase
|
||||
{
|
||||
config(['upstream_mirrors.refresh_enabled' => true, 'upstream_pipeline.sync.max_datasets' => 2]);
|
||||
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
||||
$registry->shouldReceive('definitionValue')->with(Mockery::type('string'), 'full_reconcile_hours', 0)->andReturn(0);
|
||||
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
||||
$objects = [];
|
||||
foreach (['routine-a', 'routine-b', 'catchup-z'] as $key) {
|
||||
@ -67,6 +68,7 @@ class BusinessMirrorScheduleTest extends TestCase
|
||||
$catalog = Mockery::mock(BusinessMirrorDataset::class);
|
||||
$other = Mockery::mock(BusinessMirrorDataset::class);
|
||||
$datasets = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
||||
$datasets->shouldReceive('definitionValue')->with(Mockery::type('string'), 'full_reconcile_hours', 0)->andReturn(0);
|
||||
$datasets->shouldReceive('all')->once()->andReturn([
|
||||
ProductCatalogMirrorRepository::DATASET_KEY => $catalog,
|
||||
'test.dbo.other' => $other,
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Console;
|
||||
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorSynchronizer;
|
||||
use RuntimeException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SupplierManagementMirrorSyncCommandTest extends TestCase
|
||||
{
|
||||
private const DATASETS = [
|
||||
'jiansun.dbo.js_supplier_View',
|
||||
'jiansun.dbo.js_supplier_contract_View',
|
||||
'jiansun.dbo.js_supplier_linkers',
|
||||
'jiansun.dbo.js_supplier_payinfo',
|
||||
];
|
||||
|
||||
public function test_daily_sync_finishes_all_four_datasets_in_order(): void
|
||||
{
|
||||
config(['upstream_mirrors.refresh_enabled' => true]);
|
||||
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
||||
$sync = Mockery::mock(BusinessMirrorSynchronizer::class);
|
||||
foreach (self::DATASETS as $key) {
|
||||
$registry->shouldReceive('scheduled')->once()->with($key)->andReturn(true);
|
||||
$sync->shouldReceive('sync')->once()->with($key, true, null, 0)->ordered()
|
||||
->andReturn(['status' => 'succeeded', 'batches' => 3, 'rows' => 1200]);
|
||||
}
|
||||
$this->app->instance(BusinessMirrorDatasetRegistry::class, $registry);
|
||||
$this->app->instance(BusinessMirrorSynchronizer::class, $sync);
|
||||
$this->artisan('business-mirror:sync-supplier-management')->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_one_failure_does_not_prevent_other_supplier_data_from_refreshing(): void
|
||||
{
|
||||
config(['upstream_mirrors.refresh_enabled' => true]);
|
||||
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
||||
$sync = Mockery::mock(BusinessMirrorSynchronizer::class);
|
||||
foreach (self::DATASETS as $index => $key) {
|
||||
$registry->shouldReceive('scheduled')->once()->with($key)->andReturn(true);
|
||||
$expectation = $sync->shouldReceive('sync')->once()->with($key, true, null, 0)->ordered();
|
||||
if ($index === 0) $expectation->andThrow(new RuntimeException('upstream unavailable'));
|
||||
else $expectation->andReturn(['status' => 'succeeded', 'batches' => 1, 'rows' => 5]);
|
||||
}
|
||||
$this->app->instance(BusinessMirrorDatasetRegistry::class, $registry);
|
||||
$this->app->instance(BusinessMirrorSynchronizer::class, $sync);
|
||||
$this->artisan('business-mirror:sync-supplier-management')
|
||||
->expectsOutputToContain('同步失败')->assertExitCode(1);
|
||||
}
|
||||
|
||||
public function test_local_refresh_disabled_never_reads_upstream(): void
|
||||
{
|
||||
config(['upstream_mirrors.refresh_enabled' => false]);
|
||||
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
||||
$registry->shouldNotReceive('scheduled');
|
||||
$sync = Mockery::mock(BusinessMirrorSynchronizer::class);
|
||||
$sync->shouldNotReceive('sync');
|
||||
$this->app->instance(BusinessMirrorDatasetRegistry::class, $registry);
|
||||
$this->app->instance(BusinessMirrorSynchronizer::class, $sync);
|
||||
$this->artisan('business-mirror:sync-supplier-management')->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_disabled_supplier_datasets_are_not_synced(): void
|
||||
{
|
||||
config(['upstream_mirrors.refresh_enabled' => true]);
|
||||
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
||||
$registry->shouldReceive('scheduled')->times(4)->andReturn(false);
|
||||
$sync = Mockery::mock(BusinessMirrorSynchronizer::class);
|
||||
$sync->shouldNotReceive('sync');
|
||||
$this->app->instance(BusinessMirrorDatasetRegistry::class, $registry);
|
||||
$this->app->instance(BusinessMirrorSynchronizer::class, $sync);
|
||||
$this->artisan('business-mirror:sync-supplier-management')->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_schedule_runs_at_0815_shanghai_even_when_app_timezone_is_utc(): void
|
||||
{
|
||||
config(['app.installed' => true, 'app.schedule_time' => '09:00', 'app.timezone' => 'UTC']);
|
||||
$schedule = new Schedule('UTC');
|
||||
(new \ReflectionMethod(\App\Console\Kernel::class, 'schedule'))->invoke($this->app->make(Kernel::class), $schedule);
|
||||
$events = collect($schedule->events())
|
||||
->filter(fn ($event) => str_contains($event->command ?? '', 'business-mirror:sync-supplier-management'));
|
||||
$this->assertCount(1, $events);
|
||||
$event = $events->first();
|
||||
try {
|
||||
foreach (['00:14' => false, '00:15' => true, '00:16' => false] as $time => $due) {
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-12 ' . $time . ':00', 'UTC'));
|
||||
$this->assertSame($due, $event->isDue($this->app), $time);
|
||||
}
|
||||
} finally {
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,6 +58,12 @@ class BusinessMirrorScheduleTest extends TestCase
|
||||
$this->assertStringContainsString("CRON_TZ=Asia/Shanghai\n", $contents, $script);
|
||||
$this->assertStringContainsString('0 0 * * * root', $contents, $script);
|
||||
$this->assertStringContainsString('0 5 * * * root', $contents, $script);
|
||||
$this->assertStringContainsString(
|
||||
'15 8 * * * root /usr/bin/flock -w 3600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-supplier-management --quiet',
|
||||
$contents,
|
||||
$script,
|
||||
);
|
||||
$this->assertSame(1, substr_count($contents, 'business-mirror:sync-supplier-management --quiet'), $script);
|
||||
$this->assertStringContainsString(
|
||||
'business-mirror:sync-tables operations_replica.rpt.jst_Orders --quiet',
|
||||
$contents,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user