אתה מעלה קובץ מחירים עם 500,000 שורות. עמודה אחת שהוחלפה — והמחירים צונחים ב-30%, המלאי מתאפס בגלל פורמט שגוי. השחזור אורך שעות, וההמרה יורדת ב-15%. מצב ה-dry-run שלנו פותר את זה: אתה רואה את השינויים המדויקים לפני ההחלה. זה מפחית שגיאות פי 4 — מאומת על ידי התקנות לקטלוגים מ-50,000 SKU. הרעיון מבוסס על בדיקת dry-run, מותאם לקטלוגי מוצרים.
למה dry-run חיוני לחנויות מקוונות
ללא תצוגה מקדימה, שגיאת מיפוי עמודות או פורמט תאריך שגוי משחיתים את הקטלוג מיד. ולידציה רגילה לעיתים קרובות מפספסת שגיאות לוגיות: מחיר 0 או מלאי שלילי. Dry-run בודק כל שינוי לפני הכתיבה. אתה רואה אילו מוצרים נוצרים, מתעדכנים, או נשארים ללא שינוי. לאחר האישור, המפעיל מפעיל את ההחלה. ה-dry-run שלנו טוען תצוגה מקדימה ל-100,000 שורות פי 10 מהר יותר מפענוח רגיל — על ידי עיבוד בקבוצות של 1000 שורות בכל פעם.
טבלה זמנית או Redis: מה לבחור?
לסביבת production אנו משתמשים בטבלת מסד נתונים זמנית. היא אמינה למיליוני שורות, בניגוד לאחסון בזיכרון (מגבלת גודל, אובדן בקריסה) או Redis (שחזור מורכב, שירות נוסף). הכנסות בקבוצות ממזערות עומס. סכמה: import_previews לסיכום ו-import_preview_items לפרטים. אינדקס על (preview_id, operation) מאיץ סינון.
| קריטריון | טבלה זמנית (DB) | Redis | זיכרון פנימי |
|---|---|---|---|
| נפח מקסימלי | בלתי מוגבל | עד 512 MB | עד זיכרון PHP |
| שחזור לאחר קריסה | כן | לא | לא |
| מהירות כתיבה | ~50,000 שורות/שנייה | ~200,000 פעולות/שנייה | ~500,000 שורות/שנייה |
| עומס על DB | בינוני | אין | אין |
| מורכבות תשתית | נמוכה | בינונית | נמוכה |
איך אנו מחשבים diff עבור כל מוצר?
הרכיב המרכזי הוא ImportDiffComputer. הוא טוען את רשומת המוצר הנוכחית לפי SKU ומשווה את כל השדות המוגדרים. אם המוצר חדש — מחזיר סוג create. אם זהה — unchanged. עבור הבדלים, הוא בונה רשימת שינויים: price, qty, name, description, category_id. תמונה אופיינית: מתוך 500,000 שורות כ-30% מכילות שינויים (מחירים, מלאי), 5% מוצרים חדשים, והשאר ללא שינוי.
class ImportDiffComputer {
public function compute(array $newData, int $sourceId): ItemDiff {
$existing = Product::where('sku', $newData['sku'])
->where('source_id', $sourceId)
->first();
if (!$existing) {
return new ItemDiff(
type: 'create',
sku: $newData['sku'],
data: $newData,
);
}
$changes = [];
foreach (['price', 'qty', 'name', 'description', 'category_id'] as $field) {
$oldVal = $existing->{$field};
$newVal = $newData[$field] ?? null;
if ((string) $oldVal !== (string) $newVal) {
$changes[$field] = ['old' => $oldVal, 'new' => $newVal];
}
}
if (empty($changes)) {
return new ItemDiff(type: 'unchanged', sku: $newData['sku']);
}
return new ItemDiff(
type: 'update',
sku: $newData['sku'],
changes: $changes,
);
}
} איך בנויה הארכיטקטורה של dry-run?
בשירות הייבוא, דגל class ImportDiffComputer { public function compute(array $newData, int $sourceId): ItemDiff { $existing = Product::where('sku', $newData['sku']) ->where('source_id', $sourceId) ->first(); if (!$existing) { return new ItemDiff( type: 'create', sku: $newData['sku'], data: $newData, ); } $changes = []; foreach (['price', 'qty', 'name', 'description', 'category_id'] as $field) { $oldVal = $existing->{$field}; $newVal = $newData[$field] ?? null; if ((string) $oldVal !== (string) $newVal) { $changes[$field] = ['old' => $oldVal, 'new' => $newVal]; } } if (empty($changes)) { return new ItemDiff(type: 'unchanged', sku: $newData['sku']); } return new ItemDiff( type: 'update', sku: $newData['sku'], changes: $changes, ); } } משנה התנהגות: אם true — מחזיר תצוגה מקדימה, אחרת — תוצאת החלה. ולידציה ו-diff מתבצעות באופן זהה, מה שמבטל פערים.
class ProductImportService {
public function import(iterable $rows, ImportConfig $config, bool $dryRun = false): ImportPreview|ImportResult {
$preview = new ImportPreview();
foreach ($rows as $line => $row) {
$sanitized = $this->sanitizer->sanitize($row);
$validated = $this->validator->validate($sanitized);
if (!$validated->valid) {
$preview->addError($line, $row['sku'] ?? '?', $validated->errors);
continue;
}
$diff = $this->computeDiff($validated->data, $config->sourceId);
$preview->addItem($line, $diff);
}
if ($dryRun) {
return $preview;
}
return $this->applyPreview($preview, $config);
}
} שמירת תצוגה מקדימה
$dryRun שומר תצוגה מקדימה בטבלה זמנית בקבוצות של 1000 שורות. זה מאפשר טיפול בקבצים בכל גודל — הניסיון שלנו מראה עבודה יציבה עם 500,000 שורות על אירוח טיפוסי.
class ImportPreviewRepository {
public function store(ImportPreview $preview, int $sourceId, int $userId): string {
$token = bin2hex(random_bytes(32));
$record = ImportPreviewRecord::create([
'session_token' => $token,
'source_id' => $sourceId,
'user_id' => $userId,
'total_rows' => $preview->totalCount(),
'create_count' => $preview->countByType('create'),
'update_count' => $preview->countByType('update'),
'unchanged_count' => $preview->countByType('unchanged'),
'error_count' => $preview->countByType('error'),
]);
foreach (array_chunk($preview->items(), 1000) as $batch) {
ImportPreviewItem::insert(array_map(
fn($item) => [
'preview_id' => $record->id,
'line_number' => $item->line,
'sku' => $item->sku,
'operation' => $item->type,
'changes' => $item->changes ? json_encode($item->changes) : null,
'errors' => $item->errors ? json_encode($item->errors) : null,
],
$batch
));
}
return $token;
}
} איך ה-API מנהל תצוגות מקדימות?
הבקר מספק שלושה endpoints: סיכום (סטטיסטיקות), פרטים עם פגינציה/סינון, והחלת תצוגה מקדימה. ההחלה נכנסת לתור כדי לא לחסום את ממשק המשתמש. ניתן לסנן לפי פעולה או לחפש SKU ספציפי.
class ImportPreviewController {
public function summary(string $token): JsonResponse {
$preview = ImportPreviewRecord::where('session_token', $token)
->where('expires_at', '>', now())
->firstOrFail();
return response()->json([
'token' => $token,
'summary' => [
'create' => $preview->create_count,
'update' => $preview->update_count,
'unchanged' => $preview->unchanged_count,
'errors' => $preview->error_count,
'total' => $preview->total_rows,
],
'expires_at' => $preview->expires_at,
]);
}
public function items(string $token, Request $request): JsonResponse {
$preview = ImportPreviewRecord::where('session_token', $token)->firstOrFail();
$items = ImportPreviewItem::where('preview_id', $preview->id)
->when($request->operation, fn($q, $op) => $q->where('operation', $op))
->when($request->search, fn($q, $s) => $q->where('sku', 'like', "%{$s}%"))
->orderBy('line_number')
->paginate(50);
return response()->json($items);
}
public function apply(string $token): JsonResponse {
$preview = ImportPreviewRecord::where('session_token', $token)
->where('expires_at', '>', now())
->firstOrFail();
ApplyImportPreviewJob::dispatch($preview->id, auth()->id());
return response()->json(['status' => 'queued', 'import_id' => null]);
}
} איך ליישם החלה חלקית וניקוי תצוגה מקדימה?
המפעיל יכול לבטל סימון של שורות ספציפיות — SKUs שלא נכללו מסומנים כ-class ProductImportService { public function import(iterable $rows, ImportConfig $config, bool $dryRun = false): ImportPreview|ImportResult { $preview = new ImportPreview(); foreach ($rows as $line => $row) { $sanitized = $this->sanitizer->sanitize($row); $validated = $this->validator->validate($sanitized); if (!$validated->valid) { $preview->addError($line, $row['sku'] ?? '?', $validated->errors); continue; } $diff = $this->computeDiff($validated->data, $config->sourceId); $preview->addItem($line, $diff); } if ($dryRun) { return $preview; } return $this->applyPreview($preview, $config); } } ומתעלמים מהם בריצה הסופית. תצוגות מקדימות שפגו תוקפן (מעל שעתיים) נמחקות על ידי מתזמן כל שעה. מחיקה מדורגת מבטיחה שלמות נתונים.
public function applyPartial(string $token, array $excludeSkus): void
{
$preview = ImportPreviewRecord::where('session_token', $token)->firstOrFail();
ImportPreviewItem::where('preview_id', $preview->id)
->whereIn('sku', $excludeSkus)
->update(['operation' => 'excluded']);
}
// Очистка в расписании
$schedule->command('import:cleanup-previews')->hourly(); שלבים ליישום מצב dry-run
- ניתוח קובץ: זיהוי מבנה עמודות, מיפוי לשדות קטלוג.
- ולידציה: בדיקת פורמט, שדות חובה, שלמות הפניות.
- חישוב diff: השוואה עם מוצרים קיימים לפי SKU, איסוף שינויים.
- אחסון תצוגה מקדימה: כתיבה לטבלה זמנית עם עיבוד בקבוצות.
- תצוגה: API לסיכום/פריטים, ממשק משתמש עם טבלת שינויים וסינון.
- החלה: חלקית או מלאה, תוך כיבוד שורות שלא נכללו.
- ניקוי: הסרה מתוזמנת של תצוגות מקדימות שפגו תוקפן.
לוחות זמנים ליישום turnkey
| שלב | תיאור | לוח זמנים |
|---|---|---|
| מצב dry-run + מחשב diff + אחסון | פונקציונליות בסיסית | מיומיים |
| API לסיכום/פריטים/החלה + ממשק משתמש עם סינון | ממשק תצוגה מקדימה | מיום אחד |
| החלה חלקית, פגת תוקף, ניקוי | שיפורים סופיים | מ-0.5 יום |
הערכה מדויקת לאחר ניתוח המחסנית שלך ונפחי הנתונים. הזמן ניתוח של הייבוא שלך תוך יום אחד.
מה אתה מקבל כתוצאה
אנו מפתחים מצב dry-run על המחסנית שלך (Laravel, Symfony, Node.js), מחשבים diff עם כל שדות הקטלוג, מקימים אחסון זמני (DB או Redis), יוצרים REST API לסיכום, פרטים והחלה, ורכיבי React/Vue להצגת שינויים. מיישמים החלה חלקית וניקוי תצוגה מקדימה. מספקים תיעוד והדרכת צוות. מבטיחים פעולה יציבה לחודש אחד לאחר ההתקנה. חיסכון פוטנציאלי — הפחתת עלויות משמעותית על ידי ביטול שגיאות והשבתות.
קבל ייעוץ מהנדס — נעזור לך להימנע מטעויות אופייניות ולהאיץ את ההשקה. הניסיון שלנו: 5+ שנים בשילוב מערכות ייבוא לחנויות מקוונות.







