*/ public static function get_all(): array { $result = array(); foreach ( self::MANAGED_TYPES as $type ) { $result[ $type ] = self::get_one( $type ); } return $result; } /** * 取得單一 entity 的健康快照。 */ public static function get_one( string $entity_type ): array { $mode = class_exists( 'TMDO_Mode_Manager' ) ? TMDO_Mode_Manager::get( $entity_type ) : 'disabled'; $entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() ); $entered_at = isset( $entered_all[ $entity_type ] ) ? (int) $entered_all[ $entity_type ] : 0; $mode_days = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0; $groups = self::get_groups_health( $entity_type ); $shadow_diffs = 0; if ( class_exists( 'TMDO_Shadow_Diff_Logger' ) ) { $diff_counts = TMDO_Shadow_Diff_Logger::count_by_entity(); $shadow_diffs = (int) ( $diff_counts[ $entity_type ] ?? 0 ); } $auto_promote = self::get_auto_promote_status( $entity_type, $mode, $entered_at, $shadow_diffs ); $backfill_active = self::is_backfill_active( $entity_type ); $native_counts = self::get_native_counts( $entity_type ); return array( 'entity_type' => $entity_type, 'mode' => $mode, 'entered_at' => $entered_at, 'mode_days' => $mode_days, 'groups' => $groups, 'shadow_diffs' => $shadow_diffs, 'auto_promote' => $auto_promote, 'backfill_active' => $backfill_active, 'native_entity_count' => $native_counts['entity_count'], 'native_meta_count' => $native_counts['meta_count'], 'native_entity_table' => $native_counts['entity_table'], 'native_meta_table' => $native_counts['meta_table'], 'recommendation' => self::get_recommendation( $entity_type, $mode, $groups, $shadow_diffs ), 'next_mode' => self::get_next_mode( $mode ), 'prev_mode' => self::get_prev_mode( $mode ), ); } /** * 取得 native EAV 表的總筆數(entity 本體 + meta)。 */ private static function get_native_counts( string $entity_type ): array { global $wpdb; $adapter = class_exists( 'TMDO_Entity_Registry' ) ? TMDO_Entity_Registry::get_adapter( $entity_type ) : null; if ( ! $adapter ) { return array( 'entity_table' => '', 'meta_table' => '', 'entity_count' => 0, 'meta_count' => 0, ); } $meta_table = $adapter->get_native_meta_table(); $entity_table = self::get_native_entity_table( $entity_type ); $entity_count = $entity_table ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$entity_table}`" ) : 0; $meta_count = $meta_table ? (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$meta_table}`" ) : 0; return array( 'entity_table' => $entity_table, 'meta_table' => $meta_table, 'entity_count' => $entity_count, 'meta_count' => $meta_count, ); } /** * 取得 entity 本體資料表名稱(非 meta 表)。 */ private static function get_native_entity_table( string $entity_type ): string { global $wpdb; $map = array( 'user' => $wpdb->users, 'term' => $wpdb->terms, 'comment' => $wpdb->comments, 'post' => $wpdb->posts, ); return $map[ $entity_type ] ?? ''; } // ───────────────────────────────────────────────────────── // 群組覆蓋率 // ───────────────────────────────────────────────────────── private static function get_groups_health( string $entity_type ): array { if ( ! class_exists( 'TMDO_Entity_Registry' ) || ! class_exists( 'TMDO_Schema_Manager' ) ) { return array(); } global $wpdb; $groups = TMDO_Entity_Registry::get_groups_for_type( $entity_type ); $adapter = TMDO_Entity_Registry::get_adapter( $entity_type ); $result = array(); foreach ( $groups as $group_name ) { $fields = TMDO_Entity_Registry::get_group_fields( $entity_type, $group_name ); $table = TMDO_Schema_Manager::get_table_name( $entity_type, $group_name ); $table_exists = TMDO_Schema_Manager::table_exists( $table ); $flat_rows = 0; $eav_rows = 0; if ( $table_exists && $adapter ) { $flat_rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); $managed_keys = array_column( $fields, 'key' ); if ( ! empty( $managed_keys ) ) { $meta_table = $adapter->get_native_meta_table(); $id_col = $adapter->get_entity_id_column(); $phs = implode( ',', array_fill( 0, count( $managed_keys ), '%s' ) ); $eav_rows = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT `{$id_col}`) FROM `{$meta_table}` WHERE `meta_key` IN ({$phs})", ...$managed_keys ) ); } } $coverage_pct = 0.0; if ( $eav_rows > 0 ) { $coverage_pct = round( min( $flat_rows / $eav_rows, 1.0 ) * 100, 1 ); } elseif ( $flat_rows > 0 ) { $coverage_pct = 100.0; } $migration_info = self::get_migration_status( $entity_type, $group_name ); $result[] = array( 'name' => $group_name, 'fields_count' => count( $fields ), 'table' => $table, 'table_exists' => $table_exists, 'flat_rows' => $flat_rows, 'eav_rows' => $eav_rows, 'coverage_pct' => $coverage_pct, 'migration_status' => $migration_info['status'], 'last_id' => $migration_info['last_id'], 'total_migrated' => $migration_info['total_migrated'], 'started_at' => $migration_info['started_at'], 'completed_at' => $migration_info['completed_at'], ); } return $result; } private static function get_migration_status( string $entity_type, string $group_name ): array { global $wpdb; $table = $wpdb->prefix . TMDO_TABLE_PREFIX . 'migration_status'; $row = $wpdb->get_row( $wpdb->prepare( "SELECT status, last_id, total_migrated, started_at, completed_at FROM `{$table}` WHERE entity_type = %s AND group_name = %s", $entity_type, $group_name ), ARRAY_A ); return array( 'status' => $row['status'] ?? 'not_started', 'last_id' => (int) ( $row['last_id'] ?? 0 ), 'total_migrated' => (int) ( $row['total_migrated'] ?? 0 ), 'started_at' => $row['started_at'] ?? null, 'completed_at' => $row['completed_at'] ?? null, ); } // ───────────────────────────────────────────────────────── // Auto-promote 資格評估 // ───────────────────────────────────────────────────────── private static function get_auto_promote_status( string $entity_type, string $mode, int $entered_at, int $shadow_diffs ): array { $enabled = (bool) get_option( TMDO_Auto_Promoter::OPT_ENABLED, false ); $min_days = (int) get_option( TMDO_Auto_Promoter::OPT_MIN_DAYS, TMDO_Auto_Promoter::DEFAULT_MIN_DAYS ); $days_elapsed = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0; if ( $mode !== TMDO_Mode_Manager::MODE_SHADOW_READ ) { return array( 'enabled' => $enabled, 'min_days' => $min_days, 'eligible' => false, 'reason' => 'not_shadow_read', ); } if ( $days_elapsed < $min_days ) { return array( 'enabled' => $enabled, 'min_days' => $min_days, 'days_elapsed' => $days_elapsed, 'eligible' => false, 'reason' => "need_{$min_days}_days", ); } if ( $shadow_diffs > 0 ) { return array( 'enabled' => $enabled, 'min_days' => $min_days, 'days_elapsed' => $days_elapsed, 'eligible' => false, 'reason' => "has_{$shadow_diffs}_diffs", ); } return array( 'enabled' => $enabled, 'min_days' => $min_days, 'days_elapsed' => $days_elapsed, 'eligible' => true, 'reason' => 'ready', ); } // ───────────────────────────────────────────────────────── // 建議與模式轉換 // ───────────────────────────────────────────────────────── private static function get_recommendation( string $entity_type, string $mode, array $groups, int $shadow_diffs ): string { switch ( $mode ) { case TMDO_Mode_Manager::MODE_DISABLED: return '啟用 Hook Bus 並切換到 dual_write,讓系統開始對 flat table 雙寫。'; case TMDO_Mode_Manager::MODE_DUAL_WRITE: foreach ( $groups as $g ) { if ( $g['coverage_pct'] < 99.0 ) { return '執行 Backfill 遷移歷史資料,待所有群組覆蓋率達到 100% 後再升級到 shadow_read。'; } } return '所有群組覆蓋率已達 100%,可以升級到 shadow_read 進行驗證期觀察。'; case TMDO_Mode_Manager::MODE_SHADOW_READ: if ( $shadow_diffs > 0 ) { return "發現 {$shadow_diffs} 筆 shadow diff,請先調查差異原因(Settings → Shadow Diffs)後再考慮升級。"; } $min_days = (int) get_option( TMDO_Auto_Promoter::OPT_MIN_DAYS, TMDO_Auto_Promoter::DEFAULT_MIN_DAYS ); $entered_all = get_option( TMDO_Mode_Manager::OPT_ENTERED_AT, array() ); $entered_at = isset( $entered_all[ $entity_type ] ) ? (int) $entered_all[ $entity_type ] : 0; $days = $entered_at > 0 ? (int) floor( ( time() - $entered_at ) / DAY_IN_SECONDS ) : 0; if ( $days < $min_days ) { return "繼續觀察中(已 {$days}/{$min_days} 天)。無 diff 且穩定後可升級到 aeav_only。"; } return '已達穩定期,可以升級到 aeav_only(最終生產模式)。'; case TMDO_Mode_Manager::MODE_AEAV_ONLY: return '遷移完成。所有讀寫均走 flat table,效能最佳。'; default: return ''; } } private static function get_next_mode( string $mode ): ?string { $idx = array_search( $mode, TMDO_Mode_Manager::ALL_MODES, true ); if ( $idx === false || $idx >= count( TMDO_Mode_Manager::ALL_MODES ) - 1 ) { return null; } return TMDO_Mode_Manager::ALL_MODES[ $idx + 1 ]; } private static function get_prev_mode( string $mode ): ?string { $idx = array_search( $mode, TMDO_Mode_Manager::ALL_MODES, true ); if ( $idx === false || $idx <= 0 ) { return null; } return TMDO_Mode_Manager::ALL_MODES[ $idx - 1 ]; } private static function is_backfill_active( string $entity_type ): bool { return (bool) wp_next_scheduled( 'wpdo_entity_backfill_batch', array( $entity_type ) ); } }