diff --git a/app/Console/Commands/WarmOrderDataShopCandidates.php b/app/Console/Commands/WarmOrderDataShopCandidates.php new file mode 100644 index 00000000..2ff0f41e --- /dev/null +++ b/app/Console/Commands/WarmOrderDataShopCandidates.php @@ -0,0 +1,30 @@ +refreshShopNamesCache(); + $this->info(sprintf('店铺候选已预热,共 %d 个店铺。', count($names))); + + return self::SUCCESS; + } catch (Throwable $exception) { + Log::error('店铺候选预热失败。', ['exception' => $exception::class]); + $this->error('店铺候选预热失败:' . $exception->getMessage()); + + return self::FAILURE; + } + } +} diff --git a/deploy/activate-incremental-release.sh b/deploy/activate-incremental-release.sh index d9b7e7e3..038b0f15 100644 --- a/deploy/activate-incremental-release.sh +++ b/deploy/activate-incremental-release.sh @@ -181,6 +181,9 @@ CRON_TZ=Asia/Shanghai * * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-catalog --quiet; /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 upstream:snapshots:refresh-all --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables --quiet' >/dev/null 2>&1 # 店铺经营报表按 15 分钟刷新;单轮覆盖 50000 行回看窗口,避免空闲时持续重读千万级账单。 7,22,37,52 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-store-report --max-batches=12 --quiet >/dev/null 2>&1 +# 订单售后费用明细的店铺候选:完整列表要扫两张百万级订单宽表(约 2 分钟),放定时任务预热; +# 页面冷缓存时先用毫秒级的店铺目录兜底,避免请求一直不结束、页面一直转圈。 +45 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan order-data:warm-shop-candidates --quiet >/dev/null 2>&1 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 diff --git a/deploy/activate-release.sh b/deploy/activate-release.sh index 241950e2..6260783d 100644 --- a/deploy/activate-release.sh +++ b/deploy/activate-release.sh @@ -185,6 +185,9 @@ CRON_TZ=Asia/Shanghai * * * * * root /usr/bin/flock -n /run/caiwu-upstream-sync.lock /bin/sh -c '/usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-catalog --quiet; /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 upstream:snapshots:refresh-all --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-product-prices --quiet; /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-tables --quiet' >/dev/null 2>&1 # 店铺经营报表按 15 分钟刷新;单轮覆盖 50000 行回看窗口,避免空闲时持续重读千万级账单。 7,22,37,52 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan business-mirror:sync-store-report --max-batches=12 --quiet >/dev/null 2>&1 +# 订单售后费用明细的店铺候选:完整列表要扫两张百万级订单宽表(约 2 分钟),放定时任务预热; +# 页面冷缓存时先用毫秒级的店铺目录兜底,避免请求一直不结束、页面一直转圈。 +45 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan order-data:warm-shop-candidates --quiet >/dev/null 2>&1 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 diff --git a/modules/EcommerceFinance/Repositories/OrderDataMirrorRepository.php b/modules/EcommerceFinance/Repositories/OrderDataMirrorRepository.php index 3bbca0dd..c1771179 100644 --- a/modules/EcommerceFinance/Repositories/OrderDataMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/OrderDataMirrorRepository.php @@ -58,13 +58,32 @@ class OrderDataMirrorRepository } // 完整候选要扫订单宽表(shop_name 是 LONGTEXT,非淘系约 30 秒、淘系约 - // 2 分钟),不能卡在页面请求里。冷缓存时先返回毫秒级的店铺目录, - // 完整列表放到响应结束后算好再缓存 24 小时。 - $this->warmShopNamesCache(); + // 2 分钟)。mod_php 下"响应结束后再算"并不会让浏览器提前结束请求, + // 页面会一直转圈,所以这里只返回毫秒级的店铺目录,完整列表交给 + // order-data:warm-shop-candidates 定时预热。 + return $this->shopDirectoryNames(); + } - $directory = $this->shopDirectoryNames(); + /** + * 重新计算并缓存完整店铺候选(店铺目录 ∪ 订单镜像里出现过的店铺)。 + * + * 只允许定时任务调用:单次要扫两张百万级宽表。 + * + * @return array + */ + public function refreshShopNamesCache(): array + { + $names = array_values(array_unique(array_merge( + $this->computedShopNames(), + $this->shopDirectoryNames(), + ))); + sort($names, SORT_STRING); - return $directory !== [] ? $directory : $this->computedShopNames(); + if ($names !== []) { + Cache::put(self::SHOP_NAMES_CACHE_KEY, $names, now()->addHours(24)); + } + + return $names; } /** 聚水潭店铺目录(约 1300 行):毫秒级返回,作为冷缓存时的候选兜底。 */ @@ -162,32 +181,11 @@ class OrderDataMirrorRepository return array_keys($names); } - private function warmShopNamesCache(): void - { - app()->terminating(function (): void { - $lock = Cache::lock(self::SHOP_NAMES_CACHE_KEY . ':warm', 300); - if (! $lock->get()) { - return; - } - - try { - $names = $this->computedShopNames(); - if ($names !== []) { - Cache::put(self::SHOP_NAMES_CACHE_KEY, $names, now()->addHours(24)); - } - } catch (Throwable $exception) { - report($exception); - } finally { - $lock->release(); - } - }); - } - /** * 非淘系 + 淘系两个来源合并去重(淘系店铺名形如"天猫-xx/淘宝-xx")。 * * 分开各自 DISTINCT 再在 PHP 合并:UNION 会让 MariaDB 把两个上百万行的结果 - * 一起物化去重再排序,单次要两三分钟。这里只在响应结束后或缓存不可用时执行。 + * 一起物化去重再排序,单次要两三分钟。只在定时预热时执行。 * * @return array */ diff --git a/tests/Unit/Deployment/BusinessMirrorScheduleTest.php b/tests/Unit/Deployment/BusinessMirrorScheduleTest.php index 846b73e5..d5133205 100644 --- a/tests/Unit/Deployment/BusinessMirrorScheduleTest.php +++ b/tests/Unit/Deployment/BusinessMirrorScheduleTest.php @@ -36,6 +36,11 @@ class BusinessMirrorScheduleTest extends TestCase $contents, $script, ); + $this->assertStringContainsString( + '45 * * * * root /usr/bin/flock -w 600 /run/caiwu-upstream-sync.lock /usr/bin/docker exec -u www-data caiwu_app php artisan order-data:warm-shop-candidates --quiet', + $contents, + $script, + ); $minuteLines = array_values(array_filter( preg_split('/\R/', $contents) ?: [], static fn (string $line): bool => str_starts_with($line, '* * * * * root'), diff --git a/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php index 33b06697..c3ad83f4 100644 --- a/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php @@ -147,7 +147,17 @@ class OrderDataMirrorRepositoryTest extends TestCase $names = $this->repository($db)->shopNames(); sort($names); - $this->assertSame(['京东-乙店', '拼多多-甲店'], $names); + // 冷缓存只返回毫秒级的店铺目录,不能为了下拉候选去扫订单宽表。 + $db->table('ops_jst_shop_info')->insert(['shop_id' => 1, 'shop_name' => '抖音-丙店']); + $directory = $this->repository($db)->shopNames(); + sort($directory); + $this->assertSame(['抖音-丙店'], $directory); + + // 完整候选由定时预热写入:店铺目录 ∪ 订单镜像里出现过的店铺。 + $names = $this->repository($db)->refreshShopNamesCache(); + sort($names); + + $this->assertSame(['京东-乙店', '抖音-丙店', '拼多多-甲店'], $names); } public function test_after_sales_includes_taobao_orders_and_their_refunds(): void @@ -193,8 +203,8 @@ class OrderDataMirrorRepositoryTest extends TestCase $this->assertSame(1.0, $taobao['post_return_quantity']); $this->assertArrayNotHasKey('source_branch', $taobao); - // 店铺候选也要包含淘系店铺。 - $names = $this->repository($db)->shopNames(); + // 店铺候选也要包含淘系店铺(完整候选由定时预热写入)。 + $names = $this->repository($db)->refreshShopNamesCache(); $this->assertContains('天猫-卓禾家具旗舰店', $names); }