You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
7.0 KiB
214 lines
7.0 KiB
|
2 days ago
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services\PeopleCounting;
|
||
|
|
|
||
|
|
use App\Models\PeopleCountingDaily;
|
||
|
|
use App\Models\PeopleCountingHourly;
|
||
|
|
use Illuminate\Support\Carbon;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
|
||
|
|
class PeopleCountingSyncService
|
||
|
|
{
|
||
|
|
public function __construct(
|
||
|
|
private readonly HikPeopleCountingClient $client,
|
||
|
|
) {
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return array{date: string, written: int, skipped: int, venues: int}
|
||
|
|
*/
|
||
|
|
public function syncDailyForDate(Carbon $day, bool $protectZeroOverwrite = true): array
|
||
|
|
{
|
||
|
|
$json = $this->client->fetchDailySnapshot($day);
|
||
|
|
$venues = is_array($json['venues'] ?? null) ? $json['venues'] : [];
|
||
|
|
$statDate = $day->toDateString();
|
||
|
|
$written = 0;
|
||
|
|
$skipped = 0;
|
||
|
|
|
||
|
|
foreach ($venues as $row) {
|
||
|
|
if (! is_array($row)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$venueId = (int) ($row['venueId'] ?? 0);
|
||
|
|
if ($venueId <= 0) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$enter = max(0, (int) ($row['enter'] ?? 0));
|
||
|
|
$exit = max(0, (int) ($row['exit'] ?? 0));
|
||
|
|
$passing = max(0, (int) ($row['passing'] ?? 0));
|
||
|
|
$updateCount = max(0, (int) ($row['updateCount'] ?? 0));
|
||
|
|
$lastUpdate = $this->parseLastUpdate($row['lastUpdate'] ?? null);
|
||
|
|
|
||
|
|
if ($protectZeroOverwrite && $this->shouldSkipZeroOverwrite($venueId, $statDate, $enter, $exit, $passing, $updateCount)) {
|
||
|
|
$skipped++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
PeopleCountingDaily::query()->updateOrCreate(
|
||
|
|
[
|
||
|
|
'venue_id' => $venueId,
|
||
|
|
'stat_date' => $statDate,
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'venue_name' => $this->nullableName($row['venueName'] ?? null),
|
||
|
|
'enter' => $enter,
|
||
|
|
'exit' => $exit,
|
||
|
|
'passing' => $passing,
|
||
|
|
'update_count' => $updateCount,
|
||
|
|
'last_update' => $lastUpdate,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$written++;
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'date' => $statDate,
|
||
|
|
'written' => $written,
|
||
|
|
'skipped' => $skipped,
|
||
|
|
'venues' => count($venues),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return array{date: string, written: int, skipped: bool, data_source: string|null, venues: int}
|
||
|
|
*/
|
||
|
|
public function syncHourlyForDate(Carbon $day, bool $skipIfAllZero = true): array
|
||
|
|
{
|
||
|
|
$json = $this->client->fetchHourly($day);
|
||
|
|
$venues = is_array($json['venues'] ?? null) ? $json['venues'] : [];
|
||
|
|
$statDate = $day->toDateString();
|
||
|
|
$rootSource = is_string($json['dataSource'] ?? null) ? (string) $json['dataSource'] : null;
|
||
|
|
|
||
|
|
$allZero = true;
|
||
|
|
foreach ($venues as $row) {
|
||
|
|
if (! is_array($row)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
foreach (is_array($row['hourly'] ?? null) ? $row['hourly'] : [] as $bucket) {
|
||
|
|
if (! is_array($bucket)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (((int) ($bucket['enter'] ?? 0)) !== 0
|
||
|
|
|| ((int) ($bucket['exit'] ?? 0)) !== 0
|
||
|
|
|| ((int) ($bucket['passing'] ?? 0)) !== 0) {
|
||
|
|
$allZero = false;
|
||
|
|
break 2;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 全 0 且仍是内存口径:昨日归档还没生成 / Java 小时回退未上线,写入会用假 0 盖掉已有小时
|
||
|
|
if ($skipIfAllZero && $allZero && ($rootSource === null || $rootSource === 'memory')) {
|
||
|
|
return [
|
||
|
|
'date' => $statDate,
|
||
|
|
'written' => 0,
|
||
|
|
'skipped' => true,
|
||
|
|
'data_source' => $rootSource,
|
||
|
|
'venues' => count($venues),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$written = 0;
|
||
|
|
foreach ($venues as $row) {
|
||
|
|
if (! is_array($row)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$venueId = (int) ($row['venueId'] ?? 0);
|
||
|
|
if ($venueId <= 0) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$name = $this->nullableName($row['venueName'] ?? null);
|
||
|
|
$source = is_string($row['dataSource'] ?? null) ? (string) $row['dataSource'] : $rootSource;
|
||
|
|
$hours = is_array($row['hourly'] ?? null) ? $row['hourly'] : [];
|
||
|
|
$byHour = [];
|
||
|
|
foreach ($hours as $bucket) {
|
||
|
|
if (! is_array($bucket)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$h = (int) ($bucket['hour'] ?? -1);
|
||
|
|
if ($h < 0 || $h > 23) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$byHour[$h] = [
|
||
|
|
'enter' => max(0, (int) ($bucket['enter'] ?? 0)),
|
||
|
|
'exit' => max(0, (int) ($bucket['exit'] ?? 0)),
|
||
|
|
'passing' => max(0, (int) ($bucket['passing'] ?? 0)),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
for ($h = 0; $h <= 23; $h++) {
|
||
|
|
$vals = $byHour[$h] ?? ['enter' => 0, 'exit' => 0, 'passing' => 0];
|
||
|
|
PeopleCountingHourly::query()->updateOrCreate(
|
||
|
|
[
|
||
|
|
'venue_id' => $venueId,
|
||
|
|
'stat_date' => $statDate,
|
||
|
|
'hour' => $h,
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'venue_name' => $name,
|
||
|
|
'enter' => $vals['enter'],
|
||
|
|
'exit' => $vals['exit'],
|
||
|
|
'passing' => $vals['passing'],
|
||
|
|
'data_source' => $source,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$written++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'date' => $statDate,
|
||
|
|
'written' => $written,
|
||
|
|
'skipped' => false,
|
||
|
|
'data_source' => $rootSource,
|
||
|
|
'venues' => count($venues),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
private function shouldSkipZeroOverwrite(
|
||
|
|
int $venueId,
|
||
|
|
string $statDate,
|
||
|
|
int $enter,
|
||
|
|
int $exit,
|
||
|
|
int $passing,
|
||
|
|
int $updateCount
|
||
|
|
): bool {
|
||
|
|
if ($enter > 0 || $exit > 0 || $passing > 0 || $updateCount > 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
$existing = PeopleCountingDaily::query()
|
||
|
|
->where('venue_id', $venueId)
|
||
|
|
->whereDate('stat_date', $statDate)
|
||
|
|
->first();
|
||
|
|
if ($existing === null) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
$prev = (int) $existing->enter + (int) $existing->exit + (int) $existing->passing;
|
||
|
|
|
||
|
|
return $prev > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function parseLastUpdate(mixed $raw): ?Carbon
|
||
|
|
{
|
||
|
|
if (! is_string($raw) || trim($raw) === '') {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return Carbon::parse($raw);
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
Log::debug('people counting lastUpdate parse failed', ['raw' => $raw, 'e' => $e->getMessage()]);
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function nullableName(mixed $name): ?string
|
||
|
|
{
|
||
|
|
if (! is_string($name)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
$t = trim($name);
|
||
|
|
|
||
|
|
return $t === '' ? null : mb_substr($t, 0, 191);
|
||
|
|
}
|
||
|
|
}
|