623 lines
18 KiB
TypeScript
623 lines
18 KiB
TypeScript
// 位置追踪服务 - 处理用户签到后的位置追踪和状态管理
|
||
import { UserInfo, LocationData } from '../types';
|
||
import apiService from './apiService';
|
||
import userService from './userService';
|
||
import mapService from './mapService';
|
||
|
||
/**
|
||
* 在线用户信息接口
|
||
*/
|
||
export interface OnlineUserInfo {
|
||
userId: number;
|
||
name: string;
|
||
avatarUrl: string;
|
||
role: string; // 用户角色:'admin' | 'delivery_person'
|
||
lastLocation: LocationData;
|
||
lastUpdateTime: number;
|
||
status: 'online' | 'offline' | 'timeout';
|
||
}
|
||
|
||
/**
|
||
* 位置追踪服务类
|
||
* 提供用户签到后的位置追踪、状态管理和位置分发功能
|
||
*/
|
||
class LocationTrackingService {
|
||
// 在线用户列表
|
||
private onlineUsers: Map<number, OnlineUserInfo>;
|
||
|
||
// 位置更新定时器
|
||
private locationUpdateTimer: number | null;
|
||
|
||
// 状态检查定时器
|
||
private statusCheckTimer: number | null;
|
||
|
||
// 位置更新回调集合
|
||
private locationUpdateCallbacks: Set<(locations: OnlineUserInfo[]) => void>;
|
||
|
||
// 用户状态变化回调集合
|
||
private userStatusCallbacks: Set<(userId: number, status: 'online' | 'offline' | 'timeout') => void>;
|
||
|
||
// 当前用户是否已签到
|
||
private isSignedIn: boolean;
|
||
|
||
// 用户信息缓存
|
||
private userInfo: UserInfo | null;
|
||
|
||
// 取消订阅回调函数
|
||
private unsubscribeCallback: (() => void) | null;
|
||
|
||
/**
|
||
* 构造函数
|
||
*/
|
||
constructor() {
|
||
this.onlineUsers = new Map();
|
||
this.locationUpdateTimer = null;
|
||
this.statusCheckTimer = null;
|
||
this.locationUpdateCallbacks = new Set();
|
||
this.userStatusCallbacks = new Set();
|
||
this.isSignedIn = false;
|
||
this.userInfo = null;
|
||
this.unsubscribeCallback = null;
|
||
}
|
||
|
||
/**
|
||
* 用户签到后启动位置追踪
|
||
*/
|
||
public async startTrackingAfterSignIn(): Promise<void> {
|
||
const userInfo = userService.getGlobalUserInfo();
|
||
if (!userInfo || !userInfo.id) {
|
||
throw new Error('用户未登录,无法启动位置追踪');
|
||
}
|
||
|
||
// 先获取当前位置数据
|
||
let initialLocation: { latitude: number; longitude: number; timestamp: number } ;
|
||
|
||
const location = await this.getCurrentLocation();
|
||
initialLocation = {
|
||
latitude: location.latitude,
|
||
longitude: location.longitude,
|
||
timestamp: Math.floor(Date.now() / 1000)
|
||
}
|
||
|
||
console.log('获取到初始位置数据:', initialLocation);
|
||
|
||
// 调用签到接口,传递位置数据
|
||
try {
|
||
await userService.signIn(initialLocation);
|
||
console.log('签到成功,位置数据已发送到服务器');
|
||
} catch (error) {
|
||
console.error('签到失败:', error);
|
||
throw error;
|
||
}
|
||
|
||
this.isSignedIn = true;
|
||
|
||
// 将当前用户添加到在线列表
|
||
await this.addUserToOnlineList(userInfo);
|
||
|
||
// 设置实时位置订阅(初始化WebSocket连接)
|
||
await this.setupRealTimeSubscription();
|
||
|
||
// WebSocket连接成功后立即发送第一次位置信息
|
||
try {
|
||
const location = await this.getCurrentLocation();
|
||
await this.updateUserLocation(location);
|
||
console.log('WebSocket连接成功后立即发送第一次位置信息');
|
||
} catch (error) {
|
||
console.error('立即发送位置信息失败:', error);
|
||
}
|
||
|
||
// 启动位置更新定时器(每30秒更新一次)
|
||
this.startLocationUpdateTimer();
|
||
|
||
console.log('位置追踪服务已启动');
|
||
}
|
||
|
||
/**
|
||
* 初始化位置追踪服务
|
||
*/
|
||
public async initialize(): Promise<void> {
|
||
try {
|
||
// 注册在线用户列表回调
|
||
apiService.onOnlineUserList((userList) => {
|
||
this.handleOnlineUserList(userList);
|
||
});
|
||
|
||
console.log('[LocationTrackingService] 位置追踪服务初始化完成');
|
||
} catch (error) {
|
||
console.error('[LocationTrackingService] 初始化失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 用户签退后停止位置追踪
|
||
*/
|
||
public async stopTracking(): Promise<void> {
|
||
const userInfo = userService.getGlobalUserInfo();
|
||
if (!userInfo || !userInfo.id) {
|
||
return;
|
||
}
|
||
|
||
this.isSignedIn = false;
|
||
|
||
// 签退消息已通过REST API处理,服务器会自动处理取消订阅逻辑
|
||
|
||
// 从在线列表中移除用户
|
||
this.removeUserFromOnlineList(userInfo.id);
|
||
|
||
// 停止定时器
|
||
this.stopTimers();
|
||
|
||
|
||
|
||
// 关闭实时位置订阅(服务器会自动处理取消订阅)
|
||
this.teardownRealTimeSubscription();
|
||
|
||
console.log('位置追踪服务已停止');
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 手动更新用户位置
|
||
*/
|
||
public async updateUserLocation(location: { longitude: number; latitude: number }): Promise<void> {
|
||
const userInfo = userService.getGlobalUserInfo();
|
||
if (!userInfo || !userInfo.id || !this.isSignedIn) {
|
||
console.warn('[位置追踪服务] 用户未登录或未签到,无法更新位置');
|
||
return;
|
||
}
|
||
|
||
console.log('[位置追踪服务] 前端发送给服务器的位置信息:');
|
||
console.log(`- 用户ID: ${userInfo.id}`);
|
||
console.log(`- 经度: ${location.longitude}`);
|
||
console.log(`- 纬度: ${location.latitude}`);
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
console.log(`- 时间戳: ${timestamp}`);
|
||
|
||
const locationData: LocationData = {
|
||
userId: userInfo.id,
|
||
longitude: location.longitude,
|
||
latitude: location.latitude,
|
||
timestamp: timestamp
|
||
};
|
||
|
||
// 真实模式:通过WebSocket发送位置更新
|
||
console.log('[位置追踪服务] 真实模式:通过WebSocket发送位置更新');
|
||
await apiService.sendLocationUpdate(userInfo.id, location.latitude, location.longitude);
|
||
|
||
// 同时更新本地缓存
|
||
this.updateLocalUserLocation(userInfo.id, locationData);
|
||
|
||
console.log('[位置追踪服务] 位置更新完成');
|
||
}
|
||
|
||
/**
|
||
* 获取所有在线用户信息
|
||
*/
|
||
public getOnlineUsers(): OnlineUserInfo[] {
|
||
return Array.from(this.onlineUsers.values()).filter(user =>
|
||
user.status === 'online'
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 订阅位置更新
|
||
*/
|
||
public subscribeToLocationUpdates(callback: (locations: OnlineUserInfo[]) => void): void {
|
||
console.log('📝 [LocationTrackingService] 注册位置更新回调函数');
|
||
console.log('📍 [LocationTrackingService] 当前已注册回调数量:', this.locationUpdateCallbacks.size);
|
||
this.locationUpdateCallbacks.add(callback);
|
||
console.log('✅ [LocationTrackingService] 位置更新回调注册完成,当前回调数量:', this.locationUpdateCallbacks.size);
|
||
}
|
||
|
||
/**
|
||
* 取消订阅位置更新
|
||
*/
|
||
public unsubscribeFromLocationUpdates(callback: (locations: OnlineUserInfo[]) => void): void {
|
||
this.locationUpdateCallbacks.delete(callback);
|
||
}
|
||
|
||
/**
|
||
* 订阅用户状态变化
|
||
*/
|
||
public subscribeToUserStatusChanges(callback: (userId: number, status: 'online' | 'offline' | 'timeout') => void): void {
|
||
this.userStatusCallbacks.add(callback);
|
||
}
|
||
|
||
/**
|
||
* 取消订阅用户状态变化
|
||
*/
|
||
public unsubscribeFromUserStatusChanges(callback: (userId: number, status: 'online' | 'offline' | 'timeout') => void): void {
|
||
this.userStatusCallbacks.delete(callback);
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否在线
|
||
*/
|
||
public isUserOnline(userId: number): boolean {
|
||
const userInfo = this.onlineUsers.get(userId);
|
||
return userInfo ? userInfo.status === 'online' : false;
|
||
}
|
||
|
||
// ===== 私有方法 =====
|
||
|
||
/**
|
||
* 添加用户到在线列表
|
||
*/
|
||
private async addUserToOnlineList(userInfo: UserInfo): Promise<void> {
|
||
const currentLocation = await this.getCurrentLocation();
|
||
|
||
const onlineUser: OnlineUserInfo = {
|
||
userId: userInfo.id,
|
||
name: userInfo.name || '未知用户',
|
||
avatarUrl: '/images/user-avatar.png',
|
||
role: userInfo.role || 'delivery_person', // 用户角色
|
||
lastLocation: currentLocation,
|
||
lastUpdateTime: Math.floor(Date.now() / 1000),
|
||
status: 'online'
|
||
};
|
||
|
||
this.onlineUsers.set(userInfo.id, onlineUser);
|
||
|
||
|
||
|
||
console.log(`用户 ${userInfo.id} 已添加到在线列表`);
|
||
}
|
||
|
||
/**
|
||
* 从在线列表中移除用户
|
||
*/
|
||
private removeUserFromOnlineList(userId: number): void {
|
||
if (this.onlineUsers.has(userId)) {
|
||
this.onlineUsers.delete(userId);
|
||
|
||
|
||
|
||
// 通知状态变化回调
|
||
this.notifyUserStatusChange(userId, 'offline');
|
||
|
||
console.log(`用户 ${userId} 已从在线列表移除`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新本地用户位置
|
||
*/
|
||
private updateLocalUserLocation(userId: number, location: LocationData): void {
|
||
const userInfo = this.onlineUsers.get(userId);
|
||
if (userInfo) {
|
||
userInfo.lastLocation = location;
|
||
userInfo.lastUpdateTime = Date.now();
|
||
userInfo.status = 'online';
|
||
|
||
this.onlineUsers.set(userId, userInfo);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启动位置更新定时器
|
||
*/
|
||
private startLocationUpdateTimer(): void {
|
||
this.stopLocationUpdateTimer();
|
||
|
||
this.locationUpdateTimer = setInterval(async () => {
|
||
if (this.isSignedIn) {
|
||
try {
|
||
const location = await this.getCurrentLocation();
|
||
await this.updateUserLocation(location);
|
||
} catch (error) {
|
||
console.error('自动位置更新失败:', error);
|
||
}
|
||
}
|
||
}, 30000); // 每30秒更新一次
|
||
}
|
||
|
||
/**
|
||
* 启动状态检查定时器
|
||
*/
|
||
private startStatusCheckTimer(): void {
|
||
this.stopStatusCheckTimer();
|
||
|
||
this.statusCheckTimer = setInterval(() => {
|
||
this.checkUserStatuses();
|
||
}, 60000); // 每60秒检查一次
|
||
}
|
||
|
||
/**
|
||
* 检查用户状态
|
||
*/
|
||
private checkUserStatuses(): void {
|
||
const now = Date.now();
|
||
const timeoutThreshold = 120000; // 2分钟无更新视为超时
|
||
|
||
this.onlineUsers.forEach((userInfo, userId) => {
|
||
if (now - userInfo.lastUpdateTime > timeoutThreshold) {
|
||
// 用户超时
|
||
userInfo.status = 'timeout';
|
||
this.onlineUsers.set(userId, userInfo);
|
||
this.notifyUserStatusChange(userId, 'timeout');
|
||
|
||
console.log(`用户 ${userId} 因超时被标记为离线`);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取当前位置
|
||
*/
|
||
public async getCurrentLocation(): Promise<LocationData> {
|
||
const userInfo = userService.getGlobalUserInfo();
|
||
if (!userInfo || !userInfo.id) {
|
||
throw new Error('用户未登录');
|
||
}
|
||
|
||
try {
|
||
// 使用mapService获取真实位置,保持与项目中其他地方的一致性
|
||
const location = await mapService.getLocation();
|
||
|
||
console.log('[位置追踪服务] 获取真实位置成功:', location);
|
||
|
||
return {
|
||
userId: userInfo.id,
|
||
longitude: location.longitude,
|
||
latitude: location.latitude,
|
||
timestamp: Date.now()
|
||
};
|
||
} catch (error) {
|
||
console.error('[位置追踪服务] 获取真实位置失败,使用默认位置:', error);
|
||
|
||
// 定位失败时返回默认位置作为降级方案
|
||
return {
|
||
userId: userInfo.id,
|
||
longitude: 102.833722,
|
||
latitude: 24.880095,
|
||
timestamp: Date.now()
|
||
};
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 取消订阅位置更新
|
||
*/
|
||
private async unsubscribeFromLocations(): Promise<void> {
|
||
// 真实模式:取消WebSocket订阅
|
||
await this.teardownRealTimeSubscription();
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* 设置实时位置订阅
|
||
*/
|
||
private async setupRealTimeSubscription(): Promise<void> {
|
||
// 获取用户信息
|
||
const userInfo = userService.getGlobalUserInfo();
|
||
if (!userInfo || !userInfo.id) {
|
||
console.warn('用户未登录,无法设置实时位置订阅');
|
||
return;
|
||
}
|
||
|
||
// 缓存用户信息
|
||
this.userInfo = userInfo;
|
||
|
||
// 真实环境:初始化WebSocket连接(服务器会自动处理订阅逻辑)
|
||
try {
|
||
// 初始化WebSocket连接(如果尚未连接)
|
||
await apiService.initLocationWebSocket();
|
||
|
||
// 等待WebSocket连接建立(最多等待5秒)
|
||
await new Promise<void>((resolve, reject) => {
|
||
let attempts = 0;
|
||
const maxAttempts = 50; // 5秒超时
|
||
|
||
const checkConnection = () => {
|
||
if (apiService.isWebSocketConnected()) {
|
||
resolve();
|
||
} else if (attempts < maxAttempts) {
|
||
attempts++;
|
||
setTimeout(checkConnection, 100);
|
||
} else {
|
||
reject(new Error('WebSocket连接建立超时'));
|
||
}
|
||
};
|
||
checkConnection();
|
||
});
|
||
|
||
// 服务器会自动处理订阅逻辑,无需发送订阅消息
|
||
console.log('WebSocket连接已建立,服务器将自动处理位置订阅逻辑');
|
||
|
||
|
||
|
||
} catch (error) {
|
||
console.error('设置实时位置订阅失败:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 关闭实时位置订阅
|
||
*/
|
||
private teardownRealTimeSubscription(): void {
|
||
// 真实环境:关闭WebSocket连接(服务器会自动处理取消订阅逻辑)
|
||
try {
|
||
// 服务器会自动处理取消订阅逻辑,无需发送取消订阅消息
|
||
console.log('关闭WebSocket连接,服务器将自动处理取消订阅逻辑');
|
||
|
||
// 移除位置更新回调
|
||
if (this.unsubscribeCallback) {
|
||
this.unsubscribeCallback();
|
||
this.unsubscribeCallback = null;
|
||
}
|
||
|
||
// 关闭WebSocket连接
|
||
apiService.closeLocationWebSocket();
|
||
|
||
} catch (error) {
|
||
console.error('关闭实时位置订阅失败:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止所有定时器
|
||
*/
|
||
private stopTimers(): void {
|
||
this.stopLocationUpdateTimer();
|
||
this.stopStatusCheckTimer();
|
||
}
|
||
|
||
/**
|
||
* 停止位置更新定时器
|
||
*/
|
||
private stopLocationUpdateTimer(): void {
|
||
if (this.locationUpdateTimer) {
|
||
clearInterval(this.locationUpdateTimer);
|
||
this.locationUpdateTimer = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止状态检查定时器
|
||
*/
|
||
private stopStatusCheckTimer(): void {
|
||
if (this.statusCheckTimer) {
|
||
clearInterval(this.statusCheckTimer);
|
||
this.statusCheckTimer = null;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 通知位置更新回调
|
||
*/
|
||
private notifyLocationUpdateCallbacks(): void {
|
||
const onlineUsers = this.getOnlineUsers();
|
||
console.log('🔔 [LocationTrackingService] 开始通知位置更新回调');
|
||
console.log('👥 [LocationTrackingService] 在线用户数量:', onlineUsers.length);
|
||
console.log('📞 [LocationTrackingService] 准备执行', this.locationUpdateCallbacks.size, '个回调函数');
|
||
|
||
this.locationUpdateCallbacks.forEach((callback, index) => {
|
||
try {
|
||
console.log(`🔄 [LocationTrackingService] 执行第 ${index + 1} 个位置更新回调`);
|
||
callback(onlineUsers);
|
||
console.log(`✅ [LocationTrackingService] 第 ${index + 1} 个位置更新回调执行成功`);
|
||
} catch (error) {
|
||
console.error(`❌ [LocationTrackingService] 第 ${index + 1} 个位置更新回调执行失败:`, error);
|
||
}
|
||
});
|
||
|
||
console.log('🏁 [LocationTrackingService] 位置更新回调通知完成');
|
||
}
|
||
|
||
/**
|
||
* 通知用户状态变化回调
|
||
*/
|
||
private notifyUserStatusChange(userId: number, status: 'online' | 'offline' | 'timeout'): void {
|
||
this.userStatusCallbacks.forEach(callback => {
|
||
try {
|
||
callback(userId, status);
|
||
} catch (error) {
|
||
console.error('用户状态变化回调执行失败:', error);
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* 处理在线用户列表
|
||
* @param userList 服务器下发的在线用户列表
|
||
*/
|
||
private handleOnlineUserList(userList: any[]): void {
|
||
try {
|
||
// 验证数据格式
|
||
if (!Array.isArray(userList)) {
|
||
console.warn('[LocationTrackingService] 无效的在线用户列表数据格式');
|
||
return;
|
||
}
|
||
|
||
console.log(`[LocationTrackingService] 开始处理在线用户列表,用户数量: ${userList.length}`);
|
||
|
||
// 获取当前本地用户列表的userId集合
|
||
const currentUserIds = new Set(this.onlineUsers.keys());
|
||
|
||
// 处理服务器下发的用户列表
|
||
const newUserIds = new Set();
|
||
|
||
userList.forEach(user => {
|
||
if (user && user.userId && user.latitude !== undefined && user.longitude !== undefined) {
|
||
newUserIds.add(user.userId);
|
||
|
||
// 提取位置数据(支持多种数据格式)
|
||
const latitude = user.latitude;
|
||
const longitude = user.longitude;
|
||
const timestamp = user.timestamp || user.lastUpdateTime || Date.now();
|
||
|
||
// 更新或添加用户信息
|
||
this.onlineUsers.set(user.userId, {
|
||
userId: user.userId,
|
||
name: user.name || user.userName || `用户${user.userId}`,
|
||
avatarUrl: '/images/user-avatar.png',
|
||
role: user.role || 'employee',
|
||
lastLocation: {
|
||
userId: user.userId,
|
||
longitude: longitude,
|
||
latitude: latitude,
|
||
timestamp: timestamp
|
||
},
|
||
lastUpdateTime: timestamp,
|
||
status: user.userStatus === false ? 'offline' : 'online'
|
||
});
|
||
|
||
console.log(`[LocationTrackingService] 更新用户 ${user.userId} 位置: (${latitude}, ${longitude})`);
|
||
|
||
// 如果用户是新上线的,触发用户状态回调
|
||
if (!currentUserIds.has(user.userId)) {
|
||
this.notifyUserStatusChange(user.userId, 'online');
|
||
console.log(`[LocationTrackingService] 新用户上线: ${user.userId}`);
|
||
}
|
||
} else {
|
||
console.warn(`[LocationTrackingService] 跳过无效用户数据:`, user);
|
||
}
|
||
});
|
||
|
||
// 删除本地列表中不在服务器列表中的用户
|
||
let removedCount = 0;
|
||
currentUserIds.forEach(userId => {
|
||
if (!newUserIds.has(userId)) {
|
||
// 删除离线用户
|
||
this.onlineUsers.delete(userId);
|
||
removedCount++;
|
||
|
||
// 触发用户状态回调(离线)
|
||
this.notifyUserStatusChange(userId, 'offline');
|
||
console.log(`[LocationTrackingService] 用户离线: ${userId}`);
|
||
}
|
||
});
|
||
|
||
console.log(`[LocationTrackingService] 在线用户列表更新完成: 新增 ${newUserIds.size - (currentUserIds.size - removedCount)} 个用户,删除 ${removedCount} 个用户`);
|
||
|
||
// 通知位置模块更新地图标记点(无论是否有变化都通知,确保地图标记点同步)
|
||
this.notifyLocationUpdateCallbacks();
|
||
console.log('[LocationTrackingService] 已通知位置模块更新地图标记点');
|
||
|
||
} catch (error) {
|
||
console.error('[LocationTrackingService] 处理在线用户列表失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 位置追踪服务单例实例
|
||
* 导出供应用程序全局使用
|
||
*/
|
||
export default new LocationTrackingService(); |