feat: المجموعة H — المواقع في Redis، والقاعدة من الـworker فقط

كان كل نبضة موقع (كل 1-3 ثوانٍ لكل سائق متصل) تكلّف استعلاماً + كتابة صف
السائق في Postgres — أثقل حمل كان على القاعدة، وأثقل حتى من سيرو.

- H1/H2: نبضة الموقع = Redis فقط، بعتبات سيرو (10م / 1.0 سرعة / 5° اتجاه).
  سائق واقف = EXPIRE واحد: لا كتابة، لا نقطة مسار، ولا بثّ للراكب
- H3: لقطة آخر موقع على drivers (heading/speed/loc_status/loc_updated_at)
  يكتبها الـworker دورياً. لم نُنشئ جدولاً منفصلاً — drivers أصلاً صف واحد
  لكل سائق
- H4: driver_tracks — تتراكم في قائمة Redis ويُدرجها الـworker بعبارة واحدة
- H5: فهرسا available/busy — البحث يمسح المتاحين فقط. السائق يصير busy عند
  القبول ويعود available عند الإنهاء/الإلغاء
- H8: المطابقة والاحتيال و«الرحلات المتاحة» تقرأ الموقع الحيّ من Redis.
  كشف تزوير الوصول خاصةً يجب ألّا يحكم بلقطة دورية

الـworker صار حقيقياً (كان setInterval فارغاً): سياق Nest مستقل، تفريغ كل
5s، قفل يمنع تراكب الدورات، وتفريغة أخيرة عند SIGTERM.

مؤجَّل من H: H6 (سلوك السائق) و H7 (ساعات العمل) — تحليلات تُبنى من
driver_tracks لاحقاً.

هجرة: DriverLocations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-17 02:49:45 +03:00
co-authored by Claude Opus 4.8
parent a83dd7e5e5
commit 0ed3a3160a
19 changed files with 794 additions and 98 deletions
+30 -23
View File
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Driver } from './entities/driver.entity';
import { UsersService } from '../users/users.service';
import { MatchingService } from '../matching/matching.service';
import { DriverLocationService } from '../locations/driver-location.service';
@Injectable()
export class DriversService {
@@ -11,7 +11,7 @@ export class DriversService {
@InjectRepository(Driver)
private readonly repo: Repository<Driver>,
private readonly users: UsersService,
private readonly matching: MatchingService,
private readonly locations: DriverLocationService,
) {}
findByUser(tenantId: string, userId: string): Promise<Driver | null> {
@@ -62,7 +62,7 @@ export class DriversService {
return this.repo.save(driver);
}
/** يبدّل حالة الاتصال؛ عند الاتصال يُضاف لفهرس GEO، وعند الفصل يُزال. */
/** يبدّل حالة الاتصال؛ عند الاتصال يدخل فهرس المتاحين، وعند الفصل يُزال. */
async setOnline(
tenantId: string,
userId: string,
@@ -72,35 +72,42 @@ export class DriversService {
if (!driver) throw new NotFoundException('Driver profile not found');
driver.is_online = online;
await this.repo.save(driver);
if (!online) {
await this.matching.removeDriver(tenantId, driver.service_class, driver.id);
} else if (driver.last_lat != null && driver.last_lng != null) {
await this.matching.addDriver(
tenantId,
driver.service_class,
driver.id,
driver.last_lat,
driver.last_lng,
);
}
await this.locations.setAvailability(
tenantId,
driver.id,
driver.service_class,
online ? 'available' : 'off',
);
return driver;
}
/** تحديث الموقع الجاري: Redis GEO + لقطة في Postgres. */
/**
* نبضة موقع — **Redis فقط** (docs/17 — H1). كانت تكتب صف السائق في Postgres
* على كل نبضة (كل ثانية/ثلاث لكل سائق متصل) — أثقل حمل كان على القاعدة.
* اللقطة الدائمة يكتبها الـworker دورياً.
*/
async updateLocation(
tenantId: string,
userId: string,
lat: number,
lng: number,
): Promise<{ ok: boolean }> {
extra: { heading?: number; speed?: number } = {},
): Promise<{ ok: boolean; significant: boolean }> {
const driver = await this.findByUser(tenantId, userId);
if (!driver) throw new NotFoundException('Driver profile not found');
driver.last_lat = lat;
driver.last_lng = lng;
await this.repo.save(driver);
if (driver.is_online) {
await this.matching.addDriver(tenantId, driver.service_class, driver.id, lat, lng);
}
return { ok: true };
if (!driver.is_online) return { ok: true, significant: false };
const res = await this.locations.update(tenantId, driver.id, driver.service_class, {
lat,
lng,
heading: extra.heading,
speed: extra.speed,
});
return { ok: true, significant: res.significant };
}
/** الموقع الحيّ للسائق — من Redis، واللقطة احتياط. */
liveLocation(tenantId: string, driverId: string) {
return this.locations.get(tenantId, driverId);
}
}