move_detect/server/src/Ingest/EventValidator.php
유창욱 3f2823a764 refactor: rename cloud/ to server/ (cafe24 is web hosting, not cloud)
The PHP app runs on Cafe24 shared web hosting -- calling it 'cloud'
was misleading. Renames the directory, the edge client module, and
every doc/script/identifier that used the term.

- cloud/ -> server/ (incl. server/deploy/build_cafe24_package.sh paths)
- edge/cloudclient.py -> edge/serverclient.py, CloudClient -> ServerClient
- cloud/docs/cloud-deploy.md -> server/docs/server-deploy.md
- CLOUD_* env/config keys -> SERVER_*
- tests renamed to match

Tests: PHP 109 pass (4 skipped), pytest 167 pass. dist/ regenerated.
2026-07-20 11:30:52 +09:00

64 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Ingest;
final class EventValidator
{
private const TYPES = ['fall','no_motion','immobility','ghost','zone_warning',
'zone_restricted','pinch','monitoring_degraded','device_fault'];
private const SEVERITIES = ['critical','high','warning','info'];
private const REQUIRED = ['device_id','camera_id','event_type','severity','occurred_at'];
public static function validate(array $p): array
{
$errors = [];
foreach (self::REQUIRED as $k) {
if (!array_key_exists($k, $p) || $p[$k] === '' || $p[$k] === null) $errors[] = "missing:$k";
}
if (isset($p['event_type']) && !in_array($p['event_type'], self::TYPES, true)) $errors[] = 'bad:event_type';
if (isset($p['severity']) && !in_array($p['severity'], self::SEVERITIES, true)) $errors[] = 'bad:severity';
$snaps = $p['snapshots'] ?? [];
if (!is_array($snaps)) $errors[] = 'bad:snapshots';
elseif (count($snaps) > 3) $errors[] = 'too_many:snapshots';
else foreach ($snaps as $s) if (!is_string($s)) { $errors[] = 'bad:snapshot_item'; break; }
// Type guards for required fields
if (isset($p['device_id']) && !is_string($p['device_id'])) $errors[] = 'bad:device_id';
if (isset($p['camera_id']) && !is_string($p['camera_id'])) $errors[] = 'bad:camera_id';
if (isset($p['occurred_at']) && !is_numeric($p['occurred_at'])) $errors[] = 'bad:occurred_at';
// Type guards for optional fields
if (isset($p['track_id']) && !is_numeric($p['track_id'])) $errors[] = 'bad:track_id';
if (isset($p['zone_id']) && !is_string($p['zone_id'])) $errors[] = 'bad:zone_id';
if (isset($p['duration_s']) && !is_numeric($p['duration_s'])) $errors[] = 'bad:duration_s';
// Optional idempotency key: the edge generates a stable 32-hex uuid per event and re-sends
// it on retry so the server can dedup. Enforce the exact shape (matches CHAR(32) + the
// snapshot-uuid convention); a malformed key is a bug, not a silent pass.
if (isset($p['event_uuid']) && (!is_string($p['event_uuid']) || !preg_match('/^[0-9a-f]{32}$/', $p['event_uuid']))) {
$errors[] = 'bad:event_uuid';
}
// Length caps matching the DB columns (device_id/camera_id VARCHAR(64), zone_id
// CHAR(32)). Without these an overlong id is a MySQL 'Data too long' 500 that the edge
// re-queues and retries forever; a 400 lets it drop the poison event instead.
foreach (['device_id' => 64, 'camera_id' => 64, 'zone_id' => 32] as $k => $max) {
if (isset($p[$k]) && is_string($p[$k]) && mb_strlen($p[$k]) > $max) $errors[] = "too_long:$k";
}
if ($errors) return ['ok' => false, 'errors' => $errors, 'clean' => []];
$clean = [
'event_uuid' => isset($p['event_uuid']) ? (string)$p['event_uuid'] : null,
'device_id' => (string)$p['device_id'],
'camera_id' => (string)$p['camera_id'],
'event_type' => (string)$p['event_type'],
'severity' => (string)$p['severity'],
'track_id' => isset($p['track_id']) ? (int)$p['track_id'] : null,
'zone_id' => isset($p['zone_id']) ? (string)$p['zone_id'] : null,
'occurred_at' => (float)$p['occurred_at'],
'duration_s' => (float)($p['duration_s'] ?? 0.0),
'metrics' => is_array($p['metrics'] ?? null) ? $p['metrics'] : [],
];
return ['ok' => true, 'errors' => [], 'clean' => $clean];
}
}