fix(sse): isolate price stream inbox per tenant

Shared Redis global inbox mixed other shops' product lists into this catalog and made the grid jump.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 10:58:43 +03:30
co-authored by Cursor
parent e4e9c74872
commit 166764d6b8
2 changed files with 37 additions and 9 deletions
+12 -7
View File
@@ -52,7 +52,7 @@ class PriceBroadcastService
} }
$lastChanged = is_array($lastPayload) && $lastPayload !== [] $lastChanged = is_array($lastPayload) && $lastPayload !== []
&& $this->rememberHashIfChanged(self::LAST_HASH_KEY, self::lastPriceIdentity($lastPayload)); && $this->rememberHashIfChanged($this->cacheKey(self::LAST_HASH_KEY), self::lastPriceIdentity($lastPayload));
if ($lastChanged && is_array($lastPayload)) { if ($lastChanged && is_array($lastPayload)) {
$stream->publish('last', $lastPayload); $stream->publish('last', $lastPayload);
@@ -106,7 +106,7 @@ class PriceBroadcastService
return; return;
} }
if (! $this->rememberHashIfChanged(self::LAST_HASH_KEY, self::lastPriceIdentity($lastPayload))) { if (! $this->rememberHashIfChanged($this->cacheKey(self::LAST_HASH_KEY), self::lastPriceIdentity($lastPayload))) {
return; return;
} }
@@ -117,7 +117,7 @@ class PriceBroadcastService
public function buildListPayload(bool $allowCached = false): ?array public function buildListPayload(bool $allowCached = false): ?array
{ {
if ($allowCached) { if ($allowCached) {
$cached = Cache::get(self::LIST_SNAPSHOT_KEY); $cached = Cache::get($this->cacheKey(self::LIST_SNAPSHOT_KEY));
if ( if (
is_array($cached) is_array($cached)
&& is_array($cached['body'] ?? null) && is_array($cached['body'] ?? null)
@@ -130,7 +130,7 @@ class PriceBroadcastService
$body = $this->buildProductListPayload(); $body = $this->buildProductListPayload();
if ($body !== null && $body !== []) { if ($body !== null && $body !== []) {
Cache::put(self::LIST_SNAPSHOT_KEY, [ Cache::put($this->cacheKey(self::LIST_SNAPSHOT_KEY), [
'body' => $body, 'body' => $body,
'stored_at' => now(), 'stored_at' => now(),
], now()->addSeconds(self::LIST_SNAPSHOT_SECONDS + 5)); ], now()->addSeconds(self::LIST_SNAPSHOT_SECONDS + 5));
@@ -156,15 +156,15 @@ class PriceBroadcastService
return false; return false;
} }
if (! $force && ! $this->rememberHashIfChanged(self::LIST_HASH_KEY, self::listIdentity($listPayload))) { if (! $force && ! $this->rememberHashIfChanged($this->cacheKey(self::LIST_HASH_KEY), self::listIdentity($listPayload))) {
return false; return false;
} }
if ($force) { if ($force) {
$this->rememberHashIfChanged(self::LIST_HASH_KEY, self::listIdentity($listPayload)); $this->rememberHashIfChanged($this->cacheKey(self::LIST_HASH_KEY), self::listIdentity($listPayload));
} }
Cache::forget(self::LIST_SNAPSHOT_KEY); Cache::forget($this->cacheKey(self::LIST_SNAPSHOT_KEY));
app(PriceStreamService::class)->publish('list', $listPayload); app(PriceStreamService::class)->publish('list', $listPayload);
return true; return true;
@@ -218,6 +218,11 @@ class PriceBroadcastService
/** /**
* @param array<string, mixed>|array<int, mixed> $payload * @param array<string, mixed>|array<int, mixed> $payload
*/ */
private function cacheKey(string $base): string
{
return $base.':'.app(PriceStreamService::class)->tenantScope();
}
private function rememberHashIfChanged(string $cacheKey, array $payload): bool private function rememberHashIfChanged(string $cacheKey, array $payload): bool
{ {
$hash = md5(json_encode($payload, JSON_UNESCAPED_UNICODE)); $hash = md5(json_encode($payload, JSON_UNESCAPED_UNICODE));
+25 -2
View File
@@ -126,14 +126,37 @@ class PriceStreamService
return $store; return $store;
} }
/**
* چند مستأجر روی یک Redis با پیشوند یکسان inbox را قاطی می‌کردند
* (لیست شمش یک فروشگاه روی صفحهٔ فروشگاه دیگر).
*/
public function tenantScope(): string
{
$default = (string) config('database.default', 'mysql');
$db = (string) config("database.connections.{$default}.database");
$host = '';
try {
$host = app(ConfigService::class)->resolvePublicHost();
} catch (\Throwable) {
$host = '';
}
$parts = array_values(array_filter(
[$db, $host],
static fn (string $value): bool => $value !== ''
));
return $parts !== [] ? implode(':', $parts) : 'default';
}
private function inboxKey(): string private function inboxKey(): string
{ {
return self::INBOX_PREFIX.'global'; return self::INBOX_PREFIX.$this->tenantScope();
} }
private function customerInboxKey(int $customerId): string private function customerInboxKey(int $customerId): string
{ {
return self::CUSTOMER_INBOX_PREFIX.$customerId; return self::CUSTOMER_INBOX_PREFIX.$this->tenantScope().':'.$customerId;
} }
private function currentInboxSeq(string $key): int private function currentInboxSeq(string $key): int