array( 0, 25 ), '25-50' => array( 25, 50 ), '50-75' => array( 50, 75 ), '75-100' => array( 75, 101 ), ); } private static function time_buckets() { return array( '0-10s' => array( 0, 10 ), '10-30s' => array( 10, 30 ), '30-90s' => array( 30, 90 ), '90s+' => array( 90, PHP_INT_MAX ), ); } private static function table_exists( $table ) { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s", $table ) ) > 0; } public static function compute_bucket_table( $product_id, $period_days = 90 ) { global $wpdb; $product_id = (int) $product_id; $period_days = max( 1, min( 730, (int) $period_days ) ); if ( $product_id <= 0 ) { return new WP_Error( 'aieo_invalid_input', __( 'product_id is required.', 'ai-eshop-optimizer' ) ); } $ses = $wpdb->prefix . 'aieo_sessions'; $ae = $wpdb->prefix . 'aieo_addcart_events'; $rows = self::empty_grid(); if ( ! self::table_exists( $ses ) ) { return array( 'product_id' => $product_id, 'period_days' => $period_days, 'rows' => $rows, 'note' => __( 'wp_aieo_sessions not installed.', 'ai-eshop-optimizer' ), ); } $now_ts = current_time( 'timestamp', true ); $since_ts = $now_ts - $period_days * DAY_IN_SECONDS; $since_dt = gmdate( 'Y-m-d H:i:s', $since_ts ); $until_dt = gmdate( 'Y-m-d H:i:s', $now_ts ); $views = $wpdb->get_results( $wpdb->prepare( "SELECT session_id, MAX(COALESCE(scroll_depth, 0)) AS scroll_depth, MAX(COALESCE(time_on_page, 0)) AS time_on_page FROM {$ses} WHERE event_type = 'product_view' AND product_id = %d AND timestamp BETWEEN %d AND %d GROUP BY session_id", $product_id, $since_ts, $now_ts ), ARRAY_A ); if ( empty( $views ) ) { return array( 'product_id' => $product_id, 'period_days' => $period_days, 'rows' => $rows, ); } $converters = array(); if ( self::table_exists( $ae ) ) { $conv = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT session_id FROM {$ae} WHERE product_id = %d AND ts_added BETWEEN %s AND %s", $product_id, $since_dt, $until_dt ) ); foreach ( (array) $conv as $sid ) { $converters[ (string) $sid ] = true; } } $grid = self::empty_grid_map(); foreach ( (array) $views as $v ) { $sb = self::scroll_bucket( (int) $v['scroll_depth'] ); $tb = self::time_bucket( (int) $v['time_on_page'] ); $key = $sb . '|' . $tb; $grid[ $key ]['viewers']++; if ( isset( $converters[ (string) $v['session_id'] ] ) ) { $grid[ $key ]['cart_adders']++; } } $rows = array(); foreach ( $grid as $key => $cell ) { list( $sb, $tb ) = explode( '|', $key ); $rows[] = array( 'scroll_bucket' => $sb, 'time_bucket' => $tb, 'viewers' => (int) $cell['viewers'], 'cart_adders' => (int) $cell['cart_adders'], 'p_cart_add' => $cell['viewers'] > 0 ? round( $cell['cart_adders'] / $cell['viewers'], 4 ) : null, ); } return array( 'product_id' => $product_id, 'period_days' => $period_days, 'rows' => $rows, ); } public static function find_steepest_gradient_products( $period_days = 90, $limit = 20 ) { global $wpdb; $period_days = max( 1, min( 730, (int) $period_days ) ); $limit = max( 1, min( 100, (int) $limit ) ); $ses = $wpdb->prefix . 'aieo_sessions'; $ae = $wpdb->prefix . 'aieo_addcart_events'; if ( ! self::table_exists( $ses ) || ! self::table_exists( $ae ) ) { return array( 'rows' => array(), 'note' => __( 'Session-tracker tables not installed.', 'ai-eshop-optimizer' ) ); } $now_ts = current_time( 'timestamp', true ); $since_ts = $now_ts - $period_days * DAY_IN_SECONDS; $since_dt = gmdate( 'Y-m-d H:i:s', $since_ts ); $until_dt = gmdate( 'Y-m-d H:i:s', $now_ts ); $candidates = $wpdb->get_col( $wpdb->prepare( "SELECT product_id FROM {$ses} WHERE event_type = 'product_view' AND product_id IS NOT NULL AND timestamp BETWEEN %d AND %d GROUP BY product_id HAVING COUNT(*) >= 20 ORDER BY COUNT(*) DESC LIMIT 500", $since_ts, $now_ts ) ); if ( empty( $candidates ) ) { return array( 'rows' => array(), 'period_days' => $period_days ); } $results = array(); foreach ( $candidates as $pid ) { $pid = (int) $pid; $tb = self::compute_bucket_table( $pid, $period_days ); if ( ! is_array( $tb ) || empty( $tb['rows'] ) ) { continue; } $top_v = 0; $top_a = 0; $bot_v = 0; $bot_a = 0; foreach ( $tb['rows'] as $cell ) { if ( $cell['scroll_bucket'] === '75-100' && $cell['time_bucket'] === '90s+' ) { $top_v = (int) $cell['viewers']; $top_a = (int) $cell['cart_adders']; } elseif ( $cell['scroll_bucket'] === '0-25' && $cell['time_bucket'] === '0-10s' ) { $bot_v = (int) $cell['viewers']; $bot_a = (int) $cell['cart_adders']; } } if ( $top_v < 3 ) { continue; } $top_rate = $top_v > 0 ? $top_a / $top_v : 0.0; $bot_rate = $bot_v > 0 ? $bot_a / $bot_v : 0.0; $bot_rate_s = max( $bot_rate, 1.0 / ( $bot_v + 1 ) ); $lift = $top_rate / max( $bot_rate_s, 0.0001 ); if ( $lift < 5.0 ) { continue; } $results[] = array( 'product_id' => $pid, 'product_name' => get_the_title( $pid ), 'top_viewers' => $top_v, 'top_adders' => $top_a, 'top_rate' => round( $top_rate, 4 ), 'bottom_viewers' => $bot_v, 'bottom_adders' => $bot_a, 'bottom_rate' => round( $bot_rate, 4 ), 'lift' => round( $lift, 2 ), ); } usort( $results, static function ( $a, $b ) { return $b['lift'] <=> $a['lift']; } ); return array( 'period_days' => $period_days, 'rows' => array_slice( $results, 0, $limit ), ); } private static function scroll_bucket( $depth ) { foreach ( self::scroll_buckets() as $label => $range ) { if ( $depth >= $range[0] && $depth < $range[1] ) { return $label; } } return '0-25'; } private static function time_bucket( $secs ) { foreach ( self::time_buckets() as $label => $range ) { if ( $secs >= $range[0] && $secs < $range[1] ) { return $label; } } return '0-10s'; } private static function empty_grid() { $rows = array(); foreach ( array_keys( self::scroll_buckets() ) as $sb ) { foreach ( array_keys( self::time_buckets() ) as $tb ) { $rows[] = array( 'scroll_bucket' => $sb, 'time_bucket' => $tb, 'viewers' => 0, 'cart_adders' => 0, 'p_cart_add' => null, ); } } return $rows; } private static function empty_grid_map() { $grid = array(); foreach ( array_keys( self::scroll_buckets() ) as $sb ) { foreach ( array_keys( self::time_buckets() ) as $tb ) { $grid[ $sb . '|' . $tb ] = array( 'viewers' => 0, 'cart_adders' => 0 ); } } return $grid; } }