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

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