feat: المجموعة G — Redis خط أول والقاعدة خط احتياط

الأساس: common/cache — كاش-جانبي فوق Redis. مبدأ صارم: فشل Redis لا يُسقط
الطلب (يُسجَّل ويُرجَع للقاعدة) — الكاش تحسين أداء لا مصدر حقيقة.

- G1: لغة المستخدم في Redis (كانت استعلاماً قبل كل إشعار)؛ تُكتب عند
  PATCH /users/me فيصير الإصابة دائمة
- G2/G3: توكنات FCM في Redis — يُبطَل عند register وعند اكتشاف توكن ميت.
  البث الجماعي صار بلا استعلامات قاعدة
- G4: كاش tenant resolve/config + إبطال عند الإنشاء
- G5: كاش التعرفة و ride-types + إبطال صريح عند أي إنشاء
- G6: التقييم يتراكم في Redis، والقاعدة تُكتب مرة واحدة يومياً لكل طرف
  (حجز الكتابة ذرّي بـLua). التأخير مقصود ليبعد الاحتكاك بين الطرفين.
  أُضيف ratings.target_user_id و users.rating — تقييم السائق للراكب كان
  يُخزَّن بلا هدف فلا يُجمَّع أبداً
- كاش الخرائط (route/reverse/geocode) بإحداثيات مقرَّبة 4 خانات (~11م):
  نداء انطلق كان يهيمن على p50 (2.9s). لا يُخزَّن الرجوع لخط مستقيم ولا
  استجابة فاشلة

تصحيح: ادّعيت سابقاً أن tenants.resolve يعمل على كل request — خطأ.
tenant_id يأتي من الـJWT؛ resolve يُستدعى عند الدخول و3 كنترولرات فقط.
الحمل الأثقل فعلياً هو رفع موقع السائق (المجموعة H).

هجرة: RatingsBothParties.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-17 02:39:34 +03:00
co-authored by Claude Opus 4.8
parent 857652404b
commit a83dd7e5e5
18 changed files with 694 additions and 85 deletions
+22 -9
View File
@@ -2,12 +2,14 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tenant } from '../../database/entities/tenant.entity';
import { CacheService, CacheKeys, TTL } from '../../common/cache/cache.service';
@Injectable()
export class TenantsService {
constructor(
@InjectRepository(Tenant)
private readonly repo: Repository<Tenant>,
private readonly cache: CacheService,
) {}
findAll(): Promise<Tenant[]> {
@@ -27,16 +29,26 @@ export class TenantsService {
*/
async resolve(idOrSlug: string): Promise<Tenant | null> {
if (!idOrSlug) return null;
const bySlug = await this.findBySlug(idOrSlug);
if (bySlug) return bySlug;
if (TenantsService.UUID_RE.test(idOrSlug)) {
return this.repo.findOne({ where: { id: idOrSlug } });
}
return null;
// Redis خط أول (docs/17 — G4): المستأجر يتغيّر نادراً جداً.
return this.cache.wrap(CacheKeys.tenant(idOrSlug), TTL.tenant, async () => {
const bySlug = await this.findBySlug(idOrSlug);
if (bySlug) return bySlug;
if (TenantsService.UUID_RE.test(idOrSlug)) {
return this.repo.findOne({ where: { id: idOrSlug } });
}
return null;
});
}
create(data: Partial<Tenant>): Promise<Tenant> {
return this.repo.save(this.repo.create(data));
async create(data: Partial<Tenant>): Promise<Tenant> {
const saved = await this.repo.save(this.repo.create(data));
await this.invalidate(saved);
return saved;
}
/** يُبطل مفتاحَي المستأجر (بالـslug وبالـUUID) — يُنادى بعد أي تعديل عليه. */
async invalidate(tenant: Pick<Tenant, 'id' | 'slug'>): Promise<void> {
await this.cache.del(CacheKeys.tenant(tenant.slug), CacheKeys.tenant(tenant.id));
}
/**
@@ -44,7 +56,8 @@ export class TenantsService {
* كل ما يمكن جعله ديناميكياً (نصوص، ميزات، ألوان، دفع) يأتي من هنا — راجع docs/06.
*/
async config(slug: string) {
const t = await this.findBySlug(slug);
// عبر resolve ليستفيد من الكاش — كل تطبيق يناديها عند الإقلاع.
const t = await this.resolve(slug);
if (!t) return null;
return {
slug: t.slug,