0 && $missing > $total * self::SAFETY_SHARE) { Log::warning(sprintf( 'Полная выгрузка: нет %d из %d товаров 1С — больше половины. Похоже, каталог пришёл не целиком; товары не тронуты.', $missing, $total )); Session::markSwept(); return; } Session::markChecked(); Log::debug(sprintf('Полная выгрузка: товаров, которых нет в выгрузке, — %d', $missing)); } $ids = self::missingIds($sessionId, self::BATCH); foreach ($ids as $id) { $product = wc_get_product($id); if ($product instanceof WC_Product) { self::apply($product, $action); } update_post_meta($id, self::DONE_META, $sessionId); } if (count($ids) === self::BATCH) { as_schedule_single_action(time(), self::HOOK, [$sessionId], self::GROUP); } else { Session::markSwept(); Log::debug('Полная выгрузка: обработка отсутствующих товаров завершена'); } } private static function apply(WC_Product $product, string $action): void { if ($action === 'outofstock') { $targets = $product instanceof WC_Product_Variable ? array_filter(array_map('wc_get_product', $product->get_children())) : [$product]; foreach ($targets as $target) { if ($target->get_manage_stock()) { $target->set_stock_quantity(0); } $target->set_stock_status('outofstock'); $target->save(); } if ($product instanceof WC_Product_Variable) { WC_Product_Variable::sync($product->get_id()); } Log::debug('Нет в полной выгрузке → нет в наличии: #' . $product->get_id()); return; } // draft / trash — как пометка удаления: товар вернётся, когда снова придёт из 1С. Lifecycle::hide($product, $action); } /** * Опубликованные товары из 1С (с GUID, в том числе от прежнего плагина), которых не было в этом сеансе. * * Сравнение идёт по значению GUID, а не по ID: у переводов Polylang/WPML тот же GUID, * но обновляется (и отмечается) только товар основного языка — переводы не должны считаться пропавшими. * * @return list */ private static function missingIds(int $sessionId, int $limit): array { global $wpdb; $id = (string) $sessionId; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- выборка во время обмена, кеш неуместен $ids = $wpdb->get_col($wpdb->prepare( "SELECT DISTINCT p.ID FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} g ON g.post_id = p.ID AND g.meta_key IN (%s, %s) LEFT JOIN {$wpdb->postmeta} d ON d.post_id = p.ID AND d.meta_key = %s AND d.meta_value = %s WHERE p.post_type = 'product' AND p.post_status IN ('publish', 'pending', 'private') AND d.post_id IS NULL AND NOT EXISTS ( SELECT 1 FROM {$wpdb->postmeta} g2 INNER JOIN {$wpdb->postmeta} s2 ON s2.post_id = g2.post_id AND s2.meta_key = %s AND s2.meta_value = %s WHERE g2.meta_key IN (%s, %s) AND g2.meta_value = g.meta_value ) ORDER BY p.ID ASC LIMIT %d", Repo::META, Repo::LEGACY_META, self::DONE_META, $id, Session::SEEN_META, $id, Repo::META, Repo::LEGACY_META, $limit )); return array_map('intval', $ids); } private static function count(bool $missingOnly, int $sessionId): int { if ($missingOnly) { return count(self::missingIds($sessionId, PHP_INT_MAX)); } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- выборка во время обмена, кеш неуместен return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} g ON g.post_id = p.ID AND g.meta_key IN (%s, %s) WHERE p.post_type = 'product' AND p.post_status IN ('publish', 'pending', 'private')", Repo::META, Repo::LEGACY_META )); } }