update structure
This commit is contained in:
parent
17e4eea464
commit
dc26d50c6b
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
add_action('init', function () {
|
||||||
|
|
||||||
|
if (!function_exists('wc_create_attribute')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attribute_name = 'Source';
|
||||||
|
$attribute_slug = 'source';
|
||||||
|
$taxonomy = 'pa_' . $attribute_slug;
|
||||||
|
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$exists = $wpdb->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
add_action('woocommerce_order_status_changed', function ($order_id, $old, $new, $order) {
|
||||||
|
|
||||||
|
// Chỉ khi chuyển sang completed
|
||||||
|
if ($new !== 'completed') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$order instanceof WC_Order) {
|
||||||
|
$order = wc_get_order($order_id);
|
||||||
|
}
|
||||||
|
if (!$order) return;
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
foreach ($order->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);
|
||||||
|
|
@ -115,4 +115,77 @@ class WPI_Category_Mapper
|
||||||
|
|
||||||
return $map;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,10 +5,22 @@ class WPI_API_Importer
|
||||||
|
|
||||||
private static function api_url($url)
|
private static function api_url($url)
|
||||||
{
|
{
|
||||||
$base_url = WPI_Config_Model::get_config('base_api');
|
if (empty($url)) {
|
||||||
return $base_url . $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 = [])
|
public static function products(array $args = [])
|
||||||
{
|
{
|
||||||
$page = isset($args['page'])
|
$page = isset($args['page'])
|
||||||
|
|
@ -2,23 +2,23 @@
|
||||||
|
|
||||||
class WPI_Cron
|
class WPI_Cron
|
||||||
{
|
{
|
||||||
const HOOK = 'wpi_import_products_cron';
|
const IMPORT_PRODUCTS_HOOK = 'wpi_import_products_cron';
|
||||||
const TRIGGER_HOOK = 'wpi_trigger_import_cron';
|
const TRIGGER_IMPORT_PRODUCTS_HOOK = 'wpi_trigger_import_cron';
|
||||||
const SYNC_CATEGORIES_HOOK = 'wpi_sync_categories_cron';
|
const IMPORT_CATEGORIES_HOOK = 'wpi_sync_categories_cron';
|
||||||
const DISPATCH_HOOK = 'wpi_dispatch_import_workers';
|
const DISPATCH_HOOK = 'wpi_dispatch_import_workers';
|
||||||
|
|
||||||
public static function init()
|
public static function init()
|
||||||
{
|
{
|
||||||
add_filter('cron_schedules', [self::class, 'add_schedule']);
|
add_filter('cron_schedules', [self::class, 'add_schedule']);
|
||||||
add_action(
|
add_action(
|
||||||
self::HOOK,
|
self::IMPORT_PRODUCTS_HOOK,
|
||||||
[self::class, 'run'],
|
[self::class, 'import_products'],
|
||||||
10,
|
10,
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
add_action(self::DISPATCH_HOOK, [self::class, 'dispatch_workers']);
|
add_action(self::DISPATCH_HOOK, [self::class, 'dispatch_workers']);
|
||||||
add_action(self::TRIGGER_HOOK, [self::class, 'trigger']);
|
add_action(self::TRIGGER_IMPORT_PRODUCTS_HOOK, [self::class, 'trigger_import_products']);
|
||||||
add_action(self::SYNC_CATEGORIES_HOOK, [self::class, 'sync_categories']);
|
add_action(self::IMPORT_CATEGORIES_HOOK, [self::class, 'import_categories']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function add_schedule($schedules)
|
public static function add_schedule($schedules)
|
||||||
|
|
@ -39,24 +39,26 @@ class WPI_Cron
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // Trigger cron (2h sáng mỗi ngày)
|
// // 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');
|
// $timestamp = strtotime('tomorrow 02:00');
|
||||||
|
|
||||||
// wp_schedule_event(
|
// wp_schedule_event(
|
||||||
// $timestamp,
|
// $timestamp,
|
||||||
// 'daily',
|
// 'daily',
|
||||||
// self::TRIGGER_HOOK
|
// self::TRIGGER_IMPORT_PRODUCTS_HOOK
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function deactivate()
|
public static function deactivate()
|
||||||
{
|
{
|
||||||
wp_clear_scheduled_hook(self::HOOK);
|
wp_clear_scheduled_hook(self::IMPORT_PRODUCTS_HOOK);
|
||||||
wp_clear_scheduled_hook(self::TRIGGER_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';
|
$worker = $args['worker'] ?? 'default';
|
||||||
|
|
@ -165,6 +167,8 @@ class WPI_Cron
|
||||||
}
|
}
|
||||||
|
|
||||||
WPI_Logger::log("Page {$page} done");
|
WPI_Logger::log("Page {$page} done");
|
||||||
|
} catch (\Throwable $th) {
|
||||||
|
WPI_Config_Model::set_config('status_workers', 0);
|
||||||
} finally {
|
} finally {
|
||||||
WPI_Config_Model::set_config('sync_status_' . $worker, 0);
|
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')) {
|
if (WPI_Config_Model::get_config('status_workers')) {
|
||||||
WPI_Logger::log('Trigger skipped: sync is running');
|
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')) {
|
if (!class_exists('WooCommerce')) {
|
||||||
WPI_Logger::log('WooCommerce not active');
|
WPI_Logger::log('WooCommerce not active');
|
||||||
|
|
@ -266,7 +270,13 @@ class WPI_Cron
|
||||||
|
|
||||||
$json = json_decode($body, true);
|
$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');
|
WPI_Logger::log('Invalid API response structure');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -274,8 +284,10 @@ class WPI_Cron
|
||||||
WPI_Logger::log('Fetched ' . count($json['data']) . ' root categories');
|
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(
|
WPI_Category_Mapper::upsert_wp_categories_from_tree(
|
||||||
$json['data'],
|
$data,
|
||||||
0,
|
0,
|
||||||
'product_cat'
|
'product_cat'
|
||||||
);
|
);
|
||||||
|
|
@ -28,9 +28,24 @@
|
||||||
|
|
||||||
- CLI WP
|
- 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
|
<!-- Products -->
|
||||||
|
|
||||||
|
- wp --allow-root post delete $(wp --allow-root post list --post_type=product,product_variation --format=ids) --force
|
||||||
|
|
||||||
|
<!-- Categories -->
|
||||||
|
|
||||||
|
- wp term delete product_cat \
|
||||||
|
$(wp term list product_cat --field=term_id --allow-root) \
|
||||||
|
--allow-root
|
||||||
|
|
||||||
|
<!-- Medias -->
|
||||||
|
|
||||||
|
- wp post delete \
|
||||||
|
$(wp post list --post_type=attachment --format=ids --allow-root) \
|
||||||
|
--force \
|
||||||
|
--allow-root
|
||||||
|
|
||||||
# Cài WP CLI
|
# 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
|
php wp-cli.phar --info
|
||||||
chmod +x wp-cli.phar
|
chmod +x wp-cli.phar
|
||||||
mv wp-cli.phar /usr/local/bin/wp
|
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
|
||||||
|
|
|
||||||
|
|
@ -21,23 +21,33 @@ define('WPI_URL', plugin_dir_url(__FILE__));
|
||||||
* =========================
|
* =========================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Init
|
||||||
|
require_once WPI_PATH . 'includes/configs/wpi-init.php';
|
||||||
|
|
||||||
// Migrations
|
// Migrations
|
||||||
require_once WPI_PATH . 'includes/migrations/create-config-table.php';
|
require_once WPI_PATH . 'includes/migrations/create-config-table.php';
|
||||||
|
|
||||||
// Models
|
// Models
|
||||||
require_once WPI_PATH . 'includes/models/class-wpi-config-model.php';
|
require_once WPI_PATH . 'includes/models/wpi-config-model.php';
|
||||||
|
|
||||||
// Helper
|
// Helper
|
||||||
require_once WPI_PATH . 'includes/helpers/class-product-helper.php';
|
require_once WPI_PATH . 'includes/helpers/wpi-product-helper.php';
|
||||||
require_once WPI_PATH . 'includes/helpers/class-category-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
|
// Init cron only if WooCommerce active
|
||||||
if (class_exists('WooCommerce')) {
|
// if (class_exists('WooCommerce')) {
|
||||||
|
// WPI_Cron::init();
|
||||||
|
// }
|
||||||
WPI_Cron::init();
|
WPI_Cron::init();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -89,7 +100,8 @@ register_activation_hook(__FILE__, function () {
|
||||||
// Default configs
|
// Default configs
|
||||||
WPI_Config_Model::set_config(
|
WPI_Config_Model::set_config(
|
||||||
'categories_api',
|
'categories_api',
|
||||||
'/category',
|
'https://prology.net/rest/V1/client/get-all-categories',
|
||||||
|
// '/category',
|
||||||
'Api path to get categories'
|
'Api path to get categories'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue