4b34851316
Anti-EAV Lint + Quality Gate / anti-eav-lint (pull_request) Successful in 25s
Tests / PHP Lint (pull_request) Successful in 55s
Tests / Unit Tests (pull_request) Successful in 56s
Tests / Integration Tests (pull_request) Successful in 1m46s
Tests / PHPCS (pull_request) Successful in 44s
Tests / PHPStan (pull_request) Successful in 1m4s
- Replace the index only on tables listed in tmdo_post_status_columns (ALTER ADD idx_post_status_date_id / DROP idx_post_status_date) - Warn that `wp tmdo post-status-sync` also adds and backfills copy columns on every other post group (large ALTER + UPDATE) - Found in the 2.4 rollout (dev21: aeav_only, over ten million posts)
535 lines
31 KiB
Markdown
535 lines
31 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to this plugin will be documented here.
|
||
|
||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||
Versioning follows [Semantic Versioning](https://semver.org/).
|
||
|
||
---
|
||
|
||
## [1.4.0] — 2026-09-28 — 百萬級 listing 的路由排序與日期查詢(hardening 2.4)
|
||
|
||
**性質**:效能 + 修正。無 schema 變更;post status/date 副本的索引改為三欄(見「升級後」)。
|
||
搭配 HivePress AddOn 0.5.0。來源:dev36 第 3 階壓測(130 萬 listing、121 萬 product、6.2 萬 vendor)。
|
||
|
||
### Changed
|
||
|
||
- **flat-first 排序讀 flat 的 post_id 作 tie-break**:`posts.ID` 的 tie-break(ours 與外掛追加的)
|
||
改讀相等的 `flat.post_id`,方向與排序一致且只出現一次;group 宣告的索引可以 adapter 的
|
||
entity id 欄結尾,`(欄, post_id)` 索引可直接依序讀取、不需 filesort(主查詢 850 ms → 2 ms)。
|
||
同值的順序改為依 ID 與排序同方向(原生為 ID DESC);集合、總數不變,翻頁穩定。(PR-118)
|
||
- **沒有 value 的 meta 條件以 EXISTS 路由**:clause 沒有 `value` 時(HivePress 的精選 listing
|
||
查詢)視同 EXISTS;只帶 `meta_key`、沒有 `meta_value` 也不以 meta_value 排序的查詢亦同;
|
||
沒有被 orderby 引用的排序形狀 group 直接移除。HivePress 每個搜尋頁都會跑的精選 listing
|
||
查詢(`orderby rand`)改走扁平表:2.26 s → 0.2 s。(PR-122)
|
||
- **status/date copy route 在 LIMIT 停下**:只有扁平表一個 JOIN 時去掉 WordPress 的
|
||
`GROUP BY posts.ID`,`posts.ID` tie-break 改讀 `flat.post_id`(各自保留方向、重複合併);副本索引改為
|
||
`idx_post_status_date_id (post_status, post_date, post_id)`,取代兩欄的 `idx_post_status_date`。
|
||
dev36 價格區間(130 萬中 23.5 萬筆符合)主查詢 1.7 s → 3 ms,頁面 2.7 s → 0.34 s。(PR-128)
|
||
|
||
### Fixed
|
||
|
||
- 遷移引擎(backfill、`drift-check --fix`)以 REPLACE 重建 post 列時保留 `post_status` /
|
||
`post_date` 副本;原本重建後為 NULL,一筆 NULL 就讓整個 group 關閉 copy route。(PR-127)
|
||
- 遷移引擎重建 post 列後遞增路由計數版本(每次呼叫/每批一次,dry run 不遞增),
|
||
修正後的 found rows 不再沿用舊的快取計數。(PR-129)
|
||
|
||
### Dev
|
||
|
||
- live 測試工具(`mut.php`、`qbench.php`)不再清空物件快取(Redis Object Cache 以伺服器端腳本
|
||
掃描,會卡住共用 Redis 5–20 s)。(PR-123)
|
||
|
||
### 升級後
|
||
|
||
- 只需替換索引:對 `tmdo_post_status_columns` 列出的每個 group 表執行
|
||
`ALTER TABLE <表> ADD INDEX idx_post_status_date_id (post_status, post_date, post_id), DROP INDEX idx_post_status_date`
|
||
(空表瞬間完成,dev36 130 萬列約 5 s)。未替換前 copy route 照常運作,只是維持 1.3.0 的速度。
|
||
- **不要為了換索引執行 `wp tmdo post-status-sync`**:它除了替換索引,還會替**所有**尚未有副本的
|
||
post group 加上 `post_status` / `post_date` 欄位並逐列回填(大表 ALTER + 全表 UPDATE;dev36
|
||
130 萬列約 75 s,上千萬列的站更久)。只有要讓更多 group 啟用 copy route 時才執行,並先在
|
||
離峰時段以 `--dry-run` 確認列數。
|
||
- 效能數字(dev36 前台,routed):價格區間 0.34 s、價格排序 0.37 s、時薪排序 0.35 s、
|
||
評分排序 0.51 s、關鍵字 + 價格 0.59 s(原生 6–43 s)。
|
||
|
||
---
|
||
|
||
## [1.3.0] — 2026-09-27 — 大表索引、路由計數與稽核修正(hardening 2.3)
|
||
|
||
**性質**:效能 + 稽核修正。無 schema 變更(宣告的複合索引以 ALTER ADD INDEX 就地加上)。
|
||
搭配 HivePress AddOn 0.4.0、WooCommerce AddOn 0.3.0。來源:dev36 規模壓測(90 萬 listing)
|
||
與系列稽核(2026-09-27)。
|
||
|
||
### Changed
|
||
|
||
- **group 宣告的複合索引只在 WP-CLI 或 WP-Cron 加上**:網頁請求(包括 auth_redirect 之前的
|
||
`/wp-admin/` 與 `admin-post.php`)改排單次事件 `tmdo_ensure_group_indexes`;以 MySQL
|
||
`GET_LOCK` 保證同一時間只有一個程序執行 ALTER;每個 ALTER 都成功才記錄已套用,失敗記
|
||
`schema.index_failed` 並保留在 `doctor` 的 `[INDEX]` 待辦;新表在 CREATE TABLE 時就帶上
|
||
索引。filter:`tmdo_schema_ddl_allowed`。dev36 90 萬列加兩個索引約 4.7 s。(PR-090a)
|
||
- **路由搜尋的 found rows**:帶 `post__not_in` 的查詢(HivePress 每頁送一組隨機排除清單)
|
||
改為「不含排除條件的 COUNT(快取)」減去「排除清單中符合條件的篇數(主鍵查詢)」,結果
|
||
精確,快取不再被打散。(PR-091)快取 key 另外去掉 wpdb 替 `%` 產生的 placeholder escape
|
||
(每個請求不同,關鍵字搜尋原本永遠不命中,也就是 2.2 驗收「關鍵字 + 篩選 routed 較慢」的
|
||
原因)(PR-116);計數改跟隨自有的 `tmdo_posts` last_changed(文章儲存/刪除、經 Hook Bus
|
||
的扁平寫入)與 terms 的 last_changed,不再跟隨 posts 的 last_changed——任何 post meta 寫入
|
||
都會推動它,dev36 的 Blocksy 佈景在爬蟲瀏覽每篇 listing 時都寫一筆 meta,快取幾秒就失效
|
||
(PR-117)。dev36 關鍵字 + 價格 routed 9.8 s → 首次後約 5.5 s(原生 5.0–6.2 s,剩餘為
|
||
WordPress 核心的 LIKE 全表掃描)。
|
||
- `POST /wpdo/v1/listings/{id}/view` 被限流擋下時,`wpdo_rl_stats` 改為 1/10 抽樣寫入
|
||
(每次 +10),匿名請求不再 1:1 放大成 option 寫入。(PR-094)
|
||
- `TMDO_Logger::info()` 只在 `WP_DEBUG_LOG` 時寫 error_log(filter `tmdo_log_info`);
|
||
`notice()` 一律寫入,模式切換(`bridge_mode_changed`)與快照刪除改用它,保留操作紀錄;
|
||
`debug()`、`warning()`、`error()` 不變。(PR-095、PR-113)
|
||
- 公開 listings REST 一律不輸出名稱像 email、phone、address、IP 的欄位,即使宣告
|
||
`show_in_rest`。(PR-096)
|
||
- **既有扁平表的 schema 升級只在 WP-CLI 或 WP-Cron 執行**:網頁請求遇到定義變更時記入
|
||
`tmdo_schema_deferred` 並排 `tmdo_ensure_group_indexes`;升級前該 group 的 key 走原生
|
||
(Hook Bus 不攔、router 不路由),升級後排 `tmdo_drift_resync` 補回期間跳過的扁平寫入。
|
||
新表照常立即建立;aeav_only 仍立即升級(扁平表是唯一副本);`doctor` 顯示 `[SCHEMA]`。(PR-107)
|
||
驗收審查後補強:既有表的升級與索引共用同一把 `GET_LOCK`(匿名 `/wp-cron.php` 也會通過
|
||
`ddl_allowed()`),取得鎖後重新比對 hash;延後的 group 要等 drift fix 完成才改回扁平表;
|
||
寫入扁平表的 entity 新建 group 表時同樣先走原生,由 drift fix 從原生填滿(新宣告的
|
||
HivePress 屬性不會把搜尋導到空表);定義還原時解除延後;cron 事件本身也會升級延後的表
|
||
(`ALTERNATE_WP_CRON`);dbDelta 之後每個欄位都存在才記錄定義(否則 `schema.upgrade_failed`
|
||
並重試);延後期間 `direct_read()` 回傳原生值。(PR-111、PR-114)
|
||
- 讀寫分離(HyperDB、LudicrousDB)時,取索引鎖前先 `send_reads_to_masters()`,
|
||
`GET_LOCK` 才會在 primary 上。(PR-108)
|
||
- Cold 快取的「清除」改為遞增版本號(`wpdo_cold_cache_version`),不再呼叫
|
||
`wp_cache_flush_group()`(Redis Object Cache 以伺服器端腳本掃描整個 group,大 group 會卡住
|
||
Redis);舊 key 依 TTL 過期。(PR-110)
|
||
|
||
### Fixed
|
||
|
||
- 後台主頁與設定精靈第 4 步的表單改在 `load-{page}`(輸出前)處理,`wp_safe_redirect()`
|
||
可以正常生效;精靈快照失敗改以 `snap_failed=1` 導回。(PR-097)
|
||
- `uninstall.php` 的全域變數加上 `$wpdo_` 前綴(WordPress.org Plugin Check)。(PR-098)
|
||
- 扁平表已被刪除的 group 不再留在 `tmdo_post_status_columns`:每次文章狀態變更都 UPDATE
|
||
不存在的表並印出 DB error。(PR-106)
|
||
|
||
### Security(已驗證,無程式變更)
|
||
|
||
- 快照目錄在 nginx 下的外洩風險(稽核 Medium):快照檔名含 64-bit 隨機值、遷移備份檔名含
|
||
12 字元隨機 job id,目錄有 index.php 擋列表;`doctor` 的 HTTP 探測與 nginx 後台提示已存在。
|
||
dev31 探測全為 403。不採用「目錄名加隨機後綴」:各站 nginx deny 規則比對 `wpdo-backups/`,
|
||
改名反而讓新快照脫離保護。(PR-093)
|
||
|
||
### 升級後
|
||
|
||
- 部署後第一個 WP-CLI 指令或 WP-Cron 會為 HivePress AddOn 0.4.0 宣告的索引執行 ALTER
|
||
(90 萬列約 5 s);`DISABLE_WP_CRON` 且沒有系統 cron 的站,執行一次 `wp tmdo doctor` 即可。
|
||
- CLI 輸出不再夾帶 `[WPDO][INFO]`;要看請定義 `WP_DEBUG_LOG` 或加上 `tmdo_log_info` filter。
|
||
- 已知限制:cold 快取版本遞增的瞬間,在遞增前就開始的長請求寫入後只會清掉舊版 key,新版 key
|
||
最多保留舊值到 TTL(預設 1 小時);cold module 預設 idle。
|
||
|
||
---
|
||
|
||
## [1.2.0] — 2026-09-27 — 漂移偵測、範圍收斂、CLI 與診斷(hardening 2.2)
|
||
|
||
**性質**:功能 + 資料正確性修正。無 schema 變更。延續 ADR-004 的搜尋索引模式;搭配
|
||
HivePress AddOn 0.3.0、WooCommerce AddOn 0.2.0。
|
||
|
||
### Added
|
||
|
||
- **`wp tmdo drift-check <entity> [--group] [--sample=<n>] [--format]`**(`TMDO_Drift_Check`):
|
||
逐 key 比對扁平表與原生 meta 的值(第一筆原生 row → 安全 unserialize →
|
||
`Type_Caster::to_db`),回報 miss / diff / extra。序列化 vs JSON、`50` vs `50.000000`
|
||
不算漂移;超過 255 字元存 NULL 的 text 不算 miss。結果寫入 `wpdo_drift_status`。(PR-070)
|
||
- **`drift-check --fix`**:刪除已刪實體與非本 group post type 的扁平列,其餘漂移列經
|
||
`TMDO_Entity_Migration_Engine::migrate_entity()` 從原生重建,清除列快取後再檢查一次。
|
||
不寫原生;aeav_only(扁平才是真實資料)一律拒絕。(PR-071、PR-073b)
|
||
- **每日抽樣**:`wpdo_daily_health_check` 對 dual_write 以上、非 aeav_only 的 entity 每個
|
||
group 抽 500 筆,結果取代既有紀錄。(PR-073)
|
||
- **Hook Bus 關閉期間的寫入補回**:entity 從 disabled 進入 dual_write / shadow_read,或
|
||
Hook Bus flag 由關轉開時,60 秒後以單次 WP-Cron `tmdo_drift_resync` 執行全量
|
||
`drift-check --fix`(不做 `post_modified_gmt` 增量:WooCommerce 直接 SQL 改庫存不會更新
|
||
post_modified)。停用 / 解除安裝時清除排程。(PR-074)
|
||
- **`wp tmdo bridge-mode <entity> <mode>`**:經 `TMDO_Mode_Manager::set()`,受
|
||
Promotion Guard 約束;被拒時 exit 1、模式不變。(PR-069)
|
||
- **`wp tmdo member-backfill --all`**:回填所有已註冊的 user group。(PR-068)
|
||
- **`wp tmdo cleanup-hp-transients --options [--dry-run|--confirm] [--batch=<n>]`**:
|
||
刪除 HivePress AddOn 0.3.0 之前寫進 wp_options 的 `_transient_wpdo_hp_pm_*`(含 timeout),
|
||
分批刪除(預設 5000),AddOn 未啟用也可執行。dev36 有 2,314,750 列。(PR-080)
|
||
|
||
### Changed
|
||
|
||
- **Post query router 避開漂移中的 group**:上次 drift-check 有 miss / diff / extra 的
|
||
group 不路由,整個查詢維持原生(全有或全無);從未檢查的 group 照常路由;aeav_only
|
||
不受影響。(PR-072、PR-073b)
|
||
- **純排序搜尋 flat-first**:沒有路由篩選的 HivePress `_sort` 搜尋,在該 post type 每篇都有
|
||
扁平列時改由扁平表驅動(`STRAIGHT_JOIN` + INNER JOIN);Router 在 `wp_insert_post`
|
||
替新文章建空列,覆蓋率以 anti-join 檢查並快取 15 分鐘(隨 post 模式變更失效)。
|
||
flat-first 且扁平表是唯一 JOIN 時拿掉 WordPress 為 tax query 加的 `GROUP BY posts.ID`。
|
||
dev36 `rating__desc`:原生約 3.3 s、1.1.0 約 1.5–1.9 s、現在約 0.51–0.85 s。(PR-079)
|
||
- **Auto-promoter** 的第三個條件改為 `TMDO_Drift_Check::clean_since()`:2 天內每個 group 都
|
||
檢查過(抽樣或全量)且沒有漂移;沒有紀錄就不升級。(PR-075)
|
||
- **shadow_read diff 節流**:每個 entity type + key 每小時最多記 20 列(疊加既有的
|
||
每物件 5 分鐘視窗)。(PR-075)
|
||
- **回填 group 改讀 registry**:admin「全部回填」、`post-migrate-group`、`member-backfill`、
|
||
`member-audit` 取所有已註冊 group(排除已還原的孤兒 group),不再是寫死清單;含 json
|
||
欄位的 group(attachment、nav_menu_item)逐列經 migration engine。(PR-067、PR-068)
|
||
- **不再註冊 `post/wc_product`**:WooCommerce 直接讀 product meta、以 SQL 寫庫存,扁平副本
|
||
必然漂移(ADR-004 決策 2)。既有的 `wp_wpdo_post_wc_product` 表保留,以
|
||
`wp tmdo bridge-restore-native post --group=wc_product --retire` 封存;解除安裝仍會刪除。(PR-077)
|
||
- `member-sso-sync`、`member-force-logout` 標示為 deprecated(`sso` group 已不註冊)。(PR-069)
|
||
|
||
### Fixed
|
||
|
||
- Entity 回填替沒有原生 row 的 key 寫入 DDL 預設值(`session_count` / `account_flags` = 0),
|
||
每個 user 都被捏造出 0;現在寫 NULL,required NOT NULL 欄位保留預設值。(PR-066)
|
||
- `member-cutover` 在轉換被拒時 fatal(把 `WP_Error` 當陣列)。(PR-068)
|
||
- `doctor` 依 registry 檢查 entity 表(不再因從未註冊的 `sso` 表而每站失敗),並顯示
|
||
Promotion Guard 違規與 drift 狀態;`term-comment-diagnose` 依各 filter 的 `is_enabled()`
|
||
顯示狀態;`bridge-status` 只計有值的扁平列、eav_ids 限定 group 的 post type
|
||
(dev36 wp_core 15306% → 100%)。(PR-076)
|
||
- 2.1 稽核:公開 REST route 註明公開理由;admin 23 處三元 echo 依情境 `esc_attr` /
|
||
`esc_html`;migration engine 與 health cron 的例外改記 `TMDO_Logger`。(PR-078)
|
||
|
||
### Docs
|
||
|
||
- ADR-004「設計(Release 2.2)」、`CONTEXT.md` Entity Bridge 的漂移段落。(PR-086)
|
||
|
||
### 升級後
|
||
|
||
- HivePress 站:部署 AddOn 0.3.0 後執行 `wp tmdo cleanup-hp-transients --options --confirm`。
|
||
- 曾註冊 wc_product 的站:`wp tmdo bridge-restore-native post --group=wc_product --retire`。
|
||
- 已進入 dual_write 以上的站:`wp tmdo drift-check <entity>` 建立第一筆紀錄(未檢查的
|
||
group 照常路由)。
|
||
|
||
---
|
||
|
||
## [1.1.1] — 2026-09-26 — Security:公開 REST 不再回傳私有欄位
|
||
|
||
**性質**:安全修正(Critical)。1.0.x 起即存在;1.1.0 zip 已撤回(`dist/withdrawn/`)。
|
||
|
||
### Security
|
||
|
||
- **公開的 `GET /wpdo/v1/listings`、`/wpdo/v1/listings/{id}` 會把所有已註冊的 hot / cold
|
||
欄位回傳給未登入者**:`TMDO_Schema_Registry::register()` 的 `show_in_rest` 預設為 true,
|
||
而 addon 都沒有設定它。dev36 實測可取得 infocards 的 `tmeetic_ig_access_token_enc`、
|
||
`tmeetic_ig_token_expiry`、`tmeetic_ig_user_id`,以及 WooCommerce 的 `_purchase_note`。
|
||
- `show_in_rest` 預設改為 **false**:要公開的欄位必須明確宣告。
|
||
- 受保護的 meta key(底線開頭)與名稱像憑證的 key(token、secret、password、api key、
|
||
private key、credential、salt、nonce)即使宣告公開也一律不輸出
|
||
(`TMDO_Schema_Registry::rest_exposable()`)。
|
||
- Zone A 列表路徑在沒有任何公開欄位時不再回傳整列(fail-closed);篩選參數與 orderby
|
||
只接受公開欄位,避免藉由篩選 / 排序推測隱藏值。
|
||
- **相容性**:依賴預設公開的欄位,現在需在註冊時加上 `'show_in_rest' => true`,或使用
|
||
既有的 `tmdo_rest_listing_visible_fields` filter。
|
||
- 測試:`RestApiTest` 新增 3 個;live suite `rest`(未登入 GET 不得出現私有 / 憑證類 key)。
|
||
|
||
---
|
||
|
||
## [1.1.0] — 2026-09-26 — HivePress / WooCommerce 搜尋索引模式(hardening 2.1)
|
||
|
||
**性質**:資料正確性修正 + 查詢改寫補強。無 schema 變更。決策與設計見
|
||
`docs/adr-004-search-index-mode-for-hp-wc.md`;搭配 HivePress AddOn 0.2.0。
|
||
|
||
### Added
|
||
|
||
- **Promotion Guard**(`TMDO_Promotion_Guard`):HivePress 啟用時 post / user / term /
|
||
comment、WooCommerce 啟用時 post / user 不得升到 aeav_only 或執行 cleanup
|
||
(`WP_Error( 'promotion_guarded' )`,filter `tmdo_promotion_guard_allow`);已在
|
||
aeav_only 的站只顯示後台 notice。(PR-053)
|
||
- **`wp tmdo repair-array-literals [--entity] [--confirm]`**:把扁平表中的 `"Array"`
|
||
從原生修回;原生也被污染的列出清單。(PR-055)
|
||
- **`bridge-restore-native --overwrite / --only-missing`**:預設依模式(aeav_only 覆寫、
|
||
其他只補缺);報告新增 `missing` / `differs` / `same` / `default_skipped`。(PR-058)
|
||
- **live 測試層** `tests/live/`(在真實站台以 `wp eval-file` 執行):suites
|
||
`shadow_write`、`serialized`、`resync`、`router`、`zero_side_effect`,以及 qbench /
|
||
fe.sh 等量測工具。(PR-052)
|
||
|
||
### Changed
|
||
|
||
- **Hook Bus 寫入順序**:dual_write / shadow_read 由 WordPress 寫原生,扁平表在
|
||
`added_ / updated_ / deleted_{type}_meta` 從原生重新同步(含 `delete_all`、
|
||
`update_metadata_by_mid` 改 key)。(PR-057)
|
||
- **shadow_read** 讀原生,並抽樣比對扁平值(`tmdo_shadow_compare_sample_rate`)。(PR-056)
|
||
- **Post query router**:
|
||
- 全有或全無:OR、多 post type、含已註冊 key 的巢狀 group、type 語意與欄位不符的
|
||
clause 讓整個查詢維持原生;LIKE 包 `%esc_like%`;`pre_get_posts` priority 1000。(PR-060)
|
||
- 每個 group 各自 JOIN、不加 GROUP BY;NOT EXISTS 不走狀態 / 日期副本路由。(PR-061)
|
||
- meta orderby:HivePress `_sort`(NOT EXISTS + EXISTS)形狀、具名 clause、
|
||
`meta_key` + `meta_value_num`。(PR-062)
|
||
- routed 搜尋維持 WordPress 的 split query(ID-first)。(PR-062b)
|
||
- 依扁平欄位排序且有篩選的搜尋改由扁平表驅動(`STRAIGHT_JOIN`,posts.ID tie-break 改讀等值的
|
||
`post_id`),不再以 temporary table 排序;filter `tmdo_router_flat_first` 可關閉。(PR-062c)
|
||
- `composer phpcs` 改用 `phpcs.xml`(與 CI 相同)。
|
||
|
||
### Fixed
|
||
|
||
- shadow_read 下 `update_*_meta()` 回傳 false、原生沒有寫入(資料遺失)。(PR-056)
|
||
- 陣列 / 物件存進 text 欄位變成 `"Array"`;text 超過 255 字元改存 NULL 並記錄
|
||
`text_overflow`。(PR-055)
|
||
- `bridge-restore-native` 覆蓋較新的原生值(WC 庫存)、替每個 user 捏造預設值列。(PR-058)
|
||
- Cold / Hot 表不存在時,`save_post`、`loop_start`、刪除文章仍查詢它們(DB error)。(PR-054、PR-054b)
|
||
- 刪除 post / user / term / comment 後,扁平表留下全 NULL 的孤兒列(PR-057 的 NULL 同步會重建已刪的列)。(PR-057b)
|
||
- Router:Q05(wp_core 與另一 group 的 key 並用)`Unknown column`;OR / 多 post type /
|
||
無 type 數值比較 / 具名 orderby 的結果與原生不同。(PR-060〜062)
|
||
|
||
### Docs
|
||
|
||
- ADR-004、`CONTEXT.md` Entity Bridge(寫入順序、shadow_read、搜尋索引模式、Router 規則)、
|
||
`docs/BENCHMARK-SCALE.md` 前台與寫入鏈實測。(PR-064)
|
||
|
||
---
|
||
|
||
## [1.0.2] — 2026-08-15 — NinjaFirewall 相容性:WAF 設定選項保護
|
||
|
||
**性質**:防禦性修正 + 文件。無 schema 變更,無破壞性變更。
|
||
|
||
### Added
|
||
|
||
- **`docs/WAF-COMPATIBILITY.md`** — NinjaFirewall (WP Edition) 4.9 相容性評估
|
||
|
||
結論:兩者可共存,**不需要開發 AddOn**(NinjaFirewall 全 codebase 零個
|
||
`apply_filters('nfw_*')` / `do_action('nfw_*')`,官方相容手段全在部署層)。
|
||
文件涵蓋 WP WAF 與 Full WAF 的模式差異、symlink 多租戶部署的必要設定、
|
||
三條開發約束,以及實測風險矩陣。
|
||
|
||
- **`TMDO_Options_Manager::PROTECTED_OPTIONS`** 常數與註冊守衛
|
||
(`modules/options/class-tmdo-options-manager.php`)
|
||
|
||
`register_settings_group()` 會以 `pre_update_option_{key}` 回傳 `$old_value`,
|
||
讓選項不再落地 `wp_options`、改存專屬設定表。但 NinjaFirewall 的 Full WAF 走
|
||
`auto_prepend_file`,在 WordPress 載入前就以**原生 mysqli 直查 `wp_options`**
|
||
取 `nfw_options` / `nfw_rules`。一旦這些鍵被重導向,WAF 會讀不到設定而
|
||
**靜默停止防護 —— 不報錯、不寫 log**。
|
||
|
||
現以 `PROTECTED_OPTIONS`(`nfw_options` / `nfw_rules` / `nfw_checked`)在註冊
|
||
階段擋下,並發出 `_doing_it_wrong()`。守衛置於方法開頭,全部鍵都被擋時提前
|
||
返回,不再建立空的設定表。
|
||
|
||
註:autoload 最佳化不受影響 —— `optimize_autoload()` 只改 `autoload` 欄位、
|
||
不刪列,而該處的 `SELECT *` 不看 autoload。
|
||
|
||
- **`tests/unit/OptionsManagerProtectedTest.php`** — 4 tests / 10 assertions
|
||
|
||
鎖住「受保護鍵絕不會被掛上 `pre_option_*` / `pre_update_option_*` 攔截」這個
|
||
核心保證,並驗證全數受保護時不對資料庫發出查詢。
|
||
|
||
### Changed
|
||
|
||
- **Migration Wizard 輪詢間隔 500ms → 2s**(`admin/assets/wpdo-migration-wizard.js`)
|
||
|
||
原本 2 req/s 打同一個 REST endpoint,容易觸發 WAF 的 rate-limit 與 bot 偵測。
|
||
改為 2 秒,與四個 stress-test 面板既有的輪詢節奏一致。
|
||
|
||
---
|
||
|
||
## [1.0.1] — 2026-08-08 — Schema Registry 冪等性修正(cold zone 欄位重複累積)
|
||
|
||
**性質**:核心 bug 修復。無 schema 變更,無 API 變更,無破壞性變更。
|
||
|
||
### Fixed
|
||
|
||
- **`TMDO_Schema_Registry::register()` 的 cold zone 分支會無限累積重複項**
|
||
(`includes/class-tmdo-schema-registry.php`)
|
||
|
||
三個 zone 索引的寫入方式並不一致:
|
||
- `hot` → `$this->hot_columns[$post_type][$column] = ...`(以 column 為 key,天生冪等)
|
||
- `warm` → `$this->warm_fields[$meta_key] = ...`(以 meta_key 為 key,天生冪等)
|
||
- `cold` → `$this->cold_fields[$post_type][] = ...`(**append,無去重**)
|
||
|
||
而 `wpdo_register_fields` 本來就會被觸發多次——核心自身在 `plugins_loaded:4` 觸發,
|
||
主檔另有 late-bind safety net 於 `plugins_loaded:30` 再次觸發,好讓在 `:6` 之後才
|
||
bootstrap 的 AddOn 也能趕上;再加上主外掛與其 AddOn 可能同時註冊同一批欄位。
|
||
|
||
**實測影響**:某站台 `cold_fields['hp_vendor']` 累積到 **35 筆但只有 9 個相異值**,
|
||
每個 key 重複 4 次。修正後為 9 筆、零重複。
|
||
|
||
修法:cold 分支加入 `in_array( ..., true )` 去重,使 `register()` 對同一
|
||
`(post_type, meta_key)` 成為冪等操作,與 hot / warm 的既有行為一致。
|
||
|
||
### 驗證
|
||
|
||
- 核心單元測試 **587 tests / 1156 assertions** 全綠
|
||
- `wp wpdo doctor` 於兩個站台皆 0 FAIL
|
||
- Version bump 1.0.0 → 1.0.1(2 點一致:header / `TMDO_VERSION`)
|
||
|
||
---
|
||
|
||
## [1.0.0] — 2026-07-31 — 取代 wp-data-optimizer v3.4.6
|
||
|
||
本版把 `wp-data-optimizer` v3.0.1–v3.4.6(92 個 commit)全數 backport 進來,
|
||
`wp-data-optimizer` 自此退休:兩個站台皆已停用它,本機目錄改名為
|
||
`wp-data-optimizer.retired`,其 git 歷史保留在 gitea。
|
||
|
||
### 🔒 Security / 正確性
|
||
|
||
- **7 處 `Logger::error()` 參數不足**(array 傳給 string `$hook`)修正 —— 4 個 stress
|
||
tester、auto-promoter、conflict-detector、hook-bus,皆為 TypeError fatal
|
||
- **`Logger::trace_id()` 補回**:`Audit_Logger::write_row()` 一直呼叫它,但提煉時漏了;
|
||
`Audit_Logger::init()` 從未註冊所以隱藏至今 —— 實機驗證才暴露
|
||
- REST 4 個公開端點加上 `show_in_rest` 欄位 allowlist(CVSS 5.3)
|
||
- 13 個破壞性 admin 動作由 GET 改 POST + nonce
|
||
- DDL 型別白名單(19 型別)、CLI 表名守衛
|
||
- 整欄清空改 `wpdo_allow_mass_column_clear` opt-in(原本 >500 列才擋)
|
||
- audit 表補 `group_name` / `action` 欄並註冊 `Audit_Logger::init()`
|
||
- `known_login_ips` 註冊(spoke-sso 實際寫入的欄位,先前落 `wp_usermeta`)
|
||
|
||
### ⚡ 並發
|
||
|
||
- warm 表 `UNIQUE KEY ui_post_meta`(缺它 `ON DUPLICATE KEY UPDATE` 不生效)
|
||
- Zone_Cold 改 `JSON_MERGE_PATCH` / `JSON_REMOVE`、Zone_Warm 原子 `increment()`、
|
||
hook-bus `write_to_flat` 改 upsert
|
||
|
||
### 🏗 架構
|
||
|
||
- 回填 `TMDO_Zone_Router` + `TMDO_Routing_Predicate`(Sync_Bridge 400→269 行)
|
||
- 回填 Migration Phase Strategy:interface + base + 11 個 phase 類別,
|
||
orchestrator 1107→640 行,公開介面不變
|
||
- 核心新增 `TMDO_Standard_Post_Interceptor`,HP AddOn 4 個 interceptor 573→344 行
|
||
|
||
### 🔌 相容性
|
||
|
||
- hook 橋改**雙向**:核心一律 `do_action('wpdo_*')`,原本單向轉發使所有
|
||
`tmdo_*` 契約收不到事件
|
||
- 57 個 AddOn 類別補 `WPDO_*` alias(核心 alias 表在 `plugins_loaded:4` 執行,
|
||
涵蓋不到 `:6` 才載入的 AddOn)
|
||
- 38 個 CLI 子指令補 `wp tmdo` 命名空間
|
||
- 選單 slug 統一回 `wp-data-optimizer`(B 內部 10+ 處連結本來就指向它)
|
||
- HP adapter 介面改為 `TMDO_ extends WPDO_`,讓 `instanceof WPDO_HivePress_Adapter` 成立
|
||
|
||
### 🖥 Admin
|
||
|
||
- Settings 七個分區各加「儲存此區塊」AJAX 存檔(`wp_ajax_wpdo_save_settings_section`
|
||
+ 7 個 `save_section_*()` + `admin/assets/wpdo-settings.js`),整頁送出仍可用
|
||
- inline `style=` 由 348 處降到 9 處(剩下的全是 `--wpdo-*` CSS custom property
|
||
載體);`wpdo-admin.css` 543 → 912 行,補 149 個 class
|
||
|
||
### 🚧 邊界
|
||
|
||
- 核心不再無守衛呼叫 AddOn 類別。`TMDO_Listing_Stats`(HivePress AddOn)先前
|
||
在 admin Dashboard、兩個公開 REST 端點、`wp tmdo cleanup --archive-expired`
|
||
被直呼,沒裝該 AddOn 的站台會 fatal;`TMDO_HPCT_Import` 在 admin 與
|
||
`wp tmdo import-hpct` 同樣缺守衛
|
||
- `TMDO_Zone_Warm::VIEW_KEY` / `VIEW_TTL` 由核心持有(值與 AddOn 常數相同的
|
||
`'wpdo_views'`,指向同一批列,無資料遷移)
|
||
- 新增 `CoreBoundaryTest`:靜態掃描 126 個核心生產檔,強制每處 AddOn 類別
|
||
呼叫都在同一函式內有 `class_exists()` 守衛
|
||
|
||
### ✅ 品質
|
||
|
||
- 128 個生產檔 `declare(strict_types=1)`
|
||
- PHPCS 0 errors / PHPStan level 6(baseline 710)
|
||
- 測試:核心 unit 587 / integration 416、hivepress-addon unit 154 /
|
||
integration 57、woocommerce-addon unit 10 / integration 18
|
||
- gitea CI:核心 6 個 job 全綠 + 11 個 AddOn workflow + branch protection
|
||
- 打包 10 項終檢全 PASS(569 KB / 187 entries,schema drift 0:34 CREATE / 34 DROP)
|
||
|
||
### ⚠️ ABI / 行為變更
|
||
|
||
- `doctor_callback` 由 3 參數改為 1 參數(表名)
|
||
- `wpdo_capture_before_value` 預設 `true` → `false`;需要 `value_before` 的
|
||
消費者(audit log)要 `add_filter( 'wpdo_capture_before_value', '__return_true' )`
|
||
- `wpdo_allow_mass_column_clear` 現為 opt-in,整欄清空預設被拒
|
||
- `SCHEMA_VERSION` 2.0.0 → 2.1.0(audit 表 ALTER)
|
||
|
||
---
|
||
|
||
## [0.1.0] — 2026-05-15 (initial release, Phase 0-5 完成)
|
||
|
||
### Phase 5 hotfix(同日完成)
|
||
- 修 `TMDO_WooCommerce::doctor_check()` callback signature mismatch(cli/class-tmdo-cli.php:264 改 call_user_func 傳 3 參數)
|
||
- 加 `includes/back-compat/trait-wpdo-anti-eav-aware-alias.php` — trait + interface 反向相容(PHP class_alias 不支援 trait/interface)
|
||
- `WPDO_Anti_EAV_Aware` trait wrapper 可讓 2meet-brandcards 等 sister plugin 直接 `use`
|
||
- `WPDO_Entity_Adapter_Interface` extends `TMDO_Entity_Adapter_Interface`
|
||
- 完整 `uninstall.php`(核心 244 行從 wp-data-optimizer 提煉,34 張表 DROP TABLE 對齊)
|
||
- HP AddOn + infocards AddOn `uninstall.php` 補 schema drift check 對齊
|
||
- **12 plugin ZIP 全通過 10 終檢**
|
||
|
||
### 12 ZIP 交付(dist/)
|
||
| Plugin | Size | MD5 |
|
||
|---|---:|---|
|
||
| 2meet-data-optimizer | 521 KB | a10bb191e662cb5fb53a3d23165c71e5 |
|
||
| -hivepress-addon | 117 KB | 24bd8c3211421fed86dc551658d7064a |
|
||
| -woocommerce-addon | 34 KB | fbd6f60368fc93310f2e90e58c95b53c |
|
||
| -spoke-addon | 24 KB | c1e92b755fb1d108a141086ccf1fe519 |
|
||
| -hub-addon | 22 KB | 4e010c63ed8ad1dd24f06b8741e3a1b0 |
|
||
| -infocards-addon | 22 KB | 7f095fdc4748d3ff4cd4e05f1bf3d417 |
|
||
| -latepoint-addon | 21 KB | 60eeb4ea67cdc996cb60b6ad7126f2df |
|
||
| -bookings-addon | 21 KB | ac32b85d84e912d3b5a1cdfb56f9fa96 |
|
||
| -mobile-bridge-addon | 20 KB | 59905719cfee4bd31967736517391a43 |
|
||
| -collab-addon | 20 KB | 1ed0a2983a692a94b6c42f21b62af2c2 |
|
||
| -quotation-addon | 20 KB | 5e693a4df3ce48de447cf6277deaece5 |
|
||
| -playlist-addon | 20 KB | d52552782f383abbe8f278a5b85cafef |
|
||
|
||
### CLI smoke test(12 指令 全 PASS)
|
||
- `wp tmdo status / doctor / analyze / install / cleanup / health-snapshot / site-metrics / benchmark`
|
||
- `wp wpdo status / doctor` (alias 路徑)
|
||
- `wp tmdo spoke-sync --dry-run` / `wp tmdo spoke-force-logout --user-id=1 --dry-run`
|
||
|
||
---
|
||
|
||
## [0.1.0] — 2026-05-15 (initial release, Phase 0-4 完成)
|
||
|
||
### Added — 核心反 EAV 引擎(從 wp-data-optimizer v2.16.0 提煉)
|
||
- 4 Zone 分流(Hot / Warm / Cold / Archive)+ Sync Bridge 雙寫
|
||
- 7 態 FSM 模組生命週期(idle → dual_write → backfill → verify → cutover → cleanup → complete)
|
||
- 4 entity 通用引擎(post / user / term / comment)+ Hook Bus + Mode Manager
|
||
- Query Router (`pre_get_posts` 改寫) + Query Compiler
|
||
- Migration Engine + Orchestrator + Backfill + Stress Tester + Shadow Verifier
|
||
- Snapshot / Safety / Diagnostic / Notifications (Email / Slack / Discord / Telegram)
|
||
- Crypto AES-256-GCM AEAD + Safe Unserialize
|
||
- WP-CLI 18+ subcommand
|
||
- Admin Dashboard + KPI Hero + Tab Groups + Setup Wizard + Help Tabs
|
||
|
||
### Added — 對外向後相容
|
||
- `TMDO_*` 類別 + `class_alias` 80+ 個 → 對外保留 `WPDO_*` 類別名
|
||
- 8 個常數 alias(`WPDO_VERSION` / `WPDO_PLUGIN_DIR` / `WPDO_TABLE_PREFIX` 等)
|
||
- 9 個 hook dual-fire 橋接(`tmdo_register_fields` ↔ `wpdo_register_fields` 等)
|
||
- WP-CLI 雙命名空間:`wp tmdo` (主) + `wp wpdo` (alias)
|
||
|
||
### Removed — 整合層搬出
|
||
- HivePress + 12 擴充整合(13 adapter + 7 interceptor + 5 query interceptor + bootstrap + admin + CLI)→ 搬到 `2meet-data-optimizer-hivepress-addon`
|
||
- WooCommerce 整合 → `2meet-data-optimizer-woocommerce-addon`
|
||
- LatePoint 整合 → `2meet-data-optimizer-latepoint-addon`
|
||
- 2meet-infocards / bookings / quotation / mobile-bridge / collab / playlist 整合 → 各自 6 個 AddOn
|
||
- Hub-core SSO group 與自訂表註冊 → `2meet-data-optimizer-hub-addon`
|
||
- Spoke-sso SSO group 與 CLI → `2meet-data-optimizer-spoke-addon`
|
||
- `TMDO_Core::register_hivepress_defaults()` (193 行) → HP AddOn
|
||
|
||
### Migration from `wp-data-optimizer v2.16.0`
|
||
|
||
- `wp_wpdo_*` 表結構完全相容(不 rename,新外掛沿用既有 schema)
|
||
- 舊外掛先停用即可改用新外掛接管
|
||
- 兩外掛**互斥**:本外掛偵測舊外掛仍 active 時會發 admin notice(plan §8.1)
|
||
|
||
### Limitations / Known issues (Phase 5 follow-up)
|
||
|
||
- `tests/` 目錄空殼,PHPUnit unit + integration tests 需後續搬遷
|
||
- `phpcs.xml.dist` 缺;`composer install --dev` 後可手動跑
|
||
- WP 6.5+ `Requires Plugins:` 強制 Plugin Name 比對;hub-core / spoke-sso 仍寫死 `wp-data-optimizer`,需更新或本 plugin 加偽 `Plugin Name: WP Data Optimizer` alias header(Phase 5)
|
||
- `TMDO_WooCommerce::doctor_check()` signature 與 Custom_Table_Registry callback 不對齊(warning level)
|
||
- Trait `Trait_TMDO_Anti_EAV_Aware` 暫無 alias wrapper(class_alias 不適用 trait)
|
||
|
||
### Phase 0-4 統計
|
||
- **PHP files**: 203(核心 111 + Hub 5 + Spoke 7 + HP 47 + WC 8 + LP 5 + 6 family 各 5)
|
||
- **PHP lint**: 全 PASS
|
||
- **Plugin active**: 12/12
|
||
- **Custom tables registered**: 14(Hub 12 + Spoke 2,未含 6 family no-op)
|
||
- **SSO group fields**: 7
|
||
- **Schema fields**: 23 (Hot 20 + Cold 3,HP 未安裝 → HP fields no-op)
|
||
|
||
---
|
||
|
||
## 來源外掛累計重要 release(供考古)
|
||
|
||
詳見 `/var/www/Studio/wp-local-dev30/wp-content/plugins/wp-data-optimizer/CHANGELOG.md` 與 `CLAUDE.md`:v2.0.0 → v2.16.0 共 47 次發版的累計改動。
|
||
|
||
亮點:
|
||
- v2.6.4 H-4 加密 webhook 密鑰
|
||
- v2.13.3 audit 7/7 finding 全清零
|
||
- v2.15.0 AES-256-CBC → AES-256-GCM AEAD
|
||
- v2.16.0 UI/UX KPI Hero + Grouped Tab Navigation
|
||
|
||
---
|
||
|
||
## [Unreleased]
|
||
|
||
### Phase 5 候補
|
||
- Tests 套件搬遷
|
||
- PHPCS 套件 + 0/0 達成
|
||
- 打包腳本 10 終檢驗收
|
||
- WPDO_ → TMDO_ migration CLI
|