P2: ride types + class-aware matching + round-trip + dispatch + wallet + FCM + WS live location/WebRTC
- ride-types: per-tenant catalog (economy/comfort/electric/family_van/women/scooter), seeded
- matching now class-scoped (geo:drivers:{tenant}:{class}); trips match by requested type
- trips: is_round_trip (doubles distance for fare) + payment_method + wallet settlement on paid
- dispatch: operator creates trip for phone customer (role-guarded)
- wallet: balance + credit/debit + txns + self topup; wallet-paid trips settle rider→driver
- notifications: FCM device tokens + best-effort push (assign event)
- realtime: trip:join, live driver:location broadcast, WebRTC call signaling (offer/answer/ice/end)
- migration InitP2 (ride_types, wallets, wallet_txns, device_tokens, trips.is_round_trip)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
import {
|
||||
ConnectedSocket,
|
||||
MessageBody,
|
||||
OnGatewayConnection,
|
||||
SubscribeMessage,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { DriversService } from '../modules/drivers/drivers.service';
|
||||
|
||||
/**
|
||||
* البوابة الحية (Socket.IO) — docs/09. المصادقة على الاتصال بـ JWT،
|
||||
* والانضمام لغرف مُنطَّقة بالمستأجر (لا تسريب بين المستأجرين).
|
||||
* غرف: tenant:{id}:driver:{driverId} · tenant:{id}:trip:{tripId} · tenant:{id}:dispatch
|
||||
* البوابة الحية (Socket.IO) — docs/09. مصادقة JWT + غرف مُنطَّقة بالمستأجر،
|
||||
* بث مواقع السائقين لحظياً، وإشارات مكالمات WebRTC (offer/answer/ice) عبر غرفة الرحلة.
|
||||
*/
|
||||
@WebSocketGateway({ cors: true })
|
||||
export class RealtimeGateway implements OnGatewayConnection {
|
||||
@@ -19,7 +22,10 @@ export class RealtimeGateway implements OnGatewayConnection {
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
constructor(private readonly jwt: JwtService) {}
|
||||
constructor(
|
||||
private readonly jwt: JwtService,
|
||||
private readonly drivers: DriversService,
|
||||
) {}
|
||||
|
||||
handleConnection(client: Socket) {
|
||||
try {
|
||||
@@ -28,29 +34,84 @@ export class RealtimeGateway implements OnGatewayConnection {
|
||||
(client.handshake.headers?.authorization as string)?.replace('Bearer ', '');
|
||||
if (!token) throw new Error('no token');
|
||||
const p: any = this.jwt.verify(token);
|
||||
const tenantId = p.tenant_id;
|
||||
client.data.user = { userId: p.sub, tenantId, role: p.role };
|
||||
client.join(`tenant:${tenantId}:user:${p.sub}`);
|
||||
if (p.role === 'driver') client.join(`tenant:${tenantId}:drivers`);
|
||||
this.logger.debug(`connected user=${p.sub} tenant=${tenantId} role=${p.role}`);
|
||||
client.data.user = { userId: p.sub, tenantId: p.tenant_id, role: p.role };
|
||||
client.join(`tenant:${p.tenant_id}:user:${p.sub}`);
|
||||
if (p.role === 'driver') client.join(`tenant:${p.tenant_id}:drivers`);
|
||||
} catch {
|
||||
client.disconnect(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- مساعدات البث التي تستدعيها الخدمات ----
|
||||
private tripRoom(tenantId: string, tripId: string) {
|
||||
return `tenant:${tenantId}:trip:${tripId}`;
|
||||
}
|
||||
|
||||
/** عرض رحلة لسائق محدد. */
|
||||
// ---- اشتراكات الويب سوكت ----
|
||||
|
||||
/** الطرفان ينضمّان لغرفة الرحلة لاستقبال التتبع والدردشة والمكالمات. */
|
||||
@SubscribeMessage('trip:join')
|
||||
onTripJoin(@ConnectedSocket() c: Socket, @MessageBody() body: { tripId: string }) {
|
||||
const u = c.data.user;
|
||||
if (!u || !body?.tripId) return;
|
||||
c.join(this.tripRoom(u.tenantId, body.tripId));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** موقع السائق الحي: يحدّث Redis GEO ويبثّ لغرفة الرحلة. */
|
||||
@SubscribeMessage('driver:location')
|
||||
async onDriverLocation(
|
||||
@ConnectedSocket() c: Socket,
|
||||
@MessageBody() body: { lat: number; lng: number; tripId?: string },
|
||||
) {
|
||||
const u = c.data.user;
|
||||
if (!u || u.role !== 'driver' || body?.lat == null || body?.lng == null) return;
|
||||
await this.drivers.updateLocation(u.tenantId, u.userId, body.lat, body.lng);
|
||||
if (body.tripId) {
|
||||
c.to(this.tripRoom(u.tenantId, body.tripId)).emit('driver:location', {
|
||||
tripId: body.tripId,
|
||||
lat: body.lat,
|
||||
lng: body.lng,
|
||||
});
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// ---- إشارات WebRTC (مكالمة مجانية بين الطرفين عبر غرفة الرحلة) ----
|
||||
@SubscribeMessage('call:offer')
|
||||
onCallOffer(@ConnectedSocket() c: Socket, @MessageBody() b: any) {
|
||||
this.relay(c, 'call:offer', b);
|
||||
}
|
||||
|
||||
@SubscribeMessage('call:answer')
|
||||
onCallAnswer(@ConnectedSocket() c: Socket, @MessageBody() b: any) {
|
||||
this.relay(c, 'call:answer', b);
|
||||
}
|
||||
|
||||
@SubscribeMessage('call:ice')
|
||||
onCallIce(@ConnectedSocket() c: Socket, @MessageBody() b: any) {
|
||||
this.relay(c, 'call:ice', b);
|
||||
}
|
||||
|
||||
@SubscribeMessage('call:end')
|
||||
onCallEnd(@ConnectedSocket() c: Socket, @MessageBody() b: any) {
|
||||
this.relay(c, 'call:end', b);
|
||||
}
|
||||
|
||||
private relay(c: Socket, event: string, b: any) {
|
||||
const u = c.data.user;
|
||||
if (!u || !b?.tripId) return;
|
||||
c.to(this.tripRoom(u.tenantId, b.tripId)).emit(event, { from: u.userId, ...b });
|
||||
}
|
||||
|
||||
// ---- مساعدات البث التي تستدعيها الخدمات ----
|
||||
offerToDriver(tenantId: string, driverUserId: string, payload: any) {
|
||||
this.server.to(`tenant:${tenantId}:user:${driverUserId}`).emit('trip:offer', payload);
|
||||
}
|
||||
|
||||
/** تحديث حالة الرحلة للراكب أو السائق. */
|
||||
tripUpdate(tenantId: string, userId: string, payload: any) {
|
||||
this.server.to(`tenant:${tenantId}:user:${userId}`).emit('trip:update', payload);
|
||||
}
|
||||
|
||||
/** بث للوحة المشغّل. */
|
||||
dispatch(tenantId: string, event: string, payload: any) {
|
||||
this.server.to(`tenant:${tenantId}:dispatch`).emit(event, payload);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { RealtimeGateway } from './realtime.gateway';
|
||||
import { DriversModule } from '../modules/drivers/drivers.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DriversModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
|
||||
Reference in New Issue
Block a user