class WC_Deduplicator { // 🧪 Уніфікована перевірка dry-run public static function is_dry_run(): bool { return isset($_POST['dry_run']) && $_POST['dry_run'] === '1'; } // 📝 Логування дій public static function log_action($lines) { if (self::is_protected() && !self::is_dry_run()) return; $upload_dir = wp_upload_dir(); $log_file = $upload_dir['basedir'] . '/deduplicator-log.txt'; try { $content = implode(PHP_EOL, $lines) . PHP_EOL; if (is_writable($upload_dir['basedir'])) { file_put_contents($log_file, $content, FILE_APPEND); } else { $fallback = ABSPATH . 'deduplicator-fallback-log.txt'; file_put_contents($fallback, $content, FILE_APPEND); error_log("Deduplicator fallback log записано в $fallback"); } } catch (Throwable $e) { self::log_error('log_action', $e); } } // 🛠️ Логування помилок public static function log_error($context, Throwable $e) { $timestamp = current_time('mysql'); error_log("[$timestamp] ❌ [$context] " . $e->getMessage()); } // 🔍 Вивід кількості public static function log_and_display_count($label, $count, $type = 'info') { $timestamp = current_time('mysql'); if (!self::is_protected() || self::is_dry_run()) { self::log_action(["[$timestamp] 🔍 $label: $count"]); } $class = $type === 'error' ? 'notice-error' : ($type === 'warning' ? 'notice-warning' : 'notice-info'); echo "

🔍 $label: $count

"; return $count; // ✅ Додано для передачі результату в UI } // 🔍 Пошук дублікатів SKU public static function get_duplicate_skus() { global $wpdb; if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук дублікатів SKU заблоковано. Використовуйте dry-run.

"; return []; } return $wpdb->get_results(" SELECT meta_value AS sku, GROUP_CONCAT(DISTINCT post_id ORDER BY post_id SEPARATOR ',') AS product_ids FROM {$wpdb->prefix}postmeta WHERE meta_key = '_sku' AND meta_value != '' AND post_id IN ( SELECT ID FROM {$wpdb->prefix}posts WHERE post_type = 'product' AND post_status IN ('publish', 'draft') ) GROUP BY meta_value HAVING COUNT(*) > 1 "); } // 🔍 Пошук дублікатів назв public static function get_duplicate_titles() { global $wpdb; if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук дублікатів назв заблоковано. Використовуйте dry-run.

"; return []; } return $wpdb->get_results(" SELECT post_title, GROUP_CONCAT(ID ORDER BY ID SEPARATOR ',') AS product_ids FROM {$wpdb->prefix}posts WHERE post_type = 'product' AND post_status IN ('publish', 'draft') AND post_title != '' GROUP BY post_title HAVING COUNT(*) > 1 "); } // ⚡ Видалення товарів (або dry-run) public static function delete_fast($ids, $preview = false) { global $wpdb; if (empty($ids)) return; $safe_ids = implode(',', array_map('intval', $ids)); if ($preview || self::is_dry_run()) { self::log_and_display_count('Попередній перегляд товарів для видалення', count($ids), 'warning'); echo "

🧪 Dry-run: " . count($ids) . " товарів буде видалено.
ID: $safe_ids

"; return; } if (self::is_protected()) { echo "

🔒 Режим захисту активний. Видалення заблоковано. Використовуйте dry-run.

"; return; } $wpdb->query('START TRANSACTION'); $deleted_posts = $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE ID IN ($safe_ids)"); $deleted_meta = $wpdb->query("DELETE FROM {$wpdb->prefix}postmeta WHERE post_id IN ($safe_ids)"); if ($deleted_posts === false || $deleted_meta === false) { $wpdb->query('ROLLBACK'); self::log_action(["❌ Rollback через помилку при видаленні ID: $safe_ids"]); echo "

🚫 Помилка при SQL-видаленні. Операцію скасовано.

"; return; } $wpdb->query('COMMIT'); $timestamp = current_time('mysql'); self::log_action(["[$timestamp] ✅ SQL-видалення: " . count($ids) . " товарів", "ID: $safe_ids"]); echo "

✅ Видалено " . count($ids) . " товарів через SQL.

"; } } // ⚡ Видалення всіх дублікатів public static function handle_duplicate_fast_cleanup() { if (!isset($_POST['delete_all_fast'])) return; $sku = self::get_duplicate_skus(); $title = self::get_duplicate_titles(); $ids = []; foreach (array_merge($sku, $title) as $row) { $group = explode(',', $row->product_ids); array_shift($group); // залишаємо один $ids = array_merge($ids, $group); } if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Зміни заблоковано. Використовуйте dry-run.

"; return; } self::log_and_display_count('Товарів-дублікатів для видалення', count($ids)); self::delete_fast($ids, self::is_dry_run()); } // 🧹 Видалення порожніх категорій public static function handle_empty_category_deletion() { global $wpdb; if (!isset($_POST['delete_empty_categories'])) return; $empty = $wpdb->get_col(" SELECT term_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = 'product_cat' AND count = 0 "); self::log_and_display_count('Порожніх категорій для видалення', count($empty)); if (self::is_dry_run()) { echo "

🧪 Dry-run: буде видалено " . count($empty) . " порожніх категорій.
ID: " . implode(', ', $empty) . "

"; $names = $wpdb->get_results(" SELECT term_id, name FROM {$wpdb->terms} WHERE term_id IN (" . implode(',', array_map('intval', $empty)) . ") "); if ($names) { echo ""; } echo "
"; return; } // 🔒 Захист перед реальним видаленням if (self::is_protected()) { echo "

🔒 Режим захисту активний. Видалення категорій заблоковано. Використовуйте dry-run.

"; return; } $deleted = 0; foreach ($empty as $id) { if (term_exists($id, 'product_cat')) { wp_delete_term($id, 'product_cat'); $deleted++; } } $timestamp = current_time('mysql'); self::log_action(["[$timestamp] 🧹 Видалено порожніх категорій: $deleted"]); echo "

🧹 Видалено $deleted порожніх категорій.

"; } // 🧠 Обʼєднання категорій public static function handle_category_merging() { global $wpdb; if (!isset($_POST['merge_duplicate_categories'])) return; $groups = $wpdb->get_results(" SELECT t.name, GROUP_CONCAT(t.term_id) AS ids FROM {$wpdb->terms} t JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE tt.taxonomy = 'product_cat' GROUP BY t.name HAVING COUNT(*) > 1 "); self::log_and_display_count('Груп категорій з однаковими назвами для обʼєднання', count($groups)); $moved = 0; $deleted = 0; $preview = self::is_dry_run(); // 🔒 Захист перед реальним обʼєднанням if (self::is_protected() && !$preview) { echo "

🔒 Режим захисту активний. Обʼєднання категорій заблоковано. Використовуйте dry-run.

"; return; } foreach ($groups as $group) { $ids = array_map('intval', explode(',', $group->ids)); $counts = []; foreach ($ids as $id) { $count = $wpdb->get_var(" SELECT count FROM {$wpdb->term_taxonomy} WHERE term_id = $id AND taxonomy = 'product_cat' "); $counts[$id] = intval($count); } arsort($counts); $main_id = array_key_first($counts); $to_merge = array_diff($ids, [$main_id]); if ($preview) { $names = $wpdb->get_results(" SELECT term_id, name FROM {$wpdb->terms} WHERE term_id IN (" . implode(',', array_map('intval', $to_merge)) . ") "); echo "

🧪 Dry-run: категорія $main_id ← обʼєднає:

"; $moved += count($to_merge); $deleted += count($to_merge); continue; } foreach ($to_merge as $id) { $wpdb->query(" UPDATE {$wpdb->term_relationships} tr JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id SET tr.term_taxonomy_id = ( SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id = $main_id AND taxonomy = 'product_cat' LIMIT 1 ) WHERE tt.term_id = $id AND tt.taxonomy = 'product_cat' "); if (term_exists($id, 'product_cat')) { wp_delete_term($id, 'product_cat'); } $deleted++; } $moved += count($to_merge); } $timestamp = current_time('mysql'); self::log_action(["[$timestamp] 🧠 Категорії обʼєднано: $moved, видалено: $deleted"]); if ($preview) { echo "

🧪 Dry-run завершено: $moved категорій буде перенесено, $deleted буде видалено.

"; } else { echo "

🧠 Перенесено $moved товарів у головні категорії, видалено $deleted дублікатів категорій.

"; } } // ⚠️ Видалення записів не типу 'product' public static function handle_delete() { global $wpdb; if (!isset($_POST['delete_non_products'])) return; try { $ids = $wpdb->get_col(" SELECT ID FROM {$wpdb->prefix}posts WHERE post_type != 'product' "); $count = count($ids); self::log_and_display_count('Записів не типу product для видалення', $count); if ($count === 0) { echo "

🚫 Не знайдено записів для видалення.

"; return; } $safe_ids = implode(',', array_map('intval', $ids)); $timestamp = current_time('mysql'); if (self::is_dry_run()) { echo "

🧪 Dry-run: $count записів буде видалено.
ID: $safe_ids

"; $names = $wpdb->get_results(" SELECT ID, post_title, post_type FROM {$wpdb->prefix}posts WHERE ID IN ($safe_ids) "); if ($names) { echo ""; } echo "
"; self::log_action(["[$timestamp] 🧪 Dry-run: $count записів не типу product", "ID: $safe_ids"]); return; } // 🔒 Захист перед реальним видаленням if (self::is_protected()) { echo "

🔒 Режим захисту активний. Видалення записів заблоковано. Використовуйте dry-run.

"; return; } $wpdb->query('START TRANSACTION'); $deleted_posts = $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE ID IN ($safe_ids)"); $deleted_meta = $wpdb->query("DELETE FROM {$wpdb->prefix}postmeta WHERE post_id IN ($safe_ids)"); if ($deleted_posts === false || $deleted_meta === false) { $wpdb->query('ROLLBACK'); throw new Exception('Помилка при видаленні записів або метаданих'); } $wpdb->query('COMMIT'); self::log_action([ "[$timestamp] ⚠️ Видалено записів: $count", "ID: $safe_ids" ]); echo "

