60, 'display' => 'Every Minute' ]; } return $schedules; } public static function activate() { // if (!wp_next_scheduled(self::HOOK)) { // wp_schedule_event(time(), 'every_minute', self::HOOK); // } // // Trigger cron (2h sáng mỗi ngày) // if (!wp_next_scheduled(self::TRIGGER_IMPORT_PRODUCTS_HOOK)) { // $timestamp = strtotime('tomorrow 02:00'); // wp_schedule_event( // $timestamp, // 'daily', // self::TRIGGER_IMPORT_PRODUCTS_HOOK // ); // } } public static function deactivate() { wp_clear_scheduled_hook(self::IMPORT_PRODUCTS_HOOK); wp_clear_scheduled_hook(self::TRIGGER_IMPORT_PRODUCTS_HOOK); wp_clear_scheduled_hook(self::IMPORT_CATEGORIES_HOOK); wp_clear_scheduled_hook(self::DISPATCH_HOOK); } public static function import_products(array $args = []) { $worker = $args['worker'] ?? 'default'; if (!class_exists('WooCommerce')) { WPI_Logger::log('WooCommerce not active'); return; } if (!WPI_Config_Model::get_config('status_cron')) { WPI_Logger::log('Status cron is OFF'); return; } if (WPI_Config_Model::get_config('sync_status_' . $worker)) { WPI_Logger::log('Sync already running'); return; } $import_api = WPI_Config_Model::get_config('import_api'); $authen_key = WPI_Config_Model::get_config('authen_key'); $import_workers = WPI_Config_Model::get_config('import_workers'); if (!$import_api || !$authen_key) { WPI_Logger::log('Missing API config'); return; } $limit = (int) WPI_Config_Model::get_config('limit_per_time', 20); $page = (int) WPI_Config_Model::claim_next_page(); if ($page < 1) { WPI_Logger::log('Invalid page claimed'); return; } WPI_Config_Model::set_config('sync_status_' . $worker, 1, 'Status process sync ' . $worker); try { $response = WPI_API_Importer::products([ 'page' => $page, 'limit' => $limit, ]); // WPI_Config_Model::set_config('current_page', $page + 1); if (is_wp_error($response)) { WPI_Logger::log('API Error: ' . $response->get_error_message()); return; } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); WPI_Logger::log("HTTP Code: {$code}"); if ($code !== 200) { WPI_Logger::log("API failed: {$body}"); return; } $data = json_decode($body, true)['data']; if (!is_array($data)) { WPI_Logger::log('Invalid JSON response'); return; } WPI_Logger::log('Fetched ' . count($data) . ' items'); $category_map = WPI_Category_Mapper::get_category_map(); foreach ($data as $item) { $import_data = WPI_Product_Map::from_api($item, $category_map); $validation = WPI_Product_Validator::validate($import_data); if (!$validation['valid']) { WPI_Logger::log( 'Skip product SKU ' . ($import_data['sku'] ?? 'N/A') . ' | Errors: ' . implode(' | ', $validation['errors']) ); continue; } WPI_Logger::log_product_item($import_data); WPI_Product_Mapper::upsert_from_api($import_data); } if (count($data) < $limit) { WPI_Config_Model::set_config('status_cron', 0); if (!WPI_Config_Model::any_worker_running()) { WPI_Config_Model::set_config('current_page', 0); } } if ($worker === $import_workers) { WPI_Config_Model::set_config('status_workers', 0); } WPI_Logger::log("Page {$page} done"); } catch (\Throwable $th) { WPI_Config_Model::set_config('status_workers', 0); } finally { WPI_Config_Model::set_config('sync_status_' . $worker, 0); } } public static function dispatch_workers() { if (!WPI_Config_Model::get_config('status_cron')) { WPI_Logger::log('Dispatcher aborted: status_cron OFF'); return; } if (!WPI_Config_Model::acquire_lock('status_workers')) { WPI_Logger::log('Dispatcher already running'); return; } if (WPI_Config_Model::any_worker_running()) { WPI_Logger::log('Some workers still running'); return; } WPI_Config_Model::set_config('status_workers', 1); $workers = (int) WPI_Config_Model::get_config('import_workers', 1); if ($workers < 1) { $workers = 1; } WPI_Logger::log("Dispatching {$workers} import workers"); for ($i = 1; $i <= $workers; $i++) { WPI_Logger::log("Dispatch worker {$i}"); $cmd = sprintf( 'wp eval "do_action(\'wpi_import_products_cron\', [\'worker\' => %d]);" --allow-root > /dev/null 2>&1 &', $i ); exec($cmd); } } public static function trigger_import_products() { if (WPI_Config_Model::get_config('status_workers')) { WPI_Logger::log('Trigger skipped: sync is running'); return; } WPI_Logger::log('Trigger import cron ON'); WPI_Config_Model::set_config('status_cron', 1); WPI_Config_Model::set_config('current_page', 1); } public static function import_categories() { if (!class_exists('WooCommerce')) { WPI_Logger::log('WooCommerce not active'); return; } if (!WPI_Config_Model::get_config('categories_status_cron')) { WPI_Logger::log('Status sync categories cron is OFF'); return; } if (WPI_Config_Model::get_config('categories_sync_status')) { WPI_Logger::log('Sync categories already running'); return; } // Lock WPI_Config_Model::set_config('categories_sync_status', 1); try { $response = WPI_API_Importer::categories(); if (is_wp_error($response)) { WPI_Logger::log('API Error: ' . $response->get_error_message()); return; } $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); WPI_Logger::log("HTTP Code: {$code}"); if ($code !== 200) { WPI_Logger::log("API failed: {$body}"); return; } $json = json_decode($body, true); // if (empty($json['status']) || empty($json['data']) || !is_array($json['data'])) { // WPI_Logger::log('Invalid API response structure'); // return; // } if (empty($json['code']) || empty($json['data']) || !is_array($json['data'])) { WPI_Logger::log('Invalid API response structure'); return; } WPI_Logger::log('Fetched ' . count($json['data']) . ' root categories'); $data = WPI_Category_Mapper::map_categories_to_tree($json['data']); WPI_Category_Mapper::upsert_wp_categories_from_tree( $data, 0, 'product_cat' ); WPI_Logger::log('Category sync completed successfully'); } finally { // Unlock ONLY WPI_Config_Model::set_config('categories_sync_status', 0); } } }