get_col( $wpdb->prepare( "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key LIKE %s", $postId, $wpdb->esc_like($prefix) . '%' ) ); if ($rows) $keys = array_merge($keys, $rows); } return array_values(array_unique($keys)); } /** * شناسه محصولات دیگری که به همان محصول باسلام متصل هستند. * * @return array [شناسه محصول ووکامرس => شناسه محصولات متعارض] */ public static function conflictMap(array $productIds): array { global $wpdb; $productIds = array_values(array_unique(array_filter(array_map('intval', $productIds)))); if (!$productIds) return []; $metaKey = ProductMetaKey::basalamProductId(); $idPlaceholders = implode(',', array_fill(0, count($productIds), '%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Placeholders are generated from an integer list and passed to prepare(). $ownedRows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != '' AND post_id IN ({$idPlaceholders})", array_merge([$metaKey], $productIds) ) ); if (!$ownedRows) return []; $basalamIds = array_values(array_unique(wp_list_pluck($ownedRows, 'meta_value'))); $valuePlaceholders = implode(',', array_fill(0, count($basalamIds), '%s')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Placeholders are generated from the value list and passed to prepare(). $ownerRows = $wpdb->get_results( $wpdb->prepare( "SELECT pm.post_id, pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = %s AND pm.meta_value IN ({$valuePlaceholders}) AND p.post_type = 'product'", array_merge([$metaKey], $basalamIds) ) ); $ownersByBasalamId = []; foreach ($ownerRows as $row) { $ownersByBasalamId[(string) $row->meta_value][] = (int) $row->post_id; } $conflicts = []; foreach ($ownedRows as $row) { $productId = (int) $row->post_id; $owners = $ownersByBasalamId[(string) $row->meta_value] ?? []; $others = array_values(array_diff(array_unique($owners), [$productId])); if ($others) $conflicts[$productId] = $others; } return $conflicts; } /** * آیا این محصول با محصول دیگری روی یک محصول باسلام مشترک است؟ */ public static function hasConflict($productId): bool { return self::conflictingProductIds($productId) !== []; } public static function conflictingProductIds($productId): array { $productId = (int) $productId; if ($productId <= 0) return []; $conflicts = self::conflictMap([$productId]); if (empty($conflicts[$productId])) return []; self::reportConflict($productId, $conflicts[$productId]); return $conflicts[$productId]; } /** * جلوگیری از ارسال هر درخواستی به باسلام وقتی اتصال محصول قابل اعتماد نیست. * * @throws NonRetryableException */ public static function assertUnique($productId, $basalamProductId = null): void { $productId = (int) $productId; $basalamProductId = $basalamProductId !== null && $basalamProductId !== '' ? $basalamProductId : get_post_meta($productId, ProductMetaKey::basalamProductId(), true); if (empty($basalamProductId)) { throw NonRetryableException::invalidData('این محصول به هیچ محصولی در باسلام متصل نیست.'); } $conflictingIds = self::conflictingProductIds($productId); if (!$conflictingIds) return; throw NonRetryableException::permanent(esc_html(self::conflictMessage($basalamProductId, $conflictingIds))); } public static function conflictMessage($basalamProductId, array $conflictingIds): string { return sprintf( 'این محصول با محصول(های) %s روی یک محصول باسلام (شناسه %s) اتصال مشترک دارد. برای جلوگیری از تغییر اشتباه محصول در باسلام، عملیات انجام نشد؛ ابتدا اتصال محصول تکراری (معمولا نسخه کپی شده) را قطع کنید.', implode('، ', array_map('strval', $conflictingIds)), (string) $basalamProductId ); } /** * همه اتصال‌های تکراری سایت. * * @return array */ public static function duplicateConnections(): array { global $wpdb; $metaKey = ProductMetaKey::basalamProductId(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off integrity scan over core postmeta; there is no cache to read from. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT pm.meta_value AS basalam_product_id, GROUP_CONCAT(pm.post_id ORDER BY pm.post_id ASC) AS product_ids FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = %s AND pm.meta_value != '' AND p.post_type = 'product' GROUP BY pm.meta_value HAVING COUNT(DISTINCT pm.post_id) > 1", $metaKey ) ); $duplicates = []; foreach ($rows as $row) { $productIds = array_values(array_unique(array_map('intval', explode(',', (string) $row->product_ids)))); sort($productIds); $duplicates[] = [ 'basalam_product_id' => (string) $row->basalam_product_id, 'product_ids' => $productIds, ]; } return $duplicates; } /** * قطع اتصال نسخه‌های کپی و نگه داشتن قدیمی‌ترین محصول (محصول اصلی). * * @return array */ public static function repairDuplicateConnections(): array { $repairs = []; foreach (self::duplicateConnections() as $duplicate) { $productIds = $duplicate['product_ids']; $kept = array_shift($productIds); if (!$productIds) continue; foreach ($productIds as $productId) { self::purge($productId); } $repair = [ 'basalam_product_id' => $duplicate['basalam_product_id'], 'kept' => (int) $kept, 'disconnected' => $productIds, ]; Logger::warning('اتصال تکراری به یک محصول باسلام پیدا و اصلاح شد.', [ 'شناسه_محصول_باسلام' => $repair['basalam_product_id'], 'محصول_باقی_مانده' => $repair['kept'], 'اتصال_قطع_شده' => implode('، ', array_map('strval', $repair['disconnected'])), 'عملیات' => 'اصلاح اتصال تکراری', ]); $repairs[] = $repair; } if ($repairs) DuplicateConnectionReport::recordRepairs($repairs); return $repairs; } /** * ثبت اتصال مشترک در گزارش مدیر و لاگ (فقط بار اول برای هر مورد). */ public static function reportConflict(int $productId, array $conflictingIds): void { $isNew = DuplicateConnectionReport::recordConflict($productId, $conflictingIds); if (!$isNew) return; Logger::error('همگام‌سازی متوقف شد: اتصال این محصول با محصول دیگری مشترک است.', [ 'product_id' => $productId, 'محصولات_متعارض' => implode('، ', array_map('strval', $conflictingIds)), 'عملیات' => 'بررسی اتصال محصول', ]); } private static function variationIds(int $productId): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Children are read directly so that variations are cleaned even before the parent object is loadable as a variable product. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_parent = %d AND post_type = 'product_variation'", $productId ) ); return array_map('intval', $ids ?: []); } }