⚠️ Видалено $count записів не типу product.

"; } catch (Throwable $e) { $wpdb->query('ROLLBACK'); self::log_error('handle_delete', $e); echo "

🚫 Помилка при видаленні: " . esc_html($e->getMessage()) . "

"; } } // 🔍 Пошук товарів без зображення public static function handle_count_no_image() { if (!isset($_POST['count_no_image'])) return; // 🔒 Захист: дозволити тільки dry-run у SAFE_MODE if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук товарів без зображення заблоковано. Використовуйте dry-run.

"; return; } try { $query = new WP_Query([ 'post_type' => 'product', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_thumbnail_id', 'value' => '', 'compare' => '=' ] ] ]); $count = $query->found_posts; self::log_and_display_count('Знайдено товарів без зображення', $count); } catch (Throwable $e) { self::log_error('handle_count_no_image', $e); echo "

🚫 Помилка при пошуку: " . esc_html($e->getMessage()) . "

"; } } // 🔒 Перевірка режиму захисту public static function is_protected(): bool { return defined('WC_DEDUPLICATOR_SAFE_MODE') && WC_DEDUPLICATOR_SAFE_MODE === true; } // 🔍 Пошук товарів без наявності public static function handle_count_outofstock() { // ✅ Підтримка обох кнопок: звичайної та dry-run if (!isset($_POST['count_outofstock']) && !isset($_POST['count_outofstock_dry'])) return; // ✅ Встановлення dry_run вручну $_POST['dry_run'] = isset($_POST['count_outofstock_dry']) ? '1' : '0'; // 🔒 Захист: дозволити тільки dry-run у SAFE_MODE if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук товарів без наявності заблоковано. Використовуйте dry-run.

