This commit is contained in:
2026-05-07 20:51:38 +08:00
parent 1e7de46c26
commit 349f8e11b0
14 changed files with 730 additions and 1 deletions
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Service;
/**
* 聚合视图刷新接口
*
* 隔离 PG procedure 调用,便于在单测中注入 mock 实现。
*/
interface AggregateRefresherInterface
{
/**
* 对指定聚合视图的指定日期刷新
*
* @param string $view 聚合视图名(如 'orders_daily_by_created'
* @param string $refresh_date Y-m-d 格式日期
*/
public function refresh(string $view, string $refresh_date): void;
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Service;
use Hyperf\DbConnection\Db;
/**
* TimescaleDB 连续聚合刷新实现
*
* 调用 PG 的 refresh_continuous_aggregate 存储过程,按整日窗口刷新。
*/
class ContinuousAggregateRefresher implements AggregateRefresherInterface
{
public function refresh(string $view, string $refresh_date): void
{
$start = sprintf("'%s 00:00:00+00'::timestamptz", $refresh_date);
$end = sprintf("'%s 23:59:59.999999+00'::timestamptz", $refresh_date);
// view 在 P23.2 中硬编码为 'orders_daily_by_created',但仍走 PDO quote 防御
$quoted_view = Db::getPdo()->quote($view);
Db::statement(sprintf(
'CALL refresh_continuous_aggregate(%s, %s, %s)',
$quoted_view,
$start,
$end
));
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Model\AggregateRefreshQueue;
use App\Utils\Log;
use Carbon\Carbon;
use Throwable;
/**
* 订单聚合刷新任务
*
* 消费 aggregate_refresh_queue 中 created_at < now() - 1 hour 的项,
* 逐条调用 AggregateRefresherInterface 刷新对应日期的连续聚合,
* 成功后从队列中删除;失败保留队列项由下次任务重试。
*
* 由 OrderAggregatesRefreshCommandCLI 入口)和 Crontab(定时入口)共同调用。
*/
class OrderAggregatesRefreshJob
{
public function __construct(private AggregateRefresherInterface $refresher)
{
}
/**
* @return array{processed: int, failed: int} 处理与失败计数
*/
public function run(): array
{
$items = AggregateRefreshQueue::query()
->where('created_at', '<', Carbon::now()->subHour())
->orderBy('refresh_date')
->orderBy('aggregate_view')
->get();
if ($items->isEmpty()) {
Log::get()->info('orders:refresh-aggregates queue empty');
return ['processed' => 0, 'failed' => 0];
}
$processed = 0;
$failed = 0;
foreach ($items as $item) {
$view = $item->aggregate_view;
$date = $item->refresh_date;
try {
$this->refresher->refresh($view, $date);
AggregateRefreshQueue::query()
->where('refresh_date', $date)
->where('aggregate_view', $view)
->delete();
$processed++;
} catch (Throwable $e) {
Log::get()->error('orders:refresh-aggregates failed', [
'view' => $view,
'date' => $date,
'error' => $e->getMessage(),
]);
$failed++;
}
}
Log::get()->info('orders:refresh-aggregates done', [
'processed' => $processed,
'failed' => $failed,
]);
return ['processed' => $processed, 'failed' => $failed];
}
}