212 lines
7.4 KiB
PHP
212 lines
7.4 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Console\Commands;
|
||
|
|
|
||
|
|
use App\Models\Stats\Daily\StatFinanceDaily;
|
||
|
|
use App\Models\Stats\Daily\StatSalesDaily;
|
||
|
|
use App\Models\Stats\Daily\StatSystemDaily;
|
||
|
|
use Carbon\Carbon;
|
||
|
|
use Illuminate\Console\Command;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class StatVerifyCommand extends Command
|
||
|
|
{
|
||
|
|
protected $signature = 'stat:verify
|
||
|
|
{--date= : 검증 날짜 (YYYY-MM-DD, 기본: 어제)}
|
||
|
|
{--tenant= : 특정 테넌트만 검증}
|
||
|
|
{--domain= : 특정 도메인만 검증 (sales,finance,system)}
|
||
|
|
{--fix : 불일치 시 자동 재집계}';
|
||
|
|
|
||
|
|
protected $description = '원본 DB와 sam_stat 통계 정합성 교차 검증';
|
||
|
|
|
||
|
|
private int $totalChecks = 0;
|
||
|
|
|
||
|
|
private int $passedChecks = 0;
|
||
|
|
|
||
|
|
private int $failedChecks = 0;
|
||
|
|
|
||
|
|
private array $mismatches = [];
|
||
|
|
|
||
|
|
public function handle(): int
|
||
|
|
{
|
||
|
|
$date = $this->option('date')
|
||
|
|
? Carbon::parse($this->option('date'))
|
||
|
|
: Carbon::yesterday();
|
||
|
|
$tenantId = $this->option('tenant') ? (int) $this->option('tenant') : null;
|
||
|
|
$domain = $this->option('domain');
|
||
|
|
$dateStr = $date->format('Y-m-d');
|
||
|
|
|
||
|
|
$this->info("🔍 정합성 검증: {$dateStr}");
|
||
|
|
|
||
|
|
$tenants = $this->getTargetTenants($tenantId);
|
||
|
|
|
||
|
|
$domains = $domain
|
||
|
|
? [$domain]
|
||
|
|
: ['sales', 'finance', 'system'];
|
||
|
|
|
||
|
|
foreach ($tenants as $tenant) {
|
||
|
|
$this->info('');
|
||
|
|
$this->info("── tenant={$tenant->id} ──");
|
||
|
|
|
||
|
|
foreach ($domains as $d) {
|
||
|
|
match ($d) {
|
||
|
|
'sales' => $this->verifySales($tenant->id, $dateStr),
|
||
|
|
'finance' => $this->verifyFinance($tenant->id, $dateStr),
|
||
|
|
'system' => $this->verifySystem($tenant->id, $dateStr),
|
||
|
|
default => $this->warn(" 미지원 도메인: {$d}"),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->printSummary();
|
||
|
|
|
||
|
|
if ($this->failedChecks > 0 && $this->option('fix')) {
|
||
|
|
$this->info('');
|
||
|
|
$this->info('🔧 불일치 항목 재집계...');
|
||
|
|
$this->reAggregate($date, $tenantId, $domains);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->failedChecks > 0 ? self::FAILURE : self::SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function verifySales(int $tenantId, string $dateStr): void
|
||
|
|
{
|
||
|
|
$this->line(' [sales]');
|
||
|
|
|
||
|
|
$originOrderCount = DB::connection('mysql')
|
||
|
|
->table('orders')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->whereDate('created_at', $dateStr)
|
||
|
|
->whereNull('deleted_at')
|
||
|
|
->count();
|
||
|
|
|
||
|
|
$originSalesAmount = (float) DB::connection('mysql')
|
||
|
|
->table('sales')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('sale_date', $dateStr)
|
||
|
|
->whereNull('deleted_at')
|
||
|
|
->sum('supply_amount');
|
||
|
|
|
||
|
|
$stat = StatSalesDaily::where('tenant_id', $tenantId)
|
||
|
|
->where('stat_date', $dateStr)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
$this->check('수주건수', $originOrderCount, $stat?->order_count ?? 0, $tenantId, 'sales');
|
||
|
|
$this->check('매출금액', $originSalesAmount, (float) ($stat?->sales_amount ?? 0), $tenantId, 'sales');
|
||
|
|
}
|
||
|
|
|
||
|
|
private function verifyFinance(int $tenantId, string $dateStr): void
|
||
|
|
{
|
||
|
|
$this->line(' [finance]');
|
||
|
|
|
||
|
|
$originDepositAmount = (float) DB::connection('mysql')
|
||
|
|
->table('deposits')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('deposit_date', $dateStr)
|
||
|
|
->whereNull('deleted_at')
|
||
|
|
->sum('amount');
|
||
|
|
|
||
|
|
$originWithdrawalAmount = (float) DB::connection('mysql')
|
||
|
|
->table('withdrawals')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('withdrawal_date', $dateStr)
|
||
|
|
->whereNull('deleted_at')
|
||
|
|
->sum('amount');
|
||
|
|
|
||
|
|
$stat = StatFinanceDaily::where('tenant_id', $tenantId)
|
||
|
|
->where('stat_date', $dateStr)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
$this->check('입금액', $originDepositAmount, (float) ($stat?->deposit_amount ?? 0), $tenantId, 'finance');
|
||
|
|
$this->check('출금액', $originWithdrawalAmount, (float) ($stat?->withdrawal_amount ?? 0), $tenantId, 'finance');
|
||
|
|
}
|
||
|
|
|
||
|
|
private function verifySystem(int $tenantId, string $dateStr): void
|
||
|
|
{
|
||
|
|
$this->line(' [system]');
|
||
|
|
|
||
|
|
$originApiCount = DB::connection('mysql')
|
||
|
|
->table('api_request_logs')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->whereDate('created_at', $dateStr)
|
||
|
|
->count();
|
||
|
|
|
||
|
|
$originAuditCount = DB::connection('mysql')
|
||
|
|
->table('audit_logs')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->whereDate('created_at', $dateStr)
|
||
|
|
->count();
|
||
|
|
|
||
|
|
$stat = StatSystemDaily::where('tenant_id', $tenantId)
|
||
|
|
->where('stat_date', $dateStr)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
$this->check('API요청수', $originApiCount, $stat?->api_request_count ?? 0, $tenantId, 'system');
|
||
|
|
|
||
|
|
$statAuditTotal = ($stat?->audit_create_count ?? 0)
|
||
|
|
+ ($stat?->audit_update_count ?? 0)
|
||
|
|
+ ($stat?->audit_delete_count ?? 0);
|
||
|
|
$this->check('감사로그수', $originAuditCount, $statAuditTotal, $tenantId, 'system');
|
||
|
|
}
|
||
|
|
|
||
|
|
private function check(string $label, float|int $expected, float|int $actual, int $tenantId, string $domain): void
|
||
|
|
{
|
||
|
|
$this->totalChecks++;
|
||
|
|
|
||
|
|
$tolerance = is_float($expected) ? 0.01 : 0;
|
||
|
|
$match = abs($expected - $actual) <= $tolerance;
|
||
|
|
|
||
|
|
if ($match) {
|
||
|
|
$this->passedChecks++;
|
||
|
|
$this->line(" ✅ {$label}: {$actual}");
|
||
|
|
} else {
|
||
|
|
$this->failedChecks++;
|
||
|
|
$this->error(" ❌ {$label}: 원본={$expected} / 통계={$actual} (차이=".($actual - $expected).')');
|
||
|
|
$this->mismatches[] = compact('tenantId', 'domain', 'label', 'expected', 'actual');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function printSummary(): void
|
||
|
|
{
|
||
|
|
$this->info('');
|
||
|
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
|
|
$this->info("📊 검증 결과: {$this->totalChecks}건 검사, ✅ {$this->passedChecks}건 일치, ❌ {$this->failedChecks}건 불일치");
|
||
|
|
|
||
|
|
if ($this->failedChecks > 0) {
|
||
|
|
$this->warn('');
|
||
|
|
$this->warn('불일치 목록:');
|
||
|
|
foreach ($this->mismatches as $m) {
|
||
|
|
$this->warn(" - tenant={$m['tenantId']} [{$m['domain']}] {$m['label']}: 원본={$m['expected']} / 통계={$m['actual']}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function reAggregate(Carbon $date, ?int $tenantId, array $domains): void
|
||
|
|
{
|
||
|
|
$aggregator = app(\App\Services\Stats\StatAggregatorService::class);
|
||
|
|
|
||
|
|
foreach ($domains as $d) {
|
||
|
|
$result = $aggregator->aggregateDaily($date, $d, $tenantId);
|
||
|
|
$this->line(" {$d}: 재집계 완료 ({$result['domains_processed']}건)");
|
||
|
|
|
||
|
|
if (! empty($result['errors'])) {
|
||
|
|
foreach ($result['errors'] as $error) {
|
||
|
|
$this->error(" {$error}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->info('✅ 재집계 완료. stat:verify를 다시 실행하여 확인하세요.');
|
||
|
|
}
|
||
|
|
|
||
|
|
private function getTargetTenants(?int $tenantId): \Illuminate\Database\Eloquent\Collection
|
||
|
|
{
|
||
|
|
$query = \App\Models\Tenants\Tenant::where('tenant_st_code', '!=', 'none');
|
||
|
|
if ($tenantId) {
|
||
|
|
$query->where('id', $tenantId);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $query->get();
|
||
|
|
}
|
||
|
|
}
|