feat: sam_stat 최적화 및 안정화 (Phase 5)
- StatBackfillCommand: 과거 데이터 일괄 백필 (일간+월간, 프로그레스바, 에러 리포트) - StatVerifyCommand: 원본 DB vs sam_stat 정합성 교차 검증 (--fix 자동 재집계) - 파티셔닝 준비: 7개 일간 테이블 RANGE COLUMNS(stat_date) 마이그레이션 - Redis 캐싱: StatQueryService Cache::remember TTL 5분 + invalidateCache() - StatMonitorService: 집계 실패/누락/불일치 시 stat_alerts 알림 기록 - StatAggregatorService: 모니터링 알림 + 캐시 무효화 연동 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,9 +16,12 @@
|
||||
use App\Models\Stats\StatAlert;
|
||||
use App\Services\Service;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class StatQueryService extends Service
|
||||
{
|
||||
private const CACHE_TTL = 300; // 5분
|
||||
|
||||
/**
|
||||
* 도메인별 일간 통계 조회
|
||||
*/
|
||||
@@ -28,16 +31,20 @@ public function getDailyStat(string $domain, array $params): array
|
||||
$startDate = $params['start_date'];
|
||||
$endDate = $params['end_date'];
|
||||
|
||||
$model = $this->getDailyModel($domain);
|
||||
if (! $model) {
|
||||
return [];
|
||||
}
|
||||
$cacheKey = "stat:daily:{$tenantId}:{$domain}:{$startDate}:{$endDate}";
|
||||
|
||||
return $model::where('tenant_id', $tenantId)
|
||||
->whereBetween('stat_date', [$startDate, $endDate])
|
||||
->orderBy('stat_date')
|
||||
->get()
|
||||
->toArray();
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($domain, $tenantId, $startDate, $endDate) {
|
||||
$model = $this->getDailyModel($domain);
|
||||
if (! $model) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $model::where('tenant_id', $tenantId)
|
||||
->whereBetween('stat_date', [$startDate, $endDate])
|
||||
->orderBy('stat_date')
|
||||
->get()
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,21 +56,25 @@ public function getMonthlyStat(string $domain, array $params): array
|
||||
$year = (int) $params['year'];
|
||||
$month = isset($params['month']) ? (int) $params['month'] : null;
|
||||
|
||||
$model = $this->getMonthlyModel($domain);
|
||||
if (! $model) {
|
||||
return [];
|
||||
}
|
||||
$cacheKey = "stat:monthly:{$tenantId}:{$domain}:{$year}:".($month ?? 'all');
|
||||
|
||||
$query = $model::where('tenant_id', $tenantId)
|
||||
->where('stat_year', $year);
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($domain, $tenantId, $year, $month) {
|
||||
$model = $this->getMonthlyModel($domain);
|
||||
if (! $model) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($month) {
|
||||
$query->where('stat_month', $month);
|
||||
}
|
||||
$query = $model::where('tenant_id', $tenantId)
|
||||
->where('stat_year', $year);
|
||||
|
||||
return $query->orderBy('stat_month')
|
||||
->get()
|
||||
->toArray();
|
||||
if ($month) {
|
||||
$query->where('stat_month', $month);
|
||||
}
|
||||
|
||||
return $query->orderBy('stat_month')
|
||||
->get()
|
||||
->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,13 +87,17 @@ public function getDashboardSummary(): array
|
||||
$year = Carbon::now()->year;
|
||||
$month = Carbon::now()->month;
|
||||
|
||||
return [
|
||||
'sales_today' => $this->getTodaySales($tenantId, $today),
|
||||
'finance_today' => $this->getTodayFinance($tenantId, $today),
|
||||
'production_today' => $this->getTodayProduction($tenantId, $today),
|
||||
'sales_monthly' => $this->getMonthlySalesOverview($tenantId, $year, $month),
|
||||
'alerts' => $this->getActiveAlerts($tenantId),
|
||||
];
|
||||
$cacheKey = "stat:dashboard:{$tenantId}:{$today}";
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($tenantId, $today, $year, $month) {
|
||||
return [
|
||||
'sales_today' => $this->getTodaySales($tenantId, $today),
|
||||
'finance_today' => $this->getTodayFinance($tenantId, $today),
|
||||
'production_today' => $this->getTodayProduction($tenantId, $today),
|
||||
'sales_monthly' => $this->getMonthlySalesOverview($tenantId, $year, $month),
|
||||
'alerts' => $this->getActiveAlerts($tenantId),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,6 +119,42 @@ public function getAlerts(array $params): array
|
||||
return $query->limit($limit)->get()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 집계 완료 시 관련 캐시 무효화
|
||||
*/
|
||||
public static function invalidateCache(int $tenantId, ?string $domain = null): void
|
||||
{
|
||||
$patterns = [
|
||||
"stat:dashboard:{$tenantId}:*",
|
||||
];
|
||||
|
||||
if ($domain) {
|
||||
$patterns[] = "stat:daily:{$tenantId}:{$domain}:*";
|
||||
$patterns[] = "stat:monthly:{$tenantId}:{$domain}:*";
|
||||
} else {
|
||||
$patterns[] = "stat:daily:{$tenantId}:*";
|
||||
$patterns[] = "stat:monthly:{$tenantId}:*";
|
||||
}
|
||||
|
||||
// Redis 태그 기반 또는 키 패턴 삭제
|
||||
$redis = Cache::getStore();
|
||||
if (method_exists($redis, 'getRedis')) {
|
||||
$connection = $redis->getRedis();
|
||||
$prefix = config('cache.prefix', '').':';
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
$keys = $connection->keys($prefix.$pattern);
|
||||
if (! empty($keys)) {
|
||||
$connection->del($keys);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Redis가 아닌 경우 개별 키 삭제 (대시보드만)
|
||||
$today = Carbon::today()->format('Y-m-d');
|
||||
Cache::forget("stat:dashboard:{$tenantId}:{$today}");
|
||||
}
|
||||
}
|
||||
|
||||
private function getTodaySales(int $tenantId, string $today): ?array
|
||||
{
|
||||
$stat = StatSalesDaily::where('tenant_id', $tenantId)
|
||||
|
||||
Reference in New Issue
Block a user