修复订单售后页面一直转圈:店铺候选改成定时预热

上一次把店铺候选的完整计算放在 app()->terminating() 里,但服务器是 mod_php,
响应要等整个请求(含终止回调)结束才算完成,浏览器就一直转圈、侧边栏停在骨架屏。
线上实测:发布后第一个打开订单售后费用明细的人,页面会卡两分钟以上。

改成:页面只返回毫秒级的店铺目录兜底,完整候选(目录 ∪ 订单镜像店铺)由新增的
order-data:warm-shop-candidates 每小时预热一次,并在发布脚本的 cron 里注册。
This commit is contained in:
renshen 2026-10-07 14:21:27 +08:00
parent a9da8d4447
commit c4a976751d
6 changed files with 79 additions and 30 deletions

View File

@ -0,0 +1,30 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\EcommerceFinance\Repositories\OrderDataMirrorRepository;
use Throwable;
class WarmOrderDataShopCandidates extends Command
{
protected $signature = 'order-data:warm-shop-candidates';
protected $description = '预热订单售后费用明细的店铺候选(要扫两张百万级订单宽表,不能放在页面请求里)';
public function handle(OrderDataMirrorRepository $orders): int
{
try {
$names = $orders->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;
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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<int,string>
*/
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<int,string>
*/

View File

@ -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'),

View File

@ -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);
}