76c01e44df
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入 並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
299 lines
12 KiB
PHP
299 lines
12 KiB
PHP
<?php
|
||
/**
|
||
* Post Migration Wizard template (v2.10.0).
|
||
*
|
||
* Variables in scope from render_post_migration_wizard():
|
||
* $diagnose — TMDO_Post_Migration::diagnose() output
|
||
* $garbage — TMDO_Postmeta_Cleaner::count_garbage('all') output
|
||
*
|
||
* Sync execution model — each action button POSTs back to this page with a
|
||
* query arg + nonce, the action runs to completion in one request, then
|
||
* redirects with `wpdo_msg=*` for the success/failure banner.
|
||
*
|
||
* @package WP_Data_Optimizer
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
|
||
if ( ! defined( 'ABSPATH' ) ) {
|
||
exit;
|
||
}
|
||
|
||
$page_url = admin_url( 'tools.php?page=wp-data-optimizer&tab=post-migration-wizard' );
|
||
|
||
$total_eav = 0;
|
||
foreach ( $diagnose['groups'] as $g ) {
|
||
$total_eav += (int) $g['eav_rows'];
|
||
}
|
||
$total_flat = 0;
|
||
foreach ( $diagnose['groups'] as $g ) {
|
||
$total_flat += (int) $g['flat_rows'];
|
||
}
|
||
|
||
$mode_label = array(
|
||
'disabled' => __( '🔵 disabled — wp_postmeta 仍是 source-of-truth', '2meet-data-optimizer' ),
|
||
'dual_write' => __( '🟡 dual_write — 同時寫 wp_postmeta + flat 表', '2meet-data-optimizer' ),
|
||
'shadow_read' => __( '🟠 shadow_read — 寫雙路徑,讀比對', '2meet-data-optimizer' ),
|
||
'aeav_only' => __( '🟢 aeav_only — flat 表為 source-of-truth', '2meet-data-optimizer' ),
|
||
);
|
||
|
||
|
||
// Status banner from prior action redirect.
|
||
// Read-only display banner — server-set redirect message, no form processing.
|
||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||
$msg_raw = isset( $_GET['wpdo_msg'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['wpdo_msg'] ) ) : '';
|
||
|
||
/**
|
||
* Map redirect message code → user-facing string.
|
||
*
|
||
* @param string $code Message code from wpdo_msg query arg.
|
||
* @return string
|
||
*/
|
||
$wpdo_format_msg = static function ( string $code ): string {
|
||
$prefix = strtok( $code, '_' );
|
||
$rest = substr( $code, strlen( (string) $prefix ) + 1 );
|
||
switch ( $prefix ) {
|
||
case 'backfill':
|
||
return sprintf(
|
||
/* translators: %s: total flat rows */
|
||
__( 'Backfill 完成:7 個 group 共 %s 個 post 已寫入 flat 表。', '2meet-data-optimizer' ),
|
||
$rest
|
||
);
|
||
case 'cutover':
|
||
return sprintf(
|
||
/* translators: %s: rows */
|
||
__( 'Legacy hot table cutover 完成:%s 行已 copy 至 flat 表。', '2meet-data-optimizer' ),
|
||
$rest
|
||
);
|
||
case 'promote':
|
||
return sprintf(
|
||
/* translators: %s: target mode */
|
||
__( 'Post mode 已升級至 %s。', '2meet-data-optimizer' ),
|
||
$rest
|
||
);
|
||
case 'postmetacleanupdone':
|
||
return sprintf(
|
||
/* translators: %s: deleted rows */
|
||
__( '已從 wp_postmeta 刪除 %s 行垃圾。', '2meet-data-optimizer' ),
|
||
$rest
|
||
);
|
||
case 'err':
|
||
return sprintf(
|
||
/* translators: %s: error code */
|
||
__( '錯誤:%s(請查看 wpdo_errors 日誌)。', '2meet-data-optimizer' ),
|
||
str_replace( '_', ' ', $rest )
|
||
);
|
||
default:
|
||
return $code;
|
||
}
|
||
};
|
||
?>
|
||
|
||
<style>
|
||
.wpdo-post-wizard h2 { margin-top: 1.4em; }
|
||
.wpdo-post-wizard .wpdo-card {
|
||
background: #fff;
|
||
border: 1px solid #c3c4c7;
|
||
border-radius: 4px;
|
||
padding: 1em 1.4em;
|
||
margin: 0.8em 0;
|
||
}
|
||
.wpdo-post-wizard .wpdo-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 1em;
|
||
margin: 1em 0;
|
||
}
|
||
.wpdo-post-wizard .wpdo-stat {
|
||
background: #f6f7f7;
|
||
border-left: 4px solid #2271b1;
|
||
padding: 0.8em 1em;
|
||
border-radius: 3px;
|
||
}
|
||
.wpdo-post-wizard .wpdo-stat-label { font-size: 0.85em; color: #50575e; }
|
||
.wpdo-post-wizard .wpdo-stat-value { font-size: 1.6em; font-weight: 600; }
|
||
.wpdo-post-wizard .wpdo-mode-banner {
|
||
padding: 0.6em 1em;
|
||
border-radius: 3px;
|
||
margin: 0.8em 0;
|
||
}
|
||
.wpdo-post-wizard .wpdo-mode-disabled { background: #e5f5fa; border-left: 4px solid #00a0d2; }
|
||
.wpdo-post-wizard .wpdo-mode-dual_write { background: #fffbe5; border-left: 4px solid #dba617; }
|
||
.wpdo-post-wizard .wpdo-mode-shadow_read { background: #fff4e5; border-left: 4px solid #d54e21; }
|
||
.wpdo-post-wizard .wpdo-mode-aeav_only { background: #ecf7ed; border-left: 4px solid #46b450; }
|
||
.wpdo-post-wizard table.widefat td.right { text-align: right; }
|
||
.wpdo-post-wizard .actions {
|
||
margin-top: 1.6em;
|
||
padding-top: 1em;
|
||
border-top: 1px solid #dcdcde;
|
||
}
|
||
.wpdo-post-wizard .actions .button { margin-right: 0.4em; margin-bottom: 0.4em; }
|
||
</style>
|
||
|
||
<div class="wpdo-post-wizard">
|
||
|
||
<?php if ( $msg_raw ) : ?>
|
||
<?php
|
||
$msg_class = str_starts_with( $msg_raw, 'err' ) ? 'notice-error' : 'notice-success';
|
||
$msg_text = $wpdo_format_msg( $msg_raw );
|
||
?>
|
||
<div class="notice <?php echo esc_attr( $msg_class ); ?> is-dismissible">
|
||
<p><?php echo esc_html( $msg_text ); ?></p>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<h2><?php esc_html_e( 'Post Entity 遷移精靈', '2meet-data-optimizer' ); ?></h2>
|
||
|
||
<p class="description">
|
||
<?php esc_html_e( '把 wp_postmeta 中的 47 個 managed keys 遷移到 7 張 flat 表(wp_wpdo_post_*)。每個按鈕只執行單一階段;按順序執行可達成完整 EAV → flat 切換。', '2meet-data-optimizer' ); ?>
|
||
</p>
|
||
|
||
<!-- Mode banner -->
|
||
<div class="wpdo-mode-banner wpdo-mode-<?php echo esc_attr( $diagnose['mode'] ); ?>">
|
||
<strong><?php esc_html_e( '當前 post mode:', '2meet-data-optimizer' ); ?></strong>
|
||
<?php echo esc_html( $mode_label[ $diagnose['mode'] ] ?? $diagnose['mode'] ); ?>
|
||
</div>
|
||
|
||
<!-- Stats -->
|
||
<div class="wpdo-stats">
|
||
<div class="wpdo-stat">
|
||
<div class="wpdo-stat-label"><?php esc_html_e( 'Posts', '2meet-data-optimizer' ); ?></div>
|
||
<div class="wpdo-stat-value"><?php echo esc_html( number_format_i18n( $diagnose['posts'] ) ); ?></div>
|
||
</div>
|
||
<div class="wpdo-stat">
|
||
<div class="wpdo-stat-label"><?php esc_html_e( 'Postmeta', '2meet-data-optimizer' ); ?></div>
|
||
<div class="wpdo-stat-value"><?php echo esc_html( number_format_i18n( $diagnose['postmeta'] ) ); ?></div>
|
||
</div>
|
||
<div class="wpdo-stat">
|
||
<div class="wpdo-stat-label"><?php esc_html_e( 'Ratio', '2meet-data-optimizer' ); ?></div>
|
||
<div class="wpdo-stat-value">1:<?php echo esc_html( (string) $diagnose['ratio'] ); ?></div>
|
||
</div>
|
||
<div class="wpdo-stat">
|
||
<div class="wpdo-stat-label"><?php esc_html_e( 'Total flat rows', '2meet-data-optimizer' ); ?></div>
|
||
<div class="wpdo-stat-value"><?php echo esc_html( number_format_i18n( $total_flat ) ); ?></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Group breakdown -->
|
||
<h3><?php esc_html_e( '7 個 group 詳細狀態', '2meet-data-optimizer' ); ?></h3>
|
||
<table class="widefat striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e( 'Group', '2meet-data-optimizer' ); ?></th>
|
||
<th><?php esc_html_e( 'post_type', '2meet-data-optimizer' ); ?></th>
|
||
<th class="right"><?php esc_html_e( 'Keys', '2meet-data-optimizer' ); ?></th>
|
||
<th class="right"><?php esc_html_e( 'EAV rows', '2meet-data-optimizer' ); ?></th>
|
||
<th class="right"><?php esc_html_e( 'Flat rows', '2meet-data-optimizer' ); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ( $diagnose['groups'] as $name => $g ) : ?>
|
||
<tr>
|
||
<td><code><?php echo esc_html( $name ); ?></code></td>
|
||
<td><?php echo esc_html( $g['post_type'] ?: '(any)' ); ?></td>
|
||
<td class="right"><?php echo esc_html( (string) count( $g['keys'] ) ); ?></td>
|
||
<td class="right"><?php echo esc_html( number_format_i18n( (int) $g['eav_rows'] ) ); ?></td>
|
||
<td class="right"><?php echo esc_html( number_format_i18n( (int) $g['flat_rows'] ) ); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- Phase actions -->
|
||
<div class="wpdo-card">
|
||
<h3><?php esc_html_e( '步驟 1:清 wp_postmeta 垃圾(v2.9.0 phase 0)', '2meet-data-optimizer' ); ?></h3>
|
||
<p>
|
||
<?php
|
||
printf(
|
||
/* translators: 1: total garbage rows, 2: transients, 3: wp_old_date, 4: edit_locks */
|
||
esc_html__( '偵測到 %1$s 行可清理垃圾(transients %2$s + _wp_old_date %3$s + 過期 _edit_lock %4$s)。', '2meet-data-optimizer' ),
|
||
esc_html( number_format_i18n( (int) $garbage['total'] ) ),
|
||
esc_html( number_format_i18n( (int) $garbage['transients'] ) ),
|
||
esc_html( number_format_i18n( (int) $garbage['wp_old_date'] ) ),
|
||
esc_html( number_format_i18n( (int) $garbage['edit_locks'] ) )
|
||
);
|
||
?>
|
||
</p>
|
||
<div class="actions">
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="wpdo_postmeta_cleanup" value="1">
|
||
<?php wp_nonce_field( 'wpdo_postmeta_cleanup' ); ?>
|
||
<button type="submit" class="button button-primary <?php echo (int) $garbage['total'] > 0 ? '' : 'disabled'; ?>"
|
||
onclick="return confirm(<?php echo wp_json_encode( __( '確定要刪除 wp_postmeta 中的垃圾資料?此動作不可逆。', '2meet-data-optimizer' ) ); ?>);">
|
||
<?php esc_html_e( '清理垃圾資料', '2meet-data-optimizer' ); ?>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wpdo-card">
|
||
<h3><?php esc_html_e( '步驟 2:把 wp_postmeta 既有資料 backfill 至 flat 表(v2.9.3)', '2meet-data-optimizer' ); ?></h3>
|
||
<p><?php esc_html_e( 'Idempotent — 重跑安全。每個 group 跑一次 bulk SQL pivot。', '2meet-data-optimizer' ); ?></p>
|
||
<div class="actions">
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="wpdo_post_backfill_all" value="1">
|
||
<?php wp_nonce_field( 'wpdo_post_backfill_all' ); ?>
|
||
<button type="submit" class="button button-primary"
|
||
onclick="return confirm(<?php echo wp_json_encode( __( '確定要對 7 個 group 跑 backfill?此動作 idempotent,可重跑。', '2meet-data-optimizer' ) ); ?>);">
|
||
<?php esc_html_e( '把 wp_postmeta backfill 到 7 張 flat 表', '2meet-data-optimizer' ); ?>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wpdo-card">
|
||
<h3><?php esc_html_e( '步驟 3:把 legacy wpdo_hot_hp_listing 抄到 flat 表(v2.9.5)', '2meet-data-optimizer' ); ?></h3>
|
||
<p><?php esc_html_e( '非破壞 — legacy hot 表保留作為 v3.0.0 rollback safety net。', '2meet-data-optimizer' ); ?></p>
|
||
<div class="actions">
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="wpdo_post_cutover_legacy" value="1">
|
||
<?php wp_nonce_field( 'wpdo_post_cutover_legacy' ); ?>
|
||
<button type="submit" class="button"
|
||
onclick="return confirm(<?php echo wp_json_encode( __( '確定要 copy wpdo_hot_hp_listing 到 wp_wpdo_post_hp_listing_core?', '2meet-data-optimizer' ) ); ?>);">
|
||
<?php esc_html_e( 'Copy legacy hot table', '2meet-data-optimizer' ); ?>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wpdo-card">
|
||
<h3><?php esc_html_e( '步驟 4:升級 mode 至 dual_write', '2meet-data-optimizer' ); ?></h3>
|
||
<p><?php esc_html_e( 'wp_postmeta 與 flat 表同時寫入。讀仍走 wp_postmeta(生產 safe)。建議至少觀察 24h 後再升級下一階。', '2meet-data-optimizer' ); ?></p>
|
||
<div class="actions">
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="wpdo_post_promote_dual_write" value="1">
|
||
<?php wp_nonce_field( 'wpdo_post_promote_dual_write' ); ?>
|
||
<button type="submit" class="button"
|
||
onclick="return confirm(<?php echo wp_json_encode( __( '確定要升級 post mode 至 dual_write?此後 update_post_meta() 會雙寫。', '2meet-data-optimizer' ) ); ?>);">
|
||
<?php esc_html_e( '升級 mode → dual_write', '2meet-data-optimizer' ); ?>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wpdo-card">
|
||
<h3><?php esc_html_e( '步驟 5:升級 mode 至 aeav_only(最終 cutover)', '2meet-data-optimizer' ); ?></h3>
|
||
<p><?php esc_html_e( 'flat 表成為 source-of-truth,讀寫都走 flat。完成後可 wp wpdo post-cleanup --confirm 清掉 wp_postmeta 已遷移 keys。', '2meet-data-optimizer' ); ?></p>
|
||
<div class="actions">
|
||
<form method="post" style="display:inline">
|
||
<input type="hidden" name="wpdo_post_promote_aeav" value="1">
|
||
<?php wp_nonce_field( 'wpdo_post_promote_aeav' ); ?>
|
||
<button type="submit" class="button"
|
||
onclick="return confirm(<?php echo wp_json_encode( __( '確定要升級 post mode 至 aeav_only?此後讀路徑切換到 flat 表,wp_postmeta 變成 backup。', '2meet-data-optimizer' ) ); ?>);">
|
||
<?php esc_html_e( '升級 mode → aeav_only', '2meet-data-optimizer' ); ?>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wpdo-card" style="background: #f6f7f7;">
|
||
<h3><?php esc_html_e( '回退路徑(rollback)', '2meet-data-optimizer' ); ?></h3>
|
||
<p>
|
||
<?php esc_html_e( '若任一步驟出問題,可下降 mode:', '2meet-data-optimizer' ); ?>
|
||
<code>wp option update wpdo_bridge_modes '{"post":"disabled",...}'</code>
|
||
<?php esc_html_e( '。Legacy wpdo_hot_* 表完整保留 — 直到 v3.0.0 才考慮 DROP。', '2meet-data-optimizer' ); ?>
|
||
</p>
|
||
</div>
|
||
|
||
</div>
|