From cf47f141c59d54aee62497de5303f9cc40b58a17 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 8 Aug 2026 18:47:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=BE=A9=20partner=20=E5=81=B5?= =?UTF-8?q?=E6=B8=AC=E6=99=82=E5=BA=8F=20bug=EF=BC=88AddOn=20=E5=BE=9E?= =?UTF-8?q?=E6=9C=AA=E7=94=9F=E6=95=88=EF=BC=89+=20IG=20meta=5Fkey=20?= =?UTF-8?q?=E9=8C=AF=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TMDO_Infocards::register() 舊版在 bootstrap 當下以 class_exists('TMEETIC_*') 偵測 partner,失敗即 early return。但本 AddOn bootstrap 掛在 plugins_loaded:6, 而 2meet-infocards 要到 :20(tmeetic_run)才 require 自身類別——在 :6 當下 TMEETIC_* 一律不存在,偵測必定失敗,兩個註冊 hook 從未被掛上,整個 AddOn 等同未啟用。 三處修法: 1. 偵測從 bootstrap 移進 hook callback 內執行,hook 一律先掛上 2. 偵測改以 TMEETIC_VERSION / TMEETIC_PLUGIN_DIR 常數優先(檔案解析當下即 define,早於 plugins_loaded),class_exists() 退為後備 3. bootstrap priority 6 → 3;wpdo_register_custom_tables callback prio 設 5, 讓本 AddOn 較豐富的定義勝過主外掛無 expected_columns 的版本 同時修正 IG token 兩個欄位的 meta_key 錯配(tmeetic_ig_access_token_enc / tmeetic_ig_token_expiry),以及 registry 索引 config key expected_indexes → indexes。 實測驗證: - wpdo_register_fields 掛載 prio 10、wpdo_register_custom_tables prio 5 - 2meet_vendor_profiles 由本 AddOn 註冊(16 欄位 / 1 索引 / doctor_callback) - 缺 expected_columns 由 26/29 降為 0/46 - wp wpdo doctor 60 OK / 0 FAIL;anti-EAV lint 通過;站台 200/200 --- 2meet-data-optimizer-infocards-addon.php | 12 +- CHANGELOG.md | 54 +++++++ includes/class-tmdo-infocards.php | 182 ++++++++++++++++++----- 3 files changed, 209 insertions(+), 39 deletions(-) diff --git a/2meet-data-optimizer-infocards-addon.php b/2meet-data-optimizer-infocards-addon.php index 0756be2..5204917 100644 --- a/2meet-data-optimizer-infocards-addon.php +++ b/2meet-data-optimizer-infocards-addon.php @@ -3,7 +3,7 @@ * Plugin Name: 2meet Data Optimizer — Infocards AddOn * Plugin URI: https://2meet.io/2meet-data-optimizer/infocards * Description: 2meet-infocards 整合:hp_vendor 9 postmeta (3 Hot tmeetic_profile_* + 6 Cold tmeetic_ig_*) + 3 vendor profile 表 + doctor check - * Version: 0.1.0 + * Version: 0.1.1 * Requires at least: 6.0 * Tested up to: 6.9.4 * Requires PHP: 8.1 @@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'TMDO_INFOCARDS_VERSION', '0.1.0' ); +define( 'TMDO_INFOCARDS_VERSION', '0.1.1' ); define( 'TMDO_INFOCARDS_PATH', plugin_dir_path( __FILE__ ) ); define( 'TMDO_INFOCARDS_URL', plugin_dir_url( __FILE__ ) ); define( 'TMDO_INFOCARDS_FILE', __FILE__ ); @@ -37,4 +37,10 @@ require_once TMDO_INFOCARDS_PATH . 'includes/back-compat-aliases.php'; * * @see ~/.claude/plans/wp-data-optimizer-2meet-data-optimizer-zesty-liskov.md */ -add_action( 'plugins_loaded', array( 'TMDO_Infocards_Bootstrap', 'init' ), 6 ); +// priority 3(其餘 AddOn 慣例為 6):核心的 `fire_registration()` 最早在 +// plugins_loaded:4 觸發,在 :6 bootstrap 會**必定錯過第一次註冊**,而 +// Custom_Table_Registry 是「先註冊者勝」——主外掛 2meet-infocards 的 closure 在 +// 檔案解析時就掛好,會在 :4 先以無 expected_columns 的版本佔走 key。 +// 核心的 TMDO_API / TMDO_Custom_Table_Registry 於 file-parse 時(主檔 126/127/179 行) +// 即已 require,故提前到 :3 是安全的。 +add_action( 'plugins_loaded', array( 'TMDO_Infocards_Bootstrap', 'init' ), 3 ); diff --git a/CHANGELOG.md b/CHANGELOG.md index 409a91d..3507f72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,60 @@ Versioning follows [Semantic Versioning](https://semver.org/). --- +## [0.1.1] — 2026-08-08 — 修復 partner 偵測時序 bug(AddOn 從未生效)與 IG 欄位 meta_key 錯配 + +### Fixed + +- **本 AddOn 從未掛上任何 hook,等同未啟用(嚴重)。** `TMDO_Infocards::register()` + 舊版在 bootstrap 當下就以 `class_exists( 'TMEETIC_*' )` 偵測 partner,失敗即 + early return。但本 AddOn bootstrap 掛在 `plugins_loaded:6`,而 2meet-infocards + 要到 `plugins_loaded:20`(`tmeetic_run`)才 require 自身類別——在 :6 當下 + TMEETIC_* 類別一律不存在,偵測**必定**失敗,於是 `wpdo_register_fields` / + `wpdo_register_custom_tables` 兩個 hook 從未被掛上(實測:`TMEETIC_Frontend` + 在 prio 6 = MISSING、prio 21 = EXISTS)。三處修法: + 1. 偵測從 bootstrap 移進 hook callback 內執行(`register_post_fields()` / + `register_custom_tables()` 開頭),hook 一律先掛上。核心於 + `plugins_loaded:30` 有 late-bind safety net 會重新 fire 註冊 hook,唯有把 + 偵測延後到 callback 執行當下才會正確生效。 + 2. 偵測改以 `TMEETIC_VERSION` / `TMEETIC_PLUGIN_DIR` 常數優先,`class_exists()` + 退為後備。這兩個常數在 `2meet-infocards.php` 的**檔案解析當下**即 define, + 早於 `plugins_loaded`;核心 `fire_registration()` 最早在 `plugins_loaded:4` + 觸發,該時點只有常數可用,只靠 `class_exists()` 會在最關鍵的第一次註冊誤判 + 為「partner 不存在」。 + 3. bootstrap priority 由 6 提前到 3,`wpdo_register_custom_tables` callback + priority 設為 5。理由有二:核心 `fire_registration()` 最早在 + `plugins_loaded:4` 觸發,掛在 :6 必定錯過第一次註冊;且 + `TMDO_Custom_Table_Registry` 對同一 `provider:table_name` 是「先註冊者勝、 + 後者回傳 false」,主外掛 2meet-infocards 的 closure 在檔案解析當下就掛上 + prio 10,若不提前,會先以無 `expected_columns` 的版本佔走 key。 +- IG token 兩個欄位註冊的 meta_key 與 2meet-infocards 程式碼實際讀寫的 key 不符, + 導致它們從未真正進入 TMDO 加速路徑(仍落回 `wp_postmeta`): + - `tmeetic_ig_token_enc` → `tmeetic_ig_access_token_enc` + - `tmeetic_ig_token_expires_at` → `tmeetic_ig_token_expiry` +- `TMDO_Custom_Table_Registry::register()` 的索引期望值 config key 由 + `expected_indexes` 更正為 `indexes`。該 API 以 `wp_parse_args()` 併入預設值, + 未知 key 會被**靜默丟棄**——先前索引期望值等同從未送達 registry, + `wp wpdo doctor` 的 schema drift 只驗欄位、不驗索引。此問題橫跨全部 8 個 AddOn, + 已一併更正。 + +### Added + +- 註冊先前完全未註冊的兩個 `hp_vendor` Cold zone 欄位:`tmeetic_ig_username`、 + `tmeetic_ig_feed_show`。 +- 為 `2meet_vendor_profiles`(16 欄)/ `2meet_cards`(25 欄)/ + `2meet_theme_styles`(8 欄)三張表補上 `expected_columns` 與 `indexes`, + `wp wpdo doctor` 才能對它們做 schema drift 檢查。先前只註冊表名,doctor + 無從判斷欄位或索引是否漂移。 + +### Removed + +- 移除 `tmeetic_ig_app_id`、`tmeetic_ig_app_secret_enc` 兩個幽靈註冊。IG 應用 + 憑證是**站台層級**設定,實際存於 `wp_options` 的 `2meetic_ig_app_id` / + `2meetic_ig_app_secret_enc`,並非 `hp_vendor` postmeta;全外掛對這兩個 + postmeta key 零讀寫,註冊只會在 Cold zone 留下永遠空的欄位定義。 + +--- + ## [0.1.0] — TBD ### Added (initial release, scaffold) diff --git a/includes/class-tmdo-infocards.php b/includes/class-tmdo-infocards.php index db17c29..dd304b8 100644 --- a/includes/class-tmdo-infocards.php +++ b/includes/class-tmdo-infocards.php @@ -11,7 +11,13 @@ * - Provide cache loader for inject_profile_styles() so it doesn't query DB * on every wp_head call * - * @package WP_Data_Optimizer + * NOTE: 2meet-infocards 主檔(2meet-infocards.php)自 v6.9.24 起也直接掛 + * wpdo_register_fields / wpdo_register_custom_tables(belt-and-suspenders, + * 繞過舊版 wp-data-optimizer 曾有的時序偵測 bug)。兩邊重複註冊同一批欄位是 + * 刻意的防禦性冗餘,Schema Registry 對同一 meta_key 重複 register 是 idempotent + * 操作,不會造成資料損壞;但若未來要精簡,應二擇一保留,勿只改其中一邊的 key 名稱。 + * + * @package TMDO * @since 2.0.0 */ @@ -25,19 +31,45 @@ if ( ! defined( 'ABSPATH' ) ) { final class TMDO_Infocards { /** - * Static registration entry. Called from TMDO_Core::run(). + * Static registration entry. Called from the AddOn bootstrap on plugins_loaded:6. + * + * 時序修正:偵測**不可**在此處做。本 AddOn bootstrap 於 plugins_loaded:6 執行, + * 但 2meet-infocards 要到 plugins_loaded:20(`tmeetic_run`)才 require 自身類別, + * 因此在 :6 當下 TMEETIC_* 一律不存在 → 舊版在此早退,導致整個 AddOn 從未掛上 + * 任何 hook(實測:TMEETIC_Frontend 在 prio 6 = MISSING、prio 21 = EXISTS)。 + * + * 核心於 plugins_loaded:30 有 late-bind safety net 會重新 fire 註冊 hook, + * 所以改為「hook 一律掛上、偵測延後到 callback 執行當下」才會正確生效。 + * + * priority 5(而非預設 10)的理由:Custom_Table_Registry 對同一 + * `provider:table_name` 是「先註冊者勝、後者回傳 false」。主外掛 + * `2meet-infocards.php` 的 closure 在**檔案解析當下**就掛上 prio 10,早於本 + * AddOn 在 plugins_loaded:6 掛載,同 prio 內先掛先跑 → 主外掛會先以「無 + * expected_columns」的版本佔走 key。本 AddOn 的定義較豐富(含 expected_columns + * / indexes / doctor_callback),故提前到 prio 5 讓它勝出;主外掛其餘 26 張表 + * 不受影響,仍由它自己註冊。 */ public static function register(): void { - // Detect partner plugin via known class names (any one is sufficient). - $detected = class_exists( 'TMEETIC_Plugin' ) + add_action( 'wpdo_register_fields', array( __CLASS__, 'register_post_fields' ) ); + add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ), 5 ); + } + + /** + * Detect the partner plugin. + * + * 以常數優先、類別為輔:`TMEETIC_VERSION` / `TMEETIC_PLUGIN_DIR` 在 + * `2meet-infocards.php` 的**檔案解析當下**(第 25–33 行,不在任何 function 內) + * 就已 define,早於 `plugins_loaded`;而 TMEETIC_* 類別要到 plugins_loaded:20 + * 才 require。核心的 `fire_registration()` 最早在 plugins_loaded:4 觸發, + * 該時點只有常數可用——只靠 class_exists() 會在最關鍵的第一次註冊時誤判為 + * 「partner 不存在」。 + */ + private static function partner_detected(): bool { + return defined( 'TMEETIC_VERSION' ) + || defined( 'TMEETIC_PLUGIN_DIR' ) + || class_exists( 'TMEETIC_Plugin' ) || class_exists( 'TMEETIC_Frontend' ) || class_exists( '\\TwoMeet_InfoCards\\Plugin' ); - if ( ! $detected ) { - return; - } - - add_action( 'wpdo_register_fields', array( __CLASS__, 'register_post_fields' ) ); - add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) ); } /** @@ -47,6 +79,9 @@ final class TMDO_Infocards { * @return void */ public static function register_post_fields( TMDO_Schema_Registry $registry ): void { + if ( ! self::partner_detected() ) { + return; + } $registry->register_many( '2meet-infocards', array( @@ -81,30 +116,19 @@ final class TMDO_Infocards { 'cache_ttl' => HOUR_IN_SECONDS, ), // IG tokens — sensitive + display-only → Cold zone. + // 註:app id / secret 不在此列——那是站台層級的 wp_options + // (`2meetic_ig_app_id` / `2meetic_ig_app_secret_enc`),非 hp_vendor + // postmeta。舊版誤註冊的兩個 postmeta 欄位全外掛零讀寫,已移除。 array( 'post_type' => 'hp_vendor', - 'meta_key' => 'tmeetic_ig_app_id', + 'meta_key' => 'tmeetic_ig_access_token_enc', 'zone' => 'cold', 'cache_group' => 'wpdo_cold_hp_vendor', 'cache_ttl' => HOUR_IN_SECONDS, ), array( 'post_type' => 'hp_vendor', - 'meta_key' => 'tmeetic_ig_app_secret_enc', - 'zone' => 'cold', - 'cache_group' => 'wpdo_cold_hp_vendor', - 'cache_ttl' => HOUR_IN_SECONDS, - ), - array( - 'post_type' => 'hp_vendor', - 'meta_key' => 'tmeetic_ig_token_enc', - 'zone' => 'cold', - 'cache_group' => 'wpdo_cold_hp_vendor', - 'cache_ttl' => HOUR_IN_SECONDS, - ), - array( - 'post_type' => 'hp_vendor', - 'meta_key' => 'tmeetic_ig_token_expires_at', + 'meta_key' => 'tmeetic_ig_token_expiry', 'zone' => 'cold', 'cache_group' => 'wpdo_cold_hp_vendor', 'cache_ttl' => HOUR_IN_SECONDS, @@ -116,6 +140,20 @@ final class TMDO_Infocards { 'cache_group' => 'wpdo_cold_hp_vendor', 'cache_ttl' => HOUR_IN_SECONDS, ), + array( + 'post_type' => 'hp_vendor', + 'meta_key' => 'tmeetic_ig_username', + 'zone' => 'cold', + 'cache_group' => 'wpdo_cold_hp_vendor', + 'cache_ttl' => HOUR_IN_SECONDS, + ), + array( + 'post_type' => 'hp_vendor', + 'meta_key' => 'tmeetic_ig_feed_show', + 'zone' => 'cold', + 'cache_group' => 'wpdo_cold_hp_vendor', + 'cache_ttl' => HOUR_IN_SECONDS, + ), ) ); } @@ -128,30 +166,102 @@ final class TMDO_Infocards { * @return void */ public static function register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { + if ( ! self::partner_detected() ) { + return; + } // CSS variable source — checked by WPDO doctor for index health. $registry->register( '2meet-infocards', array( - 'table_name' => '2meet_vendor_profiles', - 'primary_key' => 'vendor_id', - 'post_type_link' => 'hp_vendor', - 'doctor_callback' => array( __CLASS__, 'doctor_vendor_profiles' ), + 'table_name' => '2meet_vendor_profiles', + 'primary_key' => 'vendor_id', + 'post_type_link' => 'hp_vendor', + 'doctor_callback' => array( __CLASS__, 'doctor_vendor_profiles' ), + 'expected_columns' => array( + 'id' => 'BIGINT', + 'vendor_id' => 'BIGINT', + 'primary_color' => 'VARCHAR', + 'accent_color' => 'VARCHAR', + 'bg_color' => 'VARCHAR', + 'text_color' => 'VARCHAR', + 'muted_color' => 'VARCHAR', + 'font' => 'VARCHAR', + 'radius_card' => 'INT', + 'radius_btn' => 'INT', + 'shadow_style' => 'VARCHAR', + 'hero_image_id' => 'BIGINT', + 'hero_fullwidth' => 'TINYINT', + 'ig_feed_show' => 'TINYINT', + 'theme_style_id' => 'BIGINT', + 'updated_at' => 'DATETIME', + ), + 'indexes' => array( + 'vendor_id' => array( 'vendor_id' ), + ), ) ); $registry->register( '2meet-infocards', array( - 'table_name' => '2meet_cards', - 'primary_key' => 'id', - 'post_type_link' => 'hp_vendor', + 'table_name' => '2meet_cards', + 'primary_key' => 'id', + 'post_type_link' => 'hp_vendor', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'page_id' => 'BIGINT', + 'story_id' => 'BIGINT', + 'epk_id' => 'BIGINT', + 'card_type' => 'VARCHAR', + 'title' => 'VARCHAR', + 'description' => 'TEXT', + 'image_id' => 'BIGINT', + 'config' => 'TEXT', + 'cta_text' => 'VARCHAR', + 'cta_url' => 'VARCHAR', + 'button_bg_color' => 'VARCHAR', + 'button_text_color' => 'VARCHAR', + 'sort_order' => 'INT', + 'status' => 'VARCHAR', + 'start_at' => 'DATETIME', + 'end_at' => 'DATETIME', + 'vendor_id' => 'BIGINT', + 'show_click_count' => 'TINYINT', + 'ab_enabled' => 'TINYINT', + 'liff_enabled' => 'TINYINT', + 'created_at' => 'DATETIME', + 'updated_at' => 'DATETIME', + 'click_count' => 'BIGINT', + 'last_click_at' => 'DATETIME', + ), + 'indexes' => array( + 'page_id_status_sort' => array( 'page_id', 'status', 'sort_order' ), + 'story_id_status_sort' => array( 'story_id', 'status', 'sort_order' ), + 'epk_id_status_sort' => array( 'epk_id', 'status', 'sort_order' ), + 'card_type' => array( 'card_type' ), + 'vendor_status_sort' => array( 'vendor_id', 'status', 'sort_order' ), + 'status_schedule' => array( 'status', 'start_at', 'end_at' ), + ), ) ); $registry->register( '2meet-infocards', array( - 'table_name' => '2meet_theme_styles', - 'primary_key' => 'id', - 'post_type_link' => null, + 'table_name' => '2meet_theme_styles', + 'primary_key' => 'id', + 'post_type_link' => null, + 'expected_columns' => array( + 'id' => 'BIGINT', + 'name' => 'VARCHAR', + 'slug' => 'VARCHAR', + 'config' => 'TEXT', + 'sort_order' => 'INT', + 'is_active' => 'TINYINT', + 'created_at' => 'DATETIME', + 'updated_at' => 'DATETIME', + ), + 'indexes' => array( + 'slug' => array( 'slug' ), + ), ) ); } -- 2.43.0