diff --git a/wp-product-importer/includes/configs/wpi-init.php b/wp-product-importer/includes/configs/wpi-init.php new file mode 100644 index 0000000..7dadb7f --- /dev/null +++ b/wp-product-importer/includes/configs/wpi-init.php @@ -0,0 +1,64 @@ +get_var( + $wpdb->prepare( + "SELECT attribute_id + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_name = %s", + $attribute_slug + ) + ); + + + if (!$exists) { + wc_create_attribute([ + 'name' => $attribute_name, + 'slug' => $attribute_slug, + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => false, + ]); + + // Clear cache Woo + delete_transient('wc_attribute_taxonomies'); + } + + + if (!taxonomy_exists($taxonomy)) { + register_taxonomy( + $taxonomy, + 'product', + [ + 'label' => $attribute_name, + 'public' => false, + 'hierarchical' => false, + + 'show_ui' => true, + 'query_var' => true, + 'rewrite' => false, + + // Cho phép gán trong Product data → Attributes + 'meta_box_cb' => 'post_tags_meta_box', + ] + ); + } + + + foreach (['on_store', 'links'] as $term) { + if (!term_exists($term, $taxonomy)) { + wp_insert_term($term, $taxonomy); + } + } +}); diff --git a/wp-product-importer/includes/events/wpi-order-event.php b/wp-product-importer/includes/events/wpi-order-event.php new file mode 100644 index 0000000..66a5436 --- /dev/null +++ b/wp-product-importer/includes/events/wpi-order-event.php @@ -0,0 +1,69 @@ +get_items() as $item_id => $item) { + /** @var WC_Order_Item_Product $item */ + $product = $item->get_product(); + + $items[] = [ + 'item_id' => $item_id, + 'product_id' => $product ? $product->get_id() : null, + 'variation_id' => $item->get_variation_id(), + 'type' => $product ? $product->get_type() : null, + 'name' => $item->get_name(), + 'sku' => $product ? $product->get_sku() : null, + 'quantity' => $item->get_quantity(), + + // Giá + 'subtotal' => $item->get_subtotal(), + 'total' => $item->get_total(), + 'tax' => $item->get_total_tax(), + + // Giá đơn vị + 'price_per_item' => $item->get_quantity() > 0 + ? round($item->get_total() / $item->get_quantity(), 2) + : 0, + + // Attributes (variation) + 'attributes' => $item->get_variation_id() + ? wc_get_formatted_variation($product, true, false, true) + : null, + + // Meta item + 'item_meta' => array_map(function ($meta) { + return [ + 'key' => $meta->key, + 'value' => $meta->value, + ]; + }, $item->get_meta_data()), + ]; + } + + $log = [ + 'event' => 'order_completed', + 'order_id' => $order_id, + 'old_status' => $old, + 'new_status' => $new, + 'currency' => $order->get_currency(), + 'total' => $order->get_total(), + 'items' => $items, + ]; + + WPI_Logger::log(json_encode( + $log, + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE + )); +}, 10, 4); diff --git a/wp-product-importer/includes/helpers/class-category-helper.php b/wp-product-importer/includes/helpers/wpi-category-helper.php similarity index 58% rename from wp-product-importer/includes/helpers/class-category-helper.php rename to wp-product-importer/includes/helpers/wpi-category-helper.php index 2f279ec..5b11108 100644 --- a/wp-product-importer/includes/helpers/class-category-helper.php +++ b/wp-product-importer/includes/helpers/wpi-category-helper.php @@ -115,4 +115,77 @@ class WPI_Category_Mapper return $map; } + + public static function map_categories_to_tree( + array $categories, + $rootParentId = '0', + array $ignoreIds = [] + ) { + $map = []; + $roots = []; + + // Map ignore + $ignoreMap = array_flip(array_map('strval', $ignoreIds)); + + // Map id => parentId để truy ngược nhanh + $parentLookup = []; + foreach ($categories as $cat) { + $parentLookup[(string) $cat['id']] = (string) $cat['parentId']; + } + + // Step 1: tạo node + foreach ($categories as $item) { + if (isset($ignoreMap[(string) $item['id']])) { + continue; + } + + $map[(string) $item['id']] = [ + 'id' => (int) $item['id'], + 'name' => trim($item['name']), + 'level' => 0, + 'parent_id' => (int) $item['parentId'], + 'category_origin_id' => '', + 'group_category_id' => null, + 'is_active' => 1, + 'value' => (int) $item['id'], + 'label' => trim($item['name']), + 'children' => [], + ]; + } + + // Step 2: gắn children + foreach ($map as $id => &$node) { + + $parentId = (string) $node['parent_id']; + + // Nếu parent bị ignore → leo lên cha tiếp theo + while ( + isset($ignoreMap[$parentId]) && + isset($parentLookup[$parentId]) + ) { + $parentId = $parentLookup[$parentId]; + } + + if ($parentId == (string) $rootParentId || !isset($map[$parentId])) { + $roots[] = &$node; + } else { + $map[$parentId]['children'][] = &$node; + } + } + unset($node); + + // Step 3: set level + $setLevel = function (&$nodes, $level) use (&$setLevel) { + foreach ($nodes as &$node) { + $node['level'] = $level; + if (!empty($node['children'])) { + $setLevel($node['children'], $level + 1); + } + } + }; + + $setLevel($roots, 1); + + return $roots; + } } diff --git a/wp-product-importer/includes/helpers/class-product-helper.php b/wp-product-importer/includes/helpers/wpi-product-helper.php similarity index 100% rename from wp-product-importer/includes/helpers/class-product-helper.php rename to wp-product-importer/includes/helpers/wpi-product-helper.php diff --git a/wp-product-importer/includes/class-product-map.php b/wp-product-importer/includes/mappings/wpi-product-map.php similarity index 100% rename from wp-product-importer/includes/class-product-map.php rename to wp-product-importer/includes/mappings/wpi-product-map.php diff --git a/wp-product-importer/includes/models/class-wpi-config-model.php b/wp-product-importer/includes/models/wpi-config-model.php similarity index 100% rename from wp-product-importer/includes/models/class-wpi-config-model.php rename to wp-product-importer/includes/models/wpi-config-model.php diff --git a/wp-product-importer/includes/class-api-importer.php b/wp-product-importer/includes/wpi-api-importer.php similarity index 80% rename from wp-product-importer/includes/class-api-importer.php rename to wp-product-importer/includes/wpi-api-importer.php index 3073f29..76ca0f7 100644 --- a/wp-product-importer/includes/class-api-importer.php +++ b/wp-product-importer/includes/wpi-api-importer.php @@ -5,10 +5,22 @@ class WPI_API_Importer private static function api_url($url) { - $base_url = WPI_Config_Model::get_config('base_api'); - return $base_url . $url; + if (empty($url)) { + return $url; + } + + // Nếu đã là absolute URL (http / https) + $parsed = parse_url($url); + if (isset($parsed['scheme']) && in_array($parsed['scheme'], ['http', 'https'], true)) { + return $url; + } + + $base_url = rtrim(WPI_Config_Model::get_config('base_api'), '/'); + + return $base_url . ltrim($url, '/'); } + public static function products(array $args = []) { $page = isset($args['page']) diff --git a/wp-product-importer/includes/class-cron.php b/wp-product-importer/includes/wpi-cron.php similarity index 83% rename from wp-product-importer/includes/class-cron.php rename to wp-product-importer/includes/wpi-cron.php index dc1a1c4..7ce6976 100644 --- a/wp-product-importer/includes/class-cron.php +++ b/wp-product-importer/includes/wpi-cron.php @@ -2,23 +2,23 @@ class WPI_Cron { - const HOOK = 'wpi_import_products_cron'; - const TRIGGER_HOOK = 'wpi_trigger_import_cron'; - const SYNC_CATEGORIES_HOOK = 'wpi_sync_categories_cron'; + const IMPORT_PRODUCTS_HOOK = 'wpi_import_products_cron'; + const TRIGGER_IMPORT_PRODUCTS_HOOK = 'wpi_trigger_import_cron'; + const IMPORT_CATEGORIES_HOOK = 'wpi_sync_categories_cron'; const DISPATCH_HOOK = 'wpi_dispatch_import_workers'; public static function init() { add_filter('cron_schedules', [self::class, 'add_schedule']); add_action( - self::HOOK, - [self::class, 'run'], + self::IMPORT_PRODUCTS_HOOK, + [self::class, 'import_products'], 10, 1 ); add_action(self::DISPATCH_HOOK, [self::class, 'dispatch_workers']); - add_action(self::TRIGGER_HOOK, [self::class, 'trigger']); - add_action(self::SYNC_CATEGORIES_HOOK, [self::class, 'sync_categories']); + add_action(self::TRIGGER_IMPORT_PRODUCTS_HOOK, [self::class, 'trigger_import_products']); + add_action(self::IMPORT_CATEGORIES_HOOK, [self::class, 'import_categories']); } public static function add_schedule($schedules) @@ -39,24 +39,26 @@ class WPI_Cron // } // // Trigger cron (2h sáng mỗi ngày) - // if (!wp_next_scheduled(self::TRIGGER_HOOK)) { + // if (!wp_next_scheduled(self::TRIGGER_IMPORT_PRODUCTS_HOOK)) { // $timestamp = strtotime('tomorrow 02:00'); // wp_schedule_event( // $timestamp, // 'daily', - // self::TRIGGER_HOOK + // self::TRIGGER_IMPORT_PRODUCTS_HOOK // ); // } } public static function deactivate() { - wp_clear_scheduled_hook(self::HOOK); - wp_clear_scheduled_hook(self::TRIGGER_HOOK); + 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 run(array $args = []) + public static function import_products(array $args = []) { $worker = $args['worker'] ?? 'default'; @@ -165,6 +167,8 @@ class WPI_Cron } 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); } @@ -212,7 +216,7 @@ class WPI_Cron } - public static function trigger() + public static function trigger_import_products() { if (WPI_Config_Model::get_config('status_workers')) { WPI_Logger::log('Trigger skipped: sync is running'); @@ -226,7 +230,7 @@ class WPI_Cron } - public static function sync_categories() + public static function import_categories() { if (!class_exists('WooCommerce')) { WPI_Logger::log('WooCommerce not active'); @@ -266,7 +270,13 @@ class WPI_Cron $json = json_decode($body, true); - if (empty($json['status']) || empty($json['data']) || !is_array($json['data'])) { + + // 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; } @@ -274,8 +284,10 @@ class WPI_Cron 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( - $json['data'], + $data, 0, 'product_cat' ); diff --git a/wp-product-importer/includes/class-logger.php b/wp-product-importer/includes/wpi-logger.php similarity index 100% rename from wp-product-importer/includes/class-logger.php rename to wp-product-importer/includes/wpi-logger.php diff --git a/wp-product-importer/includes/class-product-validator.php b/wp-product-importer/includes/wpi-product-validator.php similarity index 100% rename from wp-product-importer/includes/class-product-validator.php rename to wp-product-importer/includes/wpi-product-validator.php diff --git a/wp-product-importer/readme.md b/wp-product-importer/readme.md index 6a18e94..64088df 100644 --- a/wp-product-importer/readme.md +++ b/wp-product-importer/readme.md @@ -28,9 +28,24 @@ - CLI WP -# Xoá toàn bộ products +# Xoá dữ liệu -wp --allow-root post delete $(wp --allow-root post list --post_type=product,product_variation --format=ids) --force + + +- wp --allow-root post delete $(wp --allow-root post list --post_type=product,product_variation --format=ids) --force + + + +- wp term delete product_cat \ + $(wp term list product_cat --field=term_id --allow-root) \ + --allow-root + + + +- wp post delete \ + $(wp post list --post_type=attachment --format=ids --allow-root) \ + --force \ + --allow-root # Cài WP CLI @@ -38,3 +53,7 @@ curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.pha php wp-cli.phar --info chmod +x wp-cli.phar mv wp-cli.phar /usr/local/bin/wp + +# Tìm kiếm tiến trình + +ps aux | grep "wp eval" | grep "wpi_import_products_cron" | grep -v grep diff --git a/wp-product-importer/wp-product-importer.php b/wp-product-importer/wp-product-importer.php index a3b937c..31a1aaa 100644 --- a/wp-product-importer/wp-product-importer.php +++ b/wp-product-importer/wp-product-importer.php @@ -21,23 +21,33 @@ define('WPI_URL', plugin_dir_url(__FILE__)); * ========================= */ +// Init +require_once WPI_PATH . 'includes/configs/wpi-init.php'; + // Migrations require_once WPI_PATH . 'includes/migrations/create-config-table.php'; // Models -require_once WPI_PATH . 'includes/models/class-wpi-config-model.php'; +require_once WPI_PATH . 'includes/models/wpi-config-model.php'; // Helper -require_once WPI_PATH . 'includes/helpers/class-product-helper.php'; -require_once WPI_PATH . 'includes/helpers/class-category-helper.php'; +require_once WPI_PATH . 'includes/helpers/wpi-product-helper.php'; +require_once WPI_PATH . 'includes/helpers/wpi-category-helper.php'; + +// Mappings +require_once WPI_PATH . 'includes/mappings/wpi-product-map.php'; + + +// Core WPIS +require_once WPI_PATH . 'includes/wpi-api-importer.php'; +require_once WPI_PATH . 'includes/wpi-cron.php'; +require_once WPI_PATH . 'includes/wpi-logger.php'; +require_once WPI_PATH . 'includes/wpi-product-validator.php'; + +// Events +require_once WPI_PATH . 'includes/events/wpi-order-event.php'; -// Core classes -require_once WPI_PATH . 'includes/class-api-importer.php'; -require_once WPI_PATH . 'includes/class-cron.php'; -require_once WPI_PATH . 'includes/class-logger.php'; -require_once WPI_PATH . 'includes/class-product-map.php'; -require_once WPI_PATH . 'includes/class-product-validator.php'; /** @@ -53,9 +63,10 @@ add_action('plugins_loaded', function () { // } // Init cron only if WooCommerce active - if (class_exists('WooCommerce')) { - WPI_Cron::init(); - } + // if (class_exists('WooCommerce')) { + // WPI_Cron::init(); + // } + WPI_Cron::init(); }); /** @@ -89,7 +100,8 @@ register_activation_hook(__FILE__, function () { // Default configs WPI_Config_Model::set_config( 'categories_api', - '/category', + 'https://prology.net/rest/V1/client/get-all-categories', + // '/category', 'Api path to get categories' );