From bb71e3840870bca2d2b63e9d37e18de6f20c2c5a Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 9 Aug 2026 00:47:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20partner=20=E5=81=B5=E6=B8=AC=E7=AC=A6?= =?UTF-8?q?=E8=99=9F=E5=B0=8D=E9=BD=8A=E5=AF=A6=E9=9A=9B=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E7=9A=84=E5=B8=B8=E6=95=B8=20+=20schema=20=E6=9C=9F=E6=9C=9B?= =?UTF-8?q?=E5=80=BC=E8=A3=9C=E9=BD=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TMDO_Bookings::register() 舊版偵測 TMEETIC_Booking_Plugin 類別與 TMEETIC_BOOKING_VERSION 常數(TMEETIC_Booking 則要到 :26 才載入), 在 partner 外掛中並不存在(或載入太晚),偵測實際完全靠 file_exists() 後備 撐著——程式碼讀起來像在驗證「partner 已載入」,實際只驗證了「目錄存在」。 改用 TMB_VERSION / TMB_PLUGIN_DIR,兩者在 partner 主檔的檔案解析當下即 define,早於 plugins_loaded,故本 AddOn bootstrap 的 :6 時點確實可用。 file_exists() 後備保留不動,行為不變;但偵測不再是死碼,即使將來移除後備也 不會重演 infocards-addon 那種「整個 AddOn 靜默失效」的事故。 同一版本另含先前未提交的 schema 工作:補齊 expected_columns / indexes, 並將 registry 索引 config key 由 expected_indexes 更正為 indexes (wp_parse_args() 會靜默丟棄未知 key,索引期望值先前等同從未送達)。 實測驗證(mu-plugin 於 plugins_loaded:6 量測真實請求): 新常數 AVAILABLE;wp wpdo doctor 89 OK / 0 FAIL;表註冊欄位與索引 100% 完整。 --- 2meet-data-optimizer-bookings-addon.php | 4 +- CHANGELOG.md | 29 +++ includes/class-tmdo-bookings.php | 287 ++++++++++++++++++++++-- 3 files changed, 301 insertions(+), 19 deletions(-) diff --git a/2meet-data-optimizer-bookings-addon.php b/2meet-data-optimizer-bookings-addon.php index 0527dd5..9f93a4f 100644 --- a/2meet-data-optimizer-bookings-addon.php +++ b/2meet-data-optimizer-bookings-addon.php @@ -3,7 +3,7 @@ * Plugin Name: 2meet Data Optimizer — Bookings AddOn * Plugin URI: https://2meet.io/2meet-data-optimizer/bookings * Description: 2meet-bookings 8 張自訂表 (2mb_bookings/services/availability/date_overrides/vendor_settings/service_reviews/waitlist_submissions/audit_log) 註冊 - * 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_BOOKINGS_VERSION', '0.1.0' ); +define( 'TMDO_BOOKINGS_VERSION', '0.1.1' ); define( 'TMDO_BOOKINGS_PATH', plugin_dir_path( __FILE__ ) ); define( 'TMDO_BOOKINGS_URL', plugin_dir_url( __FILE__ ) ); define( 'TMDO_BOOKINGS_FILE', __FILE__ ); diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ba1bb..646d3b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,35 @@ Versioning follows [Semantic Versioning](https://semver.org/). --- +## [0.1.1] — 2026-08-08 — 8 張 `2mb_*` 表補齊 schema 期望值 + +### Added + +- 為全部 8 張 `2mb_*` 表補上 `expected_columns` 與索引定義:`2mb_services`、 + `2mb_availability`、`2mb_date_overrides`、`2mb_bookings`、`2mb_vendor_settings`、 + `2mb_service_reviews`、`2mb_waitlist_submissions`、`2mb_audit_log`。欄位與索引 + 逐項轉錄自 2meet-bookings activator 的 `CREATE TABLE`。先前只註冊表名, + `wp wpdo doctor` 只知道表存在,無從判斷欄位或索引是否漂移——這正是升級後 + 漏建欄位/漏建索引最容易矇混過關的地方。 + +### Fixed + +- `TMDO_Custom_Table_Registry::register()` 的索引期望值 config key 由 + `expected_indexes` 更正為 `indexes`。該 API 以 `wp_parse_args()` 併入預設值, + 未知 key 會被**靜默丟棄**——先前索引期望值等同從未送達 registry, + `wp wpdo doctor` 的 schema drift 只驗欄位、不驗索引。此問題橫跨全部 8 個 AddOn, + 已一併更正。 +- `TMDO_Bookings::register()` 的 partner 偵測符號對齊實際存在的常數。舊版偵測 + `TMEETIC_Booking_Plugin` 類別與 `TMEETIC_BOOKING_VERSION` 常數,兩者在 2meet-bookings + 中**根本不存在**;`TMEETIC_Booking` 雖存在,但要到 `plugins_loaded:26`(`tmb_run`) + 才 require,在本 AddOn bootstrap 的 `:6` 當下必定 MISSING。改用 `TMB_VERSION` / + `TMB_PLUGIN_DIR`,兩者在主檔檔案解析當下即 define,早於 `plugins_loaded`。 + 實測(mu-plugin 於 `plugins_loaded:6` 量測,同一請求內對照): + `TMB_VERSION` = AVAILABLE、`TMEETIC_Booking` = MISSING。 + `file_exists()` 後備保留不動,行為不變;但偵測不再是死碼。 + +--- + ## [0.1.0] — TBD ### Added (initial release, scaffold) diff --git a/includes/class-tmdo-bookings.php b/includes/class-tmdo-bookings.php index 2af0a44..893ba65 100644 --- a/includes/class-tmdo-bookings.php +++ b/includes/class-tmdo-bookings.php @@ -26,11 +26,22 @@ if ( ! defined( 'ABSPATH' ) ) { */ final class TMDO_Bookings { + /** + * 偵測以常數優先:`TMB_VERSION` / `TMB_PLUGIN_DIR` 在 2meet-bookings 主檔的 + * **檔案解析當下**(第 22/25 行)即 define,早於 `plugins_loaded`;本 AddOn + * bootstrap 掛在 `plugins_loaded:6`,故此時常數已可用。 + * + * 舊版偵測的 `TMEETIC_Booking_Plugin` 類別與 `TMEETIC_BOOKING_VERSION` 常數在 + * 2meet-bookings 中**皆不存在**;`TMEETIC_Booking` 雖存在,但要到 `plugins_loaded:26` + * (`tmb_run`)才 require,在 :6 當下必定 MISSING。偵測實際完全靠下方的 + * `file_exists()` 撐著。保留 file_exists 作最終後備,但常數檢查現在真的有效—— + * 即使將來移除後備也不會像 infocards 那樣整個 AddOn 靜默失效。 + */ public static function register(): void { - // Detect via known class names. - $detected = class_exists( 'TMEETIC_Booking_Plugin' ) - || class_exists( 'TMEETIC_Booking' ) - || defined( 'TMEETIC_BOOKING_VERSION' ); + // Detect via constants defined at partner file-parse time (class loads too late at :6). + $detected = defined( 'TMB_VERSION' ) + || defined( 'TMB_PLUGIN_DIR' ) + || class_exists( 'TMEETIC_Booking' ); if ( ! $detected ) { // Also detect via plugin file existence (loose — partner plugin may rename main class). if ( ! file_exists( WP_PLUGIN_DIR . '/2meet-bookings/2meet-bookings.php' ) ) { @@ -47,29 +58,271 @@ final class TMDO_Bookings { * V2.1.5: added wp_2mb_audit_log (was 7 tables, now 8 — matches plugin's * TablePrefix header declaration in 2meet-bookings.php). * + * expected_columns/expected_indexes (this audit) are transcribed verbatim + * from the CREATE TABLE statements in + * 2meet-bookings/includes/class-2meetic-bookings-activator.php::create_tables() + * so `wp wpdo doctor`/`benchmark` can do schema drift detection. + * * @param TMDO_Custom_Table_Registry $registry Singleton instance. * @return void */ public static function register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { $tables = array( - '2mb_bookings' => 'id', - '2mb_services' => 'id', - '2mb_availability' => 'id', - '2mb_date_overrides' => 'id', - '2mb_vendor_settings' => 'id', - '2mb_service_reviews' => 'id', - '2mb_waitlist_submissions' => 'id', - '2mb_audit_log' => 'id', // v2.1.5: missing previously + '2mb_services' => array( + 'primary_key' => 'id', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'vendor_id' => 'BIGINT', + 'title' => 'VARCHAR', + 'slug' => 'VARCHAR', + 'description' => 'TEXT', + 'category' => 'VARCHAR', + 'tags' => 'VARCHAR', + 'duration_min' => 'INT', + 'buffer_min' => 'INT', + 'capacity_max' => 'TINYINT', + 'service_fee' => 'INT', + 'deposit_amount' => 'INT', + 'cover_image_url' => 'VARCHAR', + 'location_type' => 'VARCHAR', + 'location_county' => 'VARCHAR', + 'location_address' => 'VARCHAR', + 'location_url' => 'VARCHAR', + 'online_url' => 'VARCHAR', + 'cancellation_hours' => 'INT', + 'advance_booking_days' => 'INT', + 'min_booking_hours' => 'INT', + 'confirmation_mode' => 'VARCHAR', + 'questionnaire_json' => 'TEXT', + 'notes' => 'TEXT', + 'target_audience' => 'VARCHAR', + 'payment_cash' => 'TINYINT', + 'payment_bank_info' => 'VARCHAR', + 'payment_note' => 'TEXT', + 'status' => 'VARCHAR', + 'admin_note' => 'TEXT', + 'community_type' => 'VARCHAR', + 'community_url' => 'VARCHAR', + 'waitlist_enabled' => 'TINYINT', + 'waitlist_max' => 'INT', + 'questionnaire_form_id' => 'BIGINT', + 'booking_count' => 'INT', + 'avg_rating' => 'DECIMAL', + 'created_at' => 'DATETIME', + 'updated_at' => 'DATETIME', + ), + 'expected_indexes' => array( + 'slug' => array( 'slug' ), + 'vendor_id_status' => array( 'vendor_id', 'status' ), + 'category' => array( 'category' ), + 'location_county' => array( 'location_county' ), + 'status' => array( 'status' ), + ), + ), + '2mb_availability' => array( + 'primary_key' => 'id', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'vendor_id' => 'BIGINT', + 'service_id' => 'BIGINT', + 'day_of_week' => 'TINYINT', + 'start_time' => 'VARCHAR', + 'end_time' => 'VARCHAR', + 'is_active' => 'TINYINT', + 'created_at' => 'DATETIME', + 'updated_at' => 'DATETIME', + ), + 'expected_indexes' => array( + 'vendor_id_service' => array( 'vendor_id', 'service_id' ), + 'day_of_week' => array( 'day_of_week' ), + ), + ), + '2mb_date_overrides' => array( + 'primary_key' => 'id', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'vendor_id' => 'BIGINT', + 'service_id' => 'BIGINT', + 'override_date' => 'VARCHAR', + 'override_type' => 'VARCHAR', + 'override_source' => 'VARCHAR', + 'start_time' => 'VARCHAR', + 'end_time' => 'VARCHAR', + 'note' => 'VARCHAR', + 'created_at' => 'DATETIME', + ), + 'expected_indexes' => array( + 'vendor_date' => array( 'vendor_id', 'override_date' ), + 'service_date' => array( 'service_id', 'override_date' ), + 'source_date' => array( 'vendor_id', 'override_source', 'override_date' ), + ), + ), + '2mb_bookings' => array( + 'primary_key' => 'id', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'service_id' => 'BIGINT', + 'vendor_id' => 'BIGINT', + 'wp_user_id' => 'BIGINT', + 'customer_name' => 'VARCHAR', + 'customer_email' => 'VARCHAR', + 'customer_phone' => 'VARCHAR', + 'line_id' => 'VARCHAR', + 'booking_date' => 'VARCHAR', + 'start_time' => 'VARCHAR', + 'end_time' => 'VARCHAR', + 'headcount' => 'TINYINT', + 'custom_note' => 'TEXT', + 'booking_status' => 'VARCHAR', + 'payment_status' => 'VARCHAR', + 'payment_note' => 'VARCHAR', + 'payment_gateway' => 'VARCHAR', + 'payment_transaction_id' => 'VARCHAR', + 'payment_amount' => 'DECIMAL', + 'paid_at' => 'DATETIME', + 'vendor_note' => 'TEXT', + 'confirmation_token' => 'VARCHAR', + 'google_event_id' => 'VARCHAR', + 'confirmed_at' => 'DATETIME', + 'cancelled_at' => 'DATETIME', + 'cancel_reason' => 'VARCHAR', + 'cancelled_by' => 'VARCHAR', + 'rescheduled_from' => 'BIGINT', + 'questionnaire_answers' => 'TEXT', + 'group_label' => 'VARCHAR', + 'completed_at' => 'DATETIME', + 'order_id' => 'BIGINT', + 'commission_rate' => 'DECIMAL', + 'platform_fee' => 'INT', + 'vendor_payout' => 'INT', + 'payout_status' => 'VARCHAR', + 'payout_note' => 'TEXT', + 'settled_at' => 'DATETIME', + 'waitlist_position' => 'INT', + 'eligibility_tier' => 'VARCHAR', + 'questionnaire_status' => 'VARCHAR', + 'review_reminder_sent_at' => 'DATETIME', + 'created_at' => 'DATETIME', + 'updated_at' => 'DATETIME', + ), + 'expected_indexes' => array( + 'confirmation_token' => array( 'confirmation_token' ), + 'service_id' => array( 'service_id' ), + 'vendor_id' => array( 'vendor_id' ), + 'wp_user_id' => array( 'wp_user_id' ), + 'customer_email' => array( 'customer_email' ), + 'booking_status' => array( 'booking_status' ), + 'booking_date' => array( 'booking_date' ), + 'payment_status' => array( 'payment_status' ), + 'payout_status' => array( 'payout_status' ), + 'service_date_status' => array( 'service_id', 'booking_date', 'booking_status' ), + ), + ), + '2mb_vendor_settings' => array( + 'primary_key' => 'id', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'vendor_id' => 'BIGINT', + 'payment_linepay_url' => 'VARCHAR', + 'payment_cash_default' => 'TINYINT', + 'payment_bank_default' => 'VARCHAR', + 'payment_note_default' => 'TEXT', + 'ical_token' => 'VARCHAR', + 'line_notify_token' => 'VARCHAR', + 'line_notify_enabled' => 'TINYINT', + 'default_buffer_min' => 'INT', + 'default_cancellation_hours' => 'INT', + 'google_tokens' => 'TEXT', + 'gcal_watch_channel_id' => 'VARCHAR', + 'gcal_watch_resource_id' => 'VARCHAR', + 'gcal_watch_expiration' => 'BIGINT', + 'gcal_watch_token' => 'VARCHAR', + 'telegram_bot_token' => 'VARCHAR', + 'telegram_chat_id' => 'VARCHAR', + 'slack_webhook_url' => 'VARCHAR', + 'created_at' => 'DATETIME', + 'updated_at' => 'DATETIME', + ), + 'expected_indexes' => array( + 'vendor_id' => array( 'vendor_id' ), + 'gcal_watch_expiration' => array( 'gcal_watch_expiration' ), + 'gcal_watch_channel_id' => array( 'gcal_watch_channel_id' ), + ), + ), + '2mb_service_reviews' => array( + 'primary_key' => 'id', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'service_id' => 'BIGINT', + 'booking_id' => 'BIGINT', + 'vendor_id' => 'BIGINT', + 'customer_name' => 'VARCHAR', + 'rating' => 'TINYINT', + 'review_text' => 'TEXT', + 'vendor_reply' => 'TEXT', + 'status' => 'VARCHAR', + 'created_at' => 'DATETIME', + 'updated_at' => 'DATETIME', + ), + 'expected_indexes' => array( + 'booking_id' => array( 'booking_id' ), + 'service_id_status' => array( 'service_id', 'status' ), + 'vendor_id' => array( 'vendor_id' ), + ), + ), + '2mb_waitlist_submissions' => array( + 'primary_key' => 'id', + 'expected_columns' => array( + 'id' => 'BIGINT', + 'booking_id' => 'BIGINT', + 'fluent_entry_id' => 'BIGINT', + 'answers_snapshot' => 'TEXT', + 'submitted_at' => 'DATETIME', + 'review_status' => 'VARCHAR', + 'reviewed_by_vendor' => 'BIGINT', + 'reviewed_at_vendor' => 'DATETIME', + 'vendor_notes' => 'TEXT', + 'reviewed_by_admin' => 'BIGINT', + 'reviewed_at_admin' => 'DATETIME', + 'admin_notes' => 'TEXT', + 'revision_count' => 'INT', + ), + 'expected_indexes' => array( + 'idx_booking_id' => array( 'booking_id' ), + 'idx_fluent_entry' => array( 'fluent_entry_id' ), + 'review_status' => array( 'review_status' ), + ), + ), + '2mb_audit_log' => array( + 'primary_key' => 'id', // v2.1.5: missing previously + 'expected_columns' => array( + 'id' => 'BIGINT', + 'level' => 'VARCHAR', + 'event' => 'VARCHAR', + 'message' => 'VARCHAR', + 'context' => 'TEXT', + 'user_id' => 'BIGINT', + 'ip' => 'VARCHAR', + 'created_at' => 'DATETIME', + ), + 'expected_indexes' => array( + 'level_created' => array( 'level', 'created_at' ), + 'event_created' => array( 'event', 'created_at' ), + 'user_id' => array( 'user_id' ), + ), + ), ); - foreach ( $tables as $table_name => $primary_key ) { + foreach ( $tables as $table_name => $definition ) { $registry->register( '2meet-bookings', array( - 'table_name' => $table_name, - 'primary_key' => $primary_key, - 'post_type_link' => null, - 'doctor_callback' => array( __CLASS__, 'doctor_table_factory' ), + 'table_name' => $table_name, + 'primary_key' => $definition['primary_key'], + 'post_type_link' => null, + 'doctor_callback' => array( __CLASS__, 'doctor_table_factory' ), + 'expected_columns' => $definition['expected_columns'], + 'indexes' => $definition['expected_indexes'], ) ); }