290 lines
8.2 KiB
PHP
290 lines
8.2 KiB
PHP
<?php
|
|
|
|
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 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'],
|
|
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']);
|
|
}
|
|
|
|
public static function add_schedule($schedules)
|
|
{
|
|
if (!isset($schedules['every_minute'])) {
|
|
$schedules['every_minute'] = [
|
|
'interval' => 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_HOOK)) {
|
|
// $timestamp = strtotime('tomorrow 02:00');
|
|
|
|
// wp_schedule_event(
|
|
// $timestamp,
|
|
// 'daily',
|
|
// self::TRIGGER_HOOK
|
|
// );
|
|
// }
|
|
}
|
|
|
|
public static function deactivate()
|
|
{
|
|
wp_clear_scheduled_hook(self::HOOK);
|
|
wp_clear_scheduled_hook(self::TRIGGER_HOOK);
|
|
}
|
|
|
|
public static function run(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");
|
|
} 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()
|
|
{
|
|
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 sync_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;
|
|
}
|
|
|
|
WPI_Logger::log('Fetched ' . count($json['data']) . ' root categories');
|
|
|
|
|
|
WPI_Category_Mapper::upsert_wp_categories_from_tree(
|
|
$json['data'],
|
|
0,
|
|
'product_cat'
|
|
);
|
|
|
|
WPI_Logger::log('Category sync completed successfully');
|
|
} finally {
|
|
// Unlock ONLY
|
|
WPI_Config_Model::set_config('categories_sync_status', 0);
|
|
}
|
|
}
|
|
}
|