"; return; } try { global $wpdb; $count = (int) $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->prefix}postmeta pm JOIN {$wpdb->prefix}posts p ON pm.post_id = p.ID WHERE pm.meta_key = '_stock_status' AND pm.meta_value = 'outofstock' AND p.post_type = 'product' AND p.post_status IN ('publish', 'draft') "); self::log_and_display_count('Товарів без наявності', $count); } catch (Throwable $e) { self::log_error('handle_count_outofstock', $e); echo "

🚫 Помилка при пошуку: " . esc_html($e->getMessage()) . "

"; } } // 🔍 Пошук товарів без артикулу public static function handle_count_missing_sku() { if (!isset($_POST['count_missing_sku'])) return; // 🔒 Захист: дозволити тільки dry-run у SAFE_MODE if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук товарів без артикулу заблоковано. Використовуйте dry-run.

"; return; } try { global $wpdb; $count = $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->prefix}posts p LEFT JOIN {$wpdb->prefix}postmeta pm ON p.ID = pm.post_id AND pm.meta_key = '_sku' WHERE p.post_type = 'product' AND p.post_status IN ('publish', 'draft') AND (pm.meta_value IS NULL OR pm.meta_value = '') "); self::log_and_display_count('Товарів без артикулу', $count); } catch (Throwable $e) { self::log_error('handle_count_missing_sku', $e); echo "

🚫 Помилка при пошуку: " . esc_html($e->getMessage()) . "

"; } } // 🔍 Пошук дублікатів товарів public static function handle_count_duplicates() { if (!isset($_POST['count_duplicates'])) return; // 🔒 Захист: дозволити тільки dry-run у SAFE_MODE if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук дублікатів товарів заблоковано. Використовуйте dry-run.

"; return; } try { $sku = self::get_duplicate_skus(); $title = self::get_duplicate_titles(); $count = count($sku) + count($title); self::log_and_display_count('Груп дублікатів товарів', $count); } catch (Throwable $e) { self::log_error('handle_count_duplicates', $e); echo "

🚫 Помилка при пошуку: " . esc_html($e->getMessage()) . "

"; } } // 🔍 Пошук дублікатів категорій public static function handle_count_duplicate_categories() { global $wpdb; if (!isset($_POST['count_duplicate_categories'])) return; // 🔒 Захист: дозволити тільки dry-run у SAFE_MODE if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук дублікатів категорій заблоковано. Використовуйте dry-run.

"; return; } try { $groups = $wpdb->get_results(" SELECT t.name, COUNT(*) AS total FROM {$wpdb->terms} t JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE tt.taxonomy = 'product_cat' GROUP BY t.name HAVING total > 1 "); self::log_and_display_count('Груп дублікатів категорій', count($groups)); } catch (Throwable $e) { self::log_error('handle_count_duplicate_categories', $e); echo "

🚫 Помилка при пошуку: " . esc_html($e->getMessage()) . "

"; } } // 🔍 Пошук записів не типу product public static function handle_count_non_products() { global $wpdb; if (!isset($_POST['count_non_products'])) return; // 🔒 Захист: дозволити тільки dry-run у SAFE_MODE if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Пошук записів не типу product заблоковано. Використовуйте dry-run.

"; return; } try { $count = $wpdb->get_var(" SELECT COUNT(*) FROM {$wpdb->prefix}posts WHERE post_type != 'product' "); self::log_and_display_count('Записів не типу product', $count); } catch (Throwable $e) { self::log_error('handle_count_non_products', $e); echo "

🚫 Помилка при пошуку: " . esc_html($e->getMessage()) . "

"; } } // ⚡ Видалення товарів без артикулу public static function handle_missing_sku_fast_deletion() { global $wpdb; if (!isset($_POST['delete_missing_sku_fast'])) return; try { $ids = $wpdb->get_col(" SELECT p.ID FROM {$wpdb->prefix}posts p LEFT JOIN {$wpdb->prefix}postmeta pm ON p.ID = pm.post_id AND pm.meta_key = '_sku' WHERE p.post_type = 'product' AND p.post_status IN ('publish', 'draft') AND (pm.meta_value IS NULL OR pm.meta_value = '') "); self::log_and_display_count('Товарів без артикулу для видалення', count($ids)); // 🔒 Захист перед реальним видаленням if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Видалення товарів без артикулу заблоковано. Використовуйте dry-run.

"; return; } self::delete_fast($ids, self::is_dry_run()); } catch (Throwable $e) { self::log_error('handle_missing_sku_fast_deletion', $e); echo "

🚫 Помилка: " . esc_html($e->getMessage()) . "

"; } } // ⚡ Видалення товарів без фото / без наявності public static function handle_fast_deletions() { global $wpdb; $key = isset($_POST['delete_no_image_fast']) ? 'image' : (isset($_POST['delete_outofstock_fast']) ? 'stock' : null); if (!$key) return; try { $ids = []; if ($key === 'image') { $ids = $wpdb->get_col(" SELECT p.ID FROM {$wpdb->prefix}posts p LEFT JOIN {$wpdb->prefix}postmeta pm ON p.ID = pm.post_id AND pm.meta_key = '_thumbnail_id' WHERE p.post_type = 'product' AND p.post_status IN ('publish', 'draft') AND (pm.meta_value IS NULL OR pm.meta_value = '') "); self::log_and_display_count('Товарів без зображення для видалення', count($ids)); } else { $ids = $wpdb->get_col(" SELECT p.ID FROM {$wpdb->prefix}postmeta pm JOIN {$wpdb->prefix}posts p ON pm.post_id = p.ID WHERE pm.meta_key = '_stock_status' AND pm.meta_value = 'outofstock' AND p.post_type = 'product' AND p.post_status IN ('publish', 'draft') "); self::log_and_display_count('Товарів без наявності для видалення', count($ids)); } // 🔒 Захист перед реальним видаленням if (self::is_protected() && !self::is_dry_run()) { echo "

🔒 Режим захисту активний. Видалення товарів заблоковано. Використовуйте dry-run.

"; return; } self::delete_fast($ids, self::is_dry_run()); } catch (Throwable $e) { self::log_error('handle_fast_deletions', $e); echo "

🚫 Помилка: " . esc_html($e->getMessage()) . "

"; } } // ⚡ Видалення товарів з найменшою ціною серед дублікатів public static function handle_low_price_cleanup() { global $wpdb; if (!isset($_POST['delete_low_price_duplicates'])) return; $sku_groups = $wpdb->get_results(" SELECT meta_value AS sku, GROUP_CONCAT(DISTINCT post_id ORDER BY post_id SEPARATOR ',') AS product_ids FROM {$wpdb->prefix}postmeta WHERE meta_key = '_sku' AND meta_value != '' AND post_id IN ( SELECT ID FROM {$wpdb->prefix}posts WHERE post_type = 'product' AND post_status IN ('publish', 'draft') ) GROUP BY meta_value HAVING COUNT(*) > 1 "); $ids_to_delete = []; foreach ($sku_groups as $group) { $ids = explode(',', $group->product_ids); $prices = []; foreach ($ids as $id) { $price = $wpdb->get_var($wpdb->prepare(" SELECT meta_value FROM {$wpdb->prefix}postmeta WHERE post_id = %d AND meta_key = '_price' ", $id)); $prices[$id] = floatval($price); } if (count($prices) <= 1) continue; asort($prices); // lowest first $to_delete = array_keys($prices); array_shift($to_delete); // keep one $ids_to_delete = array_merge($ids_to_delete, $to_delete); } self::log_and_display_count('Товарів з найменшою ціною для видалення', count($ids_to_delete)); self::delete_fast($ids_to_delete, self::is_dry_run()); } } TAY Товари для дітей – Сторінка 4

Магазин товарів для дітей

Увага!

🛍 Tay.com.ua — купуй вигідно!
🔔 Мінімальне замовлення — 600 грн
📦 Відправка протягом 1–3 днів  до 19:00 (пʼятниця — вихідний)
🎁 Знижка  при замовленні від 1000 грн 
🚚 Кожне третє замовлення безкоштовна доставка (крім габаритних моделей)
🌟 Постійним клієнтаміндивідуальні знижки та приємні бонуси
📲 Замовляйте на tay.com.uaловіть вигоду!

Асортимент