Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6fd831416 | |||
| 763f705ac1 | |||
| 5953a5ed3b | |||
| 6909bf6a99 | |||
| 9803f8379d | |||
| f7f9bc5cc4 | |||
| bfe8912600 | |||
| 15bbf335f4 | |||
| 0cd2a9db5b | |||
| d802bf0145 |
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: 2meet Data Optimizer — Hivepress AddOn
|
* Plugin Name: 2meet Data Optimizer — Hivepress AddOn
|
||||||
* Plugin URI: https://2meet.io/2meet-data-optimizer/hivepress
|
* Plugin URI: https://2meet.io/2meet-data-optimizer/hivepress
|
||||||
* Description: HivePress + 12 擴充整合:13 adapter (core/reviews/bookings/messages/memberships/requests/favorites/statistics/tags/seo/social-links/blocks/marketplace) + 7 HPCT interceptor + 5 query interceptor + admin tab + CLI namespace
|
* Description: HivePress + 12 擴充整合:13 adapter (core/reviews/bookings/messages/memberships/requests/favorites/statistics/tags/seo/social-links/blocks/marketplace) + 7 HPCT interceptor + 5 query interceptor + admin tab + CLI namespace
|
||||||
* Version: 0.4.0
|
* Version: 0.5.3
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Tested up to: 6.9.4
|
* Tested up to: 6.9.4
|
||||||
* Requires PHP: 8.1
|
* Requires PHP: 8.1
|
||||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
define( 'TMDO_HIVEPRESS_VERSION', '0.4.0' );
|
define( 'TMDO_HIVEPRESS_VERSION', '0.5.3' );
|
||||||
define( 'TMDO_HIVEPRESS_PATH', plugin_dir_path( __FILE__ ) );
|
define( 'TMDO_HIVEPRESS_PATH', plugin_dir_path( __FILE__ ) );
|
||||||
define( 'TMDO_HIVEPRESS_URL', plugin_dir_url( __FILE__ ) );
|
define( 'TMDO_HIVEPRESS_URL', plugin_dir_url( __FILE__ ) );
|
||||||
define( 'TMDO_HIVEPRESS_FILE', __FILE__ );
|
define( 'TMDO_HIVEPRESS_FILE', __FILE__ );
|
||||||
|
|||||||
@@ -11,6 +11,57 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.5.3] — 2026-10-07 — 瀏覽數 flush 不再造成漂移
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`TMDO_Listing_Stats::flush_views_to_postmeta()` 寫完後同步扁平表與快取**:每小時把 warm zone 的瀏覽數
|
||||||
|
用原子 `UPDATE … meta_value + N` 加回 `hp_view_count`,繞過了 meta API:Hook Bus 看不到這次寫入,
|
||||||
|
dual_write/shadow_read 下 `hp_listing_core.hp_view_count` 每批落後一次(夜間 drift-check 每晚 diff),
|
||||||
|
物件快取(Redis 下跨請求)也一直是舊值。現在 UPDATE 之後清掉該篇的 `post_meta` 快取,
|
||||||
|
並在原生仍是真相時(同時寫原生與扁平)用 `TMDO_Entity_Migration_Engine::migrate_entity()` 重建那一列。
|
||||||
|
`hp_view_count` 同時在 `hp_listing_core` 與 `hp_request_core`:只重建含這個 key、而且適用該篇 post type 的群組
|
||||||
|
(不帶 post type 的 `get_field()` 只回最後登記的那組)。aeav_only 不動(扁平才是真相)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.5.2] — 2026-10-05 — attribute bridge 的 meta key
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`TMDO_HivePress_Attribute_Bridge` 組出正確的 meta key**:HivePress 把每個模型的屬性存成
|
||||||
|
`hp_{slug}`(例如 `hp_warranty`),不是 `hp_{model}_{slug}`。舊的 key 在資料庫裡不存在,
|
||||||
|
照它建議的欄位登記後永遠是空的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.5.1] — 2026-10-02 — 重新打包
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 重新打包(無功能變更):與 WP-MVB v8 樞紐站(dev39)一同驗收後發版;程式碼與前一版相同。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.5.0] — 2026-09-28
|
||||||
|
|
||||||
|
Released with 2meet-data-optimizer 1.4.0 (hardening 2.4).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `hp_listing_core` declares `(hp_price, post_id)`, `(hp_rating, post_id)` and
|
||||||
|
`(hp_hourly_rate, post_id)` indexes for the listing sorts HivePress offers:
|
||||||
|
with core 1.4.0 reading the ID tie-break from the flat `post_id`, a sort walks
|
||||||
|
the index and stops at the page instead of sorting every match (dev36, 1.3M
|
||||||
|
listings: price sort 14 s native → 0.37 s routed, hourly rate 11.5 s → 0.35 s,
|
||||||
|
rating 43 s → 0.51 s). Core 1.3.0 ignores indexes ending with `post_id`, so
|
||||||
|
nothing changes there. The pinned schema hash is unchanged. (PR-119)
|
||||||
|
- Transient entries in a persistent object cache live a day (`MAX_CACHE_TTL`,
|
||||||
|
HivePress's own default) instead of a week; a timeout still outlives its value
|
||||||
|
by a minute. At a week dev36 kept 4.7M entries (1 GB) in the shared Redis and
|
||||||
|
pushed other sites' keys out. Entries already cached keep their week. (PR-120)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.4.0] — 2026-09-27
|
## [0.4.0] — 2026-09-27
|
||||||
|
|
||||||
Released with 2meet-data-optimizer 1.3.0 (hardening 2.3).
|
Released with 2meet-data-optimizer 1.3.0 (hardening 2.3).
|
||||||
|
|||||||
@@ -266,8 +266,13 @@ final class TMDO_HivePress_Entity_Groups {
|
|||||||
// Only with core 1.3.0+ (ddl_allowed()): older cores run the ALTER
|
// Only with core 1.3.0+ (ddl_allowed()): older cores run the ALTER
|
||||||
// inside whatever request comes first, a visitor's included.
|
// inside whatever request comes first, a visitor's included.
|
||||||
'indexes' => method_exists( 'TMDO_Schema_Manager', 'ddl_allowed' ) ? array(
|
'indexes' => method_exists( 'TMDO_Schema_Manager', 'ddl_allowed' ) ? array(
|
||||||
'hourly_rate' => array( 'hp_hourly_rate' ),
|
'hourly_rate' => array( 'hp_hourly_rate' ),
|
||||||
'rating_count' => array( 'hp_rating_count' ),
|
'rating_count' => array( 'hp_rating_count' ),
|
||||||
|
// HivePress sorts: the value then the ID tie-break, which core 1.4.0
|
||||||
|
// reads in order (flat-first) instead of sorting every match.
|
||||||
|
'price_id' => array( 'hp_price', 'post_id' ),
|
||||||
|
'rating_id' => array( 'hp_rating', 'post_id' ),
|
||||||
|
'hourly_rate_id' => array( 'hp_hourly_rate', 'post_id' ),
|
||||||
) : array(),
|
) : array(),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ final class TMDO_Hivepress_Transient_Filter {
|
|||||||
/** Object cache group used instead of wp_options when a persistent cache is present. */
|
/** Object cache group used instead of wp_options when a persistent cache is present. */
|
||||||
public const CACHE_GROUP = 'tmdo_hp_transient';
|
public const CACHE_GROUP = 'tmdo_hp_transient';
|
||||||
|
|
||||||
/** Longest an object-cache entry lives: a week (HivePress's default is a day). */
|
/** Longest an object-cache entry lives: a day, HivePress's own default (2.4 PR-120). */
|
||||||
public const MAX_CACHE_TTL = 604800;
|
public const MAX_CACHE_TTL = DAY_IN_SECONDS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the four metadata filters on `init`. Idempotent — safe to call
|
* Register the four metadata filters on `init`. Idempotent — safe to call
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ class TMDO_Listing_Stats {
|
|||||||
// Seed the key when it does not exist yet.
|
// Seed the key when it does not exist yet.
|
||||||
if ( ! $updated ) {
|
if ( ! $updated ) {
|
||||||
add_post_meta( $post_id, 'hp_view_count', $new_views, true );
|
add_post_meta( $post_id, 'hp_view_count', $new_views, true );
|
||||||
|
} else {
|
||||||
|
self::resync_view_count( $post_id );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the warm entry after flush.
|
// Remove the warm entry after flush.
|
||||||
@@ -164,6 +166,40 @@ class TMDO_Listing_Stats {
|
|||||||
return $flushed;
|
return $flushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The atomic UPDATE above bypasses the meta API: the object cache (persistent
|
||||||
|
* under Redis) keeps the old count, and the Hook Bus never sees the write, so in
|
||||||
|
* dual_write / shadow_read the flat column drifts by every flushed batch
|
||||||
|
* (drift-check diff on hp_view_count each night). Drop the cached meta and
|
||||||
|
* rebuild the entity's flat row from native — only while native is the source
|
||||||
|
* of truth (aeav_only keeps flat authoritative).
|
||||||
|
*
|
||||||
|
* @param int $post_id Listing post ID.
|
||||||
|
*/
|
||||||
|
private static function resync_view_count( int $post_id ): void {
|
||||||
|
wp_cache_delete( $post_id, 'post_meta' );
|
||||||
|
if ( ! class_exists( 'TMDO_Mode_Manager' ) || ! class_exists( 'TMDO_Entity_Registry' ) || ! class_exists( 'TMDO_Entity_Migration_Engine' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( ! TMDO_Mode_Manager::writes_to_flat( 'post' ) || ! TMDO_Mode_Manager::writes_to_eav( 'post' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// hp_view_count lives in more than one group (hp_listing_core, hp_request_core):
|
||||||
|
// get_field() without a post type returns whichever registered last. Rebuild only
|
||||||
|
// the groups that hold the key and apply to this post's type.
|
||||||
|
$type = (string) get_post_type( $post_id );
|
||||||
|
foreach ( TMDO_Entity_Registry::get_groups_for_type( 'post' ) as $group ) {
|
||||||
|
$types = TMDO_Entity_Registry::get_group_post_types( 'post', $group );
|
||||||
|
if ( ! in_array( 'hp_view_count', TMDO_Entity_Registry::get_group_keys( 'post', $group ), true ) || ( $types && ! in_array( $type, $types, true ) ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TMDO_Entity_Migration_Engine::migrate_entity( 'post', $group, $post_id );
|
||||||
|
if ( class_exists( 'TMDO_Cache_Orchestrator' ) ) {
|
||||||
|
TMDO_Cache_Orchestrator::invalidate( 'post', $post_id, $group );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Zone D: Expired Listing Archival ─────────────────────────────────────
|
// ── Zone D: Expired Listing Archival ─────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
* Usage:
|
* Usage:
|
||||||
* $bridge = new TMDO_HivePress_Attribute_Bridge();
|
* $bridge = new TMDO_HivePress_Attribute_Bridge();
|
||||||
* $candidates = $bridge->discover_attributes();
|
* $candidates = $bridge->discover_attributes();
|
||||||
* // returns: [['slug' => 'beds', 'type' => 'integer', 'filterable' => true, 'searchable' => true, 'meta_key' => 'hp_listing_beds'], ...]
|
* // returns: [['slug' => 'beds', 'type' => 'integer', 'filterable' => true, 'searchable' => true, 'meta_key' => 'hp_beds'], ...]
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
@@ -123,7 +123,8 @@ if ( ! class_exists( 'TMDO_HivePress_Attribute_Bridge' ) ) {
|
|||||||
return array(
|
return array(
|
||||||
'model' => $model,
|
'model' => $model,
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'meta_key' => sprintf( 'hp_%s_%s', $model, $slug ),
|
// HivePress stores every model's attribute as hp_{slug} (e.g. hp_warranty), not hp_{model}_{slug}.
|
||||||
|
'meta_key' => 'hp_' . $slug,
|
||||||
'type' => (string) ( $cfg['edit_field']['type'] ?? $cfg['search_field']['type'] ?? 'text' ),
|
'type' => (string) ( $cfg['edit_field']['type'] ?? $cfg['search_field']['type'] ?? 'text' ),
|
||||||
'filterable' => (bool) ( $cfg['filterable'] ?? false ),
|
'filterable' => (bool) ( $cfg['filterable'] ?? false ),
|
||||||
'sortable' => (bool) ( $cfg['sortable'] ?? false ),
|
'sortable' => (bool) ( $cfg['sortable'] ?? false ),
|
||||||
|
|||||||
@@ -215,11 +215,14 @@ class HivepressTransientFilterTest extends TestCase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Entries expire in the object cache too (2.3 PR-099): the value (written
|
* Entries expire in the object cache too (2.3 PR-099): the value (written
|
||||||
* first, before its timeout is known) after a week, the timeout after the
|
* first, before its timeout is known) after a day, the timeout after the
|
||||||
* later of that and its own time, plus a minute. Orphans of deleted posts
|
* later of that and its own time, plus a minute. Orphans of deleted posts
|
||||||
* no longer pile up.
|
* no longer pile up.
|
||||||
*/
|
*/
|
||||||
public function test_with_an_object_cache_entries_get_a_ttl(): void {
|
public function test_with_an_object_cache_entries_get_a_ttl(): void {
|
||||||
|
// A day, HivePress's own default: at a week, 1.3M listings kept 4.7M
|
||||||
|
// entries (1 GB) in a shared Redis and pushed other sites' keys out (2.4 PR-120).
|
||||||
|
$this->assertSame( 86400, WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL );
|
||||||
$GLOBALS['_wp_using_ext_object_cache'] = true;
|
$GLOBALS['_wp_using_ext_object_cache'] = true;
|
||||||
$key = '_transient_hp_models/cat/v1';
|
$key = '_transient_hp_models/cat/v1';
|
||||||
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class HivePressAttributeBridgeTest extends TestCase {
|
|||||||
|
|
||||||
$this->assertSame( 'listing', $record['model'] );
|
$this->assertSame( 'listing', $record['model'] );
|
||||||
$this->assertSame( 'beds', $record['slug'] );
|
$this->assertSame( 'beds', $record['slug'] );
|
||||||
$this->assertSame( 'hp_listing_beds', $record['meta_key'] );
|
$this->assertSame( 'hp_beds', $record['meta_key'] );
|
||||||
$this->assertSame( 'integer', $record['type'] );
|
$this->assertSame( 'integer', $record['type'] );
|
||||||
$this->assertTrue( $record['filterable'] );
|
$this->assertTrue( $record['filterable'] );
|
||||||
$this->assertFalse( $record['sortable'] );
|
$this->assertFalse( $record['sortable'] );
|
||||||
|
|||||||
@@ -37,9 +37,15 @@ class HivePressEntityGroupsTest extends TestCase {
|
|||||||
'hp_user' => 'hp_vendor_core',
|
'hp_user' => 'hp_vendor_core',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private function post_adapter(): TMDO_Entity_Adapter_Interface {
|
||||||
|
$adapter = $this->createMock( TMDO_Entity_Adapter_Interface::class );
|
||||||
|
$adapter->method( 'get_entity_id_column' )->willReturn( 'post_id' );
|
||||||
|
return $adapter;
|
||||||
|
}
|
||||||
|
|
||||||
protected function setUp(): void {
|
protected function setUp(): void {
|
||||||
TMDO_Entity_Registry::init();
|
TMDO_Entity_Registry::init();
|
||||||
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
|
||||||
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||||
}
|
}
|
||||||
@@ -66,8 +72,12 @@ class HivePressEntityGroupsTest extends TestCase {
|
|||||||
public function test_listing_columns_filtered_and_sorted_on_are_indexed(): void {
|
public function test_listing_columns_filtered_and_sorted_on_are_indexed(): void {
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
array(
|
array(
|
||||||
'hourly_rate' => array( 'hp_hourly_rate' ),
|
'hourly_rate' => array( 'hp_hourly_rate' ),
|
||||||
'rating_count' => array( 'hp_rating_count' ),
|
'rating_count' => array( 'hp_rating_count' ),
|
||||||
|
// Sorts: the value then the ID tie-break, read in order (2.4 PR-119).
|
||||||
|
'price_id' => array( 'hp_price', 'post_id' ),
|
||||||
|
'rating_id' => array( 'hp_rating', 'post_id' ),
|
||||||
|
'hourly_rate_id' => array( 'hp_hourly_rate', 'post_id' ),
|
||||||
),
|
),
|
||||||
TMDO_Entity_Registry::get_group_indexes( 'post', 'hp_listing_core' )
|
TMDO_Entity_Registry::get_group_indexes( 'post', 'hp_listing_core' )
|
||||||
);
|
);
|
||||||
@@ -94,7 +104,7 @@ class HivePressEntityGroupsTest extends TestCase {
|
|||||||
$GLOBALS['_wp_did_action']['after_setup_theme'] = 1;
|
$GLOBALS['_wp_did_action']['after_setup_theme'] = 1;
|
||||||
try {
|
try {
|
||||||
TMDO_Entity_Registry::init();
|
TMDO_Entity_Registry::init();
|
||||||
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
|
||||||
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -118,7 +128,7 @@ class HivePressEntityGroupsTest extends TestCase {
|
|||||||
add_filter( 'tmdo_hp_listing_attribute_fields', static fn() => array( 'hp_warranty' => 'integer' ) );
|
add_filter( 'tmdo_hp_listing_attribute_fields', static fn() => array( 'hp_warranty' => 'integer' ) );
|
||||||
try {
|
try {
|
||||||
TMDO_Entity_Registry::init();
|
TMDO_Entity_Registry::init();
|
||||||
TMDO_Entity_Registry::register_adapter( 'post', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
TMDO_Entity_Registry::register_adapter( 'post', $this->post_adapter() );
|
||||||
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
|
||||||
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
TMDO_HivePress_Entity_Groups::register_entity_fields();
|
||||||
$this->assertSame( array(), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ), 'plugins_loaded run: not yet' );
|
$this->assertSame( array(), TMDO_Entity_Registry::get_group_keys( 'post', 'hp_listing_attrs' ), 'plugins_loaded run: not yet' );
|
||||||
|
|||||||
Reference in New Issue
Block a user